use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
const AFINFO_SYMBOLS: &[(&str, &str)] = &[
("tcp_seq_afinfo", "tcp"),
("udp_seq_afinfo", "udp"),
("tcp6_seq_afinfo", "tcp6"),
("udp6_seq_afinfo", "udp6"),
("raw_seq_afinfo", "raw"),
];
const SEQ_OPS_FIELDS: &[&str] = &["show", "start", "next", "stop"];
#[derive(Debug, Clone, serde::Serialize)]
pub struct AfInfoHookInfo {
pub protocol: String,
pub struct_name: String,
pub field: String,
pub hook_address: u64,
pub expected_module: String,
pub actual_module: String,
pub is_hooked: bool,
}
pub use crate::heuristics::classify_afinfo_hook;
pub fn walk_check_afinfo<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<AfInfoHookInfo>> {
let symbols = reader.symbols();
let Some(kernel_start) = symbols.symbol_address("_stext") else {
return Ok(Vec::new());
};
let Some(kernel_end) = symbols.symbol_address("_etext") else {
return Ok(Vec::new());
};
let mut results = Vec::new();
for &(sym_name, protocol) in AFINFO_SYMBOLS {
let Some(afinfo_addr) = symbols.symbol_address(sym_name) else {
continue;
};
let seq_ops_addr: u64 = match reader.read_pointer(afinfo_addr, "seq_afinfo", "seq_ops") {
Ok(addr) => addr,
Err(_) => continue, };
if seq_ops_addr == 0 {
continue; }
for &field_name in SEQ_OPS_FIELDS {
let ptr: u64 = match reader.read_pointer(seq_ops_addr, "seq_operations", field_name) {
Ok(p) => p,
Err(_) => continue,
};
let is_hooked = classify_afinfo_hook(ptr, kernel_start, kernel_end);
let actual_module = if ptr == 0 {
"null".to_string()
} else if is_hooked {
format!("unknown (0x{ptr:016x})")
} else {
"kernel".to_string()
};
results.push(AfInfoHookInfo {
protocol: protocol.to_string(),
struct_name: sym_name.to_string(),
field: format!("seq_ops.{field_name}"),
hook_address: ptr,
expected_module: "kernel".to_string(),
actual_module,
is_hooked,
});
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use memf_core::test_builders::{flags as ptflags, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
#[test]
fn hook_outside_kernel_suspicious() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
assert!(classify_afinfo_hook(
0xFFFF_C900_DEAD_BEEF,
kernel_start,
kernel_end
));
assert!(classify_afinfo_hook(
kernel_start - 1,
kernel_start,
kernel_end
));
assert!(classify_afinfo_hook(
kernel_end + 1,
kernel_start,
kernel_end
));
}
#[test]
fn hook_inside_kernel_benign() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
assert!(!classify_afinfo_hook(
kernel_start,
kernel_start,
kernel_end
));
assert!(!classify_afinfo_hook(
kernel_start + 0x1000,
kernel_start,
kernel_end
));
assert!(!classify_afinfo_hook(kernel_end, kernel_start, kernel_end));
}
#[test]
fn hook_zero_benign() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
assert!(!classify_afinfo_hook(0, kernel_start, kernel_end));
}
#[test]
fn classify_multiple_protocols() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
let kernel_func = kernel_start + 0x5000;
let module_func = 0xFFFF_C900_1234_0000u64;
assert!(!classify_afinfo_hook(kernel_func, kernel_start, kernel_end));
assert!(classify_afinfo_hook(module_func, kernel_start, kernel_end));
assert!(!classify_afinfo_hook(0, kernel_start, kernel_end));
assert!(classify_afinfo_hook(
kernel_end + 0x100,
kernel_start,
kernel_end
));
}
#[test]
fn afinfo_hook_info_serializes() {
let info = AfInfoHookInfo {
protocol: "tcp".to_string(),
struct_name: "tcp_seq_afinfo".to_string(),
field: "seq_ops.show".to_string(),
hook_address: 0xFFFF_C900_DEAD_BEEF,
expected_module: "kernel".to_string(),
actual_module: "rootkit.ko".to_string(),
is_hooked: true,
};
let json = serde_json::to_value(&info).unwrap();
assert_eq!(json["protocol"], "tcp");
assert_eq!(json["struct_name"], "tcp_seq_afinfo");
assert_eq!(json["field"], "seq_ops.show");
assert_eq!(json["is_hooked"], true);
}
#[test]
fn walk_check_afinfo_no_symbols_returns_empty() {
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 = walk_check_afinfo(&reader).unwrap();
assert!(
results.is_empty(),
"expected empty results when no afinfo symbols exist"
);
}
#[test]
fn walk_check_afinfo_detects_hooked_seq_ops() {
let kernel_start: u64 = 0xFFFF_8000_0000_0000;
let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
let afinfo_vaddr: u64 = 0xFFFF_8000_0010_0000;
let afinfo_paddr: u64 = 0x0080_0000;
let kernel_func: u64 = kernel_start + 0x1000;
let hooked_func: u64 = 0xFFFF_C900_DEAD_0000;
let seq_ops_vaddr: u64 = 0xFFFF_8000_0020_0000;
let seq_ops_paddr: u64 = 0x0090_0000;
let mut seq_ops_data = vec![0u8; 4096];
seq_ops_data[0..8].copy_from_slice(&hooked_func.to_le_bytes()); seq_ops_data[8..16].copy_from_slice(&kernel_func.to_le_bytes()); seq_ops_data[16..24].copy_from_slice(&kernel_func.to_le_bytes()); seq_ops_data[24..32].copy_from_slice(&kernel_func.to_le_bytes());
let mut afinfo_data = vec![0u8; 4096];
afinfo_data[0..8].copy_from_slice(&seq_ops_vaddr.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("seq_afinfo", 64)
.add_field("seq_afinfo", "seq_ops", 0, "pointer")
.add_struct("seq_operations", 32)
.add_field("seq_operations", "show", 0, "pointer")
.add_field("seq_operations", "start", 8, "pointer")
.add_field("seq_operations", "next", 16, "pointer")
.add_field("seq_operations", "stop", 24, "pointer")
.add_symbol("_stext", kernel_start)
.add_symbol("_etext", kernel_end)
.add_symbol("tcp_seq_afinfo", afinfo_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(afinfo_vaddr, afinfo_paddr, ptflags::WRITABLE)
.write_phys(afinfo_paddr, &afinfo_data)
.map_4k(seq_ops_vaddr, seq_ops_paddr, ptflags::WRITABLE)
.write_phys(seq_ops_paddr, &seq_ops_data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let results = walk_check_afinfo(&reader).unwrap();
assert!(!results.is_empty(), "expected non-empty results");
let hooked: Vec<_> = results.iter().filter(|r| r.is_hooked).collect();
assert_eq!(hooked.len(), 1, "expected exactly one hooked entry");
assert_eq!(hooked[0].protocol, "tcp");
assert_eq!(hooked[0].field, "seq_ops.show");
assert_eq!(hooked[0].hook_address, hooked_func);
let benign: Vec<_> = results.iter().filter(|r| !r.is_hooked).collect();
assert_eq!(benign.len(), 3, "expected 3 benign entries");
}
}