1use memf_core::object_reader::ObjectReader;
10use memf_format::PhysicalMemoryProvider;
11
12use crate::Result;
13
14#[derive(Debug, Clone, serde::Serialize)]
16pub struct FtraceHookInfo {
17 pub address: u64,
19 pub func: u64,
21 pub func_name: String,
23 pub flags: u32,
25 pub is_suspicious: bool,
27}
28
29pub fn walk_ftrace_hooks<P: PhysicalMemoryProvider>(
38 reader: &ObjectReader<P>,
39) -> Result<Vec<FtraceHookInfo>> {
40 const MAX_HOOKS: usize = 1_000;
41 let Some(list_head_addr) = reader.symbols().symbol_address("ftrace_ops_list") else {
42 return Ok(Vec::new());
43 };
44
45 let stext = reader.symbols().symbol_address("_stext").unwrap_or(0);
46 let etext = reader
47 .symbols()
48 .symbol_address("_etext")
49 .unwrap_or(u64::MAX);
50
51 let list_offset: u64 = reader
55 .symbols()
56 .field_offset("ftrace_ops", "list")
57 .unwrap_or(8);
58 let func_offset: u64 = reader
59 .symbols()
60 .field_offset("ftrace_ops", "func")
61 .unwrap_or(0);
62 let flags_offset: u64 = reader
63 .symbols()
64 .field_offset("ftrace_ops", "flags")
65 .unwrap_or(0x18);
66
67 let next_field_offset: u64 = reader
69 .symbols()
70 .field_offset("list_head", "next")
71 .unwrap_or(0);
72
73 let mut hooks = Vec::new();
74
75 let first_ptr = match reader.read_bytes(list_head_addr + next_field_offset, 8) {
76 Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
77 _ => return Ok(Vec::new()),
78 };
79
80 let mut current_list_ptr = first_ptr;
81
82 for _ in 0..MAX_HOOKS {
83 if current_list_ptr == list_head_addr || current_list_ptr == 0 {
85 break;
86 }
87
88 let ops_addr = current_list_ptr.wrapping_sub(list_offset);
90
91 let func = match reader.read_bytes(ops_addr + func_offset, 8) {
92 Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
93 _ => 0,
94 };
95
96 let flags = match reader.read_bytes(ops_addr + flags_offset, 4) {
97 Ok(b) if b.len() == 4 => b.try_into().map_or(0, u32::from_le_bytes),
98 _ => 0,
99 };
100
101 let is_suspicious = classify_ftrace_hook(func, stext, etext);
102 let func_name = format!("{func:#018x}");
103
104 hooks.push(FtraceHookInfo {
105 address: ops_addr,
106 func,
107 func_name,
108 flags,
109 is_suspicious,
110 });
111
112 current_list_ptr = match reader.read_bytes(current_list_ptr + next_field_offset, 8) {
114 Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
115 _ => break,
116 };
117 }
118
119 Ok(hooks)
120}
121
122pub use crate::heuristics::classify_ftrace_hook;
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
129 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
130 use memf_symbols::isf::IsfResolver;
131 use memf_symbols::test_builders::IsfBuilder;
132
133 fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
134 let isf = IsfBuilder::new().build_json();
135 let resolver = IsfResolver::from_value(&isf).unwrap();
136 let (cr3, mem) = PageTableBuilder::new().build();
137 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
138 ObjectReader::new(vas, Box::new(resolver))
139 }
140
141 #[test]
142 fn no_symbol_returns_empty() {
143 let reader = make_no_symbol_reader();
144 let result = walk_ftrace_hooks(&reader).unwrap();
145 assert!(result.is_empty(), "no ftrace_ops_list symbol → empty vec");
146 }
147
148 #[test]
149 fn classify_in_kernel_benign() {
150 let stext = 0xFFFF_FFFF_8100_0000_u64;
151 let etext = 0xFFFF_FFFF_8200_0000_u64;
152 let func = 0xFFFF_FFFF_8150_0000_u64; assert!(
154 !classify_ftrace_hook(func, stext, etext),
155 "in-kernel func should be benign"
156 );
157 }
158
159 #[test]
160 fn classify_out_of_kernel_suspicious() {
161 let stext = 0xFFFF_FFFF_8100_0000_u64;
162 let etext = 0xFFFF_FFFF_8200_0000_u64;
163 let func = 0xFFFF_C900_0000_0000_u64; assert!(
165 classify_ftrace_hook(func, stext, etext),
166 "out-of-kernel func should be suspicious"
167 );
168 }
169
170 #[test]
172 fn walk_ftrace_hooks_with_symbol_returns_entries() {
173 use memf_core::test_builders::flags;
174
175 let list_head_vaddr: u64 = 0xFFFF_8000_0010_0000;
188 let list_head_paddr: u64 = 0x0080_0000;
189 let ops_vaddr: u64 = 0xFFFF_8000_0010_1000;
190 let ops_paddr: u64 = 0x0081_0000;
191
192 let func_ptr: u64 = 0xFFFF_FFFF_8150_0000; let ops_flags: u32 = 0x0001;
198
199 let mut list_head_data = [0u8; 0x1000];
201 list_head_data[0..8].copy_from_slice(&(ops_vaddr + 8).to_le_bytes());
202 list_head_data[8..16].copy_from_slice(&(ops_vaddr + 8).to_le_bytes());
203
204 let mut ops_data = [0u8; 0x1000];
210 ops_data[0x00..0x08].copy_from_slice(&func_ptr.to_le_bytes());
211 ops_data[0x08..0x10].copy_from_slice(&list_head_vaddr.to_le_bytes());
212 ops_data[0x10..0x18].copy_from_slice(&list_head_vaddr.to_le_bytes());
213 ops_data[0x18..0x1C].copy_from_slice(&ops_flags.to_le_bytes());
214
215 let stext: u64 = 0xFFFF_FFFF_8100_0000;
216 let etext: u64 = 0xFFFF_FFFF_8200_0000;
217
218 let isf = IsfBuilder::new()
219 .add_struct("ftrace_ops", 64)
220 .add_field("ftrace_ops", "func", 0, "pointer")
221 .add_field("ftrace_ops", "list", 8, "list_head")
222 .add_field("ftrace_ops", "flags", 0x18, "unsigned int")
223 .add_struct("list_head", 16)
224 .add_field("list_head", "next", 0, "pointer")
225 .add_field("list_head", "prev", 8, "pointer")
226 .add_symbol("ftrace_ops_list", list_head_vaddr)
227 .add_symbol("_stext", stext)
228 .add_symbol("_etext", etext)
229 .build_json();
230
231 let resolver = IsfResolver::from_value(&isf).unwrap();
232 let (cr3, mut mem) = PageTableBuilder::new()
233 .map_4k(
234 list_head_vaddr,
235 list_head_paddr,
236 flags::PRESENT | flags::WRITABLE,
237 )
238 .map_4k(ops_vaddr, ops_paddr, flags::PRESENT | flags::WRITABLE)
239 .build();
240 mem.write_bytes(list_head_paddr, &list_head_data);
241 mem.write_bytes(ops_paddr, &ops_data);
242
243 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
244 let reader = ObjectReader::new(vas, Box::new(resolver));
245
246 let hooks = walk_ftrace_hooks(&reader).unwrap();
247 assert_eq!(hooks.len(), 1, "should find one ftrace hook");
248 assert_eq!(hooks[0].func, func_ptr);
249 assert!(!hooks[0].is_suspicious, "in-kernel func should be benign");
250 }
251}