1use memf_core::object_reader::ObjectReader;
11use memf_format::PhysicalMemoryProvider;
12use serde::Serialize;
13
14use crate::Result;
15
16#[derive(Debug, Clone, Serialize)]
18pub struct HiddenDentryInfo {
19 pub pid: u32,
21 pub comm: String,
23 pub fd: u32,
25 pub dentry_addr: u64,
27 pub filename: String,
29 pub inode_num: u64,
31 pub file_size: u64,
33 pub nlink: u32,
35 pub is_suspicious: bool,
37}
38
39pub use crate::heuristics::classify_hidden_dentry;
49
50pub fn walk_dentry_cache<P: PhysicalMemoryProvider>(
58 reader: &ObjectReader<P>,
59) -> Result<Vec<HiddenDentryInfo>> {
60 let init_task_addr = match reader.symbols().symbol_address("init_task") {
62 Some(a) => a,
63 None => return Ok(vec![]),
64 };
65 let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
66 Some(o) => o,
67 None => return Ok(vec![]),
68 };
69 if reader
70 .symbols()
71 .field_offset("task_struct", "files")
72 .is_none()
73 {
74 return Ok(vec![]);
75 }
76
77 let head_vaddr = init_task_addr + tasks_offset;
78 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
79
80 let mut results: Vec<HiddenDentryInfo> = Vec::new();
81
82 collect_hidden_dentries_for_task(reader, init_task_addr, &mut results);
83 for &task_addr in &task_addrs {
84 collect_hidden_dentries_for_task(reader, task_addr, &mut results);
85 }
86
87 results.sort_by_key(|r| (r.pid, r.fd));
88 Ok(results)
89}
90
91fn collect_hidden_dentries_for_task<P: PhysicalMemoryProvider>(
93 reader: &ObjectReader<P>,
94 task_addr: u64,
95 out: &mut Vec<HiddenDentryInfo>,
96) {
97 let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
98 Ok(v) => v,
99 Err(_) => return,
100 };
101 let comm = reader
102 .read_field_string(task_addr, "task_struct", "comm", 16)
103 .unwrap_or_default();
104
105 let files_ptr: u64 = match reader.read_field(task_addr, "task_struct", "files") {
107 Ok(v) => v,
108 Err(_) => return,
109 };
110 if files_ptr == 0 {
111 return;
112 }
113
114 let fdt_ptr: u64 = match reader.read_field(files_ptr, "files_struct", "fdt") {
116 Ok(v) => v,
117 Err(_) => return,
118 };
119 if fdt_ptr == 0 {
120 return;
121 }
122
123 let max_fds: u64 = match reader.read_field::<u32>(fdt_ptr, "fdtable", "max_fds") {
126 Ok(v) => u64::from(v).min(65536),
127 Err(_) => return,
128 };
129
130 let fd_array_ptr: u64 = match reader.read_field(fdt_ptr, "fdtable", "fd") {
132 Ok(v) => v,
133 Err(_) => return,
134 };
135 if fd_array_ptr == 0 {
136 return;
137 }
138
139 for fd_index in 0u64..max_fds {
140 let file_slot_addr = fd_array_ptr + fd_index * 8;
141 let file_ptr_raw = match reader.read_bytes(file_slot_addr, 8) {
142 Ok(b) => b,
143 Err(_) => break,
144 };
145 let file_ptr = u64::from_le_bytes(match file_ptr_raw.try_into() {
146 Ok(b) => b,
147 Err(_) => break,
148 });
149 if file_ptr == 0 {
150 continue;
151 }
152
153 if let Some(info) = try_read_hidden_dentry(reader, pid, &comm, fd_index as u32, file_ptr) {
154 out.push(info);
155 }
156 }
157}
158
159fn try_read_hidden_dentry<P: PhysicalMemoryProvider>(
163 reader: &ObjectReader<P>,
164 pid: u32,
165 comm: &str,
166 fd: u32,
167 file_ptr: u64,
168) -> Option<HiddenDentryInfo> {
169 let f_path_offset = reader.symbols().field_offset("file", "f_path")?;
171 let dentry_in_path = reader.symbols().field_offset("path", "dentry")?;
172
173 let dentry_slot = file_ptr + f_path_offset + dentry_in_path;
174 let dentry_raw = reader.read_bytes(dentry_slot, 8).ok()?;
175 let dentry_ptr = u64::from_le_bytes(dentry_raw.try_into().ok()?);
176 if dentry_ptr == 0 {
177 return None;
178 }
179
180 let inode_ptr: u64 = reader.read_field(dentry_ptr, "dentry", "d_inode").ok()?;
182 if inode_ptr == 0 {
183 return None;
184 }
185
186 let nlink: u32 = reader.read_field(inode_ptr, "inode", "i_nlink").ok()?;
188 let file_size: u64 = reader
190 .read_field::<u64>(inode_ptr, "inode", "i_size")
191 .unwrap_or(0);
192 let inode_num: u64 = reader
194 .read_field::<u64>(inode_ptr, "inode", "i_ino")
195 .unwrap_or(0);
196
197 let filename = read_dentry_name(reader, dentry_ptr).unwrap_or_default();
199
200 let is_suspicious = classify_hidden_dentry(nlink, &filename);
201
202 if nlink > 0 && !is_suspicious {
204 return None;
205 }
206
207 Some(HiddenDentryInfo {
208 pid,
209 comm: comm.to_string(),
210 fd,
211 dentry_addr: dentry_ptr,
212 filename,
213 inode_num,
214 file_size,
215 nlink,
216 is_suspicious,
217 })
218}
219
220fn read_dentry_name<P: PhysicalMemoryProvider>(
222 reader: &ObjectReader<P>,
223 dentry_ptr: u64,
224) -> Option<String> {
225 let d_name_offset = reader.symbols().field_offset("dentry", "d_name")?;
226 let name_in_qstr = reader.symbols().field_offset("qstr", "name")?;
227
228 let name_ptr_addr = dentry_ptr + d_name_offset + name_in_qstr;
229 let name_raw = reader.read_bytes(name_ptr_addr, 8).ok()?;
230 let name_ptr = u64::from_le_bytes(name_raw.try_into().ok()?);
231 if name_ptr == 0 {
232 return None;
233 }
234
235 reader.read_string(name_ptr, 256).ok()
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use memf_core::object_reader::ObjectReader;
242 use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
243 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
244 use memf_symbols::isf::IsfResolver;
245 use memf_symbols::test_builders::IsfBuilder;
246
247 #[test]
252 fn classify_hidden_nlink_zero_is_suspicious() {
253 assert!(
254 classify_hidden_dentry(0, "rootkit.so"),
255 "nlink==0 file must be suspicious"
256 );
257 }
258
259 #[test]
260 fn classify_hidden_so_file_suspicious() {
261 assert!(
262 classify_hidden_dentry(0, "libevil.so"),
263 "unlinked .so file must be suspicious"
264 );
265 }
266
267 #[test]
268 fn classify_hidden_nlink_positive_not_suspicious() {
269 assert!(
270 !classify_hidden_dentry(1, "normal.txt"),
271 "file with nlink>0 and no suspicious extension must not be suspicious"
272 );
273 }
274
275 #[test]
276 fn classify_hidden_empty_filename_not_suspicious() {
277 assert!(
278 !classify_hidden_dentry(0, ""),
279 "empty filename (kernel internal) must not be suspicious"
280 );
281 }
282
283 #[test]
284 fn classify_hidden_sh_script_suspicious() {
285 assert!(
286 classify_hidden_dentry(0, "dropper.sh"),
287 "unlinked .sh script must be suspicious"
288 );
289 }
290
291 #[test]
292 fn classify_hidden_py_script_suspicious() {
293 assert!(
294 classify_hidden_dentry(0, "stage2.py"),
295 "unlinked .py script must be suspicious"
296 );
297 }
298
299 fn make_reader_no_init_task() -> ObjectReader<SyntheticPhysMem> {
304 let isf = IsfBuilder::new()
305 .add_struct("task_struct", 128)
306 .add_field("task_struct", "pid", 0, "int")
307 .add_struct("list_head", 16)
308 .add_field("list_head", "next", 0, "pointer")
309 .add_field("list_head", "prev", 8, "pointer")
310 .build_json();
311
312 let resolver = IsfResolver::from_value(&isf).unwrap();
313 let (cr3, mem) = PageTableBuilder::new().build();
314 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
315 ObjectReader::new(vas, Box::new(resolver))
316 }
317
318 fn make_reader_no_open_files() -> ObjectReader<SyntheticPhysMem> {
319 let vaddr: u64 = 0xFFFF_8000_0010_0000;
320 let paddr: u64 = 0x0080_0000;
321
322 let mut data = vec![0u8; 4096];
323 data[0..4].copy_from_slice(&1u32.to_le_bytes());
324 let tasks_next = vaddr + 16;
325 data[16..24].copy_from_slice(&tasks_next.to_le_bytes());
326 data[24..32].copy_from_slice(&tasks_next.to_le_bytes());
327 data[32..39].copy_from_slice(b"kthread");
328 data[48..56].copy_from_slice(&0u64.to_le_bytes());
330
331 let isf = IsfBuilder::new()
332 .add_struct("task_struct", 128)
333 .add_field("task_struct", "pid", 0, "int")
334 .add_field("task_struct", "tasks", 16, "list_head")
335 .add_field("task_struct", "comm", 32, "char")
336 .add_field("task_struct", "files", 48, "pointer")
337 .add_struct("list_head", 16)
338 .add_field("list_head", "next", 0, "pointer")
339 .add_field("list_head", "prev", 8, "pointer")
340 .add_symbol("init_task", vaddr)
341 .build_json();
342
343 let resolver = IsfResolver::from_value(&isf).unwrap();
344 let (cr3, mem) = PageTableBuilder::new()
345 .map_4k(vaddr, paddr, flags::WRITABLE)
346 .write_phys(paddr, &data)
347 .build();
348 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
349 ObjectReader::new(vas, Box::new(resolver))
350 }
351
352 #[test]
353 fn walk_dentry_missing_init_task_returns_empty() {
354 let reader = make_reader_no_init_task();
355 let result = walk_dentry_cache(&reader).expect("should not error");
356 assert!(
357 result.is_empty(),
358 "missing init_task must yield empty results (graceful degradation)"
359 );
360 }
361
362 #[test]
363 fn walk_dentry_no_open_files_returns_empty() {
364 let reader = make_reader_no_open_files();
365 let result = walk_dentry_cache(&reader).expect("should not error");
366 assert!(
367 result.is_empty(),
368 "kernel thread with files==NULL must produce no hidden-dentry results"
369 );
370 }
371
372 #[test]
373 fn walk_dentry_missing_tasks_field_returns_empty() {
374 let isf = IsfBuilder::new()
376 .add_struct("task_struct", 128)
377 .add_field("task_struct", "pid", 0, "int")
378 .add_field("task_struct", "files", 48, "pointer")
380 .add_symbol("init_task", 0xFFFF_8000_0000_0000)
381 .build_json();
382
383 let resolver = IsfResolver::from_value(&isf).unwrap();
384 let (cr3, mem) = PageTableBuilder::new().build();
385 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
386 let reader = ObjectReader::new(vas, Box::new(resolver));
387
388 let result = walk_dentry_cache(&reader).expect("should not error");
389 assert!(
390 result.is_empty(),
391 "missing tasks field must yield empty (graceful degradation)"
392 );
393 }
394
395 #[test]
396 fn walk_dentry_missing_files_field_returns_empty() {
397 let isf = IsfBuilder::new()
399 .add_struct("task_struct", 128)
400 .add_field("task_struct", "pid", 0, "int")
401 .add_field("task_struct", "tasks", 16, "list_head")
402 .add_struct("list_head", 16)
404 .add_field("list_head", "next", 0, "pointer")
405 .add_field("list_head", "prev", 8, "pointer")
406 .add_symbol("init_task", 0xFFFF_8000_0000_0000)
407 .build_json();
408
409 let resolver = IsfResolver::from_value(&isf).unwrap();
410 let (cr3, mem) = PageTableBuilder::new().build();
411 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
412 let reader = ObjectReader::new(vas, Box::new(resolver));
413
414 let result = walk_dentry_cache(&reader).expect("should not error");
415 assert!(
416 result.is_empty(),
417 "missing files field must yield empty (graceful degradation)"
418 );
419 }
420
421 #[test]
426 fn walk_dentry_symbol_present_empty_list() {
427 let sym_vaddr: u64 = 0xFFFF_8800_0030_0000;
430 let sym_paddr: u64 = 0x0040_0000;
431 let tasks_offset = 16u64;
432
433 let mut page = [0u8; 4096];
434 page[0..4].copy_from_slice(&1u32.to_le_bytes());
436 let list_self = sym_vaddr + tasks_offset;
438 page[tasks_offset as usize..tasks_offset as usize + 8]
439 .copy_from_slice(&list_self.to_le_bytes());
440 page[tasks_offset as usize + 8..tasks_offset as usize + 16]
441 .copy_from_slice(&list_self.to_le_bytes());
442 page[32..36].copy_from_slice(b"init");
444 page[48..56].copy_from_slice(&0u64.to_le_bytes());
446
447 let isf = IsfBuilder::new()
448 .add_struct("task_struct", 128)
449 .add_field("task_struct", "pid", 0, "unsigned int")
450 .add_field("task_struct", "tasks", 16, "pointer")
451 .add_field("task_struct", "comm", 32, "char")
452 .add_field("task_struct", "files", 48, "pointer")
453 .add_symbol("init_task", sym_vaddr)
454 .build_json();
455
456 let resolver = IsfResolver::from_value(&isf).unwrap();
457 let (cr3, mem) = PageTableBuilder::new()
458 .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
459 .write_phys(sym_paddr, &page)
460 .build();
461 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
462 let reader = ObjectReader::new(vas, Box::new(resolver));
463
464 let result = walk_dentry_cache(&reader).unwrap_or_default();
465 assert!(
466 result.is_empty(),
467 "no hidden dentries expected for task with files==NULL"
468 );
469 }
470
471 #[test]
476 fn classify_hidden_nlink_positive_so_is_suspicious() {
477 assert!(
479 classify_hidden_dentry(2, "libplugin.so"),
480 "linked .so file must be suspicious due to extension"
481 );
482 }
483
484 #[test]
485 fn classify_hidden_nlink_positive_bin_is_suspicious() {
486 assert!(
487 classify_hidden_dentry(1, "stage2.bin"),
488 "linked .bin file must be suspicious due to extension"
489 );
490 }
491
492 #[test]
493 fn classify_hidden_nlink_positive_elf_is_suspicious() {
494 assert!(
495 classify_hidden_dentry(1, "payload.elf"),
496 "linked .elf file must be suspicious due to extension"
497 );
498 }
499
500 #[test]
501 fn classify_hidden_nlink_positive_py_is_suspicious() {
502 assert!(
503 classify_hidden_dentry(3, "backdoor.py"),
504 "linked .py file must be suspicious due to extension"
505 );
506 }
507
508 #[test]
509 fn classify_hidden_nlink_positive_sh_is_suspicious() {
510 assert!(
511 classify_hidden_dentry(1, "install.sh"),
512 "linked .sh file must be suspicious due to extension"
513 );
514 }
515
516 #[test]
517 fn classify_hidden_extension_check_is_case_insensitive() {
518 assert!(
520 classify_hidden_dentry(1, "PAYLOAD.SO"),
521 "extension check should be case-insensitive"
522 );
523 }
524
525 #[test]
526 fn hidden_dentry_info_serializes() {
527 let info = HiddenDentryInfo {
528 pid: 42,
529 comm: "evil".to_string(),
530 fd: 3,
531 dentry_addr: 0xFFFF_8000_0001_0000,
532 filename: "rootkit.so".to_string(),
533 inode_num: 12345,
534 file_size: 65536,
535 nlink: 0,
536 is_suspicious: true,
537 };
538 let json = serde_json::to_string(&info).unwrap();
539 assert!(json.contains("\"pid\":42"));
540 assert!(json.contains("rootkit.so"));
541 assert!(json.contains("\"is_suspicious\":true"));
542 }
543
544 #[test]
559 fn walk_dentry_unlinked_file_detected() {
560 let task_vaddr: u64 = 0xFFFF_C800_0100_0000;
562 let files_vaddr: u64 = 0xFFFF_C800_0101_0000;
563 let fdt_vaddr: u64 = 0xFFFF_C800_0102_0000;
564 let fd_arr_vaddr: u64 = 0xFFFF_C800_0103_0000;
565 let file_vaddr: u64 = 0xFFFF_C800_0104_0000;
566 let dentry_vaddr: u64 = 0xFFFF_C800_0105_0000;
567 let inode_vaddr: u64 = 0xFFFF_C800_0106_0000;
568 let name_vaddr: u64 = 0xFFFF_C800_0107_0000;
569
570 let task_paddr: u64 = 0x010_000;
572 let files_paddr: u64 = 0x011_000;
573 let fdt_paddr: u64 = 0x012_000;
574 let fd_arr_paddr: u64 = 0x013_000;
575 let file_paddr: u64 = 0x014_000;
576 let dentry_paddr: u64 = 0x015_000;
577 let inode_paddr: u64 = 0x016_000;
578 let name_paddr: u64 = 0x017_000;
579
580 let tasks_offset: u64 = 8;
583 let task_comm_offset: u64 = 24;
584 let task_files_offset: u64 = 40;
585
586 let files_fdt_offset: u64 = 0;
588 let fdt_max_fds_offset: u64 = 0;
590 let fdt_fd_offset: u64 = 8;
591 let file_fpath_offset: u64 = 0;
593 let path_dentry_offset: u64 = 8;
594 let dentry_inode_offset: u64 = 0;
596 let dentry_dname_offset: u64 = 16;
597 let qstr_name_offset: u64 = 0;
599 let inode_nlink_offset: u64 = 0;
601 let inode_size_offset: u64 = 8;
602 let inode_ino_offset: u64 = 16;
603
604 let mut task_page = [0u8; 4096];
606 task_page[0..4].copy_from_slice(&999u32.to_le_bytes());
608 let list_self = task_vaddr + tasks_offset;
610 task_page[tasks_offset as usize..tasks_offset as usize + 8]
611 .copy_from_slice(&list_self.to_le_bytes());
612 task_page[tasks_offset as usize + 8..tasks_offset as usize + 16]
613 .copy_from_slice(&list_self.to_le_bytes());
614 task_page[task_comm_offset as usize..task_comm_offset as usize + 7]
616 .copy_from_slice(b"malware");
617 task_page[task_files_offset as usize..task_files_offset as usize + 8]
619 .copy_from_slice(&files_vaddr.to_le_bytes());
620
621 let mut files_page = [0u8; 4096];
623 files_page[files_fdt_offset as usize..files_fdt_offset as usize + 8]
624 .copy_from_slice(&fdt_vaddr.to_le_bytes());
625
626 let mut fdt_page = [0u8; 4096];
628 fdt_page[fdt_max_fds_offset as usize..fdt_max_fds_offset as usize + 4]
629 .copy_from_slice(&10u32.to_le_bytes()); fdt_page[fdt_fd_offset as usize..fdt_fd_offset as usize + 8]
631 .copy_from_slice(&fd_arr_vaddr.to_le_bytes());
632
633 let mut fd_arr_page = [0u8; 4096];
635 fd_arr_page[0..8].copy_from_slice(&file_vaddr.to_le_bytes());
636 let mut file_page = [0u8; 4096];
641 file_page[(file_fpath_offset + path_dentry_offset) as usize
642 ..(file_fpath_offset + path_dentry_offset) as usize + 8]
643 .copy_from_slice(&dentry_vaddr.to_le_bytes());
644
645 let mut dentry_page = [0u8; 4096];
649 dentry_page[dentry_inode_offset as usize..dentry_inode_offset as usize + 8]
650 .copy_from_slice(&inode_vaddr.to_le_bytes());
651 dentry_page[(dentry_dname_offset + qstr_name_offset) as usize
652 ..(dentry_dname_offset + qstr_name_offset) as usize + 8]
653 .copy_from_slice(&name_vaddr.to_le_bytes());
654
655 let mut inode_page = [0u8; 4096];
657 inode_page[inode_nlink_offset as usize..inode_nlink_offset as usize + 4]
658 .copy_from_slice(&0u32.to_le_bytes()); inode_page[inode_size_offset as usize..inode_size_offset as usize + 8]
660 .copy_from_slice(&4096u64.to_le_bytes());
661 inode_page[inode_ino_offset as usize..inode_ino_offset as usize + 8]
662 .copy_from_slice(&42u64.to_le_bytes());
663
664 let mut name_page = [0u8; 4096];
666 name_page[..10].copy_from_slice(b"hidden.so\0");
667
668 let isf = IsfBuilder::new()
669 .add_struct("task_struct", 256)
670 .add_field("task_struct", "pid", 0u64, "unsigned int")
671 .add_field("task_struct", "tasks", tasks_offset, "list_head")
672 .add_field("task_struct", "comm", task_comm_offset, "char")
673 .add_field("task_struct", "files", task_files_offset, "pointer")
674 .add_struct("list_head", 16)
675 .add_field("list_head", "next", 0u64, "pointer")
676 .add_field("list_head", "prev", 8u64, "pointer")
677 .add_struct("files_struct", 64)
678 .add_field("files_struct", "fdt", files_fdt_offset, "pointer")
679 .add_struct("fdtable", 64)
680 .add_field("fdtable", "max_fds", fdt_max_fds_offset, "unsigned int")
681 .add_field("fdtable", "fd", fdt_fd_offset, "pointer")
682 .add_struct("file", 256)
683 .add_field("file", "f_path", file_fpath_offset, "path")
684 .add_struct("path", 16)
685 .add_field("path", "dentry", path_dentry_offset, "pointer")
686 .add_struct("dentry", 256)
687 .add_field("dentry", "d_inode", dentry_inode_offset, "pointer")
688 .add_field("dentry", "d_name", dentry_dname_offset, "qstr")
689 .add_struct("qstr", 16)
690 .add_field("qstr", "name", qstr_name_offset, "pointer")
691 .add_struct("inode", 256)
692 .add_field("inode", "i_nlink", inode_nlink_offset, "unsigned int")
693 .add_field("inode", "i_size", inode_size_offset, "long")
694 .add_field("inode", "i_ino", inode_ino_offset, "unsigned long")
695 .add_symbol("init_task", task_vaddr)
696 .build_json();
697
698 let resolver = IsfResolver::from_value(&isf).unwrap();
699 let (cr3, mem) = PageTableBuilder::new()
700 .map_4k(task_vaddr, task_paddr, flags::WRITABLE)
701 .write_phys(task_paddr, &task_page)
702 .map_4k(files_vaddr, files_paddr, flags::WRITABLE)
703 .write_phys(files_paddr, &files_page)
704 .map_4k(fdt_vaddr, fdt_paddr, flags::WRITABLE)
705 .write_phys(fdt_paddr, &fdt_page)
706 .map_4k(fd_arr_vaddr, fd_arr_paddr, flags::WRITABLE)
707 .write_phys(fd_arr_paddr, &fd_arr_page)
708 .map_4k(file_vaddr, file_paddr, flags::WRITABLE)
709 .write_phys(file_paddr, &file_page)
710 .map_4k(dentry_vaddr, dentry_paddr, flags::WRITABLE)
711 .write_phys(dentry_paddr, &dentry_page)
712 .map_4k(inode_vaddr, inode_paddr, flags::WRITABLE)
713 .write_phys(inode_paddr, &inode_page)
714 .map_4k(name_vaddr, name_paddr, flags::WRITABLE)
715 .write_phys(name_paddr, &name_page)
716 .build();
717
718 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
719 let reader = ObjectReader::new(vas, Box::new(resolver));
720
721 let results = walk_dentry_cache(&reader).expect("walk should succeed");
722 assert_eq!(results.len(), 1, "should detect exactly one hidden dentry");
723 let entry = &results[0];
724 assert_eq!(entry.pid, 999);
725 assert_eq!(entry.fd, 0);
726 assert_eq!(entry.nlink, 0, "file must be unlinked");
727 assert!(entry.is_suspicious, "unlinked .so must be suspicious");
728 assert_eq!(entry.filename, "hidden.so");
729 assert_eq!(entry.inode_num, 42);
730 assert_eq!(entry.file_size, 4096);
731 assert!(
732 entry.comm.contains("malware"),
733 "comm should contain 'malware'"
734 );
735 }
736
737 #[test]
741 fn walk_dentry_null_dentry_ptr_skipped() {
742 let task_vaddr: u64 = 0xFFFF_C900_0100_0000;
744 let files_vaddr: u64 = 0xFFFF_C900_0101_0000;
745 let fdt_vaddr: u64 = 0xFFFF_C900_0102_0000;
746 let fd_arr_vaddr: u64 = 0xFFFF_C900_0103_0000;
747 let file_vaddr: u64 = 0xFFFF_C900_0104_0000;
748
749 let task_paddr: u64 = 0x018_000;
750 let files_paddr: u64 = 0x019_000;
751 let fdt_paddr: u64 = 0x01A_000;
752 let fd_arr_paddr: u64 = 0x01B_000;
753 let file_paddr: u64 = 0x01C_000;
754
755 let tasks_offset: u64 = 8;
756 let task_files_offset: u64 = 40;
757 let path_dentry_offset: u64 = 8;
758
759 let mut task_page = [0u8; 4096];
760 task_page[0..4].copy_from_slice(&1001u32.to_le_bytes());
761 let list_self = task_vaddr + tasks_offset;
762 task_page[tasks_offset as usize..tasks_offset as usize + 8]
763 .copy_from_slice(&list_self.to_le_bytes());
764 task_page[tasks_offset as usize + 8..tasks_offset as usize + 16]
765 .copy_from_slice(&list_self.to_le_bytes());
766 task_page[task_files_offset as usize..task_files_offset as usize + 8]
767 .copy_from_slice(&files_vaddr.to_le_bytes());
768
769 let mut files_page = [0u8; 4096];
770 files_page[0..8].copy_from_slice(&fdt_vaddr.to_le_bytes());
771
772 let mut fdt_page = [0u8; 4096];
774 fdt_page[0..4].copy_from_slice(&8u32.to_le_bytes()); fdt_page[8..16].copy_from_slice(&fd_arr_vaddr.to_le_bytes());
776
777 let mut fd_arr_page = [0u8; 4096];
778 fd_arr_page[0..8].copy_from_slice(&file_vaddr.to_le_bytes());
779
780 let mut file_page = [0u8; 4096];
782 file_page[path_dentry_offset as usize..path_dentry_offset as usize + 8]
783 .copy_from_slice(&0u64.to_le_bytes());
784
785 let isf = IsfBuilder::new()
786 .add_struct("task_struct", 256)
787 .add_field("task_struct", "pid", 0u64, "unsigned int")
788 .add_field("task_struct", "tasks", tasks_offset, "list_head")
789 .add_field("task_struct", "comm", 24u64, "char")
790 .add_field("task_struct", "files", task_files_offset, "pointer")
791 .add_struct("list_head", 16)
792 .add_field("list_head", "next", 0u64, "pointer")
793 .add_field("list_head", "prev", 8u64, "pointer")
794 .add_struct("files_struct", 64)
795 .add_field("files_struct", "fdt", 0u64, "pointer")
796 .add_struct("fdtable", 64)
797 .add_field("fdtable", "max_fds", 0u64, "unsigned int")
798 .add_field("fdtable", "fd", 8u64, "pointer")
799 .add_struct("file", 256)
800 .add_field("file", "f_path", 0u64, "path")
801 .add_struct("path", 16)
802 .add_field("path", "dentry", path_dentry_offset, "pointer")
803 .add_struct("dentry", 256)
804 .add_field("dentry", "d_inode", 0u64, "pointer")
805 .add_field("dentry", "d_name", 16u64, "qstr")
806 .add_struct("qstr", 16)
807 .add_field("qstr", "name", 0u64, "pointer")
808 .add_struct("inode", 256)
809 .add_field("inode", "i_nlink", 0u64, "unsigned int")
810 .add_field("inode", "i_size", 8u64, "long")
811 .add_field("inode", "i_ino", 16u64, "unsigned long")
812 .add_symbol("init_task", task_vaddr)
813 .build_json();
814
815 let resolver = IsfResolver::from_value(&isf).unwrap();
816 let (cr3, mem) = PageTableBuilder::new()
817 .map_4k(task_vaddr, task_paddr, flags::WRITABLE)
818 .write_phys(task_paddr, &task_page)
819 .map_4k(files_vaddr, files_paddr, flags::WRITABLE)
820 .write_phys(files_paddr, &files_page)
821 .map_4k(fdt_vaddr, fdt_paddr, flags::WRITABLE)
822 .write_phys(fdt_paddr, &fdt_page)
823 .map_4k(fd_arr_vaddr, fd_arr_paddr, flags::WRITABLE)
824 .write_phys(fd_arr_paddr, &fd_arr_page)
825 .map_4k(file_vaddr, file_paddr, flags::WRITABLE)
826 .write_phys(file_paddr, &file_page)
827 .build();
828
829 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
830 let reader = ObjectReader::new(vas, Box::new(resolver));
831
832 let results = walk_dentry_cache(&reader).expect("walk should succeed");
833 assert!(
834 results.is_empty(),
835 "null dentry ptr should produce no results"
836 );
837 }
838
839 #[test]
843 fn walk_dentry_linked_benign_file_skipped() {
844 let task_vaddr: u64 = 0xFFFF_CA00_0100_0000;
847 let files_vaddr: u64 = 0xFFFF_CA00_0101_0000;
848 let fdt_vaddr: u64 = 0xFFFF_CA00_0102_0000;
849 let fd_arr_vaddr: u64 = 0xFFFF_CA00_0103_0000;
850 let file_vaddr: u64 = 0xFFFF_CA00_0104_0000;
851 let dentry_vaddr: u64 = 0xFFFF_CA00_0105_0000;
852 let inode_vaddr: u64 = 0xFFFF_CA00_0106_0000;
853 let name_vaddr: u64 = 0xFFFF_CA00_0107_0000;
854
855 let task_paddr: u64 = 0x01D_000;
856 let files_paddr: u64 = 0x01E_000;
857 let fdt_paddr: u64 = 0x01F_000;
858 let fd_arr_paddr: u64 = 0x020_000;
859 let file_paddr: u64 = 0x021_000;
860 let dentry_paddr: u64 = 0x022_000;
861 let inode_paddr: u64 = 0x023_000;
862 let name_paddr: u64 = 0x024_000;
863
864 let tasks_offset: u64 = 8;
865 let task_files_offset: u64 = 40;
866 let path_dentry_offset: u64 = 8;
867
868 let mut task_page = [0u8; 4096];
869 task_page[0..4].copy_from_slice(&1002u32.to_le_bytes());
870 let list_self = task_vaddr + tasks_offset;
871 task_page[tasks_offset as usize..tasks_offset as usize + 8]
872 .copy_from_slice(&list_self.to_le_bytes());
873 task_page[tasks_offset as usize + 8..tasks_offset as usize + 16]
874 .copy_from_slice(&list_self.to_le_bytes());
875 task_page[24..31].copy_from_slice(b"benign\0");
876 task_page[task_files_offset as usize..task_files_offset as usize + 8]
877 .copy_from_slice(&files_vaddr.to_le_bytes());
878
879 let mut files_page = [0u8; 4096];
880 files_page[0..8].copy_from_slice(&fdt_vaddr.to_le_bytes());
881
882 let mut fdt_page = [0u8; 4096];
884 fdt_page[0..4].copy_from_slice(&8u32.to_le_bytes()); fdt_page[8..16].copy_from_slice(&fd_arr_vaddr.to_le_bytes());
886
887 let mut fd_arr_page = [0u8; 4096];
888 fd_arr_page[0..8].copy_from_slice(&file_vaddr.to_le_bytes());
889
890 let mut file_page = [0u8; 4096];
891 file_page[path_dentry_offset as usize..path_dentry_offset as usize + 8]
892 .copy_from_slice(&dentry_vaddr.to_le_bytes());
893
894 let mut dentry_page = [0u8; 4096];
895 dentry_page[0..8].copy_from_slice(&inode_vaddr.to_le_bytes());
896 dentry_page[16..24].copy_from_slice(&name_vaddr.to_le_bytes());
898
899 let mut inode_page = [0u8; 4096];
901 inode_page[0..4].copy_from_slice(&2u32.to_le_bytes());
902 inode_page[8..16].copy_from_slice(&1024u64.to_le_bytes());
903 inode_page[16..24].copy_from_slice(&99u64.to_le_bytes());
904
905 let mut name_page = [0u8; 4096];
907 name_page[..9].copy_from_slice(b"data.txt\0");
908
909 let isf = IsfBuilder::new()
910 .add_struct("task_struct", 256)
911 .add_field("task_struct", "pid", 0u64, "unsigned int")
912 .add_field("task_struct", "tasks", tasks_offset, "list_head")
913 .add_field("task_struct", "comm", 24u64, "char")
914 .add_field("task_struct", "files", task_files_offset, "pointer")
915 .add_struct("list_head", 16)
916 .add_field("list_head", "next", 0u64, "pointer")
917 .add_field("list_head", "prev", 8u64, "pointer")
918 .add_struct("files_struct", 64)
919 .add_field("files_struct", "fdt", 0u64, "pointer")
920 .add_struct("fdtable", 64)
921 .add_field("fdtable", "max_fds", 0u64, "unsigned int")
922 .add_field("fdtable", "fd", 8u64, "pointer")
923 .add_struct("file", 256)
924 .add_field("file", "f_path", 0u64, "path")
925 .add_struct("path", 16)
926 .add_field("path", "dentry", path_dentry_offset, "pointer")
927 .add_struct("dentry", 256)
928 .add_field("dentry", "d_inode", 0u64, "pointer")
929 .add_field("dentry", "d_name", 16u64, "qstr")
930 .add_struct("qstr", 16)
931 .add_field("qstr", "name", 0u64, "pointer")
932 .add_struct("inode", 256)
933 .add_field("inode", "i_nlink", 0u64, "unsigned int")
934 .add_field("inode", "i_size", 8u64, "long")
935 .add_field("inode", "i_ino", 16u64, "unsigned long")
936 .add_symbol("init_task", task_vaddr)
937 .build_json();
938
939 let resolver = IsfResolver::from_value(&isf).unwrap();
940 let (cr3, mem) = PageTableBuilder::new()
941 .map_4k(task_vaddr, task_paddr, flags::WRITABLE)
942 .write_phys(task_paddr, &task_page)
943 .map_4k(files_vaddr, files_paddr, flags::WRITABLE)
944 .write_phys(files_paddr, &files_page)
945 .map_4k(fdt_vaddr, fdt_paddr, flags::WRITABLE)
946 .write_phys(fdt_paddr, &fdt_page)
947 .map_4k(fd_arr_vaddr, fd_arr_paddr, flags::WRITABLE)
948 .write_phys(fd_arr_paddr, &fd_arr_page)
949 .map_4k(file_vaddr, file_paddr, flags::WRITABLE)
950 .write_phys(file_paddr, &file_page)
951 .map_4k(dentry_vaddr, dentry_paddr, flags::WRITABLE)
952 .write_phys(dentry_paddr, &dentry_page)
953 .map_4k(inode_vaddr, inode_paddr, flags::WRITABLE)
954 .write_phys(inode_paddr, &inode_page)
955 .map_4k(name_vaddr, name_paddr, flags::WRITABLE)
956 .write_phys(name_paddr, &name_page)
957 .build();
958
959 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
960 let reader = ObjectReader::new(vas, Box::new(resolver));
961
962 let results = walk_dentry_cache(&reader).expect("walk should succeed");
963 assert!(
964 results.is_empty(),
965 "benign linked file should not appear in results"
966 );
967 }
968}