1use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::Result;
11
12#[derive(Debug, Clone)]
14pub struct PamHookInfo {
15 pub pid: u32,
17 pub comm: String,
19 pub library_path: String,
21 pub is_system_path: bool,
23 pub is_suspicious: bool,
25}
26
27const SYSTEM_LIB_PREFIXES: &[&str] =
29 &["/lib", "/usr/lib", "/usr/lib64", "/lib64", "/usr/local/lib"];
30
31pub use crate::heuristics::classify_pam_hook;
36
37pub fn walk_pam_hooks<P: PhysicalMemoryProvider>(
42 reader: &ObjectReader<P>,
43) -> Result<Vec<PamHookInfo>> {
44 let init_task_addr = match reader.symbols().symbol_address("init_task") {
45 Some(a) => a,
46 None => return Ok(vec![]),
47 };
48
49 let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
50 Some(o) => o,
51 None => return Ok(vec![]),
52 };
53
54 let head_vaddr = init_task_addr + tasks_offset;
55 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
56
57 let mut findings = Vec::new();
58 scan_process_pam(reader, init_task_addr, &mut findings);
59 for &task_addr in &task_addrs {
60 scan_process_pam(reader, task_addr, &mut findings);
61 }
62
63 Ok(findings)
64}
65
66fn scan_process_pam<P: PhysicalMemoryProvider>(
68 reader: &ObjectReader<P>,
69 task_addr: u64,
70 out: &mut Vec<PamHookInfo>,
71) {
72 let mm_ptr: u64 = match reader.read_field(task_addr, "task_struct", "mm") {
73 Ok(v) => v,
74 Err(_) => return,
75 };
76 if mm_ptr == 0 {
77 return; }
79
80 let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
81 Ok(v) => v,
82 Err(_) => return,
83 };
84 let comm = reader
85 .read_field_string(task_addr, "task_struct", "comm", 16)
86 .unwrap_or_default();
87
88 let mmap_ptr: u64 = match reader.read_field(mm_ptr, "mm_struct", "mmap") {
89 Ok(v) => v,
90 Err(_) => return,
91 };
92
93 let mut vma_addr = mmap_ptr;
94 while vma_addr != 0 {
95 let vm_file: u64 = if let Ok(v) = reader.read_field(vma_addr, "vm_area_struct", "vm_file") {
96 v
97 } else {
98 vma_addr = reader
99 .read_field(vma_addr, "vm_area_struct", "vm_next")
100 .unwrap_or(0);
101 continue;
102 };
103
104 if vm_file != 0 {
105 if let Some(library_path) = read_dentry_name(reader, vm_file) {
107 if library_path.to_lowercase().contains("pam") {
108 let is_system_path = SYSTEM_LIB_PREFIXES
109 .iter()
110 .any(|prefix| library_path.starts_with(prefix));
111 let is_suspicious = classify_pam_hook(&library_path);
112 out.push(PamHookInfo {
113 pid,
114 comm: comm.clone(),
115 library_path,
116 is_system_path,
117 is_suspicious,
118 });
119 }
120 }
121 }
122
123 vma_addr = reader
124 .read_field(vma_addr, "vm_area_struct", "vm_next")
125 .unwrap_or(0);
126 }
127}
128
129fn read_dentry_name<P: PhysicalMemoryProvider>(
133 reader: &ObjectReader<P>,
134 file_ptr: u64,
135) -> Option<String> {
136 let f_path_dentry: u64 = reader.read_field(file_ptr, "file", "f_path").ok()?;
138 if f_path_dentry == 0 {
139 return None;
140 }
141 let name_ptr: u64 = reader.read_field(f_path_dentry, "dentry", "d_name").ok()?;
143 if name_ptr == 0 {
144 return None;
145 }
146 let bytes = reader.read_bytes(name_ptr, 256).ok()?;
148 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
149 String::from_utf8(bytes[..end].to_vec()).ok()
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
156 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
157 use memf_symbols::isf::IsfResolver;
158 use memf_symbols::test_builders::IsfBuilder;
159
160 #[test]
165 fn classify_pam_hook_tmp_path_suspicious() {
166 assert!(classify_pam_hook("/tmp/libpam_evil.so"));
167 }
168
169 #[test]
170 fn classify_pam_hook_home_path_suspicious() {
171 assert!(classify_pam_hook(
172 "/home/attacker/.local/libpam_backdoor.so"
173 ));
174 }
175
176 #[test]
177 fn classify_pam_hook_system_lib_not_suspicious() {
178 assert!(!classify_pam_hook("/lib/x86_64-linux-gnu/libpam.so.0"));
179 assert!(!classify_pam_hook("/usr/lib/libpam.so.0"));
180 assert!(!classify_pam_hook("/usr/lib64/libpam.so.0"));
181 assert!(!classify_pam_hook("/lib64/libpam.so.0"));
182 assert!(!classify_pam_hook("/usr/local/lib/libpam.so.0"));
183 }
184
185 #[test]
186 fn classify_pam_hook_empty_path_not_suspicious() {
187 assert!(!classify_pam_hook(""));
188 }
189
190 #[test]
191 fn classify_pam_hook_devshm_suspicious() {
192 assert!(classify_pam_hook("/dev/shm/libpam_hook.so"));
193 }
194
195 fn make_minimal_reader_no_init_task() -> ObjectReader<SyntheticPhysMem> {
200 let isf = IsfBuilder::new()
201 .add_struct("task_struct", 64)
202 .add_field("task_struct", "pid", 0, "int")
203 .add_field("task_struct", "tasks", 8, "list_head")
204 .add_struct("list_head", 16)
205 .add_field("list_head", "next", 0, "pointer")
206 .add_field("list_head", "prev", 8, "pointer")
207 .build_json();
209
210 let resolver = IsfResolver::from_value(&isf).unwrap();
211 let (cr3, mem) = PageTableBuilder::new().build();
212 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
213 ObjectReader::new(vas, Box::new(resolver))
214 }
215
216 #[test]
217 fn walk_pam_hooks_missing_init_task_returns_empty() {
218 let reader = make_minimal_reader_no_init_task();
219 let result = walk_pam_hooks(&reader).unwrap();
220 assert!(result.is_empty());
221 }
222
223 fn make_kernel_thread_reader() -> ObjectReader<SyntheticPhysMem> {
228 let vaddr: u64 = 0xFFFF_8000_0010_0000;
229 let paddr: u64 = 0x0080_0000;
230 let mut data = vec![0u8; 4096];
231
232 data[0..4].copy_from_slice(&0u32.to_le_bytes());
234 let tasks_addr = vaddr + 16;
235 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
236 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
237 data[32..41].copy_from_slice(b"swapper/0");
238 data[48..56].copy_from_slice(&0u64.to_le_bytes()); let isf = IsfBuilder::new()
241 .add_struct("task_struct", 128)
242 .add_field("task_struct", "pid", 0, "int")
243 .add_field("task_struct", "tasks", 16, "list_head")
244 .add_field("task_struct", "comm", 32, "char")
245 .add_field("task_struct", "mm", 48, "pointer")
246 .add_struct("list_head", 16)
247 .add_field("list_head", "next", 0, "pointer")
248 .add_field("list_head", "prev", 8, "pointer")
249 .add_struct("mm_struct", 64)
250 .add_field("mm_struct", "mmap", 8, "pointer")
251 .add_struct("vm_area_struct", 64)
252 .add_field("vm_area_struct", "vm_next", 16, "pointer")
253 .add_field("vm_area_struct", "vm_file", 40, "pointer")
254 .add_symbol("init_task", vaddr)
255 .build_json();
256
257 let resolver = IsfResolver::from_value(&isf).unwrap();
258 let (cr3, mem) = PageTableBuilder::new()
259 .map_4k(vaddr, paddr, ptflags::WRITABLE)
260 .write_phys(paddr, &data)
261 .build();
262 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
263 ObjectReader::new(vas, Box::new(resolver))
264 }
265
266 #[test]
267 fn walk_pam_hooks_kernel_thread_returns_empty() {
268 let reader = make_kernel_thread_reader();
269 let result = walk_pam_hooks(&reader).unwrap();
270 assert!(result.is_empty());
271 }
272
273 #[test]
278 fn classify_pam_hook_no_pam_in_path_not_suspicious() {
279 assert!(!classify_pam_hook("/tmp/libssl.so"));
281 assert!(!classify_pam_hook("/home/user/.local/libfoo.so"));
282 }
283
284 #[test]
285 fn classify_pam_hook_uppercase_pam_suspicious() {
286 assert!(classify_pam_hook("/tmp/libPAM_evil.so"));
288 }
289
290 #[test]
291 fn classify_pam_hook_mixed_case_pam_suspicious() {
292 assert!(classify_pam_hook("/opt/libPam.so"));
293 }
294
295 #[test]
296 fn classify_pam_hook_system_lib64_not_suspicious() {
297 assert!(!classify_pam_hook("/usr/lib64/security/libpam_unix.so"));
299 }
300
301 #[test]
306 fn walk_pam_hooks_symbol_present_empty_list() {
307 let sym_vaddr: u64 = 0xFFFF_8800_0040_0000;
310 let sym_paddr: u64 = 0x0050_0000;
311 let tasks_offset = 16u64;
312
313 let mut page = [0u8; 4096];
314 page[0..4].copy_from_slice(&0u32.to_le_bytes());
316 let list_self = sym_vaddr + tasks_offset;
318 page[tasks_offset as usize..tasks_offset as usize + 8]
319 .copy_from_slice(&list_self.to_le_bytes());
320 page[tasks_offset as usize + 8..tasks_offset as usize + 16]
321 .copy_from_slice(&list_self.to_le_bytes());
322 page[32..39].copy_from_slice(b"swapper");
324 page[48..56].copy_from_slice(&0u64.to_le_bytes());
326
327 let isf = IsfBuilder::new()
328 .add_struct("task_struct", 128)
329 .add_field("task_struct", "pid", 0, "unsigned int")
330 .add_field("task_struct", "tasks", 16, "pointer")
331 .add_field("task_struct", "comm", 32, "char")
332 .add_field("task_struct", "mm", 48, "pointer")
333 .add_symbol("init_task", sym_vaddr)
334 .build_json();
335
336 let resolver = IsfResolver::from_value(&isf).unwrap();
337 let (cr3, mem) = PageTableBuilder::new()
338 .map_4k(sym_vaddr, sym_paddr, ptflags::WRITABLE)
339 .write_phys(sym_paddr, &page)
340 .build();
341 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
342 let reader = ObjectReader::new(vas, Box::new(resolver));
343
344 let result = walk_pam_hooks(&reader).unwrap_or_default();
345 assert!(
346 result.is_empty(),
347 "kernel thread with mm==NULL should produce no PAM findings"
348 );
349 }
350
351 #[test]
352 fn walk_pam_hooks_missing_tasks_field_returns_empty() {
353 let isf = IsfBuilder::new()
356 .add_struct("task_struct", 64)
357 .add_field("task_struct", "pid", 0, "int")
358 .add_struct("list_head", 16)
360 .add_field("list_head", "next", 0, "pointer")
361 .add_field("list_head", "prev", 8, "pointer")
362 .add_symbol("init_task", 0xFFFF_8000_0010_0000)
363 .build_json();
364
365 let resolver = IsfResolver::from_value(&isf).unwrap();
366 let (cr3, mem) = PageTableBuilder::new().build();
367 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
368 let reader = ObjectReader::new(vas, Box::new(resolver));
369
370 let result = walk_pam_hooks(&reader).unwrap();
372 assert!(result.is_empty());
373 }
374
375 #[test]
388 fn walk_pam_hooks_detects_suspicious_pam_lib() {
389 use memf_core::object_reader::ObjectReader;
390
391 let task_vaddr: u64 = 0xFFFF_D800_0200_0000;
392 let mm_vaddr: u64 = 0xFFFF_D800_0201_0000;
393 let vma_vaddr: u64 = 0xFFFF_D800_0202_0000;
394 let file_vaddr: u64 = 0xFFFF_D800_0203_0000;
395 let dentry_vaddr: u64 = 0xFFFF_D800_0204_0000;
396 let name_vaddr: u64 = 0xFFFF_D800_0205_0000;
397
398 let task_paddr: u64 = 0x030_000;
399 let mm_paddr: u64 = 0x031_000;
400 let vma_paddr: u64 = 0x032_000;
401 let file_paddr: u64 = 0x033_000;
402 let dentry_paddr: u64 = 0x034_000;
403 let name_paddr: u64 = 0x035_000;
404
405 let tasks_offset: u64 = 8;
407 let task_comm_offset: u64 = 24;
408 let task_mm_offset: u64 = 40;
409 let task_pid_offset: u64 = 0;
410
411 let mm_mmap_offset: u64 = 0;
413 let vma_vm_next_offset: u64 = 0;
415 let vma_vm_file_offset: u64 = 16;
416 let file_fpath_offset: u64 = 0;
420 let dentry_dname_offset: u64 = 0;
421
422 let mut task_page = [0u8; 4096];
424 task_page[task_pid_offset as usize..task_pid_offset as usize + 4]
425 .copy_from_slice(&5000u32.to_le_bytes());
426 let list_self = task_vaddr + tasks_offset;
427 task_page[tasks_offset as usize..tasks_offset as usize + 8]
428 .copy_from_slice(&list_self.to_le_bytes());
429 task_page[tasks_offset as usize + 8..tasks_offset as usize + 16]
430 .copy_from_slice(&list_self.to_le_bytes());
431 task_page[task_comm_offset as usize..task_comm_offset as usize + 4]
432 .copy_from_slice(b"sshd");
433 task_page[task_mm_offset as usize..task_mm_offset as usize + 8]
434 .copy_from_slice(&mm_vaddr.to_le_bytes());
435
436 let mut mm_page = [0u8; 4096];
438 mm_page[mm_mmap_offset as usize..mm_mmap_offset as usize + 8]
439 .copy_from_slice(&vma_vaddr.to_le_bytes());
440
441 let mut vma_page = [0u8; 4096];
443 vma_page[vma_vm_next_offset as usize..vma_vm_next_offset as usize + 8]
444 .copy_from_slice(&0u64.to_le_bytes()); vma_page[vma_vm_file_offset as usize..vma_vm_file_offset as usize + 8]
446 .copy_from_slice(&file_vaddr.to_le_bytes());
447
448 let mut file_page = [0u8; 4096];
453 file_page[file_fpath_offset as usize..file_fpath_offset as usize + 8]
454 .copy_from_slice(&dentry_vaddr.to_le_bytes());
455
456 let mut dentry_page = [0u8; 4096];
458 dentry_page[dentry_dname_offset as usize..dentry_dname_offset as usize + 8]
459 .copy_from_slice(&name_vaddr.to_le_bytes());
460
461 let libname = b"/tmp/libpam_rootkit.so\0";
463 let mut name_page = [0u8; 4096];
464 name_page[..libname.len()].copy_from_slice(libname);
465
466 let isf = IsfBuilder::new()
467 .add_struct("task_struct", 256)
468 .add_field("task_struct", "pid", task_pid_offset, "unsigned int")
469 .add_field("task_struct", "tasks", tasks_offset, "list_head")
470 .add_field("task_struct", "comm", task_comm_offset, "char")
471 .add_field("task_struct", "mm", task_mm_offset, "pointer")
472 .add_struct("list_head", 16)
473 .add_field("list_head", "next", 0u64, "pointer")
474 .add_field("list_head", "prev", 8u64, "pointer")
475 .add_struct("mm_struct", 256)
476 .add_field("mm_struct", "mmap", mm_mmap_offset, "pointer")
477 .add_struct("vm_area_struct", 256)
478 .add_field("vm_area_struct", "vm_next", vma_vm_next_offset, "pointer")
479 .add_field("vm_area_struct", "vm_file", vma_vm_file_offset, "pointer")
480 .add_struct("file", 256)
481 .add_field("file", "f_path", file_fpath_offset, "pointer")
482 .add_struct("dentry", 256)
483 .add_field("dentry", "d_name", dentry_dname_offset, "pointer")
484 .add_symbol("init_task", task_vaddr)
485 .build_json();
486
487 let resolver = IsfResolver::from_value(&isf).unwrap();
488 let (cr3, mem) = PageTableBuilder::new()
489 .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
490 .write_phys(task_paddr, &task_page)
491 .map_4k(mm_vaddr, mm_paddr, ptflags::WRITABLE)
492 .write_phys(mm_paddr, &mm_page)
493 .map_4k(vma_vaddr, vma_paddr, ptflags::WRITABLE)
494 .write_phys(vma_paddr, &vma_page)
495 .map_4k(file_vaddr, file_paddr, ptflags::WRITABLE)
496 .write_phys(file_paddr, &file_page)
497 .map_4k(dentry_vaddr, dentry_paddr, ptflags::WRITABLE)
498 .write_phys(dentry_paddr, &dentry_page)
499 .map_4k(name_vaddr, name_paddr, ptflags::WRITABLE)
500 .write_phys(name_paddr, &name_page)
501 .build();
502
503 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
504 let reader: ObjectReader<memf_core::test_builders::SyntheticPhysMem> =
505 ObjectReader::new(vas, Box::new(resolver));
506
507 let result = walk_pam_hooks(&reader).expect("walk should not error");
508 assert_eq!(
509 result.len(),
510 1,
511 "should detect exactly one suspicious PAM entry"
512 );
513 let entry = &result[0];
514 assert_eq!(entry.pid, 5000);
515 assert!(
516 entry.is_suspicious,
517 "non-system PAM path must be suspicious"
518 );
519 assert!(
520 !entry.is_system_path,
521 "path must not be considered a system path"
522 );
523 assert!(
524 entry.library_path.contains("pam"),
525 "library_path should contain 'pam'"
526 );
527 }
528
529 #[test]
533 fn walk_pam_hooks_null_vm_file_skipped() {
534 use memf_core::object_reader::ObjectReader;
535
536 let task_vaddr: u64 = 0xFFFF_D900_0200_0000;
537 let mm_vaddr: u64 = 0xFFFF_D900_0201_0000;
538 let vma_vaddr: u64 = 0xFFFF_D900_0202_0000;
539
540 let task_paddr: u64 = 0x036_000;
541 let mm_paddr: u64 = 0x037_000;
542 let vma_paddr: u64 = 0x038_000;
543
544 let tasks_offset: u64 = 8;
545 let task_mm_offset: u64 = 40;
546
547 let mut task_page = [0u8; 4096];
548 task_page[0..4].copy_from_slice(&6000u32.to_le_bytes());
549 let list_self = task_vaddr + tasks_offset;
550 task_page[tasks_offset as usize..tasks_offset as usize + 8]
551 .copy_from_slice(&list_self.to_le_bytes());
552 task_page[tasks_offset as usize + 8..tasks_offset as usize + 16]
553 .copy_from_slice(&list_self.to_le_bytes());
554 task_page[task_mm_offset as usize..task_mm_offset as usize + 8]
555 .copy_from_slice(&mm_vaddr.to_le_bytes());
556
557 let mut mm_page = [0u8; 4096];
558 mm_page[0..8].copy_from_slice(&vma_vaddr.to_le_bytes());
559
560 let mut vma_page = [0u8; 4096];
562 vma_page[0..8].copy_from_slice(&0u64.to_le_bytes()); vma_page[16..24].copy_from_slice(&0u64.to_le_bytes()); let isf = IsfBuilder::new()
566 .add_struct("task_struct", 256)
567 .add_field("task_struct", "pid", 0u64, "unsigned int")
568 .add_field("task_struct", "tasks", tasks_offset, "list_head")
569 .add_field("task_struct", "comm", 24u64, "char")
570 .add_field("task_struct", "mm", task_mm_offset, "pointer")
571 .add_struct("list_head", 16)
572 .add_field("list_head", "next", 0u64, "pointer")
573 .add_field("list_head", "prev", 8u64, "pointer")
574 .add_struct("mm_struct", 256)
575 .add_field("mm_struct", "mmap", 0u64, "pointer")
576 .add_struct("vm_area_struct", 256)
577 .add_field("vm_area_struct", "vm_next", 0u64, "pointer")
578 .add_field("vm_area_struct", "vm_file", 16u64, "pointer")
579 .add_struct("file", 256)
580 .add_field("file", "f_path", 0u64, "pointer")
581 .add_struct("dentry", 256)
582 .add_field("dentry", "d_name", 0u64, "pointer")
583 .add_symbol("init_task", task_vaddr)
584 .build_json();
585
586 let resolver = IsfResolver::from_value(&isf).unwrap();
587 let (cr3, mem) = PageTableBuilder::new()
588 .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
589 .write_phys(task_paddr, &task_page)
590 .map_4k(mm_vaddr, mm_paddr, ptflags::WRITABLE)
591 .write_phys(mm_paddr, &mm_page)
592 .map_4k(vma_vaddr, vma_paddr, ptflags::WRITABLE)
593 .write_phys(vma_paddr, &vma_page)
594 .build();
595
596 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
597 let reader: ObjectReader<memf_core::test_builders::SyntheticPhysMem> =
598 ObjectReader::new(vas, Box::new(resolver));
599
600 let result = walk_pam_hooks(&reader).expect("walk should not error");
601 assert!(
602 result.is_empty(),
603 "anonymous VMA (vm_file==0) should produce no PAM findings"
604 );
605 }
606}