1use std::collections::HashMap;
10
11use memf_core::object_reader::ObjectReader;
12use memf_format::PhysicalMemoryProvider;
13
14use crate::Result;
15
16#[derive(Debug, Clone, serde::Serialize)]
19pub struct SharedCredInfo {
20 pub pid: u32,
22 pub process_name: String,
24 pub uid: u32,
26 pub cred_address: u64,
28 pub shared_with_pids: Vec<u32>,
30 pub is_suspicious: bool,
32}
33
34pub use crate::heuristics::classify_shared_creds;
45
46pub fn walk_check_creds<P: PhysicalMemoryProvider>(
53 reader: &ObjectReader<P>,
54) -> Result<Vec<SharedCredInfo>> {
55 let init_task_addr = match reader.symbols().symbol_address("init_task") {
57 Some(addr) => addr,
58 None => return Ok(Vec::new()),
59 };
60
61 let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
62 Some(off) => off,
63 None => return Ok(Vec::new()),
64 };
65
66 let head_vaddr = init_task_addr + tasks_offset;
68 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
69
70 let mut tasks: Vec<(u32, u32, String, u64)> = Vec::new();
72
73 let collect_task = |addr: u64| -> Option<(u32, u32, String, u64)> {
75 let pid: u32 = reader.read_field(addr, "task_struct", "pid").ok()?;
76 let tgid: u32 = reader
77 .read_field(addr, "task_struct", "tgid")
78 .unwrap_or(pid);
79 let name = reader
80 .read_field_string(addr, "task_struct", "comm", 16)
81 .unwrap_or_else(|_| "<unknown>".to_string());
82 let cred_ptr: u64 = reader.read_field(addr, "task_struct", "cred").ok()?;
83 Some((pid, tgid, name, cred_ptr))
84 };
85
86 if let Some(info) = collect_task(init_task_addr) {
88 tasks.push(info);
89 }
90 for &task_addr in &task_addrs {
91 if let Some(info) = collect_task(task_addr) {
92 tasks.push(info);
93 }
94 }
95
96 let mut cred_map: HashMap<u64, Vec<(u32, u32, String)>> = HashMap::new();
98 for (pid, tgid, name, cred_addr) in &tasks {
99 if *cred_addr == 0 {
101 continue;
102 }
103 cred_map
104 .entry(*cred_addr)
105 .or_default()
106 .push((*pid, *tgid, name.clone()));
107 }
108
109 let mut results = Vec::new();
111
112 for (cred_addr, group) in &cred_map {
113 if group.len() < 2 {
114 continue;
115 }
116
117 let mut by_tgid: HashMap<u32, Vec<u32>> = HashMap::new();
121 for (pid, tgid, _) in group {
122 by_tgid.entry(*tgid).or_default().push(*pid);
123 }
124
125 if by_tgid.len() < 2 {
128 continue;
129 }
130
131 let uid: u32 = reader
133 .read_field(*cred_addr, "cred", "uid")
134 .unwrap_or(u32::MAX);
135
136 for (pid, _tgid, name) in group {
138 let shared_with: Vec<u32> = group
139 .iter()
140 .filter(|(other_pid, _, _)| other_pid != pid)
141 .map(|(other_pid, _, _)| *other_pid)
142 .collect();
143
144 let is_suspicious = classify_shared_creds(*pid, &shared_with, uid);
145
146 if is_suspicious {
147 results.push(SharedCredInfo {
148 pid: *pid,
149 process_name: name.clone(),
150 uid,
151 cred_address: *cred_addr,
152 shared_with_pids: shared_with,
153 is_suspicious,
154 });
155 }
156 }
157 }
158
159 Ok(results)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use memf_core::object_reader::ObjectReader;
166 use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
167 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
168 use memf_symbols::isf::IsfResolver;
169 use memf_symbols::test_builders::IsfBuilder;
170
171 #[test]
176 fn shared_with_init_suspicious() {
177 assert!(classify_shared_creds(500, &[1], 1000));
180 }
181
182 #[test]
183 fn unrelated_sharing_suspicious() {
184 assert!(classify_shared_creds(200, &[300], 1000));
186 }
187
188 #[test]
189 fn thread_sharing_benign() {
190 assert!(!classify_shared_creds(2, &[1], 0));
193 }
194
195 #[test]
196 fn kernel_thread_benign() {
197 assert!(!classify_shared_creds(2, &[1], 0));
199 }
200
201 #[test]
202 fn no_sharing_benign() {
203 assert!(!classify_shared_creds(100, &[], 1000));
205 }
206
207 #[test]
212 fn walk_check_creds_no_symbol_returns_empty() {
213 let isf = IsfBuilder::new()
215 .add_struct("task_struct", 128)
216 .add_field("task_struct", "pid", 0, "int")
217 .add_field("task_struct", "tasks", 16, "list_head")
218 .add_field("task_struct", "comm", 32, "char")
219 .add_field("task_struct", "cred", 96, "pointer")
220 .add_field("task_struct", "real_cred", 104, "pointer")
221 .add_field("task_struct", "tgid", 112, "int")
222 .add_struct("list_head", 16)
223 .add_field("list_head", "next", 0, "pointer")
224 .add_field("list_head", "prev", 8, "pointer")
225 .add_struct("cred", 64)
226 .add_field("cred", "uid", 4, "unsigned int")
227 .build_json();
229
230 let resolver = IsfResolver::from_value(&isf).unwrap();
231 let (cr3, mem) = PageTableBuilder::new().build();
232 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
233 let reader = ObjectReader::new(vas, Box::new(resolver));
234
235 let result = walk_check_creds(&reader);
236 assert!(result.is_ok());
238 assert!(result.unwrap().is_empty());
239 }
240
241 #[test]
246 fn is_likely_kernel_thread_pid_0_benign() {
247 assert!(!classify_shared_creds(0, &[2], 0));
249 }
250
251 #[test]
252 fn is_likely_kernel_thread_pid_1_shares_with_pid_2_suspicious() {
253 assert!(!classify_shared_creds(1, &[2], 0));
257 }
258
259 #[test]
260 fn is_likely_kernel_thread_pid_3_uid_0_suspicious_when_sharing_non_init() {
261 assert!(classify_shared_creds(3, &[100], 0));
264 }
265
266 #[test]
267 fn classify_sharing_with_pid_1_uid_0_kernel_thread_benign() {
268 assert!(!classify_shared_creds(2, &[1], 0));
270 }
271
272 #[test]
273 fn classify_sharing_with_pid_1_uid_0_non_kernel_thread_suspicious() {
274 assert!(classify_shared_creds(100, &[1], 0));
277 }
278
279 #[test]
280 fn classify_uid_0_kernel_thread_no_sharing_benign() {
281 assert!(!classify_shared_creds(2, &[], 0));
283 }
284
285 #[test]
286 fn classify_uid_0_non_kernel_thread_sharing_suspicious() {
287 assert!(classify_shared_creds(50, &[60], 0));
290 }
291
292 #[test]
293 fn classify_is_pid_1_self_not_suspicious() {
294 assert!(!classify_shared_creds(1, &[500], 0));
297 }
298
299 #[test]
304 fn shared_cred_info_clone_debug_serialize() {
305 let info = SharedCredInfo {
306 pid: 42,
307 process_name: "evil".to_string(),
308 uid: 0,
309 cred_address: 0xDEAD_BEEF,
310 shared_with_pids: vec![1],
311 is_suspicious: true,
312 };
313 let cloned = info.clone();
314 assert_eq!(cloned.pid, 42);
315 let dbg = format!("{cloned:?}");
316 assert!(dbg.contains("evil"));
317 let json = serde_json::to_string(&cloned).unwrap();
318 assert!(json.contains("\"pid\":42"));
319 assert!(json.contains("\"is_suspicious\":true"));
320 }
321
322 #[test]
327 fn walk_check_creds_symbol_present_single_task_no_sharing() {
328 let sym_vaddr: u64 = 0xFFFF_8800_0090_0000;
331 let sym_paddr: u64 = 0x00A0_0000;
332 let tasks_offset = 16u64;
333
334 let mut page = [0u8; 4096];
335 page[0..4].copy_from_slice(&1u32.to_le_bytes());
337 page[4..8].copy_from_slice(&1u32.to_le_bytes());
339 let list_self = sym_vaddr + tasks_offset;
341 page[tasks_offset as usize..tasks_offset as usize + 8]
342 .copy_from_slice(&list_self.to_le_bytes());
343 page[tasks_offset as usize + 8..tasks_offset as usize + 16]
344 .copy_from_slice(&list_self.to_le_bytes());
345 page[32..39].copy_from_slice(b"systemd");
347 let cred_ptr: u64 = 0xFFFF_8800_DEAD_0000;
349 page[96..104].copy_from_slice(&cred_ptr.to_le_bytes());
350
351 let isf = IsfBuilder::new()
352 .add_struct("task_struct", 256)
353 .add_field("task_struct", "pid", 0, "unsigned int")
354 .add_field("task_struct", "tgid", 4, "unsigned int")
355 .add_field("task_struct", "tasks", 16, "pointer")
356 .add_field("task_struct", "comm", 32, "char")
357 .add_field("task_struct", "cred", 96, "pointer")
358 .add_struct("cred", 64)
359 .add_field("cred", "uid", 4, "unsigned int")
360 .add_symbol("init_task", sym_vaddr)
361 .build_json();
362
363 let resolver = IsfResolver::from_value(&isf).unwrap();
364 let (cr3, mem) = PageTableBuilder::new()
365 .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
366 .write_phys(sym_paddr, &page)
367 .build();
368 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
369 let reader = ObjectReader::new(vas, Box::new(resolver));
370
371 let result = walk_check_creds(&reader).unwrap_or_default();
372 assert!(
373 result.is_empty(),
374 "single task with unique cred should not be flagged"
375 );
376 }
377
378 #[test]
385 fn walk_check_creds_with_list_head_single_task_no_sharing() {
386 let sym_vaddr: u64 = 0xFFFF_8800_0010_0000;
387 let sym_paddr: u64 = 0x0010_0000; let tasks_offset: u64 = 16;
389
390 let mut page = [0u8; 4096];
391 page[0..4].copy_from_slice(&42u32.to_le_bytes());
393 page[4..8].copy_from_slice(&42u32.to_le_bytes());
395 let self_ptr = sym_vaddr + tasks_offset;
397 page[tasks_offset as usize..tasks_offset as usize + 8]
398 .copy_from_slice(&self_ptr.to_le_bytes());
399 page[tasks_offset as usize + 8..tasks_offset as usize + 16]
400 .copy_from_slice(&self_ptr.to_le_bytes());
401 page[32..36].copy_from_slice(b"init");
403 let cred_ptr: u64 = 0xFFFF_8800_CAFE_0000;
405 page[96..104].copy_from_slice(&cred_ptr.to_le_bytes());
406
407 let isf = IsfBuilder::new()
408 .add_struct("list_head", 0x10)
409 .add_field("list_head", "next", 0x00, "pointer")
410 .add_field("list_head", "prev", 0x08, "pointer")
411 .add_struct("task_struct", 256)
412 .add_field("task_struct", "pid", 0, "unsigned int")
413 .add_field("task_struct", "tgid", 4, "unsigned int")
414 .add_field("task_struct", "tasks", 16, "pointer")
415 .add_field("task_struct", "comm", 32, "char")
416 .add_field("task_struct", "cred", 96, "pointer")
417 .add_struct("cred", 64)
418 .add_field("cred", "uid", 4, "unsigned int")
419 .add_symbol("init_task", sym_vaddr)
420 .build_json();
421
422 let resolver = IsfResolver::from_value(&isf).unwrap();
423 let (cr3, mem) = PageTableBuilder::new()
424 .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
425 .write_phys(sym_paddr, &page)
426 .build();
427 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
428 let reader = ObjectReader::new(vas, Box::new(resolver));
429
430 let result = walk_check_creds(&reader).unwrap();
431 assert!(
433 result.is_empty(),
434 "single task cannot share creds with another"
435 );
436 }
437
438 #[test]
445 fn walk_check_creds_two_tasks_share_cred_different_tgids_flagged() {
446 let tasks_offset: u64 = 0x10;
456 let pid_offset: u64 = 0x00;
457 let tgid_offset: u64 = 0x04;
458 let comm_offset: u64 = 0x20;
459 let cred_offset: u64 = 0x60;
460 let uid_cred_off: u64 = 0x04;
461
462 let init_vaddr: u64 = 0xFFFF_8800_0090_0000;
463 let init_paddr: u64 = 0x0090_0000;
464 let t2_vaddr: u64 = 0xFFFF_8800_0091_0000;
465 let t2_paddr: u64 = 0x0091_0000;
466 let cred_vaddr: u64 = 0xFFFF_8800_0092_0000;
467 let cred_paddr: u64 = 0x0092_0000;
468
469 let mut init_page = [0u8; 4096];
471 init_page[pid_offset as usize..pid_offset as usize + 4]
472 .copy_from_slice(&100u32.to_le_bytes());
473 init_page[tgid_offset as usize..tgid_offset as usize + 4]
474 .copy_from_slice(&100u32.to_le_bytes());
475 let t2_list_node = t2_vaddr + tasks_offset;
476 init_page[tasks_offset as usize..tasks_offset as usize + 8]
477 .copy_from_slice(&t2_list_node.to_le_bytes());
478 init_page[tasks_offset as usize + 8..tasks_offset as usize + 16]
479 .copy_from_slice(&t2_list_node.to_le_bytes()); init_page[comm_offset as usize..comm_offset as usize + 5].copy_from_slice(b"evil1");
481 init_page[cred_offset as usize..cred_offset as usize + 8]
482 .copy_from_slice(&cred_vaddr.to_le_bytes());
483
484 let mut t2_page = [0u8; 4096];
486 t2_page[pid_offset as usize..pid_offset as usize + 4]
487 .copy_from_slice(&200u32.to_le_bytes());
488 t2_page[tgid_offset as usize..tgid_offset as usize + 4]
489 .copy_from_slice(&200u32.to_le_bytes()); let init_list_node = init_vaddr + tasks_offset;
491 t2_page[tasks_offset as usize..tasks_offset as usize + 8]
492 .copy_from_slice(&init_list_node.to_le_bytes()); t2_page[tasks_offset as usize + 8..tasks_offset as usize + 16]
494 .copy_from_slice(&init_list_node.to_le_bytes());
495 t2_page[comm_offset as usize..comm_offset as usize + 5].copy_from_slice(b"evil2");
496 t2_page[cred_offset as usize..cred_offset as usize + 8]
497 .copy_from_slice(&cred_vaddr.to_le_bytes()); let mut cred_page = [0u8; 4096];
501 cred_page[uid_cred_off as usize..uid_cred_off as usize + 4]
502 .copy_from_slice(&1000u32.to_le_bytes());
503
504 let isf = IsfBuilder::new()
505 .add_symbol("init_task", init_vaddr)
506 .add_struct("list_head", 0x10)
507 .add_field("list_head", "next", 0x00u64, "pointer")
508 .add_field("list_head", "prev", 0x08u64, "pointer")
509 .add_struct("task_struct", 0x200)
510 .add_field("task_struct", "pid", pid_offset, "unsigned int")
511 .add_field("task_struct", "tgid", tgid_offset, "unsigned int")
512 .add_field("task_struct", "tasks", tasks_offset, "pointer")
513 .add_field("task_struct", "comm", comm_offset, "char")
514 .add_field("task_struct", "cred", cred_offset, "pointer")
515 .add_struct("cred", 0x80)
516 .add_field("cred", "uid", uid_cred_off, "unsigned int")
517 .build_json();
518
519 let resolver = IsfResolver::from_value(&isf).unwrap();
520 let (cr3, mem) = PageTableBuilder::new()
521 .map_4k(init_vaddr, init_paddr, flags::WRITABLE)
522 .write_phys(init_paddr, &init_page)
523 .map_4k(t2_vaddr, t2_paddr, flags::WRITABLE)
524 .write_phys(t2_paddr, &t2_page)
525 .map_4k(cred_vaddr, cred_paddr, flags::WRITABLE)
526 .write_phys(cred_paddr, &cred_page)
527 .build();
528
529 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
530 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
531
532 let result = walk_check_creds(&reader).unwrap();
533 assert!(
535 !result.is_empty(),
536 "cross-tgid cred sharing should produce suspicious entries"
537 );
538 assert_eq!(result.len(), 2, "both tasks should be flagged");
540 for entry in &result {
541 assert!(entry.is_suspicious);
542 assert_eq!(entry.cred_address, cred_vaddr);
543 assert_eq!(entry.uid, 1000);
544 }
545 }
546
547 #[test]
552 fn walk_check_creds_missing_tasks_field_returns_empty() {
553 let isf = IsfBuilder::new()
555 .add_struct("task_struct", 128)
556 .add_field("task_struct", "pid", 0, "int")
557 .add_symbol("init_task", 0xFFFF_8000_0010_0000)
559 .build_json();
560
561 let resolver = IsfResolver::from_value(&isf).unwrap();
562 let (cr3, mem) = PageTableBuilder::new().build();
563 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
564 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
565
566 let result = walk_check_creds(&reader);
567 assert!(result.is_ok());
568 assert!(result.unwrap().is_empty());
569 }
570}