use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
#[derive(Debug, Clone, serde::Serialize)]
pub struct FtraceHookInfo {
pub address: u64,
pub func: u64,
pub func_name: String,
pub flags: u32,
pub is_suspicious: bool,
}
pub fn walk_ftrace_hooks<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<FtraceHookInfo>> {
const MAX_HOOKS: usize = 1_000;
let Some(list_head_addr) = reader.symbols().symbol_address("ftrace_ops_list") else {
return Ok(Vec::new());
};
let stext = reader.symbols().symbol_address("_stext").unwrap_or(0);
let etext = reader
.symbols()
.symbol_address("_etext")
.unwrap_or(u64::MAX);
let list_offset: u64 = reader
.symbols()
.field_offset("ftrace_ops", "list")
.unwrap_or(8);
let func_offset: u64 = reader
.symbols()
.field_offset("ftrace_ops", "func")
.unwrap_or(0);
let flags_offset: u64 = reader
.symbols()
.field_offset("ftrace_ops", "flags")
.unwrap_or(0x18);
let next_field_offset: u64 = reader
.symbols()
.field_offset("list_head", "next")
.unwrap_or(0);
let mut hooks = Vec::new();
let first_ptr = match reader.read_bytes(list_head_addr + next_field_offset, 8) {
Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
_ => return Ok(Vec::new()),
};
let mut current_list_ptr = first_ptr;
for _ in 0..MAX_HOOKS {
if current_list_ptr == list_head_addr || current_list_ptr == 0 {
break;
}
let ops_addr = current_list_ptr.wrapping_sub(list_offset);
let func = match reader.read_bytes(ops_addr + func_offset, 8) {
Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
_ => 0,
};
let flags = match reader.read_bytes(ops_addr + flags_offset, 4) {
Ok(b) if b.len() == 4 => b.try_into().map_or(0, u32::from_le_bytes),
_ => 0,
};
let is_suspicious = classify_ftrace_hook(func, stext, etext);
let func_name = format!("{func:#018x}");
hooks.push(FtraceHookInfo {
address: ops_addr,
func,
func_name,
flags,
is_suspicious,
});
current_list_ptr = match reader.read_bytes(current_list_ptr + next_field_offset, 8) {
Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
_ => break,
};
}
Ok(hooks)
}
pub use crate::heuristics::classify_ftrace_hook;
#[cfg(test)]
mod tests {
use super::*;
use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
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);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn no_symbol_returns_empty() {
let reader = make_no_symbol_reader();
let result = walk_ftrace_hooks(&reader).unwrap();
assert!(result.is_empty(), "no ftrace_ops_list symbol → empty vec");
}
#[test]
fn classify_in_kernel_benign() {
let stext = 0xFFFF_FFFF_8100_0000_u64;
let etext = 0xFFFF_FFFF_8200_0000_u64;
let func = 0xFFFF_FFFF_8150_0000_u64; assert!(
!classify_ftrace_hook(func, stext, etext),
"in-kernel func should be benign"
);
}
#[test]
fn classify_out_of_kernel_suspicious() {
let stext = 0xFFFF_FFFF_8100_0000_u64;
let etext = 0xFFFF_FFFF_8200_0000_u64;
let func = 0xFFFF_C900_0000_0000_u64; assert!(
classify_ftrace_hook(func, stext, etext),
"out-of-kernel func should be suspicious"
);
}
#[test]
fn walk_ftrace_hooks_with_symbol_returns_entries() {
use memf_core::test_builders::flags;
let list_head_vaddr: u64 = 0xFFFF_8000_0010_0000;
let list_head_paddr: u64 = 0x0080_0000;
let ops_vaddr: u64 = 0xFFFF_8000_0010_1000;
let ops_paddr: u64 = 0x0081_0000;
let func_ptr: u64 = 0xFFFF_FFFF_8150_0000; let ops_flags: u32 = 0x0001;
let mut list_head_data = [0u8; 0x1000];
list_head_data[0..8].copy_from_slice(&(ops_vaddr + 8).to_le_bytes());
list_head_data[8..16].copy_from_slice(&(ops_vaddr + 8).to_le_bytes());
let mut ops_data = [0u8; 0x1000];
ops_data[0x00..0x08].copy_from_slice(&func_ptr.to_le_bytes());
ops_data[0x08..0x10].copy_from_slice(&list_head_vaddr.to_le_bytes());
ops_data[0x10..0x18].copy_from_slice(&list_head_vaddr.to_le_bytes());
ops_data[0x18..0x1C].copy_from_slice(&ops_flags.to_le_bytes());
let stext: u64 = 0xFFFF_FFFF_8100_0000;
let etext: u64 = 0xFFFF_FFFF_8200_0000;
let isf = IsfBuilder::new()
.add_struct("ftrace_ops", 64)
.add_field("ftrace_ops", "func", 0, "pointer")
.add_field("ftrace_ops", "list", 8, "list_head")
.add_field("ftrace_ops", "flags", 0x18, "unsigned int")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_symbol("ftrace_ops_list", list_head_vaddr)
.add_symbol("_stext", stext)
.add_symbol("_etext", etext)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mut mem) = PageTableBuilder::new()
.map_4k(
list_head_vaddr,
list_head_paddr,
flags::PRESENT | flags::WRITABLE,
)
.map_4k(ops_vaddr, ops_paddr, flags::PRESENT | flags::WRITABLE)
.build();
mem.write_bytes(list_head_paddr, &list_head_data);
mem.write_bytes(ops_paddr, &ops_data);
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let hooks = walk_ftrace_hooks(&reader).unwrap();
assert_eq!(hooks.len(), 1, "should find one ftrace hook");
assert_eq!(hooks[0].func, func_ptr);
assert!(!hooks[0].is_suspicious, "in-kernel func should be benign");
}
}