use std::collections::{HashMap, HashSet};
use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::{vma_walker::for_each_task_vma, Error, Result};
const MAX_LIBS: usize = 4096;
#[derive(Debug, Clone, serde::Serialize)]
pub struct SharedLibraryInfo {
pub pid: u32,
pub process_name: String,
pub lib_path: String,
pub base_addr: u64,
pub size: u64,
pub is_suspicious: bool,
}
pub use crate::heuristics::classify_library;
pub fn walk_library_list<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
task_addr: u64,
pid: u32,
process_name: &str,
) -> Result<Vec<SharedLibraryInfo>> {
let f_path_offset = reader
.symbols()
.field_offset("file", "f_path")
.ok_or_else(|| Error::MissingField {
struct_name: "file".into(),
field_name: "f_path".into(),
})?;
let dentry_in_path_offset =
reader
.symbols()
.field_offset("path", "dentry")
.ok_or_else(|| Error::MissingField {
struct_name: "path".into(),
field_name: "dentry".into(),
})?;
let d_name_offset = reader
.symbols()
.field_offset("dentry", "d_name")
.ok_or_else(|| Error::MissingField {
struct_name: "dentry".into(),
field_name: "d_name".into(),
})?;
let name_in_qstr_offset = reader
.symbols()
.field_offset("qstr", "name")
.ok_or_else(|| Error::MissingField {
struct_name: "qstr".into(),
field_name: "name".into(),
})?;
let mut lib_map: HashMap<String, (u64, u64)> = HashMap::new();
let mut seen_addrs: HashSet<u64> = HashSet::new();
let mut limit_reached = false;
for_each_task_vma(reader, task_addr, &mut |e| {
if !seen_addrs.insert(e.vma_addr) {
limit_reached = true;
return;
}
if limit_reached || lib_map.len() >= MAX_LIBS {
limit_reached = true;
return;
}
if e.file_ptr != 0 {
if let Some(name) = read_vma_file_path(
reader,
e.file_ptr,
f_path_offset,
dentry_in_path_offset,
d_name_offset,
name_in_qstr_offset,
) {
if name.contains(".so") {
let size = e.end.saturating_sub(e.start);
let entry = lib_map.entry(name).or_insert((e.start, 0));
entry.0 = entry.0.min(e.start);
entry.1 += size;
}
}
}
});
let mut libs: Vec<SharedLibraryInfo> = lib_map
.into_iter()
.map(|(lib_path, (base_addr, size))| {
let is_suspicious = classify_library(&lib_path);
SharedLibraryInfo {
pid,
process_name: process_name.to_string(),
lib_path,
base_addr,
size,
is_suspicious,
}
})
.collect();
libs.sort_by_key(|lib| lib.base_addr);
Ok(libs)
}
fn read_vma_file_path<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
file_ptr: u64,
f_path_offset: u64,
dentry_in_path_offset: u64,
d_name_offset: u64,
name_in_qstr_offset: u64,
) -> Option<String> {
let dentry_addr = file_ptr + f_path_offset + dentry_in_path_offset;
let dentry_raw = reader.read_bytes(dentry_addr, 8).ok()?;
let dentry_ptr = u64::from_le_bytes(dentry_raw.try_into().ok()?);
if dentry_ptr == 0 {
return None;
}
let name_addr = dentry_ptr + d_name_offset + name_in_qstr_offset;
let name_raw = reader.read_bytes(name_addr, 8).ok()?;
let name_ptr = u64::from_le_bytes(name_raw.try_into().ok()?);
if name_ptr == 0 {
return None;
}
let name = reader.read_string(name_ptr, 256).ok()?;
if name.is_empty() {
return None;
}
Some(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_standard_lib_benign() {
assert!(
!classify_library("/usr/lib/x86_64-linux-gnu/libc.so.6"),
"standard libc path should not be suspicious"
);
assert!(
!classify_library("/usr/lib/libpthread.so.0"),
"standard libpthread should not be suspicious"
);
assert!(
!classify_library("/lib64/ld-linux-x86-64.so.2"),
"dynamic linker should not be suspicious"
);
}
#[test]
fn classify_tmp_suspicious() {
assert!(
classify_library("/tmp/evil.so"),
"/tmp library should be suspicious"
);
assert!(
classify_library("/tmp/subdir/payload.so"),
"/tmp subdirectory should be suspicious"
);
}
#[test]
fn classify_devshm_suspicious() {
assert!(
classify_library("/dev/shm/inject.so"),
"/dev/shm library should be suspicious"
);
assert!(
classify_library("/dev/shm/hidden/hook.so.1"),
"/dev/shm subdirectory should be suspicious"
);
}
#[test]
fn classify_deleted_suspicious() {
assert!(
classify_library("/usr/lib/libfoo.so (deleted)"),
"deleted library should be suspicious"
);
assert!(
classify_library("/tmp/rootkit.so (deleted)"),
"deleted library from /tmp should be suspicious"
);
}
#[test]
fn classify_hidden_file_suspicious() {
assert!(
classify_library("/home/user/.hidden_lib.so"),
"hidden file should be suspicious"
);
assert!(
classify_library("/opt/app/.sneaky.so.1"),
"hidden file with version should be suspicious"
);
}
#[test]
fn classify_non_so_suspicious() {
assert!(
classify_library("/usr/lib/not_a_library.bin"),
"non-.so file should be suspicious"
);
assert!(
classify_library("/usr/lib/strange_mapping"),
"file without .so extension should be suspicious"
);
}
#[test]
fn classify_var_tmp_suspicious() {
assert!(
classify_library("/var/tmp/staged.so"),
"/var/tmp library should be suspicious"
);
}
use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
fn make_test_reader(data: &[u8], vaddr: u64, paddr: u64) -> ObjectReader<SyntheticPhysMem> {
let isf = IsfBuilder::new()
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "comm", 32, "char")
.add_field("task_struct", "mm", 48, "pointer")
.add_struct("mm_struct", 128)
.add_field("mm_struct", "mmap", 8, "pointer")
.add_struct("vm_area_struct", 64)
.add_field("vm_area_struct", "vm_start", 0, "unsigned long")
.add_field("vm_area_struct", "vm_end", 8, "unsigned long")
.add_field("vm_area_struct", "vm_next", 16, "pointer")
.add_field("vm_area_struct", "vm_file", 40, "pointer")
.add_struct("file", 64)
.add_field("file", "f_path", 0, "path")
.add_struct("path", 16)
.add_field("path", "dentry", 8, "pointer")
.add_struct("dentry", 64)
.add_field("dentry", "d_name", 0, "qstr")
.add_struct("qstr", 16)
.add_field("qstr", "name", 8, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(vaddr, paddr, flags::WRITABLE)
.write_phys(paddr, data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn walk_no_vma_returns_empty() {
let vaddr: u64 = 0xFFFF_8000_0010_0000;
let paddr: u64 = 0x0080_0000;
let mut data = vec![0u8; 4096];
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);
let result = walk_library_list(&reader, vaddr, 2, "kthreadd").unwrap();
assert!(result.is_empty(), "kernel thread should have no libraries");
}
#[test]
fn walk_single_so_library() {
let vaddr: u64 = 0xFFFF_8000_0010_0000;
let paddr: u64 = 0x0080_0000;
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&1u32.to_le_bytes()); data[32..36].copy_from_slice(b"bash"); let mm_addr = vaddr + 0x200;
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let vma_addr = vaddr + 0x300;
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;
data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes());
let dentry_addr = vaddr + 0x500;
data[0x408..0x410].copy_from_slice(&dentry_addr.to_le_bytes());
let name_str_addr = vaddr + 0x600;
data[0x508..0x510].copy_from_slice(&name_str_addr.to_le_bytes());
let name = b"libc.so.6";
data[0x600..0x600 + name.len()].copy_from_slice(name);
let reader = make_test_reader(&data, vaddr, paddr);
let libs = walk_library_list(&reader, vaddr, 1, "bash").unwrap();
assert_eq!(libs.len(), 1);
assert_eq!(libs[0].pid, 1);
assert_eq!(libs[0].process_name, "bash");
assert_eq!(libs[0].lib_path, "libc.so.6");
assert_eq!(libs[0].base_addr, 0x7F00_0000);
assert_eq!(libs[0].size, 0x0001_0000);
assert!(!libs[0].is_suspicious);
}
#[test]
fn walk_deduplicates_multi_vma_library() {
let vaddr: u64 = 0xFFFF_8000_0010_0000;
let paddr: u64 = 0x0080_0000;
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&1u32.to_le_bytes()); data[32..36].copy_from_slice(b"cat\0"); let mm_addr = vaddr + 0x200;
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let vma1_addr = vaddr + 0x300;
data[0x208..0x210].copy_from_slice(&vma1_addr.to_le_bytes());
let file_addr = vaddr + 0x500;
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;
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;
data[0x508..0x510].copy_from_slice(&dentry_addr.to_le_bytes());
let name_addr = vaddr + 0x700;
data[0x608..0x610].copy_from_slice(&name_addr.to_le_bytes());
let name = b"libpthread.so.0";
data[0x700..0x700 + name.len()].copy_from_slice(name);
let reader = make_test_reader(&data, vaddr, paddr);
let libs = walk_library_list(&reader, vaddr, 1, "cat").unwrap();
assert_eq!(libs.len(), 1);
assert_eq!(libs[0].lib_path, "libpthread.so.0");
assert_eq!(libs[0].base_addr, 0x7F00_0000);
assert_eq!(libs[0].size, 0x6000);
assert!(!libs[0].is_suspicious);
}
#[test]
fn walk_skips_non_file_backed_vmas() {
let vaddr: u64 = 0xFFFF_8000_0010_0000;
let paddr: u64 = 0x0080_0000;
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&1u32.to_le_bytes());
data[32..36].copy_from_slice(b"test");
let mm_addr = vaddr + 0x200;
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let vma_addr = vaddr + 0x300;
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);
let libs = walk_library_list(&reader, vaddr, 1, "test").unwrap();
assert!(
libs.is_empty(),
"anonymous VMA should not produce library entries"
);
}
#[test]
fn classify_library_exact_tmp_dir() {
assert!(
classify_library("/tmp"),
"exact /tmp path must be suspicious"
);
assert!(
classify_library("/dev/shm"),
"/dev/shm exact match must be suspicious"
);
assert!(
classify_library("/var/tmp"),
"/var/tmp exact match must be suspicious"
);
}
#[test]
fn classify_library_just_dot_basename_not_suspicious() {
assert!(
!classify_library("/usr/lib/normallib.so"),
"normal .so must be benign"
);
}
#[test]
fn walk_cycle_detection_breaks_loop() {
let vaddr: u64 = 0xFFFF_8000_0050_0000;
let paddr: u64 = 0x0083_0000;
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&10u32.to_le_bytes()); data[32..36].copy_from_slice(b"cycl"); let mm_addr = vaddr + 0x200;
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let vma_addr = vaddr + 0x300;
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(&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);
let libs = walk_library_list(&reader, vaddr, 10, "cycl").unwrap();
assert!(
libs.is_empty(),
"cycle VMA with null vm_file should yield no libraries"
);
}
#[test]
fn walk_second_vma_with_lower_base_updates_min() {
let vaddr: u64 = 0xFFFF_8000_0060_0000;
let paddr: u64 = 0x0084_0000;
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&20u32.to_le_bytes()); data[32..37].copy_from_slice(b"proc\0"); let mm_addr = vaddr + 0x100;
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let vma1_addr = vaddr + 0x200;
data[0x108..0x110].copy_from_slice(&vma1_addr.to_le_bytes());
let file_addr = vaddr + 0x600;
let vma2_addr = vaddr + 0x300;
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;
data[0x608..0x610].copy_from_slice(&dentry_addr.to_le_bytes());
let name_addr = vaddr + 0x800;
data[0x708..0x710].copy_from_slice(&name_addr.to_le_bytes());
let name = b"libtest.so.1";
data[0x800..0x800 + name.len()].copy_from_slice(name);
let reader = make_test_reader(&data, vaddr, paddr);
let libs = walk_library_list(&reader, vaddr, 20, "proc").unwrap();
assert_eq!(libs.len(), 1, "single deduplicated library expected");
assert_eq!(
libs[0].base_addr, 0x7F00_0000,
"base_addr must be the minimum vm_start"
);
assert_eq!(libs[0].size, 0x4000);
}
#[test]
fn walk_skips_vma_when_dentry_null() {
let vaddr: u64 = 0xFFFF_8000_0070_0000;
let paddr: u64 = 0x0085_0000;
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&30u32.to_le_bytes()); data[32..36].copy_from_slice(b"null"); let mm_addr = vaddr + 0x200;
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let vma_addr = vaddr + 0x300;
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(&0x7F00_2000u64.to_le_bytes()); data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); let file_addr = vaddr + 0x400;
data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes());
data[0x408..0x410].copy_from_slice(&0u64.to_le_bytes());
let reader = make_test_reader(&data, vaddr, paddr);
let libs = walk_library_list(&reader, vaddr, 30, "null").unwrap();
assert!(
libs.is_empty(),
"null dentry_ptr → read_vma_file_path returns None → no library"
);
}
#[test]
fn walk_skips_vma_when_name_ptr_null() {
let vaddr: u64 = 0xFFFF_8000_0078_0000;
let paddr: u64 = 0x0086_0000;
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&31u32.to_le_bytes());
data[32..36].copy_from_slice(b"npnl");
let mm_addr = vaddr + 0x200;
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let vma_addr = vaddr + 0x300;
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(&0x7F00_2000u64.to_le_bytes());
data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); let file_addr = vaddr + 0x400;
data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes());
let dentry_addr = vaddr + 0x500;
data[0x408..0x410].copy_from_slice(&dentry_addr.to_le_bytes());
data[0x508..0x510].copy_from_slice(&0u64.to_le_bytes());
let reader = make_test_reader(&data, vaddr, paddr);
let libs = walk_library_list(&reader, vaddr, 31, "npnl").unwrap();
assert!(
libs.is_empty(),
"name_ptr == 0 → read_vma_file_path returns None → no library"
);
}
#[test]
fn walk_skips_vma_when_name_empty() {
let vaddr: u64 = 0xFFFF_8000_0079_0000;
let paddr: u64 = 0x0087_0000;
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&32u32.to_le_bytes());
data[32..36].copy_from_slice(b"empt");
let mm_addr = vaddr + 0x200;
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let vma_addr = vaddr + 0x300;
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(&0x7F00_2000u64.to_le_bytes());
data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes());
let file_addr = vaddr + 0x400;
data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes());
let dentry_addr = vaddr + 0x500;
data[0x408..0x410].copy_from_slice(&dentry_addr.to_le_bytes());
let name_str_addr = vaddr + 0x600;
data[0x508..0x510].copy_from_slice(&name_str_addr.to_le_bytes());
data[0x600] = 0u8;
let reader = make_test_reader(&data, vaddr, paddr);
let libs = walk_library_list(&reader, vaddr, 32, "empt").unwrap();
assert!(
libs.is_empty(),
"empty name → read_vma_file_path returns None"
);
}
#[test]
fn shared_library_info_debug_clone_serialize() {
let info = SharedLibraryInfo {
pid: 1,
process_name: "test".to_string(),
lib_path: "/usr/lib/libfoo.so".to_string(),
base_addr: 0x7F00_0000,
size: 0x1000,
is_suspicious: false,
};
let cloned = info.clone();
let dbg = format!("{cloned:?}");
assert!(dbg.contains("libfoo"));
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("\"pid\":1"));
assert!(json.contains("is_suspicious"));
}
#[test]
fn walk_classifies_suspicious_library() {
let vaddr: u64 = 0xFFFF_8000_0010_0000;
let paddr: u64 = 0x0080_0000;
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&42u32.to_le_bytes());
data[32..37].copy_from_slice(b"sshd\0");
let mm_addr = vaddr + 0x200;
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let vma_addr = vaddr + 0x300;
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(&0x7F00_2000u64.to_le_bytes());
data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); let file_addr = vaddr + 0x400;
data[0x328..0x330].copy_from_slice(&file_addr.to_le_bytes());
let dentry_addr = vaddr + 0x500;
data[0x408..0x410].copy_from_slice(&dentry_addr.to_le_bytes());
let name_addr = vaddr + 0x600;
data[0x508..0x510].copy_from_slice(&name_addr.to_le_bytes());
let name = b"/tmp/evil.so";
data[0x600..0x600 + name.len()].copy_from_slice(name);
let reader = make_test_reader(&data, vaddr, paddr);
let libs = walk_library_list(&reader, vaddr, 42, "sshd").unwrap();
assert_eq!(libs.len(), 1);
assert_eq!(libs[0].lib_path, "/tmp/evil.so");
assert!(libs[0].is_suspicious, "/tmp library should be suspicious");
}
#[test]
fn walk_library_list_missing_f_path_field_returns_error() {
let vaddr: u64 = 0xFFFF_8000_0088_0000;
let paddr: u64 = 0x0088_1000;
let mut data = vec![0u8; 4096];
let mm_addr = vaddr + 0x200;
data[0..4].copy_from_slice(&9u32.to_le_bytes());
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "comm", 32, "char")
.add_field("task_struct", "mm", 48, "pointer")
.add_struct("mm_struct", 128)
.add_field("mm_struct", "mmap", 8, "pointer")
.add_struct("path", 16)
.add_field("path", "dentry", 8, "pointer")
.add_struct("dentry", 64)
.add_field("dentry", "d_name", 0, "qstr")
.add_struct("qstr", 16)
.add_field("qstr", "name", 8, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(vaddr, paddr, flags::WRITABLE)
.write_phys(paddr, &data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_library_list(&reader, vaddr, 9, "proc");
assert!(
result.is_err(),
"missing file.f_path field must return an error"
);
}
#[test]
fn walk_library_list_missing_path_dentry_field_returns_error() {
let vaddr: u64 = 0xFFFF_8000_0089_0000;
let paddr: u64 = 0x0089_0000;
let mut data = vec![0u8; 4096];
let mm_addr = vaddr + 0x200;
data[0..4].copy_from_slice(&10u32.to_le_bytes());
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "comm", 32, "char")
.add_field("task_struct", "mm", 48, "pointer")
.add_struct("mm_struct", 128)
.add_field("mm_struct", "mmap", 8, "pointer")
.add_struct("file", 64)
.add_field("file", "f_path", 0, "path")
.add_struct("path", 16)
.add_struct("dentry", 64)
.add_field("dentry", "d_name", 0, "qstr")
.add_struct("qstr", 16)
.add_field("qstr", "name", 8, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(vaddr, paddr, flags::WRITABLE)
.write_phys(paddr, &data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_library_list(&reader, vaddr, 10, "proc");
assert!(
result.is_err(),
"missing path.dentry field must return an error"
);
}
#[test]
fn walk_library_list_missing_d_name_field_returns_error() {
let vaddr: u64 = 0xFFFF_8000_008A_0000;
let paddr: u64 = 0x008A_0000;
let mut data = vec![0u8; 4096];
let mm_addr = vaddr + 0x200;
data[0..4].copy_from_slice(&11u32.to_le_bytes());
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "comm", 32, "char")
.add_field("task_struct", "mm", 48, "pointer")
.add_struct("mm_struct", 128)
.add_field("mm_struct", "mmap", 8, "pointer")
.add_struct("file", 64)
.add_field("file", "f_path", 0, "path")
.add_struct("path", 16)
.add_field("path", "dentry", 8, "pointer")
.add_struct("dentry", 64)
.add_struct("qstr", 16)
.add_field("qstr", "name", 8, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(vaddr, paddr, flags::WRITABLE)
.write_phys(paddr, &data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_library_list(&reader, vaddr, 11, "proc");
assert!(
result.is_err(),
"missing dentry.d_name field must return an error"
);
}
#[test]
fn walk_library_list_missing_qstr_name_field_returns_error() {
let vaddr: u64 = 0xFFFF_8000_008B_0000;
let paddr: u64 = 0x008B_0000;
let mut data = vec![0u8; 4096];
let mm_addr = vaddr + 0x200;
data[0..4].copy_from_slice(&12u32.to_le_bytes());
data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "comm", 32, "char")
.add_field("task_struct", "mm", 48, "pointer")
.add_struct("mm_struct", 128)
.add_field("mm_struct", "mmap", 8, "pointer")
.add_struct("file", 64)
.add_field("file", "f_path", 0, "path")
.add_struct("path", 16)
.add_field("path", "dentry", 8, "pointer")
.add_struct("dentry", 64)
.add_field("dentry", "d_name", 0, "qstr")
.add_struct("qstr", 16)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(vaddr, paddr, flags::WRITABLE)
.write_phys(paddr, &data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_library_list(&reader, vaddr, 12, "proc");
assert!(
result.is_err(),
"missing qstr.name field must return an error"
);
}
#[test]
fn classify_library_no_slash_path() {
assert!(
!classify_library("libc.so.6"),
"bare name with .so. must be benign"
);
assert!(
classify_library(".hidden.so.1"),
"hidden bare name must be suspicious"
);
}
#[test]
fn missing_file_f_path_field_returns_missing_field() {
let isf = IsfBuilder::new()
.add_struct("file", 64)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_library_list(&reader, 0xFFFF_8000_0010_0000, 1, "init");
assert!(
matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "file" && field_name == "f_path"),
"expected MissingField file.f_path, got {result:?}"
);
}
}