1use std::collections::{HashMap, HashSet};
12
13use memf_core::object_reader::ObjectReader;
14use memf_format::PhysicalMemoryProvider;
15
16use crate::{vma_walker::for_each_task_vma, Error, Result};
17
18const MAX_LIBS: usize = 4096;
20
21#[derive(Debug, Clone, serde::Serialize)]
23pub struct SharedLibraryInfo {
24 pub pid: u32,
26 pub process_name: String,
28 pub lib_path: String,
30 pub base_addr: u64,
32 pub size: u64,
34 pub is_suspicious: bool,
36}
37
38pub use crate::heuristics::classify_library;
46
47pub fn walk_library_list<P: PhysicalMemoryProvider>(
56 reader: &ObjectReader<P>,
57 task_addr: u64,
58 pid: u32,
59 process_name: &str,
60) -> Result<Vec<SharedLibraryInfo>> {
61 let f_path_offset = reader
63 .symbols()
64 .field_offset("file", "f_path")
65 .ok_or_else(|| Error::MissingField {
66 struct_name: "file".into(),
67 field_name: "f_path".into(),
68 })?;
69 let dentry_in_path_offset =
70 reader
71 .symbols()
72 .field_offset("path", "dentry")
73 .ok_or_else(|| Error::MissingField {
74 struct_name: "path".into(),
75 field_name: "dentry".into(),
76 })?;
77 let d_name_offset = reader
78 .symbols()
79 .field_offset("dentry", "d_name")
80 .ok_or_else(|| Error::MissingField {
81 struct_name: "dentry".into(),
82 field_name: "d_name".into(),
83 })?;
84 let name_in_qstr_offset = reader
85 .symbols()
86 .field_offset("qstr", "name")
87 .ok_or_else(|| Error::MissingField {
88 struct_name: "qstr".into(),
89 field_name: "name".into(),
90 })?;
91
92 let mut lib_map: HashMap<String, (u64, u64)> = HashMap::new();
94 let mut seen_addrs: HashSet<u64> = HashSet::new();
96 let mut limit_reached = false;
98
99 for_each_task_vma(reader, task_addr, &mut |e| {
100 if !seen_addrs.insert(e.vma_addr) {
102 limit_reached = true;
103 return;
104 }
105 if limit_reached || lib_map.len() >= MAX_LIBS {
106 limit_reached = true;
107 return;
108 }
109
110 if e.file_ptr != 0 {
111 if let Some(name) = read_vma_file_path(
113 reader,
114 e.file_ptr,
115 f_path_offset,
116 dentry_in_path_offset,
117 d_name_offset,
118 name_in_qstr_offset,
119 ) {
120 if name.contains(".so") {
122 let size = e.end.saturating_sub(e.start);
123 let entry = lib_map.entry(name).or_insert((e.start, 0));
124 entry.0 = entry.0.min(e.start);
126 entry.1 += size;
127 }
128 }
129 }
130 });
131
132 let mut libs: Vec<SharedLibraryInfo> = lib_map
134 .into_iter()
135 .map(|(lib_path, (base_addr, size))| {
136 let is_suspicious = classify_library(&lib_path);
137 SharedLibraryInfo {
138 pid,
139 process_name: process_name.to_string(),
140 lib_path,
141 base_addr,
142 size,
143 is_suspicious,
144 }
145 })
146 .collect();
147
148 libs.sort_by_key(|lib| lib.base_addr);
150
151 Ok(libs)
152}
153
154fn read_vma_file_path<P: PhysicalMemoryProvider>(
159 reader: &ObjectReader<P>,
160 file_ptr: u64,
161 f_path_offset: u64,
162 dentry_in_path_offset: u64,
163 d_name_offset: u64,
164 name_in_qstr_offset: u64,
165) -> Option<String> {
166 let dentry_addr = file_ptr + f_path_offset + dentry_in_path_offset;
169 let dentry_raw = reader.read_bytes(dentry_addr, 8).ok()?;
170 let dentry_ptr = u64::from_le_bytes(dentry_raw.try_into().ok()?);
171 if dentry_ptr == 0 {
172 return None;
173 }
174
175 let name_addr = dentry_ptr + d_name_offset + name_in_qstr_offset;
177 let name_raw = reader.read_bytes(name_addr, 8).ok()?;
178 let name_ptr = u64::from_le_bytes(name_raw.try_into().ok()?);
179 if name_ptr == 0 {
180 return None;
181 }
182
183 let name = reader.read_string(name_ptr, 256).ok()?;
184 if name.is_empty() {
185 return None;
186 }
187
188 Some(name)
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
200 fn classify_standard_lib_benign() {
201 assert!(
202 !classify_library("/usr/lib/x86_64-linux-gnu/libc.so.6"),
203 "standard libc path should not be suspicious"
204 );
205 assert!(
206 !classify_library("/usr/lib/libpthread.so.0"),
207 "standard libpthread should not be suspicious"
208 );
209 assert!(
210 !classify_library("/lib64/ld-linux-x86-64.so.2"),
211 "dynamic linker should not be suspicious"
212 );
213 }
214
215 #[test]
216 fn classify_tmp_suspicious() {
217 assert!(
218 classify_library("/tmp/evil.so"),
219 "/tmp library should be suspicious"
220 );
221 assert!(
222 classify_library("/tmp/subdir/payload.so"),
223 "/tmp subdirectory should be suspicious"
224 );
225 }
226
227 #[test]
228 fn classify_devshm_suspicious() {
229 assert!(
230 classify_library("/dev/shm/inject.so"),
231 "/dev/shm library should be suspicious"
232 );
233 assert!(
234 classify_library("/dev/shm/hidden/hook.so.1"),
235 "/dev/shm subdirectory should be suspicious"
236 );
237 }
238
239 #[test]
240 fn classify_deleted_suspicious() {
241 assert!(
242 classify_library("/usr/lib/libfoo.so (deleted)"),
243 "deleted library should be suspicious"
244 );
245 assert!(
246 classify_library("/tmp/rootkit.so (deleted)"),
247 "deleted library from /tmp should be suspicious"
248 );
249 }
250
251 #[test]
252 fn classify_hidden_file_suspicious() {
253 assert!(
254 classify_library("/home/user/.hidden_lib.so"),
255 "hidden file should be suspicious"
256 );
257 assert!(
258 classify_library("/opt/app/.sneaky.so.1"),
259 "hidden file with version should be suspicious"
260 );
261 }
262
263 #[test]
264 fn classify_non_so_suspicious() {
265 assert!(
266 classify_library("/usr/lib/not_a_library.bin"),
267 "non-.so file should be suspicious"
268 );
269 assert!(
270 classify_library("/usr/lib/strange_mapping"),
271 "file without .so extension should be suspicious"
272 );
273 }
274
275 #[test]
276 fn classify_var_tmp_suspicious() {
277 assert!(
278 classify_library("/var/tmp/staged.so"),
279 "/var/tmp library should be suspicious"
280 );
281 }
282
283 use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
288 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
289 use memf_symbols::isf::IsfResolver;
290 use memf_symbols::test_builders::IsfBuilder;
291
292 fn make_test_reader(data: &[u8], vaddr: u64, paddr: u64) -> ObjectReader<SyntheticPhysMem> {
296 let isf = IsfBuilder::new()
297 .add_struct("task_struct", 128)
299 .add_field("task_struct", "pid", 0, "int")
300 .add_field("task_struct", "comm", 32, "char")
301 .add_field("task_struct", "mm", 48, "pointer")
302 .add_struct("mm_struct", 128)
304 .add_field("mm_struct", "mmap", 8, "pointer")
305 .add_struct("vm_area_struct", 64)
307 .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
308 .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
309 .add_field("vm_area_struct", "vm_next", 16, "pointer")
310 .add_field("vm_area_struct", "vm_file", 40, "pointer")
311 .add_struct("file", 64)
313 .add_field("file", "f_path", 0, "path")
314 .add_struct("path", 16)
316 .add_field("path", "dentry", 8, "pointer")
317 .add_struct("dentry", 64)
319 .add_field("dentry", "d_name", 0, "qstr")
320 .add_struct("qstr", 16)
322 .add_field("qstr", "name", 8, "pointer")
323 .build_json();
324
325 let resolver = IsfResolver::from_value(&isf).unwrap();
326 let (cr3, mem) = PageTableBuilder::new()
327 .map_4k(vaddr, paddr, flags::WRITABLE)
328 .write_phys(paddr, data)
329 .build();
330 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
331 ObjectReader::new(vas, Box::new(resolver))
332 }
333
334 #[test]
335 fn walk_no_vma_returns_empty() {
336 let vaddr: u64 = 0xFFFF_8000_0010_0000;
338 let paddr: u64 = 0x0080_0000;
339 let mut data = vec![0u8; 4096];
340
341 data[0..4].copy_from_slice(&2u32.to_le_bytes()); data[32..41].copy_from_slice(b"kthreadd\0"); data[48..56].copy_from_slice(&0u64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr);
347
348 let result = walk_library_list(&reader, vaddr, 2, "kthreadd").unwrap();
349 assert!(result.is_empty(), "kernel thread should have no libraries");
350 }
351
352 #[test]
354 fn walk_single_so_library() {
355 let vaddr: u64 = 0xFFFF_8000_0010_0000;
357 let paddr: u64 = 0x0080_0000;
358 let mut data = vec![0u8; 4096];
359
360 data[0..4].copy_from_slice(&1u32.to_le_bytes()); data[32..36].copy_from_slice(b"bash"); let mm_addr = vaddr + 0x200;
364 data[48..56].copy_from_slice(&mm_addr.to_le_bytes()); let vma_addr = vaddr + 0x300;
368 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes()); data[0x300..0x308].copy_from_slice(&0x7F00_0000u64.to_le_bytes()); data[0x308..0x310].copy_from_slice(&0x7F01_0000u64.to_le_bytes()); data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); let file_addr = vaddr + 0x400;
376 data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes()); let dentry_addr = vaddr + 0x500;
380 data[0x408..0x410].copy_from_slice(&dentry_addr.to_le_bytes()); let name_str_addr = vaddr + 0x600;
384 data[0x508..0x510].copy_from_slice(&name_str_addr.to_le_bytes()); let name = b"libc.so.6";
388 data[0x600..0x600 + name.len()].copy_from_slice(name);
389
390 let reader = make_test_reader(&data, vaddr, paddr);
391 let libs = walk_library_list(&reader, vaddr, 1, "bash").unwrap();
392
393 assert_eq!(libs.len(), 1);
394 assert_eq!(libs[0].pid, 1);
395 assert_eq!(libs[0].process_name, "bash");
396 assert_eq!(libs[0].lib_path, "libc.so.6");
397 assert_eq!(libs[0].base_addr, 0x7F00_0000);
398 assert_eq!(libs[0].size, 0x0001_0000);
399 assert!(!libs[0].is_suspicious);
400 }
401
402 #[test]
403 fn walk_deduplicates_multi_vma_library() {
404 let vaddr: u64 = 0xFFFF_8000_0010_0000;
406 let paddr: u64 = 0x0080_0000;
407 let mut data = vec![0u8; 4096];
408
409 data[0..4].copy_from_slice(&1u32.to_le_bytes()); data[32..36].copy_from_slice(b"cat\0"); let mm_addr = vaddr + 0x200;
413 data[48..56].copy_from_slice(&mm_addr.to_le_bytes()); let vma1_addr = vaddr + 0x300;
417 data[0x208..0x210].copy_from_slice(&vma1_addr.to_le_bytes()); let file_addr = vaddr + 0x500;
421
422 data[0x300..0x308].copy_from_slice(&0x7F00_0000u64.to_le_bytes()); data[0x308..0x310].copy_from_slice(&0x7F00_4000u64.to_le_bytes()); let vma2_addr = vaddr + 0x400;
426 data[0x310..0x318].copy_from_slice(&vma2_addr.to_le_bytes()); data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes()); data[0x400..0x408].copy_from_slice(&0x7F00_4000u64.to_le_bytes()); data[0x408..0x410].copy_from_slice(&0x7F00_6000u64.to_le_bytes()); data[0x410..0x418].copy_from_slice(&0u64.to_le_bytes()); data[0x428..0x430].copy_from_slice(&file_addr.to_le_bytes()); let dentry_addr = vaddr + 0x600;
437 data[0x508..0x510].copy_from_slice(&dentry_addr.to_le_bytes()); let name_addr = vaddr + 0x700;
441 data[0x608..0x610].copy_from_slice(&name_addr.to_le_bytes()); let name = b"libpthread.so.0";
445 data[0x700..0x700 + name.len()].copy_from_slice(name);
446
447 let reader = make_test_reader(&data, vaddr, paddr);
448 let libs = walk_library_list(&reader, vaddr, 1, "cat").unwrap();
449
450 assert_eq!(libs.len(), 1);
452 assert_eq!(libs[0].lib_path, "libpthread.so.0");
453 assert_eq!(libs[0].base_addr, 0x7F00_0000);
454 assert_eq!(libs[0].size, 0x6000);
456 assert!(!libs[0].is_suspicious);
457 }
458
459 #[test]
461 fn walk_skips_non_file_backed_vmas() {
462 let vaddr: u64 = 0xFFFF_8000_0010_0000;
464 let paddr: u64 = 0x0080_0000;
465 let mut data = vec![0u8; 4096];
466
467 data[0..4].copy_from_slice(&1u32.to_le_bytes());
469 data[32..36].copy_from_slice(b"test");
470 let mm_addr = vaddr + 0x200;
471 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
472
473 let vma_addr = vaddr + 0x300;
475 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes()); data[0x300..0x308].copy_from_slice(&0x7FFF_0000u64.to_le_bytes()); data[0x308..0x310].copy_from_slice(&0x7FFF_2000u64.to_le_bytes()); data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); data[0x328..0x330].copy_from_slice(&0u64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr);
484 let libs = walk_library_list(&reader, vaddr, 1, "test").unwrap();
485
486 assert!(
487 libs.is_empty(),
488 "anonymous VMA should not produce library entries"
489 );
490 }
491
492 #[test]
493 fn classify_library_exact_tmp_dir() {
494 assert!(
496 classify_library("/tmp"),
497 "exact /tmp path must be suspicious"
498 );
499 assert!(
500 classify_library("/dev/shm"),
501 "/dev/shm exact match must be suspicious"
502 );
503 assert!(
504 classify_library("/var/tmp"),
505 "/var/tmp exact match must be suspicious"
506 );
507 }
508
509 #[test]
510 fn classify_library_just_dot_basename_not_suspicious() {
511 assert!(
517 !classify_library("/usr/lib/normallib.so"),
518 "normal .so must be benign"
519 );
520 }
521
522 #[test]
523 fn walk_cycle_detection_breaks_loop() {
524 let vaddr: u64 = 0xFFFF_8000_0050_0000;
527 let paddr: u64 = 0x0083_0000;
528 let mut data = vec![0u8; 4096];
529
530 data[0..4].copy_from_slice(&10u32.to_le_bytes()); data[32..36].copy_from_slice(b"cycl"); let mm_addr = vaddr + 0x200;
534 data[48..56].copy_from_slice(&mm_addr.to_le_bytes()); let vma_addr = vaddr + 0x300;
538 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
539
540 data[0x300..0x308].copy_from_slice(&0x7F00_0000u64.to_le_bytes()); data[0x308..0x310].copy_from_slice(&0x7F00_1000u64.to_le_bytes()); data[0x310..0x318].copy_from_slice(&vma_addr.to_le_bytes()); data[0x328..0x330].copy_from_slice(&0u64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr);
547 let libs = walk_library_list(&reader, vaddr, 10, "cycl").unwrap();
549 assert!(
550 libs.is_empty(),
551 "cycle VMA with null vm_file should yield no libraries"
552 );
553 }
554
555 #[test]
556 fn walk_second_vma_with_lower_base_updates_min() {
557 let vaddr: u64 = 0xFFFF_8000_0060_0000;
560 let paddr: u64 = 0x0084_0000;
561 let mut data = vec![0u8; 4096];
562
563 data[0..4].copy_from_slice(&20u32.to_le_bytes()); data[32..37].copy_from_slice(b"proc\0"); let mm_addr = vaddr + 0x100;
567 data[48..56].copy_from_slice(&mm_addr.to_le_bytes()); let vma1_addr = vaddr + 0x200;
571 data[0x108..0x110].copy_from_slice(&vma1_addr.to_le_bytes());
572
573 let file_addr = vaddr + 0x600;
574
575 let vma2_addr = vaddr + 0x300;
577 data[0x200..0x208].copy_from_slice(&0x7F00_2000u64.to_le_bytes()); data[0x208..0x210].copy_from_slice(&0x7F00_4000u64.to_le_bytes()); data[0x210..0x218].copy_from_slice(&vma2_addr.to_le_bytes()); data[0x228..0x230].copy_from_slice(&file_addr.to_le_bytes()); data[0x300..0x308].copy_from_slice(&0x7F00_0000u64.to_le_bytes()); data[0x308..0x310].copy_from_slice(&0x7F00_2000u64.to_le_bytes()); data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes()); let dentry_addr = vaddr + 0x700;
590 data[0x608..0x610].copy_from_slice(&dentry_addr.to_le_bytes());
591
592 let name_addr = vaddr + 0x800;
594 data[0x708..0x710].copy_from_slice(&name_addr.to_le_bytes());
595
596 let name = b"libtest.so.1";
598 data[0x800..0x800 + name.len()].copy_from_slice(name);
599
600 let reader = make_test_reader(&data, vaddr, paddr);
601 let libs = walk_library_list(&reader, vaddr, 20, "proc").unwrap();
602
603 assert_eq!(libs.len(), 1, "single deduplicated library expected");
604 assert_eq!(
606 libs[0].base_addr, 0x7F00_0000,
607 "base_addr must be the minimum vm_start"
608 );
609 assert_eq!(libs[0].size, 0x4000);
611 }
612
613 #[test]
615 fn walk_skips_vma_when_dentry_null() {
616 let vaddr: u64 = 0xFFFF_8000_0070_0000;
617 let paddr: u64 = 0x0085_0000;
618 let mut data = vec![0u8; 4096];
619
620 data[0..4].copy_from_slice(&30u32.to_le_bytes()); data[32..36].copy_from_slice(b"null"); let mm_addr = vaddr + 0x200;
624 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
625
626 let vma_addr = vaddr + 0x300;
628 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
629
630 data[0x300..0x308].copy_from_slice(&0x7F00_0000u64.to_le_bytes()); data[0x308..0x310].copy_from_slice(&0x7F00_2000u64.to_le_bytes()); data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); let file_addr = vaddr + 0x400;
635 data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes());
636
637 data[0x408..0x410].copy_from_slice(&0u64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr);
642 let libs = walk_library_list(&reader, vaddr, 30, "null").unwrap();
643 assert!(
644 libs.is_empty(),
645 "null dentry_ptr → read_vma_file_path returns None → no library"
646 );
647 }
648
649 #[test]
651 fn walk_skips_vma_when_name_ptr_null() {
652 let vaddr: u64 = 0xFFFF_8000_0078_0000;
653 let paddr: u64 = 0x0086_0000;
654 let mut data = vec![0u8; 4096];
655
656 data[0..4].copy_from_slice(&31u32.to_le_bytes());
658 data[32..36].copy_from_slice(b"npnl");
659 let mm_addr = vaddr + 0x200;
660 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
661
662 let vma_addr = vaddr + 0x300;
664 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
665
666 data[0x300..0x308].copy_from_slice(&0x7F00_0000u64.to_le_bytes());
668 data[0x308..0x310].copy_from_slice(&0x7F00_2000u64.to_le_bytes());
669 data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); let file_addr = vaddr + 0x400;
671 data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes());
672
673 let dentry_addr = vaddr + 0x500;
675 data[0x408..0x410].copy_from_slice(&dentry_addr.to_le_bytes());
676
677 data[0x508..0x510].copy_from_slice(&0u64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr);
681 let libs = walk_library_list(&reader, vaddr, 31, "npnl").unwrap();
682 assert!(
683 libs.is_empty(),
684 "name_ptr == 0 → read_vma_file_path returns None → no library"
685 );
686 }
687
688 #[test]
690 fn walk_skips_vma_when_name_empty() {
691 let vaddr: u64 = 0xFFFF_8000_0079_0000;
692 let paddr: u64 = 0x0087_0000;
693 let mut data = vec![0u8; 4096];
694
695 data[0..4].copy_from_slice(&32u32.to_le_bytes());
696 data[32..36].copy_from_slice(b"empt");
697 let mm_addr = vaddr + 0x200;
698 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
699
700 let vma_addr = vaddr + 0x300;
701 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
702
703 data[0x300..0x308].copy_from_slice(&0x7F00_0000u64.to_le_bytes());
704 data[0x308..0x310].copy_from_slice(&0x7F00_2000u64.to_le_bytes());
705 data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes());
706 let file_addr = vaddr + 0x400;
707 data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes());
708
709 let dentry_addr = vaddr + 0x500;
711 data[0x408..0x410].copy_from_slice(&dentry_addr.to_le_bytes());
712
713 let name_str_addr = vaddr + 0x600;
715 data[0x508..0x510].copy_from_slice(&name_str_addr.to_le_bytes());
716 data[0x600] = 0u8;
718
719 let reader = make_test_reader(&data, vaddr, paddr);
720 let libs = walk_library_list(&reader, vaddr, 32, "empt").unwrap();
721 assert!(
723 libs.is_empty(),
724 "empty name → read_vma_file_path returns None"
725 );
726 }
727
728 #[test]
730 fn shared_library_info_debug_clone_serialize() {
731 let info = SharedLibraryInfo {
732 pid: 1,
733 process_name: "test".to_string(),
734 lib_path: "/usr/lib/libfoo.so".to_string(),
735 base_addr: 0x7F00_0000,
736 size: 0x1000,
737 is_suspicious: false,
738 };
739 let cloned = info.clone();
740 let dbg = format!("{cloned:?}");
741 assert!(dbg.contains("libfoo"));
742 let json = serde_json::to_string(&info).unwrap();
743 assert!(json.contains("\"pid\":1"));
744 assert!(json.contains("is_suspicious"));
745 }
746
747 #[test]
748 fn walk_classifies_suspicious_library() {
749 let vaddr: u64 = 0xFFFF_8000_0010_0000;
751 let paddr: u64 = 0x0080_0000;
752 let mut data = vec![0u8; 4096];
753
754 data[0..4].copy_from_slice(&42u32.to_le_bytes());
756 data[32..37].copy_from_slice(b"sshd\0");
757 let mm_addr = vaddr + 0x200;
758 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
759
760 let vma_addr = vaddr + 0x300;
762 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
763
764 data[0x300..0x308].copy_from_slice(&0x7F00_0000u64.to_le_bytes());
766 data[0x308..0x310].copy_from_slice(&0x7F00_2000u64.to_le_bytes());
767 data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); let file_addr = vaddr + 0x400;
769 data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes());
770
771 let dentry_addr = vaddr + 0x500;
773 data[0x408..0x410].copy_from_slice(&dentry_addr.to_le_bytes());
774
775 let name_addr = vaddr + 0x600;
777 data[0x508..0x510].copy_from_slice(&name_addr.to_le_bytes());
778
779 let name = b"/tmp/evil.so";
781 data[0x600..0x600 + name.len()].copy_from_slice(name);
782
783 let reader = make_test_reader(&data, vaddr, paddr);
784 let libs = walk_library_list(&reader, vaddr, 42, "sshd").unwrap();
785
786 assert_eq!(libs.len(), 1);
787 assert_eq!(libs[0].lib_path, "/tmp/evil.so");
788 assert!(libs[0].is_suspicious, "/tmp library should be suspicious");
789 }
790
791 #[test]
794 fn walk_library_list_missing_f_path_field_returns_error() {
795 let vaddr: u64 = 0xFFFF_8000_0088_0000;
796 let paddr: u64 = 0x0088_1000;
797 let mut data = vec![0u8; 4096];
798
799 let mm_addr = vaddr + 0x200;
801 data[0..4].copy_from_slice(&9u32.to_le_bytes());
802 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
803 let isf = IsfBuilder::new()
807 .add_struct("task_struct", 128)
808 .add_field("task_struct", "pid", 0, "int")
809 .add_field("task_struct", "comm", 32, "char")
810 .add_field("task_struct", "mm", 48, "pointer")
811 .add_struct("mm_struct", 128)
812 .add_field("mm_struct", "mmap", 8, "pointer")
813 .add_struct("path", 16)
815 .add_field("path", "dentry", 8, "pointer")
816 .add_struct("dentry", 64)
817 .add_field("dentry", "d_name", 0, "qstr")
818 .add_struct("qstr", 16)
819 .add_field("qstr", "name", 8, "pointer")
820 .build_json();
821
822 let resolver = IsfResolver::from_value(&isf).unwrap();
823 let (cr3, mem) = PageTableBuilder::new()
824 .map_4k(vaddr, paddr, flags::WRITABLE)
825 .write_phys(paddr, &data)
826 .build();
827 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
828 let reader = ObjectReader::new(vas, Box::new(resolver));
829
830 let result = walk_library_list(&reader, vaddr, 9, "proc");
831 assert!(
832 result.is_err(),
833 "missing file.f_path field must return an error"
834 );
835 }
836
837 #[test]
839 fn walk_library_list_missing_path_dentry_field_returns_error() {
840 let vaddr: u64 = 0xFFFF_8000_0089_0000;
841 let paddr: u64 = 0x0089_0000;
842 let mut data = vec![0u8; 4096];
843
844 let mm_addr = vaddr + 0x200;
845 data[0..4].copy_from_slice(&10u32.to_le_bytes());
846 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
847
848 let isf = IsfBuilder::new()
849 .add_struct("task_struct", 128)
850 .add_field("task_struct", "pid", 0, "int")
851 .add_field("task_struct", "comm", 32, "char")
852 .add_field("task_struct", "mm", 48, "pointer")
853 .add_struct("mm_struct", 128)
854 .add_field("mm_struct", "mmap", 8, "pointer")
855 .add_struct("file", 64)
856 .add_field("file", "f_path", 0, "path")
857 .add_struct("path", 16)
859 .add_struct("dentry", 64)
860 .add_field("dentry", "d_name", 0, "qstr")
861 .add_struct("qstr", 16)
862 .add_field("qstr", "name", 8, "pointer")
863 .build_json();
864
865 let resolver = IsfResolver::from_value(&isf).unwrap();
866 let (cr3, mem) = PageTableBuilder::new()
867 .map_4k(vaddr, paddr, flags::WRITABLE)
868 .write_phys(paddr, &data)
869 .build();
870 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
871 let reader = ObjectReader::new(vas, Box::new(resolver));
872
873 let result = walk_library_list(&reader, vaddr, 10, "proc");
874 assert!(
875 result.is_err(),
876 "missing path.dentry field must return an error"
877 );
878 }
879
880 #[test]
882 fn walk_library_list_missing_d_name_field_returns_error() {
883 let vaddr: u64 = 0xFFFF_8000_008A_0000;
884 let paddr: u64 = 0x008A_0000;
885 let mut data = vec![0u8; 4096];
886
887 let mm_addr = vaddr + 0x200;
888 data[0..4].copy_from_slice(&11u32.to_le_bytes());
889 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
890
891 let isf = IsfBuilder::new()
892 .add_struct("task_struct", 128)
893 .add_field("task_struct", "pid", 0, "int")
894 .add_field("task_struct", "comm", 32, "char")
895 .add_field("task_struct", "mm", 48, "pointer")
896 .add_struct("mm_struct", 128)
897 .add_field("mm_struct", "mmap", 8, "pointer")
898 .add_struct("file", 64)
899 .add_field("file", "f_path", 0, "path")
900 .add_struct("path", 16)
901 .add_field("path", "dentry", 8, "pointer")
902 .add_struct("dentry", 64)
904 .add_struct("qstr", 16)
905 .add_field("qstr", "name", 8, "pointer")
906 .build_json();
907
908 let resolver = IsfResolver::from_value(&isf).unwrap();
909 let (cr3, mem) = PageTableBuilder::new()
910 .map_4k(vaddr, paddr, flags::WRITABLE)
911 .write_phys(paddr, &data)
912 .build();
913 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
914 let reader = ObjectReader::new(vas, Box::new(resolver));
915
916 let result = walk_library_list(&reader, vaddr, 11, "proc");
917 assert!(
918 result.is_err(),
919 "missing dentry.d_name field must return an error"
920 );
921 }
922
923 #[test]
925 fn walk_library_list_missing_qstr_name_field_returns_error() {
926 let vaddr: u64 = 0xFFFF_8000_008B_0000;
927 let paddr: u64 = 0x008B_0000;
928 let mut data = vec![0u8; 4096];
929
930 let mm_addr = vaddr + 0x200;
931 data[0..4].copy_from_slice(&12u32.to_le_bytes());
932 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
933
934 let isf = IsfBuilder::new()
935 .add_struct("task_struct", 128)
936 .add_field("task_struct", "pid", 0, "int")
937 .add_field("task_struct", "comm", 32, "char")
938 .add_field("task_struct", "mm", 48, "pointer")
939 .add_struct("mm_struct", 128)
940 .add_field("mm_struct", "mmap", 8, "pointer")
941 .add_struct("file", 64)
942 .add_field("file", "f_path", 0, "path")
943 .add_struct("path", 16)
944 .add_field("path", "dentry", 8, "pointer")
945 .add_struct("dentry", 64)
946 .add_field("dentry", "d_name", 0, "qstr")
947 .add_struct("qstr", 16)
949 .build_json();
950
951 let resolver = IsfResolver::from_value(&isf).unwrap();
952 let (cr3, mem) = PageTableBuilder::new()
953 .map_4k(vaddr, paddr, flags::WRITABLE)
954 .write_phys(paddr, &data)
955 .build();
956 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
957 let reader = ObjectReader::new(vas, Box::new(resolver));
958
959 let result = walk_library_list(&reader, vaddr, 12, "proc");
960 assert!(
961 result.is_err(),
962 "missing qstr.name field must return an error"
963 );
964 }
965
966 #[test]
970 fn classify_library_no_slash_path() {
971 assert!(
974 !classify_library("libc.so.6"),
975 "bare name with .so. must be benign"
976 );
977 assert!(
979 classify_library(".hidden.so.1"),
980 "hidden bare name must be suspicious"
981 );
982 }
983
984 #[test]
985 fn missing_file_f_path_field_returns_missing_field() {
986 let isf = IsfBuilder::new()
988 .add_struct("file", 64)
990 .build_json();
991 let resolver = IsfResolver::from_value(&isf).unwrap();
992 let (cr3, mem) = PageTableBuilder::new().build();
993 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
994 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
995 let result = walk_library_list(&reader, 0xFFFF_8000_0010_0000, 1, "init");
996 assert!(
997 matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "file" && field_name == "f_path"),
998 "expected MissingField file.f_path, got {result:?}"
999 );
1000 }
1001}