use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::{Error, KernelHookInfo, Result};
const PROLOGUE_SIZE: usize = 16;
const FUNCTIONS_TO_CHECK: &[&str] = &[
"sys_read",
"sys_write",
"sys_open",
"sys_close",
"vfs_read",
"vfs_write",
"tcp4_seq_show",
"filldir",
"filldir64",
];
pub fn check_inline_hooks<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<KernelHookInfo>> {
let stext =
reader
.symbols()
.symbol_address("_stext")
.ok_or_else(|| Error::MissingKernelSymbol {
name: "_stext".into(),
})?;
let etext =
reader
.symbols()
.symbol_address("_etext")
.ok_or_else(|| Error::MissingKernelSymbol {
name: "_etext".into(),
})?;
let mut results = Vec::new();
for &func_name in FUNCTIONS_TO_CHECK {
let Some(func_addr) = reader.symbols().symbol_address(func_name) else {
continue; };
let Ok(prologue) = reader.read_bytes(func_addr, PROLOGUE_SIZE) else {
continue;
};
let (hook_type, target) = analyze_prologue(&prologue, func_addr);
let suspicious = hook_type != "none" && target.map_or(true, |t| t < stext || t > etext);
results.push(KernelHookInfo {
symbol: func_name.to_string(),
address: func_addr,
hook_type,
target,
suspicious,
});
}
Ok(results)
}
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
fn analyze_prologue(bytes: &[u8], func_addr: u64) -> (String, Option<u64>) {
if bytes.len() < PROLOGUE_SIZE {
return ("none".to_string(), None);
}
if bytes[0] == 0xE9 {
let offset = bytes[1..5].try_into().map_or(0, i32::from_le_bytes);
let target = (func_addr as i64 + 5 + i64::from(offset)) as u64;
return ("jmp_rel32".to_string(), Some(target));
}
if bytes[0] == 0xFF && bytes[1] == 0x25 {
let offset = bytes[2..6].try_into().map_or(0, i32::from_le_bytes);
let target = (func_addr as i64 + 6 + i64::from(offset)) as u64;
return ("jmp_indirect".to_string(), Some(target));
}
if bytes.len() >= 12
&& bytes[0] == 0x48
&& bytes[1] == 0xB8
&& bytes[10] == 0xFF
&& bytes[11] == 0xE0
{
let target = bytes[2..10].try_into().map_or(0, u64::from_le_bytes);
return ("mov_rax_jmp".to_string(), Some(target));
}
("none".to_string(), None)
}
#[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],
func_vaddr: u64,
func_paddr: u64,
stext: u64,
etext: u64,
func_symbols: &[(&str, u64)],
) -> ObjectReader<SyntheticPhysMem> {
let mut builder = IsfBuilder::new()
.add_struct("task_struct", 64)
.add_field("task_struct", "pid", 0, "int")
.add_symbol("_stext", stext)
.add_symbol("_etext", etext);
for &(name, addr) in func_symbols {
builder = builder.add_symbol(name, addr);
}
let isf = builder.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(func_vaddr, func_paddr, ptflags::WRITABLE)
.write_phys(func_paddr, data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn clean_function_no_hook() {
let mut prologue = vec![0u8; 4096];
prologue[0] = 0x55; prologue[1] = 0x48; prologue[2] = 0x89; prologue[3] = 0xE5;
let func_vaddr: u64 = 0xFFFF_8000_0001_0000;
let func_paddr: u64 = 0x0080_0000;
let stext: u64 = 0xFFFF_8000_0000_0000;
let etext: u64 = 0xFFFF_8000_00FF_FFFF;
let reader = make_test_reader(
&prologue,
func_vaddr,
func_paddr,
stext,
etext,
&[("sys_read", func_vaddr)],
);
let results = check_inline_hooks(&reader).unwrap();
assert_eq!(results.len(), 1);
assert!(!results[0].suspicious);
assert_eq!(results[0].symbol, "sys_read");
assert_eq!(results[0].hook_type, "none");
}
#[test]
fn detects_relative_jmp_hook() {
let mut prologue = vec![0u8; 4096];
prologue[0] = 0xE9; prologue[1..5].copy_from_slice(&0x1000i32.to_le_bytes());
let func_vaddr: u64 = 0xFFFF_8000_0001_0000;
let func_paddr: u64 = 0x0080_0000;
let stext: u64 = 0xFFFF_8000_0000_0000;
let etext: u64 = 0xFFFF_8000_00FF_FFFF;
let reader = make_test_reader(
&prologue,
func_vaddr,
func_paddr,
stext,
etext,
&[("sys_read", func_vaddr)],
);
let results = check_inline_hooks(&reader).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].hook_type, "jmp_rel32");
assert!(results[0].target.is_some());
assert!(
!results[0].suspicious,
"jmp into kernel text should not be suspicious"
);
}
#[test]
fn detects_movabs_jmp_rax_hook() {
let mut prologue = vec![0u8; 4096];
prologue[0] = 0x48; prologue[1] = 0xB8; let target: u64 = 0xFFFF_C900_DEAD_BEEF;
prologue[2..10].copy_from_slice(&target.to_le_bytes());
prologue[10] = 0xFF; prologue[11] = 0xE0;
let func_vaddr: u64 = 0xFFFF_8000_0001_0000;
let func_paddr: u64 = 0x0080_0000;
let stext: u64 = 0xFFFF_8000_0000_0000;
let etext: u64 = 0xFFFF_8000_00FF_FFFF;
let reader = make_test_reader(
&prologue,
func_vaddr,
func_paddr,
stext,
etext,
&[("sys_read", func_vaddr)],
);
let results = check_inline_hooks(&reader).unwrap();
assert_eq!(results.len(), 1);
assert!(results[0].suspicious);
assert_eq!(results[0].hook_type, "mov_rax_jmp");
assert_eq!(results[0].target, Some(target));
}
#[test]
fn analyze_prologue_normal() {
let bytes = [
0x55, 0x48, 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x20, 0, 0, 0, 0, 0, 0, 0, 0,
];
let (hook_type, target) = analyze_prologue(&bytes, 0xFFFF_8000_0001_0000);
assert_eq!(hook_type, "none");
assert_eq!(target, None);
}
#[test]
fn detects_indirect_jmp_hook() {
let mut prologue = vec![0u8; 4096];
prologue[0] = 0xFF;
prologue[1] = 0x25;
prologue[2..6].copy_from_slice(&0i32.to_le_bytes());
let func_vaddr: u64 = 0xFFFF_8000_0002_0000;
let func_paddr: u64 = 0x0081_0000;
let stext: u64 = 0xFFFF_8000_0000_0000;
let etext: u64 = 0xFFFF_8000_00FF_FFFF;
let reader = make_test_reader(
&prologue,
func_vaddr,
func_paddr,
stext,
etext,
&[("sys_write", func_vaddr)],
);
let results = check_inline_hooks(&reader).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].hook_type, "jmp_indirect");
assert_eq!(results[0].target, Some(func_vaddr + 6));
assert!(
!results[0].suspicious,
"jmp_indirect targeting kernel text must not be suspicious"
);
}
#[test]
fn skips_symbol_with_unreadable_prologue() {
let isf = IsfBuilder::new()
.add_struct("task_struct", 64)
.add_field("task_struct", "pid", 0, "int")
.add_symbol("_stext", 0xFFFF_8000_0000_0000)
.add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
.add_symbol("sys_read", 0xFFFF_DEAD_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 = check_inline_hooks(&reader).unwrap();
assert!(results.is_empty(), "unreadable prologue should be skipped");
}
#[test]
fn detects_rel_jmp_hook_outside_text_region() {
let mut prologue = vec![0u8; 4096];
prologue[0] = 0xE9;
prologue[1..5].copy_from_slice(&0x0FFF_0000i32.to_le_bytes());
let func_vaddr: u64 = 0xFFFF_8000_0003_0000;
let func_paddr: u64 = 0x0082_0000;
let stext: u64 = 0xFFFF_8000_0000_0000;
let etext: u64 = 0xFFFF_8000_0005_0000;
let reader = make_test_reader(
&prologue,
func_vaddr,
func_paddr,
stext,
etext,
&[("vfs_read", func_vaddr)],
);
let results = check_inline_hooks(&reader).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].hook_type, "jmp_rel32");
assert!(
results[0].suspicious,
"JMP to outside text region must be suspicious"
);
}
#[test]
fn analyze_prologue_short_bytes_returns_none() {
let short = [0x55u8; 4]; let (hook_type, target) = analyze_prologue(&short, 0xFFFF_8000_0001_0000);
assert_eq!(hook_type, "none");
assert_eq!(target, None);
}
#[test]
fn skips_missing_symbols() {
let isf = IsfBuilder::new()
.add_struct("task_struct", 64)
.add_field("task_struct", "pid", 0, "int")
.add_symbol("_stext", 0xFFFF_8000_0000_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 = check_inline_hooks(&reader).unwrap();
assert!(results.is_empty());
}
#[test]
fn missing_stext_returns_missing_kernel_symbol() {
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<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = check_inline_hooks(&reader);
assert!(
matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "_stext"),
"expected MissingKernelSymbol {{name: \"_stext\"}}, got {result:?}"
);
}
#[test]
fn missing_etext_returns_missing_kernel_symbol() {
let isf = IsfBuilder::new()
.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<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = check_inline_hooks(&reader);
assert!(
matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "_etext"),
"expected MissingKernelSymbol {{name: \"_etext\"}}, got {result:?}"
);
}
}