use std::collections::HashSet;
use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
const MAX_MODULES: usize = 4096;
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, serde::Serialize)]
pub struct ModXviewEntry {
pub name: String,
pub base_addr: u64,
pub size: u32,
pub in_module_list: bool,
pub in_kobj_list: bool,
pub in_memory_map: bool,
pub is_hidden: bool,
}
pub use crate::heuristics::classify_module_visibility;
pub fn walk_modxview<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<ModXviewEntry>> {
let modules_addr = match reader.symbols().symbol_address("modules") {
Some(addr) => addr,
None => return Ok(Vec::new()),
};
let module_addrs = reader.walk_list(modules_addr, "module", "list")?;
let mut seen = HashSet::new();
let mut entries = Vec::new();
for &mod_addr in module_addrs.iter().take(MAX_MODULES) {
if !seen.insert(mod_addr) {
break; }
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_module_list = true;
let in_kobj_list = check_kobj_linkage(reader, mod_addr);
let in_memory_map = check_memory_mapped(reader, base_addr, size);
let is_hidden = classify_module_visibility(in_module_list, in_kobj_list, in_memory_map);
entries.push(ModXviewEntry {
name,
base_addr,
size,
in_module_list,
in_kobj_list,
in_memory_map,
is_hidden,
});
}
Ok(entries)
}
fn check_kobj_linkage<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
}
fn check_memory_mapped<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
base_addr: u64,
size: u32,
) -> bool {
if base_addr == 0 || size == 0 {
return true; }
reader.read_bytes(base_addr, 1).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_all_visible_benign() {
assert!(!classify_module_visibility(true, true, true));
}
#[test]
fn classify_missing_from_list_suspicious() {
assert!(classify_module_visibility(false, true, true));
}
#[test]
fn classify_missing_from_kobj_suspicious() {
assert!(classify_module_visibility(true, false, true));
}
#[test]
fn classify_all_missing_not_suspicious() {
assert!(!classify_module_visibility(false, false, false));
}
#[test]
fn walk_no_symbol_returns_empty() {
use memf_core::test_builders::PageTableBuilder;
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
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 = walk_modxview(&reader);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn modxview_entry_serializes() {
let entry = ModXviewEntry {
name: "test_module".to_string(),
base_addr: 0xFFFF_8000_0000_1000,
size: 4096,
in_module_list: true,
in_kobj_list: true,
in_memory_map: false,
is_hidden: true,
};
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("test_module"));
assert!(json.contains("\"is_hidden\":true"));
}
#[test]
fn classify_missing_from_memory_suspicious() {
assert!(classify_module_visibility(true, true, false));
}
#[test]
fn classify_only_in_memory_suspicious() {
assert!(classify_module_visibility(false, false, true));
}
#[test]
fn classify_only_in_module_list_suspicious() {
assert!(classify_module_visibility(true, false, false));
}
#[test]
fn classify_only_in_kobj_suspicious() {
assert!(classify_module_visibility(false, true, false));
}
#[test]
fn check_memory_mapped_zero_base_returns_true() {
use memf_core::test_builders::PageTableBuilder;
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let isf = IsfBuilder::new().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));
assert!(check_memory_mapped(&reader, 0, 4096));
}
#[test]
fn check_memory_mapped_zero_size_returns_true() {
use memf_core::test_builders::PageTableBuilder;
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let isf = IsfBuilder::new().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));
assert!(check_memory_mapped(&reader, 0xFFFF_8000_0000_1000, 0));
}
#[test]
fn check_memory_mapped_unreadable_returns_false() {
use memf_core::test_builders::PageTableBuilder;
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let isf = IsfBuilder::new().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));
assert!(!check_memory_mapped(&reader, 0xDEAD_BEEF_0000_1000, 4096));
}
#[test]
fn check_kobj_linkage_missing_mkobj_offset_returns_true() {
use memf_core::test_builders::PageTableBuilder;
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let isf = IsfBuilder::new().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));
assert!(check_kobj_linkage(&reader, 0xFFFF_8000_0000_0000));
}
#[test]
fn check_kobj_linkage_missing_kobj_offset_returns_true() {
use memf_core::test_builders::PageTableBuilder;
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let isf = IsfBuilder::new()
.add_struct("module", 128)
.add_field("module", "mkobj", 0, "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));
assert!(check_kobj_linkage(&reader, 0xFFFF_8000_0000_0000));
}
#[test]
fn check_kobj_linkage_missing_entry_offset_returns_true() {
use memf_core::test_builders::PageTableBuilder;
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let isf = IsfBuilder::new()
.add_struct("module", 128)
.add_field("module", "mkobj", 0, "pointer")
.add_struct("module_kobject", 64)
.add_field("module_kobject", "kobj", 0, "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));
assert!(check_kobj_linkage(&reader, 0xFFFF_8000_0000_0000));
}
#[test]
fn walk_modxview_with_one_module_entry() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let head_vaddr: u64 = 0xFFFF_8800_00E0_0000;
let head_paddr: u64 = 0x00E0_0000;
let mod_a_vaddr: u64 = 0xFFFF_8800_00E1_0000;
let mod_a_paddr: u64 = 0x00E1_0000;
let mut head_page = [0u8; 4096];
head_page[0..8].copy_from_slice(&mod_a_vaddr.to_le_bytes());
head_page[8..16].copy_from_slice(&mod_a_vaddr.to_le_bytes());
let mut mod_a_page = [0u8; 4096];
mod_a_page[0..8].copy_from_slice(&head_vaddr.to_le_bytes());
mod_a_page[8..16].copy_from_slice(&head_vaddr.to_le_bytes());
mod_a_page[0x10..0x15].copy_from_slice(b"dummy");
mod_a_page[0x48..0x50].copy_from_slice(&0xFFFF_A000_0000u64.to_le_bytes());
mod_a_page[0x50..0x54].copy_from_slice(&0x4000u32.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("module", 256)
.add_field("module", "list", 0x00u64, "list_head")
.add_field("module", "name", 0x10u64, "char")
.add_field("module", "module_core", 0x48u64, "pointer")
.add_field("module", "core_size", 0x50u64, "unsigned int")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0x00u64, "pointer")
.add_field("list_head", "prev", 0x08u64, "pointer")
.add_symbol("modules", head_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(head_vaddr, head_paddr, ptf::WRITABLE)
.write_phys(head_paddr, &head_page)
.map_4k(mod_a_vaddr, mod_a_paddr, ptf::WRITABLE)
.write_phys(mod_a_paddr, &mod_a_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_modxview(&reader).unwrap_or_default();
assert_eq!(result.len(), 1, "should find exactly one module entry");
assert_eq!(result[0].name, "dummy");
assert_eq!(result[0].base_addr, 0xFFFF_A000_0000);
assert_eq!(result[0].size, 0x4000);
assert!(result[0].in_module_list, "module found in list");
}
#[test]
fn check_kobj_linkage_unreadable_memory_returns_true() {
use memf_core::test_builders::PageTableBuilder;
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let isf = IsfBuilder::new()
.add_struct("module", 128)
.add_field("module", "mkobj", 0, "pointer")
.add_struct("module_kobject", 64)
.add_field("module_kobject", "kobj", 0, "pointer")
.add_struct("kobject", 64)
.add_field("kobject", "entry", 0, "pointer")
.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));
assert!(check_kobj_linkage(&reader, 0xDEAD_BEEF_0000_0000));
}
}