1use memf_core::object_reader::ObjectReader;
11use memf_format::PhysicalMemoryProvider;
12
13use crate::{Error, KernelHookInfo, Result};
14
15const PROLOGUE_SIZE: usize = 16;
17
18const FUNCTIONS_TO_CHECK: &[&str] = &[
20 "sys_read",
21 "sys_write",
22 "sys_open",
23 "sys_close",
24 "vfs_read",
25 "vfs_write",
26 "tcp4_seq_show",
27 "filldir",
28 "filldir64",
29];
30
31pub fn check_inline_hooks<P: PhysicalMemoryProvider>(
36 reader: &ObjectReader<P>,
37) -> Result<Vec<KernelHookInfo>> {
38 let stext =
39 reader
40 .symbols()
41 .symbol_address("_stext")
42 .ok_or_else(|| Error::MissingKernelSymbol {
43 name: "_stext".into(),
44 })?;
45 let etext =
46 reader
47 .symbols()
48 .symbol_address("_etext")
49 .ok_or_else(|| Error::MissingKernelSymbol {
50 name: "_etext".into(),
51 })?;
52
53 let mut results = Vec::new();
54
55 for &func_name in FUNCTIONS_TO_CHECK {
56 let Some(func_addr) = reader.symbols().symbol_address(func_name) else {
57 continue; };
59
60 let Ok(prologue) = reader.read_bytes(func_addr, PROLOGUE_SIZE) else {
61 continue;
62 };
63
64 let (hook_type, target) = analyze_prologue(&prologue, func_addr);
65 let suspicious = hook_type != "none" && target.map_or(true, |t| t < stext || t > etext);
68
69 results.push(KernelHookInfo {
70 symbol: func_name.to_string(),
71 address: func_addr,
72 hook_type,
73 target,
74 suspicious,
75 });
76 }
77
78 Ok(results)
79}
80
81#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
85fn analyze_prologue(bytes: &[u8], func_addr: u64) -> (String, Option<u64>) {
86 if bytes.len() < PROLOGUE_SIZE {
87 return ("none".to_string(), None);
88 }
89
90 if bytes[0] == 0xE9 {
92 let offset = bytes[1..5].try_into().map_or(0, i32::from_le_bytes);
93 let target = (func_addr as i64 + 5 + i64::from(offset)) as u64;
94 return ("jmp_rel32".to_string(), Some(target));
95 }
96
97 if bytes[0] == 0xFF && bytes[1] == 0x25 {
99 let offset = bytes[2..6].try_into().map_or(0, i32::from_le_bytes);
100 let target = (func_addr as i64 + 6 + i64::from(offset)) as u64;
101 return ("jmp_indirect".to_string(), Some(target));
102 }
103
104 if bytes.len() >= 12
106 && bytes[0] == 0x48
107 && bytes[1] == 0xB8
108 && bytes[10] == 0xFF
109 && bytes[11] == 0xE0
110 {
111 let target = bytes[2..10].try_into().map_or(0, u64::from_le_bytes);
112 return ("mov_rax_jmp".to_string(), Some(target));
113 }
114
115 ("none".to_string(), None)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
122 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
123 use memf_symbols::isf::IsfResolver;
124 use memf_symbols::test_builders::IsfBuilder;
125
126 fn make_test_reader(
127 data: &[u8],
128 func_vaddr: u64,
129 func_paddr: u64,
130 stext: u64,
131 etext: u64,
132 func_symbols: &[(&str, u64)],
133 ) -> ObjectReader<SyntheticPhysMem> {
134 let mut builder = IsfBuilder::new()
135 .add_struct("task_struct", 64)
136 .add_field("task_struct", "pid", 0, "int")
137 .add_symbol("_stext", stext)
138 .add_symbol("_etext", etext);
139
140 for &(name, addr) in func_symbols {
141 builder = builder.add_symbol(name, addr);
142 }
143
144 let isf = builder.build_json();
145 let resolver = IsfResolver::from_value(&isf).unwrap();
146 let (cr3, mem) = PageTableBuilder::new()
147 .map_4k(func_vaddr, func_paddr, ptflags::WRITABLE)
148 .write_phys(func_paddr, data)
149 .build();
150 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
151 ObjectReader::new(vas, Box::new(resolver))
152 }
153
154 #[test]
155 fn clean_function_no_hook() {
156 let mut prologue = vec![0u8; 4096];
158 prologue[0] = 0x55; prologue[1] = 0x48; prologue[2] = 0x89; prologue[3] = 0xE5; let func_vaddr: u64 = 0xFFFF_8000_0001_0000;
164 let func_paddr: u64 = 0x0080_0000;
165 let stext: u64 = 0xFFFF_8000_0000_0000;
166 let etext: u64 = 0xFFFF_8000_00FF_FFFF;
167
168 let reader = make_test_reader(
169 &prologue,
170 func_vaddr,
171 func_paddr,
172 stext,
173 etext,
174 &[("sys_read", func_vaddr)],
175 );
176 let results = check_inline_hooks(&reader).unwrap();
177
178 assert_eq!(results.len(), 1);
179 assert!(!results[0].suspicious);
180 assert_eq!(results[0].symbol, "sys_read");
181 assert_eq!(results[0].hook_type, "none");
182 }
183
184 #[test]
185 fn detects_relative_jmp_hook() {
186 let mut prologue = vec![0u8; 4096];
190 prologue[0] = 0xE9; prologue[1..5].copy_from_slice(&0x1000i32.to_le_bytes());
192
193 let func_vaddr: u64 = 0xFFFF_8000_0001_0000;
194 let func_paddr: u64 = 0x0080_0000;
195 let stext: u64 = 0xFFFF_8000_0000_0000;
196 let etext: u64 = 0xFFFF_8000_00FF_FFFF;
197
198 let reader = make_test_reader(
199 &prologue,
200 func_vaddr,
201 func_paddr,
202 stext,
203 etext,
204 &[("sys_read", func_vaddr)],
205 );
206 let results = check_inline_hooks(&reader).unwrap();
207
208 assert_eq!(results.len(), 1);
210 assert_eq!(results[0].hook_type, "jmp_rel32");
211 assert!(results[0].target.is_some());
212 assert!(
214 !results[0].suspicious,
215 "jmp into kernel text should not be suspicious"
216 );
217 }
218
219 #[test]
220 fn detects_movabs_jmp_rax_hook() {
221 let mut prologue = vec![0u8; 4096];
223 prologue[0] = 0x48; prologue[1] = 0xB8; let target: u64 = 0xFFFF_C900_DEAD_BEEF;
226 prologue[2..10].copy_from_slice(&target.to_le_bytes());
227 prologue[10] = 0xFF; prologue[11] = 0xE0; let func_vaddr: u64 = 0xFFFF_8000_0001_0000;
231 let func_paddr: u64 = 0x0080_0000;
232 let stext: u64 = 0xFFFF_8000_0000_0000;
233 let etext: u64 = 0xFFFF_8000_00FF_FFFF;
234
235 let reader = make_test_reader(
236 &prologue,
237 func_vaddr,
238 func_paddr,
239 stext,
240 etext,
241 &[("sys_read", func_vaddr)],
242 );
243 let results = check_inline_hooks(&reader).unwrap();
244
245 assert_eq!(results.len(), 1);
246 assert!(results[0].suspicious);
247 assert_eq!(results[0].hook_type, "mov_rax_jmp");
248 assert_eq!(results[0].target, Some(target));
249 }
250
251 #[test]
252 fn analyze_prologue_normal() {
253 let bytes = [
254 0x55, 0x48, 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x20, 0, 0, 0, 0, 0, 0, 0, 0,
255 ];
256 let (hook_type, target) = analyze_prologue(&bytes, 0xFFFF_8000_0001_0000);
257 assert_eq!(hook_type, "none");
258 assert_eq!(target, None);
259 }
260
261 #[test]
262 fn detects_indirect_jmp_hook() {
263 let mut prologue = vec![0u8; 4096];
266 prologue[0] = 0xFF;
267 prologue[1] = 0x25;
268 prologue[2..6].copy_from_slice(&0i32.to_le_bytes());
270
271 let func_vaddr: u64 = 0xFFFF_8000_0002_0000;
272 let func_paddr: u64 = 0x0081_0000;
273 let stext: u64 = 0xFFFF_8000_0000_0000;
274 let etext: u64 = 0xFFFF_8000_00FF_FFFF;
275
276 let reader = make_test_reader(
277 &prologue,
278 func_vaddr,
279 func_paddr,
280 stext,
281 etext,
282 &[("sys_write", func_vaddr)],
283 );
284 let results = check_inline_hooks(&reader).unwrap();
285
286 assert_eq!(results.len(), 1);
287 assert_eq!(results[0].hook_type, "jmp_indirect");
288 assert_eq!(results[0].target, Some(func_vaddr + 6));
290 assert!(
292 !results[0].suspicious,
293 "jmp_indirect targeting kernel text must not be suspicious"
294 );
295 }
296
297 #[test]
298 fn skips_symbol_with_unreadable_prologue() {
299 let isf = IsfBuilder::new()
302 .add_struct("task_struct", 64)
303 .add_field("task_struct", "pid", 0, "int")
304 .add_symbol("_stext", 0xFFFF_8000_0000_0000)
305 .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
306 .add_symbol("sys_read", 0xFFFF_DEAD_0000_0000)
308 .build_json();
309
310 let resolver = IsfResolver::from_value(&isf).unwrap();
311 let (cr3, mem) = PageTableBuilder::new().build();
312 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
313 let reader = ObjectReader::new(vas, Box::new(resolver));
314
315 let results = check_inline_hooks(&reader).unwrap();
316 assert!(results.is_empty(), "unreadable prologue should be skipped");
318 }
319
320 #[test]
321 fn detects_rel_jmp_hook_outside_text_region() {
322 let mut prologue = vec![0u8; 4096];
325 prologue[0] = 0xE9;
326 prologue[1..5].copy_from_slice(&0x0FFF_0000i32.to_le_bytes());
328
329 let func_vaddr: u64 = 0xFFFF_8000_0003_0000;
330 let func_paddr: u64 = 0x0082_0000;
331 let stext: u64 = 0xFFFF_8000_0000_0000;
332 let etext: u64 = 0xFFFF_8000_0005_0000; let reader = make_test_reader(
335 &prologue,
336 func_vaddr,
337 func_paddr,
338 stext,
339 etext,
340 &[("vfs_read", func_vaddr)],
341 );
342 let results = check_inline_hooks(&reader).unwrap();
343 assert_eq!(results.len(), 1);
344 assert_eq!(results[0].hook_type, "jmp_rel32");
345 assert!(
346 results[0].suspicious,
347 "JMP to outside text region must be suspicious"
348 );
349 }
350
351 #[test]
352 fn analyze_prologue_short_bytes_returns_none() {
353 let short = [0x55u8; 4]; let (hook_type, target) = analyze_prologue(&short, 0xFFFF_8000_0001_0000);
356 assert_eq!(hook_type, "none");
357 assert_eq!(target, None);
358 }
359
360 #[test]
361 fn skips_missing_symbols() {
362 let isf = IsfBuilder::new()
364 .add_struct("task_struct", 64)
365 .add_field("task_struct", "pid", 0, "int")
366 .add_symbol("_stext", 0xFFFF_8000_0000_0000)
367 .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
368 .build_json();
370
371 let resolver = IsfResolver::from_value(&isf).unwrap();
372 let (cr3, mem) = PageTableBuilder::new().build();
373 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
374 let reader = ObjectReader::new(vas, Box::new(resolver));
375
376 let results = check_inline_hooks(&reader).unwrap();
377 assert!(results.is_empty());
378 }
379
380 #[test]
381 fn missing_stext_returns_missing_kernel_symbol() {
382 let isf = IsfBuilder::new().build_json();
383 let resolver = IsfResolver::from_value(&isf).unwrap();
384 let (cr3, mem) = PageTableBuilder::new().build();
385 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
386 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
387 let result = check_inline_hooks(&reader);
388 assert!(
389 matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "_stext"),
390 "expected MissingKernelSymbol {{name: \"_stext\"}}, got {result:?}"
391 );
392 }
393
394 #[test]
395 fn missing_etext_returns_missing_kernel_symbol() {
396 let isf = IsfBuilder::new()
397 .add_symbol("_stext", 0xFFFF_8000_0000_0000)
398 .build_json();
399 let resolver = IsfResolver::from_value(&isf).unwrap();
400 let (cr3, mem) = PageTableBuilder::new().build();
401 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
402 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
403 let result = check_inline_hooks(&reader);
404 assert!(
405 matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "_etext"),
406 "expected MissingKernelSymbol {{name: \"_etext\"}}, got {result:?}"
407 );
408 }
409}