1use memf_core::object_reader::ObjectReader;
10use memf_format::PhysicalMemoryProvider;
11
12use crate::Result;
13
14const FOP_FIELDS: &[&str] = &[
16 "read",
17 "write",
18 "open",
19 "release",
20 "unlocked_ioctl",
21 "llseek",
22 "mmap",
23 "poll",
24 "read_iter",
25 "write_iter",
26];
27
28#[derive(Debug, Clone, serde::Serialize)]
30pub struct FopsHookInfo {
31 pub path: String,
33 pub struct_address: u64,
35 pub hooked_functions: Vec<HookedFop>,
37 pub is_suspicious: bool,
39}
40
41#[derive(Debug, Clone, serde::Serialize)]
43pub struct HookedFop {
44 pub function_name: String,
46 pub target_address: u64,
48 pub is_in_kernel_text: bool,
50}
51
52pub fn is_kernel_text_address(addr: u64, kernel_start: u64, kernel_end: u64) -> bool {
57 addr >= kernel_start && addr < kernel_end
58}
59
60pub fn check_fops_entry<P: PhysicalMemoryProvider>(
65 reader: &ObjectReader<P>,
66 fops_addr: u64,
67 kernel_start: u64,
68 kernel_end: u64,
69) -> Vec<HookedFop> {
70 let mut results = Vec::new();
71
72 for &field_name in FOP_FIELDS {
73 let ptr: u64 = match reader.read_pointer(fops_addr, "file_operations", field_name) {
74 Ok(p) => p,
75 Err(_) => continue, };
77
78 if ptr == 0 {
80 continue;
81 }
82
83 results.push(HookedFop {
84 function_name: field_name.to_string(),
85 target_address: ptr,
86 is_in_kernel_text: is_kernel_text_address(ptr, kernel_start, kernel_end),
87 });
88 }
89
90 results
91}
92
93const MAX_PROC_ENTRIES: usize = 10_000;
95
96pub fn scan_proc_fops<P: PhysicalMemoryProvider>(
106 reader: &ObjectReader<P>,
107) -> Result<Vec<FopsHookInfo>> {
108 let Some(proc_root) = reader.symbols().symbol_address("proc_root") else {
110 return Ok(Vec::new());
111 };
112 let Some(kernel_start) = reader.symbols().symbol_address("_stext") else {
113 return Ok(Vec::new());
114 };
115 let Some(kernel_end) = reader.symbols().symbol_address("_etext") else {
116 return Ok(Vec::new());
117 };
118
119 let mut results = Vec::new();
120
121 let mut stack = Vec::new();
123 let subdir: u64 = reader
124 .read_pointer(proc_root, "proc_dir_entry", "subdir")
125 .unwrap_or(0);
126 if subdir != 0 {
127 stack.push((subdir, "/proc".to_string()));
128 }
129
130 let mut visited = 0usize;
131 while let Some((entry_addr, parent_path)) = stack.pop() {
132 if visited >= MAX_PROC_ENTRIES {
133 break;
134 }
135 visited += 1;
136
137 let name = reader
139 .read_field_string(entry_addr, "proc_dir_entry", "name", 128)
140 .unwrap_or_else(|_| "<unknown>".to_string());
141 let path = format!("{parent_path}/{name}");
142
143 let fops_addr: u64 = reader
145 .read_pointer(entry_addr, "proc_dir_entry", "proc_fops")
146 .unwrap_or(0);
147
148 if fops_addr != 0 {
149 let hooked_functions = check_fops_entry(reader, fops_addr, kernel_start, kernel_end);
150 let is_suspicious = hooked_functions.iter().any(|f| !f.is_in_kernel_text);
151
152 results.push(FopsHookInfo {
153 path: path.clone(),
154 struct_address: fops_addr,
155 hooked_functions,
156 is_suspicious,
157 });
158 }
159
160 let child: u64 = reader
162 .read_pointer(entry_addr, "proc_dir_entry", "subdir")
163 .unwrap_or(0);
164 if child != 0 {
165 stack.push((child, path));
166 }
167
168 let next: u64 = reader
170 .read_pointer(entry_addr, "proc_dir_entry", "next")
171 .unwrap_or(0);
172 if next != 0 {
173 stack.push((next, parent_path));
174 }
175 }
176
177 Ok(results)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
184 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
185 use memf_symbols::isf::IsfResolver;
186 use memf_symbols::test_builders::IsfBuilder;
187
188 #[test]
193 fn is_kernel_text_address_inside() {
194 let start = 0xFFFF_8000_0000_0000u64;
195 let end = 0xFFFF_8000_00FF_FFFFu64;
196
197 assert!(is_kernel_text_address(start, start, end));
199 assert!(is_kernel_text_address(start + 0x1000, start, end));
201 assert!(is_kernel_text_address(end - 1, start, end));
203 }
204
205 #[test]
206 fn is_kernel_text_address_outside() {
207 let start = 0xFFFF_8000_0000_0000u64;
208 let end = 0xFFFF_8000_00FF_FFFFu64;
209
210 assert!(!is_kernel_text_address(start - 1, start, end));
212 assert!(!is_kernel_text_address(end, start, end));
214 assert!(!is_kernel_text_address(end + 1, start, end));
216 assert!(!is_kernel_text_address(0xFFFF_C900_DEAD_BEEF, start, end));
218 assert!(!is_kernel_text_address(0, start, end));
220 }
221
222 fn make_fops_reader(
228 fops_data: &[u8],
229 fops_vaddr: u64,
230 fops_paddr: u64,
231 kernel_start: u64,
232 kernel_end: u64,
233 ) -> ObjectReader<SyntheticPhysMem> {
234 let isf = IsfBuilder::new()
235 .add_struct("file_operations", 256)
236 .add_field("file_operations", "read", 0, "pointer")
237 .add_field("file_operations", "write", 8, "pointer")
238 .add_field("file_operations", "open", 16, "pointer")
239 .add_field("file_operations", "release", 24, "pointer")
240 .add_field("file_operations", "unlocked_ioctl", 32, "pointer")
241 .add_field("file_operations", "llseek", 40, "pointer")
242 .add_field("file_operations", "mmap", 48, "pointer")
243 .add_field("file_operations", "poll", 56, "pointer")
244 .add_field("file_operations", "read_iter", 64, "pointer")
245 .add_field("file_operations", "write_iter", 72, "pointer")
246 .add_symbol("_stext", kernel_start)
247 .add_symbol("_etext", kernel_end)
248 .build_json();
249
250 let resolver = IsfResolver::from_value(&isf).unwrap();
251 let (cr3, mem) = PageTableBuilder::new()
252 .map_4k(fops_vaddr, fops_paddr, ptflags::WRITABLE)
253 .write_phys(fops_paddr, fops_data)
254 .build();
255 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
256 ObjectReader::new(vas, Box::new(resolver))
257 }
258
259 #[test]
260 fn classify_fops_all_kernel() {
261 let kernel_start: u64 = 0xFFFF_8000_0000_0000;
262 let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
263 let fops_vaddr: u64 = 0xFFFF_8000_0010_0000;
264 let fops_paddr: u64 = 0x0080_0000;
265
266 let mut fops_data = vec![0u8; 4096];
268 let kernel_func = kernel_start + 0x1000; for i in 0..FOP_FIELDS.len() {
270 let offset = i * 8;
271 fops_data[offset..offset + 8].copy_from_slice(&kernel_func.to_le_bytes());
272 }
273
274 let reader = make_fops_reader(&fops_data, fops_vaddr, fops_paddr, kernel_start, kernel_end);
275
276 let results = check_fops_entry(&reader, fops_vaddr, kernel_start, kernel_end);
277
278 assert!(!results.is_empty());
280 for fop in &results {
281 assert!(
282 fop.is_in_kernel_text,
283 "function {} at {:#x} should be in kernel text",
284 fop.function_name, fop.target_address,
285 );
286 }
287 }
288
289 #[test]
290 fn classify_fops_hooked_pointer() {
291 let kernel_start: u64 = 0xFFFF_8000_0000_0000;
292 let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
293 let fops_vaddr: u64 = 0xFFFF_8000_0010_0000;
294 let fops_paddr: u64 = 0x0080_0000;
295
296 let mut fops_data = vec![0u8; 4096];
298 let kernel_func = kernel_start + 0x1000;
299 let hooked_addr: u64 = 0xFFFF_C900_DEAD_BEEF; fops_data[0..8].copy_from_slice(&hooked_addr.to_le_bytes());
303 for i in 1..FOP_FIELDS.len() {
305 let offset = i * 8;
306 fops_data[offset..offset + 8].copy_from_slice(&kernel_func.to_le_bytes());
307 }
308
309 let reader = make_fops_reader(&fops_data, fops_vaddr, fops_paddr, kernel_start, kernel_end);
310
311 let results = check_fops_entry(&reader, fops_vaddr, kernel_start, kernel_end);
312
313 let read_fop = results.iter().find(|f| f.function_name == "read").unwrap();
315 assert!(!read_fop.is_in_kernel_text);
316 assert_eq!(read_fop.target_address, hooked_addr);
317
318 for fop in results.iter().filter(|f| f.function_name != "read") {
320 assert!(
321 fop.is_in_kernel_text,
322 "function {} should be in kernel text",
323 fop.function_name,
324 );
325 }
326 }
327
328 #[test]
333 fn scan_proc_fops_no_symbol() {
334 let isf = IsfBuilder::new()
336 .add_struct("file_operations", 256)
337 .add_field("file_operations", "read", 0, "pointer")
338 .build_json();
339
340 let resolver = IsfResolver::from_value(&isf).unwrap();
341 let (cr3, mem) = PageTableBuilder::new().build();
342 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
343 let reader = ObjectReader::new(vas, Box::new(resolver));
344
345 let results = scan_proc_fops(&reader).unwrap();
346 assert!(results.is_empty());
347 }
348
349 #[test]
350 fn scan_proc_fops_missing_stext_returns_empty() {
351 let isf = IsfBuilder::new()
353 .add_struct("file_operations", 256)
354 .add_field("file_operations", "read", 0, "pointer")
355 .add_symbol("proc_root", 0xFFFF_8000_0010_0000)
356 .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
358 .build_json();
359
360 let resolver = IsfResolver::from_value(&isf).unwrap();
361 let (cr3, mem) = PageTableBuilder::new().build();
362 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
363 let reader = ObjectReader::new(vas, Box::new(resolver));
364
365 let results = scan_proc_fops(&reader).unwrap();
366 assert!(results.is_empty(), "missing _stext should yield empty vec");
367 }
368
369 #[test]
370 fn scan_proc_fops_missing_etext_returns_empty() {
371 let isf = IsfBuilder::new()
373 .add_struct("file_operations", 256)
374 .add_field("file_operations", "read", 0, "pointer")
375 .add_symbol("proc_root", 0xFFFF_8000_0010_0000)
376 .add_symbol("_stext", 0xFFFF_8000_0000_0000)
377 .build_json();
379
380 let resolver = IsfResolver::from_value(&isf).unwrap();
381 let (cr3, mem) = PageTableBuilder::new().build();
382 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
383 let reader = ObjectReader::new(vas, Box::new(resolver));
384
385 let results = scan_proc_fops(&reader).unwrap();
386 assert!(results.is_empty(), "missing _etext should yield empty vec");
387 }
388
389 #[test]
394 fn scan_proc_fops_symbol_present_empty_proc_tree() {
395 let proc_root_vaddr: u64 = 0xFFFF_8800_0060_0000;
398 let proc_root_paddr: u64 = 0x0070_0000;
399 let kernel_start: u64 = 0xFFFF_8000_0000_0000;
400 let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
401
402 let page = [0u8; 4096];
404
405 let isf = IsfBuilder::new()
406 .add_struct("proc_dir_entry", 256)
407 .add_field("proc_dir_entry", "subdir", 0, "pointer")
408 .add_field("proc_dir_entry", "next", 8, "pointer")
409 .add_field("proc_dir_entry", "proc_fops", 16, "pointer")
410 .add_field("proc_dir_entry", "name", 24, "char")
411 .add_struct("file_operations", 256)
412 .add_field("file_operations", "read", 0, "pointer")
413 .add_symbol("proc_root", proc_root_vaddr)
414 .add_symbol("_stext", kernel_start)
415 .add_symbol("_etext", kernel_end)
416 .build_json();
417
418 let resolver = IsfResolver::from_value(&isf).unwrap();
419 let (cr3, mem) = PageTableBuilder::new()
420 .map_4k(proc_root_vaddr, proc_root_paddr, ptflags::WRITABLE)
421 .write_phys(proc_root_paddr, &page)
422 .build();
423 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
424 let reader = ObjectReader::new(vas, Box::new(resolver));
425
426 let results = scan_proc_fops(&reader).unwrap_or_default();
427 assert!(
428 results.is_empty(),
429 "empty proc tree should produce no fops hook entries"
430 );
431 }
432
433 #[test]
434 fn scan_proc_fops_with_entry_no_proc_fops() {
435 let proc_root_vaddr: u64 = 0xFFFF_8800_0070_0000;
437 let proc_root_paddr: u64 = 0x0040_0000;
438 let entry_vaddr: u64 = 0xFFFF_8800_0071_0000;
439 let entry_paddr: u64 = 0x0041_0000;
440 let kernel_start: u64 = 0xFFFF_8000_0000_0000;
441 let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
442
443 let mut root_page = [0u8; 4096];
445 root_page[0..8].copy_from_slice(&entry_vaddr.to_le_bytes()); let mut entry_page = [0u8; 4096];
450 entry_page[24..31].copy_from_slice(b"modules"); let isf = IsfBuilder::new()
454 .add_struct("proc_dir_entry", 256)
455 .add_field("proc_dir_entry", "subdir", 0x00u64, "pointer")
456 .add_field("proc_dir_entry", "next", 0x08u64, "pointer")
457 .add_field("proc_dir_entry", "proc_fops", 0x10u64, "pointer")
458 .add_field("proc_dir_entry", "name", 0x18u64, "char")
459 .add_struct("file_operations", 256)
460 .add_field("file_operations", "read", 0x00u64, "pointer")
461 .add_symbol("proc_root", proc_root_vaddr)
462 .add_symbol("_stext", kernel_start)
463 .add_symbol("_etext", kernel_end)
464 .build_json();
465 let resolver = IsfResolver::from_value(&isf).unwrap();
466
467 let (cr3, mem) = PageTableBuilder::new()
468 .map_4k(proc_root_vaddr, proc_root_paddr, ptflags::WRITABLE)
469 .write_phys(proc_root_paddr, &root_page)
470 .map_4k(entry_vaddr, entry_paddr, ptflags::WRITABLE)
471 .write_phys(entry_paddr, &entry_page)
472 .build();
473 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
474 let reader = ObjectReader::new(vas, Box::new(resolver));
475
476 let results = scan_proc_fops(&reader).unwrap_or_default();
477 assert!(
479 results.is_empty(),
480 "entry with proc_fops==0 should produce no hook entries"
481 );
482 }
483
484 #[test]
485 fn scan_proc_fops_with_entry_and_proc_fops_in_kernel() {
486 let proc_root_vaddr: u64 = 0xFFFF_8800_0080_0000;
489 let proc_root_paddr: u64 = 0x0042_0000;
490 let entry_vaddr: u64 = 0xFFFF_8800_0081_0000;
491 let entry_paddr: u64 = 0x0043_0000;
492 let fops_vaddr: u64 = 0xFFFF_8800_0082_0000;
493 let fops_paddr: u64 = 0x0044_0000;
494 let kernel_start: u64 = 0xFFFF_8000_0000_0000;
495 let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
496 let kernel_func: u64 = kernel_start + 0x5000;
497
498 let mut root_page = [0u8; 4096];
500 root_page[0..8].copy_from_slice(&entry_vaddr.to_le_bytes());
501
502 let mut entry_page = [0u8; 4096];
504 entry_page[0x10..0x18].copy_from_slice(&fops_vaddr.to_le_bytes()); entry_page[0x18..0x1b].copy_from_slice(b"net"); let mut fops_page = [0u8; 4096];
511 fops_page[0..8].copy_from_slice(&kernel_func.to_le_bytes()); let isf = IsfBuilder::new()
514 .add_struct("proc_dir_entry", 256)
515 .add_field("proc_dir_entry", "subdir", 0x00u64, "pointer")
516 .add_field("proc_dir_entry", "next", 0x08u64, "pointer")
517 .add_field("proc_dir_entry", "proc_fops", 0x10u64, "pointer")
518 .add_field("proc_dir_entry", "name", 0x18u64, "char")
519 .add_struct("file_operations", 256)
520 .add_field("file_operations", "read", 0x00u64, "pointer")
521 .add_field("file_operations", "write", 0x08u64, "pointer")
522 .add_field("file_operations", "open", 0x10u64, "pointer")
523 .add_field("file_operations", "release", 0x18u64, "pointer")
524 .add_field("file_operations", "unlocked_ioctl", 0x20u64, "pointer")
525 .add_field("file_operations", "llseek", 0x28u64, "pointer")
526 .add_field("file_operations", "mmap", 0x30u64, "pointer")
527 .add_field("file_operations", "poll", 0x38u64, "pointer")
528 .add_field("file_operations", "read_iter", 0x40u64, "pointer")
529 .add_field("file_operations", "write_iter", 0x48u64, "pointer")
530 .add_symbol("proc_root", proc_root_vaddr)
531 .add_symbol("_stext", kernel_start)
532 .add_symbol("_etext", kernel_end)
533 .build_json();
534 let resolver = IsfResolver::from_value(&isf).unwrap();
535
536 let (cr3, mem) = PageTableBuilder::new()
537 .map_4k(proc_root_vaddr, proc_root_paddr, ptflags::WRITABLE)
538 .write_phys(proc_root_paddr, &root_page)
539 .map_4k(entry_vaddr, entry_paddr, ptflags::WRITABLE)
540 .write_phys(entry_paddr, &entry_page)
541 .map_4k(fops_vaddr, fops_paddr, ptflags::WRITABLE)
542 .write_phys(fops_paddr, &fops_page)
543 .build();
544 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
545 let reader = ObjectReader::new(vas, Box::new(resolver));
546
547 let results = scan_proc_fops(&reader).unwrap_or_default();
548 assert_eq!(
549 results.len(),
550 1,
551 "should find exactly one entry with proc_fops"
552 );
553 let entry = &results[0];
554 assert!(
555 !entry.is_suspicious,
556 "kernel-text pointer should not be suspicious"
557 );
558 assert!(
559 entry.path.contains("net") || entry.path.contains("/proc"),
560 "path should contain entry name"
561 );
562 }
563
564 #[test]
565 fn check_fops_entry_null_pointer_skipped() {
566 let kernel_start: u64 = 0xFFFF_8000_0000_0000;
568 let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
569 let fops_vaddr: u64 = 0xFFFF_8000_0010_0000;
570 let fops_paddr: u64 = 0x0080_0000;
571
572 let fops_data = vec![0u8; 4096];
574
575 let reader = make_fops_reader(&fops_data, fops_vaddr, fops_paddr, kernel_start, kernel_end);
576 let results = check_fops_entry(&reader, fops_vaddr, kernel_start, kernel_end);
577
578 assert!(
579 results.is_empty(),
580 "all-null fops struct should produce no HookedFop entries"
581 );
582 }
583}