use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::{Error, HiddenModuleInfo, Result};
fn check_module_in_sysfs<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
mod_addr: u64,
) -> bool {
let mkobj_offset = match reader.symbols().field_offset("module", "mkobj") {
Some(off) => off,
None => return true, };
let kobj_offset = match reader.symbols().field_offset("module_kobject", "kobj") {
Some(off) => off,
None => return true,
};
let entry_offset = match reader.symbols().field_offset("kobject", "entry") {
Some(off) => off,
None => return true,
};
let entry_addr = mod_addr + mkobj_offset + kobj_offset + entry_offset;
let next_ptr: u64 = match reader.read_field(entry_addr, "list_head", "next") {
Ok(v) => v,
Err(_) => return true, };
next_ptr != 0
}
pub fn check_hidden_modules<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<HiddenModuleInfo>> {
let modules_addr =
reader
.symbols()
.symbol_address("modules")
.ok_or_else(|| Error::MissingKernelSymbol {
name: "modules".into(),
})?;
let _list_offset = reader
.symbols()
.field_offset("module", "list")
.ok_or_else(|| Error::MissingField {
struct_name: "module".into(),
field_name: "list".into(),
})?;
let module_addrs = reader.walk_list(modules_addr, "module", "list")?;
let mut results = Vec::new();
for &mod_addr in &module_addrs {
let name = reader
.read_field_string(mod_addr, "module", "name", 56)
.unwrap_or_else(|_| "<unknown>".to_string());
let base_addr: u64 = reader
.read_field(mod_addr, "module", "module_core")
.unwrap_or(0);
let size: u32 = reader
.read_field(mod_addr, "module", "core_size")
.unwrap_or(0);
let in_sysfs = check_module_in_sysfs(reader, mod_addr);
results.push(HiddenModuleInfo {
name,
base_addr,
size: u64::from(size),
in_modules_list: true,
in_sysfs,
});
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use memf_core::test_builders::{flags as ptflags, 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("module", 256)
.add_field("module", "name", 0, "char")
.add_field("module", "list", 56, "list_head")
.add_field("module", "module_core", 128, "pointer")
.add_field("module", "core_size", 136, "unsigned int")
.add_field("module", "mkobj", 160, "module_kobject")
.add_struct("module_kobject", 64)
.add_field("module_kobject", "kobj", 0, "kobject")
.add_struct("kobject", 64)
.add_field("kobject", "name", 0, "pointer")
.add_field("kobject", "entry", 16, "list_head")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_symbol("modules", vaddr + 0x800)
.add_symbol("module_kset", vaddr + 0x900)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(vaddr, paddr, ptflags::WRITABLE)
.write_phys(paddr, data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn empty_module_list() {
let vaddr: u64 = 0xFFFF_8000_0010_0000;
let paddr: u64 = 0x0080_0000;
let mut data = vec![0u8; 4096];
let modules_head = vaddr + 0x800;
data[0x800..0x808].copy_from_slice(&modules_head.to_le_bytes());
data[0x808..0x810].copy_from_slice(&modules_head.to_le_bytes());
let reader = make_test_reader(&data, vaddr, paddr);
let results = check_hidden_modules(&reader).unwrap();
assert!(results.is_empty());
}
#[test]
fn missing_modules_symbol() {
let isf = IsfBuilder::new()
.add_struct("module", 64)
.add_field("module", "name", 0, "char")
.add_field("module", "list", 8, "list_head")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.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::new(vas, Box::new(resolver));
let result = check_hidden_modules(&reader);
assert!(
matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "modules"),
"expected MissingKernelSymbol {{name: \"modules\"}}, got {result:?}"
);
}
#[test]
fn missing_module_list_field_returns_error() {
let isf = IsfBuilder::new()
.add_struct("module", 64)
.add_field("module", "name", 0, "char")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_symbol("modules", 0xFFFF_8000_0010_0800)
.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::new(vas, Box::new(resolver));
let result = check_hidden_modules(&reader);
assert!(
matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "module" && field_name == "list"),
"expected MissingField module.list, got {result:?}"
);
}
#[test]
fn single_module_in_list_with_sysfs() {
let vaddr: u64 = 0xFFFF_8000_0010_0000;
let paddr: u64 = 0x0080_0000;
let mut data = vec![0u8; 4096];
let module_list_vaddr = vaddr; let module_list_field_vaddr = module_list_vaddr + 56;
let modules_head = vaddr + 0x800;
data[0x800..0x808].copy_from_slice(&module_list_field_vaddr.to_le_bytes());
data[0x808..0x810].copy_from_slice(&modules_head.to_le_bytes());
data[0..8].copy_from_slice(b"rootkit\0");
data[56..64].copy_from_slice(&modules_head.to_le_bytes());
data[64..72].copy_from_slice(&modules_head.to_le_bytes());
let base: u64 = 0xFFFF_C000_0000_0000;
data[128..136].copy_from_slice(&base.to_le_bytes());
data[136..140].copy_from_slice(&4096u32.to_le_bytes());
let kobj_entry_next_sentinel: u64 = 0xFFFF_8000_DEAD_0001;
data[176..184].copy_from_slice(&kobj_entry_next_sentinel.to_le_bytes());
let reader = make_test_reader(&data, vaddr, paddr);
let results = check_hidden_modules(&reader).unwrap();
assert_eq!(results.len(), 1, "should find one module");
assert!(
results[0].name.starts_with("rootkit"),
"name should match: {}",
results[0].name
);
assert_eq!(results[0].base_addr, base);
assert_eq!(results[0].size, 4096);
assert!(results[0].in_modules_list);
assert!(
results[0].in_sysfs,
"module with non-zero kobj entry.next should be in sysfs"
);
}
#[test]
fn single_module_not_in_sysfs() {
use memf_core::test_builders::{flags as ptflags, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
let vaddr: u64 = 0xFFFF_8000_0011_0000;
let paddr: u64 = 0x0081_0000;
let mut data = vec![0u8; 4096];
let module_list_field_vaddr = vaddr + 56;
let modules_head = vaddr + 0x800;
data[0x800..0x808].copy_from_slice(&module_list_field_vaddr.to_le_bytes());
data[0x808..0x810].copy_from_slice(&modules_head.to_le_bytes());
data[0..8].copy_from_slice(b"hidden\0\0");
data[56..64].copy_from_slice(&modules_head.to_le_bytes());
data[64..72].copy_from_slice(&modules_head.to_le_bytes());
let base: u64 = 0xFFFF_C001_0000_0000;
data[128..136].copy_from_slice(&base.to_le_bytes());
data[136..140].copy_from_slice(&4096u32.to_le_bytes());
let isf = memf_symbols::test_builders::IsfBuilder::new()
.add_struct("module", 256)
.add_field("module", "name", 0, "char")
.add_field("module", "list", 56, "list_head")
.add_field("module", "module_core", 128, "pointer")
.add_field("module", "core_size", 136, "unsigned int")
.add_field("module", "mkobj", 160, "module_kobject")
.add_struct("module_kobject", 64)
.add_field("module_kobject", "kobj", 0, "kobject")
.add_struct("kobject", 64)
.add_field("kobject", "name", 0, "pointer")
.add_field("kobject", "entry", 16, "list_head")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_symbol("modules", vaddr + 0x800)
.add_symbol("module_kset", vaddr + 0x900)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(vaddr, paddr, ptflags::WRITABLE)
.write_phys(paddr, &data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let results = check_hidden_modules(&reader).unwrap();
assert_eq!(results.len(), 1, "should find one module");
assert!(results[0].name.starts_with("hidden"));
assert!(results[0].in_modules_list);
assert!(
!results[0].in_sysfs,
"module with kobj entry.next==0 should not be in sysfs"
);
}
}