use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
const FOP_FIELDS: &[&str] = &[
"read",
"write",
"open",
"release",
"unlocked_ioctl",
"llseek",
"mmap",
"poll",
"read_iter",
"write_iter",
];
#[derive(Debug, Clone, serde::Serialize)]
pub struct FopsHookInfo {
pub path: String,
pub struct_address: u64,
pub hooked_functions: Vec<HookedFop>,
pub is_suspicious: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct HookedFop {
pub function_name: String,
pub target_address: u64,
pub is_in_kernel_text: bool,
}
pub fn is_kernel_text_address(addr: u64, kernel_start: u64, kernel_end: u64) -> bool {
addr >= kernel_start && addr < kernel_end
}
pub fn check_fops_entry<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
fops_addr: u64,
kernel_start: u64,
kernel_end: u64,
) -> Vec<HookedFop> {
let mut results = Vec::new();
for &field_name in FOP_FIELDS {
let ptr: u64 = match reader.read_pointer(fops_addr, "file_operations", field_name) {
Ok(p) => p,
Err(_) => continue, };
if ptr == 0 {
continue;
}
results.push(HookedFop {
function_name: field_name.to_string(),
target_address: ptr,
is_in_kernel_text: is_kernel_text_address(ptr, kernel_start, kernel_end),
});
}
results
}
const MAX_PROC_ENTRIES: usize = 10_000;
pub fn scan_proc_fops<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<FopsHookInfo>> {
let Some(proc_root) = reader.symbols().symbol_address("proc_root") else {
return Ok(Vec::new());
};
let Some(kernel_start) = reader.symbols().symbol_address("_stext") else {
return Ok(Vec::new());
};
let Some(kernel_end) = reader.symbols().symbol_address("_etext") else {
return Ok(Vec::new());
};
let mut results = Vec::new();
let mut stack = Vec::new();
let subdir: u64 = reader
.read_pointer(proc_root, "proc_dir_entry", "subdir")
.unwrap_or(0);
if subdir != 0 {
stack.push((subdir, "/proc".to_string()));
}
let mut visited = 0usize;
while let Some((entry_addr, parent_path)) = stack.pop() {
if visited >= MAX_PROC_ENTRIES {
break;
}
visited += 1;
let name = reader
.read_field_string(entry_addr, "proc_dir_entry", "name", 128)
.unwrap_or_else(|_| "<unknown>".to_string());
let path = format!("{parent_path}/{name}");
let fops_addr: u64 = reader
.read_pointer(entry_addr, "proc_dir_entry", "proc_fops")
.unwrap_or(0);
if fops_addr != 0 {
let hooked_functions = check_fops_entry(reader, fops_addr, kernel_start, kernel_end);
let is_suspicious = hooked_functions.iter().any(|f| !f.is_in_kernel_text);
results.push(FopsHookInfo {
path: path.clone(),
struct_address: fops_addr,
hooked_functions,
is_suspicious,
});
}
let child: u64 = reader
.read_pointer(entry_addr, "proc_dir_entry", "subdir")
.unwrap_or(0);
if child != 0 {
stack.push((child, path));
}
let next: u64 = reader
.read_pointer(entry_addr, "proc_dir_entry", "next")
.unwrap_or(0);
if next != 0 {
stack.push((next, parent_path));
}
}
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;
#[test]
fn is_kernel_text_address_inside() {
let start = 0xFFFF_8000_0000_0000u64;
let end = 0xFFFF_8000_00FF_FFFFu64;
assert!(is_kernel_text_address(start, start, end));
assert!(is_kernel_text_address(start + 0x1000, start, end));
assert!(is_kernel_text_address(end - 1, start, end));
}
#[test]
fn is_kernel_text_address_outside() {
let start = 0xFFFF_8000_0000_0000u64;
let end = 0xFFFF_8000_00FF_FFFFu64;
assert!(!is_kernel_text_address(start - 1, start, end));
assert!(!is_kernel_text_address(end, start, end));
assert!(!is_kernel_text_address(end + 1, start, end));
assert!(!is_kernel_text_address(0xFFFF_C900_DEAD_BEEF, start, end));
assert!(!is_kernel_text_address(0, start, end));
}
fn make_fops_reader(
fops_data: &[u8],
fops_vaddr: u64,
fops_paddr: u64,
kernel_start: u64,
kernel_end: u64,
) -> ObjectReader<SyntheticPhysMem> {
let isf = IsfBuilder::new()
.add_struct("file_operations", 256)
.add_field("file_operations", "read", 0, "pointer")
.add_field("file_operations", "write", 8, "pointer")
.add_field("file_operations", "open", 16, "pointer")
.add_field("file_operations", "release", 24, "pointer")
.add_field("file_operations", "unlocked_ioctl", 32, "pointer")
.add_field("file_operations", "llseek", 40, "pointer")
.add_field("file_operations", "mmap", 48, "pointer")
.add_field("file_operations", "poll", 56, "pointer")
.add_field("file_operations", "read_iter", 64, "pointer")
.add_field("file_operations", "write_iter", 72, "pointer")
.add_symbol("_stext", kernel_start)
.add_symbol("_etext", kernel_end)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(fops_vaddr, fops_paddr, ptflags::WRITABLE)
.write_phys(fops_paddr, fops_data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn classify_fops_all_kernel() {
let kernel_start: u64 = 0xFFFF_8000_0000_0000;
let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
let fops_vaddr: u64 = 0xFFFF_8000_0010_0000;
let fops_paddr: u64 = 0x0080_0000;
let mut fops_data = vec![0u8; 4096];
let kernel_func = kernel_start + 0x1000; for i in 0..FOP_FIELDS.len() {
let offset = i * 8;
fops_data[offset..offset + 8].copy_from_slice(&kernel_func.to_le_bytes());
}
let reader = make_fops_reader(&fops_data, fops_vaddr, fops_paddr, kernel_start, kernel_end);
let results = check_fops_entry(&reader, fops_vaddr, kernel_start, kernel_end);
assert!(!results.is_empty());
for fop in &results {
assert!(
fop.is_in_kernel_text,
"function {} at {:#x} should be in kernel text",
fop.function_name, fop.target_address,
);
}
}
#[test]
fn classify_fops_hooked_pointer() {
let kernel_start: u64 = 0xFFFF_8000_0000_0000;
let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
let fops_vaddr: u64 = 0xFFFF_8000_0010_0000;
let fops_paddr: u64 = 0x0080_0000;
let mut fops_data = vec![0u8; 4096];
let kernel_func = kernel_start + 0x1000;
let hooked_addr: u64 = 0xFFFF_C900_DEAD_BEEF;
fops_data[0..8].copy_from_slice(&hooked_addr.to_le_bytes());
for i in 1..FOP_FIELDS.len() {
let offset = i * 8;
fops_data[offset..offset + 8].copy_from_slice(&kernel_func.to_le_bytes());
}
let reader = make_fops_reader(&fops_data, fops_vaddr, fops_paddr, kernel_start, kernel_end);
let results = check_fops_entry(&reader, fops_vaddr, kernel_start, kernel_end);
let read_fop = results.iter().find(|f| f.function_name == "read").unwrap();
assert!(!read_fop.is_in_kernel_text);
assert_eq!(read_fop.target_address, hooked_addr);
for fop in results.iter().filter(|f| f.function_name != "read") {
assert!(
fop.is_in_kernel_text,
"function {} should be in kernel text",
fop.function_name,
);
}
}
#[test]
fn scan_proc_fops_no_symbol() {
let isf = IsfBuilder::new()
.add_struct("file_operations", 256)
.add_field("file_operations", "read", 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));
let results = scan_proc_fops(&reader).unwrap();
assert!(results.is_empty());
}
#[test]
fn scan_proc_fops_missing_stext_returns_empty() {
let isf = IsfBuilder::new()
.add_struct("file_operations", 256)
.add_field("file_operations", "read", 0, "pointer")
.add_symbol("proc_root", 0xFFFF_8000_0010_0000)
.add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
.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 results = scan_proc_fops(&reader).unwrap();
assert!(results.is_empty(), "missing _stext should yield empty vec");
}
#[test]
fn scan_proc_fops_missing_etext_returns_empty() {
let isf = IsfBuilder::new()
.add_struct("file_operations", 256)
.add_field("file_operations", "read", 0, "pointer")
.add_symbol("proc_root", 0xFFFF_8000_0010_0000)
.add_symbol("_stext", 0xFFFF_8000_0000_0000)
.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 results = scan_proc_fops(&reader).unwrap();
assert!(results.is_empty(), "missing _etext should yield empty vec");
}
#[test]
fn scan_proc_fops_symbol_present_empty_proc_tree() {
let proc_root_vaddr: u64 = 0xFFFF_8800_0060_0000;
let proc_root_paddr: u64 = 0x0070_0000;
let kernel_start: u64 = 0xFFFF_8000_0000_0000;
let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
let page = [0u8; 4096];
let isf = IsfBuilder::new()
.add_struct("proc_dir_entry", 256)
.add_field("proc_dir_entry", "subdir", 0, "pointer")
.add_field("proc_dir_entry", "next", 8, "pointer")
.add_field("proc_dir_entry", "proc_fops", 16, "pointer")
.add_field("proc_dir_entry", "name", 24, "char")
.add_struct("file_operations", 256)
.add_field("file_operations", "read", 0, "pointer")
.add_symbol("proc_root", proc_root_vaddr)
.add_symbol("_stext", kernel_start)
.add_symbol("_etext", kernel_end)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(proc_root_vaddr, proc_root_paddr, ptflags::WRITABLE)
.write_phys(proc_root_paddr, &page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let results = scan_proc_fops(&reader).unwrap_or_default();
assert!(
results.is_empty(),
"empty proc tree should produce no fops hook entries"
);
}
#[test]
fn scan_proc_fops_with_entry_no_proc_fops() {
let proc_root_vaddr: u64 = 0xFFFF_8800_0070_0000;
let proc_root_paddr: u64 = 0x0040_0000;
let entry_vaddr: u64 = 0xFFFF_8800_0071_0000;
let entry_paddr: u64 = 0x0041_0000;
let kernel_start: u64 = 0xFFFF_8000_0000_0000;
let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
let mut root_page = [0u8; 4096];
root_page[0..8].copy_from_slice(&entry_vaddr.to_le_bytes());
let mut entry_page = [0u8; 4096];
entry_page[24..31].copy_from_slice(b"modules");
let isf = IsfBuilder::new()
.add_struct("proc_dir_entry", 256)
.add_field("proc_dir_entry", "subdir", 0x00u64, "pointer")
.add_field("proc_dir_entry", "next", 0x08u64, "pointer")
.add_field("proc_dir_entry", "proc_fops", 0x10u64, "pointer")
.add_field("proc_dir_entry", "name", 0x18u64, "char")
.add_struct("file_operations", 256)
.add_field("file_operations", "read", 0x00u64, "pointer")
.add_symbol("proc_root", proc_root_vaddr)
.add_symbol("_stext", kernel_start)
.add_symbol("_etext", kernel_end)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(proc_root_vaddr, proc_root_paddr, ptflags::WRITABLE)
.write_phys(proc_root_paddr, &root_page)
.map_4k(entry_vaddr, entry_paddr, ptflags::WRITABLE)
.write_phys(entry_paddr, &entry_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let results = scan_proc_fops(&reader).unwrap_or_default();
assert!(
results.is_empty(),
"entry with proc_fops==0 should produce no hook entries"
);
}
#[test]
fn scan_proc_fops_with_entry_and_proc_fops_in_kernel() {
let proc_root_vaddr: u64 = 0xFFFF_8800_0080_0000;
let proc_root_paddr: u64 = 0x0042_0000;
let entry_vaddr: u64 = 0xFFFF_8800_0081_0000;
let entry_paddr: u64 = 0x0043_0000;
let fops_vaddr: u64 = 0xFFFF_8800_0082_0000;
let fops_paddr: u64 = 0x0044_0000;
let kernel_start: u64 = 0xFFFF_8000_0000_0000;
let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
let kernel_func: u64 = kernel_start + 0x5000;
let mut root_page = [0u8; 4096];
root_page[0..8].copy_from_slice(&entry_vaddr.to_le_bytes());
let mut entry_page = [0u8; 4096];
entry_page[0x10..0x18].copy_from_slice(&fops_vaddr.to_le_bytes()); entry_page[0x18..0x1b].copy_from_slice(b"net");
let mut fops_page = [0u8; 4096];
fops_page[0..8].copy_from_slice(&kernel_func.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("proc_dir_entry", 256)
.add_field("proc_dir_entry", "subdir", 0x00u64, "pointer")
.add_field("proc_dir_entry", "next", 0x08u64, "pointer")
.add_field("proc_dir_entry", "proc_fops", 0x10u64, "pointer")
.add_field("proc_dir_entry", "name", 0x18u64, "char")
.add_struct("file_operations", 256)
.add_field("file_operations", "read", 0x00u64, "pointer")
.add_field("file_operations", "write", 0x08u64, "pointer")
.add_field("file_operations", "open", 0x10u64, "pointer")
.add_field("file_operations", "release", 0x18u64, "pointer")
.add_field("file_operations", "unlocked_ioctl", 0x20u64, "pointer")
.add_field("file_operations", "llseek", 0x28u64, "pointer")
.add_field("file_operations", "mmap", 0x30u64, "pointer")
.add_field("file_operations", "poll", 0x38u64, "pointer")
.add_field("file_operations", "read_iter", 0x40u64, "pointer")
.add_field("file_operations", "write_iter", 0x48u64, "pointer")
.add_symbol("proc_root", proc_root_vaddr)
.add_symbol("_stext", kernel_start)
.add_symbol("_etext", kernel_end)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(proc_root_vaddr, proc_root_paddr, ptflags::WRITABLE)
.write_phys(proc_root_paddr, &root_page)
.map_4k(entry_vaddr, entry_paddr, ptflags::WRITABLE)
.write_phys(entry_paddr, &entry_page)
.map_4k(fops_vaddr, fops_paddr, ptflags::WRITABLE)
.write_phys(fops_paddr, &fops_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let results = scan_proc_fops(&reader).unwrap_or_default();
assert_eq!(
results.len(),
1,
"should find exactly one entry with proc_fops"
);
let entry = &results[0];
assert!(
!entry.is_suspicious,
"kernel-text pointer should not be suspicious"
);
assert!(
entry.path.contains("net") || entry.path.contains("/proc"),
"path should contain entry name"
);
}
#[test]
fn check_fops_entry_null_pointer_skipped() {
let kernel_start: u64 = 0xFFFF_8000_0000_0000;
let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
let fops_vaddr: u64 = 0xFFFF_8000_0010_0000;
let fops_paddr: u64 = 0x0080_0000;
let fops_data = vec![0u8; 4096];
let reader = make_fops_reader(&fops_data, fops_vaddr, fops_paddr, kernel_start, kernel_end);
let results = check_fops_entry(&reader, fops_vaddr, kernel_start, kernel_end);
assert!(
results.is_empty(),
"all-null fops struct should produce no HookedFop entries"
);
}
}