1use memf_core::object_reader::ObjectReader;
11use memf_format::PhysicalMemoryProvider;
12
13use crate::Result;
14
15const AFINFO_SYMBOLS: &[(&str, &str)] = &[
17 ("tcp_seq_afinfo", "tcp"),
18 ("udp_seq_afinfo", "udp"),
19 ("tcp6_seq_afinfo", "tcp6"),
20 ("udp6_seq_afinfo", "udp6"),
21 ("raw_seq_afinfo", "raw"),
22];
23
24const SEQ_OPS_FIELDS: &[&str] = &["show", "start", "next", "stop"];
26
27#[derive(Debug, Clone, serde::Serialize)]
29pub struct AfInfoHookInfo {
30 pub protocol: String,
32 pub struct_name: String,
34 pub field: String,
36 pub hook_address: u64,
38 pub expected_module: String,
40 pub actual_module: String,
42 pub is_hooked: bool,
44}
45
46pub use crate::heuristics::classify_afinfo_hook;
52
53pub fn walk_check_afinfo<P: PhysicalMemoryProvider>(
63 reader: &ObjectReader<P>,
64) -> Result<Vec<AfInfoHookInfo>> {
65 let symbols = reader.symbols();
66
67 let Some(kernel_start) = symbols.symbol_address("_stext") else {
69 return Ok(Vec::new());
70 };
71 let Some(kernel_end) = symbols.symbol_address("_etext") else {
72 return Ok(Vec::new());
73 };
74
75 let mut results = Vec::new();
76
77 for &(sym_name, protocol) in AFINFO_SYMBOLS {
78 let Some(afinfo_addr) = symbols.symbol_address(sym_name) else {
80 continue;
81 };
82
83 let seq_ops_addr: u64 = match reader.read_pointer(afinfo_addr, "seq_afinfo", "seq_ops") {
85 Ok(addr) => addr,
86 Err(_) => continue, };
88
89 if seq_ops_addr == 0 {
90 continue; }
92
93 for &field_name in SEQ_OPS_FIELDS {
95 let ptr: u64 = match reader.read_pointer(seq_ops_addr, "seq_operations", field_name) {
96 Ok(p) => p,
97 Err(_) => continue,
98 };
99
100 let is_hooked = classify_afinfo_hook(ptr, kernel_start, kernel_end);
101
102 let actual_module = if ptr == 0 {
103 "null".to_string()
104 } else if is_hooked {
105 format!("unknown (0x{ptr:016x})")
106 } else {
107 "kernel".to_string()
108 };
109
110 results.push(AfInfoHookInfo {
111 protocol: protocol.to_string(),
112 struct_name: sym_name.to_string(),
113 field: format!("seq_ops.{field_name}"),
114 hook_address: ptr,
115 expected_module: "kernel".to_string(),
116 actual_module,
117 is_hooked,
118 });
119 }
120 }
121
122 Ok(results)
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use memf_core::test_builders::{flags as ptflags, PageTableBuilder};
129 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
130 use memf_symbols::isf::IsfResolver;
131 use memf_symbols::test_builders::IsfBuilder;
132
133 #[test]
138 fn hook_outside_kernel_suspicious() {
139 let kernel_start = 0xFFFF_8000_0000_0000u64;
140 let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
141
142 assert!(classify_afinfo_hook(
144 0xFFFF_C900_DEAD_BEEF,
145 kernel_start,
146 kernel_end
147 ));
148 assert!(classify_afinfo_hook(
150 kernel_start - 1,
151 kernel_start,
152 kernel_end
153 ));
154 assert!(classify_afinfo_hook(
156 kernel_end + 1,
157 kernel_start,
158 kernel_end
159 ));
160 }
161
162 #[test]
163 fn hook_inside_kernel_benign() {
164 let kernel_start = 0xFFFF_8000_0000_0000u64;
165 let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
166
167 assert!(!classify_afinfo_hook(
169 kernel_start,
170 kernel_start,
171 kernel_end
172 ));
173 assert!(!classify_afinfo_hook(
175 kernel_start + 0x1000,
176 kernel_start,
177 kernel_end
178 ));
179 assert!(!classify_afinfo_hook(kernel_end, kernel_start, kernel_end));
181 }
182
183 #[test]
184 fn hook_zero_benign() {
185 let kernel_start = 0xFFFF_8000_0000_0000u64;
186 let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
187
188 assert!(!classify_afinfo_hook(0, kernel_start, kernel_end));
190 }
191
192 #[test]
193 fn classify_multiple_protocols() {
194 let kernel_start = 0xFFFF_8000_0000_0000u64;
195 let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
196 let kernel_func = kernel_start + 0x5000;
197 let module_func = 0xFFFF_C900_1234_0000u64;
198
199 assert!(!classify_afinfo_hook(kernel_func, kernel_start, kernel_end));
202 assert!(classify_afinfo_hook(module_func, kernel_start, kernel_end));
204 assert!(!classify_afinfo_hook(0, kernel_start, kernel_end));
206 assert!(classify_afinfo_hook(
208 kernel_end + 0x100,
209 kernel_start,
210 kernel_end
211 ));
212 }
213
214 #[test]
219 fn afinfo_hook_info_serializes() {
220 let info = AfInfoHookInfo {
221 protocol: "tcp".to_string(),
222 struct_name: "tcp_seq_afinfo".to_string(),
223 field: "seq_ops.show".to_string(),
224 hook_address: 0xFFFF_C900_DEAD_BEEF,
225 expected_module: "kernel".to_string(),
226 actual_module: "rootkit.ko".to_string(),
227 is_hooked: true,
228 };
229
230 let json = serde_json::to_value(&info).unwrap();
231 assert_eq!(json["protocol"], "tcp");
232 assert_eq!(json["struct_name"], "tcp_seq_afinfo");
233 assert_eq!(json["field"], "seq_ops.show");
234 assert_eq!(json["is_hooked"], true);
235 }
236
237 #[test]
242 fn walk_check_afinfo_no_symbols_returns_empty() {
243 let isf = IsfBuilder::new()
245 .add_struct("task_struct", 64)
246 .add_field("task_struct", "pid", 0, "int")
247 .add_symbol("_stext", 0xFFFF_8000_0000_0000)
248 .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
249 .build_json();
250
251 let resolver = IsfResolver::from_value(&isf).unwrap();
252 let (cr3, mem) = PageTableBuilder::new().build();
253 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
254 let reader = ObjectReader::new(vas, Box::new(resolver));
255
256 let results = walk_check_afinfo(&reader).unwrap();
257 assert!(
258 results.is_empty(),
259 "expected empty results when no afinfo symbols exist"
260 );
261 }
262
263 #[test]
264 fn walk_check_afinfo_detects_hooked_seq_ops() {
265 let kernel_start: u64 = 0xFFFF_8000_0000_0000;
266 let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
267 let afinfo_vaddr: u64 = 0xFFFF_8000_0010_0000;
268 let afinfo_paddr: u64 = 0x0080_0000;
269 let kernel_func: u64 = kernel_start + 0x1000;
270 let hooked_func: u64 = 0xFFFF_C900_DEAD_0000; let seq_ops_vaddr: u64 = 0xFFFF_8000_0020_0000;
277 let seq_ops_paddr: u64 = 0x0090_0000;
278
279 let mut seq_ops_data = vec![0u8; 4096];
281 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];
288 afinfo_data[0..8].copy_from_slice(&seq_ops_vaddr.to_le_bytes());
289
290 let isf = IsfBuilder::new()
291 .add_struct("seq_afinfo", 64)
292 .add_field("seq_afinfo", "seq_ops", 0, "pointer")
293 .add_struct("seq_operations", 32)
294 .add_field("seq_operations", "show", 0, "pointer")
295 .add_field("seq_operations", "start", 8, "pointer")
296 .add_field("seq_operations", "next", 16, "pointer")
297 .add_field("seq_operations", "stop", 24, "pointer")
298 .add_symbol("_stext", kernel_start)
299 .add_symbol("_etext", kernel_end)
300 .add_symbol("tcp_seq_afinfo", afinfo_vaddr)
301 .build_json();
302
303 let resolver = IsfResolver::from_value(&isf).unwrap();
304 let (cr3, mem) = PageTableBuilder::new()
305 .map_4k(afinfo_vaddr, afinfo_paddr, ptflags::WRITABLE)
306 .write_phys(afinfo_paddr, &afinfo_data)
307 .map_4k(seq_ops_vaddr, seq_ops_paddr, ptflags::WRITABLE)
308 .write_phys(seq_ops_paddr, &seq_ops_data)
309 .build();
310 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
311 let reader = ObjectReader::new(vas, Box::new(resolver));
312
313 let results = walk_check_afinfo(&reader).unwrap();
314
315 assert!(!results.is_empty(), "expected non-empty results");
317
318 let hooked: Vec<_> = results.iter().filter(|r| r.is_hooked).collect();
320 assert_eq!(hooked.len(), 1, "expected exactly one hooked entry");
321 assert_eq!(hooked[0].protocol, "tcp");
322 assert_eq!(hooked[0].field, "seq_ops.show");
323 assert_eq!(hooked[0].hook_address, hooked_func);
324
325 let benign: Vec<_> = results.iter().filter(|r| !r.is_hooked).collect();
327 assert_eq!(benign.len(), 3, "expected 3 benign entries");
328 }
329}