Skip to main content

memf_linux/
check_afinfo.rs

1//! Linux network protocol handler (`seq_afinfo`) hook detector.
2//!
3//! Linux rootkits commonly replace the `seq_show` function pointer in
4//! `tcp_seq_afinfo`, `udp_seq_afinfo`, and similar protocol handler
5//! structures to hide network connections from `/proc/net/tcp` and
6//! `/proc/net/udp`. This module reads those structs from memory and
7//! compares each `seq_ops` function pointer against the kernel text
8//! range (`_stext`..`_etext`) to detect hooks.
9
10use memf_core::object_reader::ObjectReader;
11use memf_format::PhysicalMemoryProvider;
12
13use crate::Result;
14
15/// Protocol handler symbols to check, paired with human-readable names.
16const 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
24/// Function pointer field names within the `seq_operations` struct.
25const SEQ_OPS_FIELDS: &[&str] = &["show", "start", "next", "stop"];
26
27/// Information about a network protocol handler with potential hooks.
28#[derive(Debug, Clone, serde::Serialize)]
29pub struct AfInfoHookInfo {
30    /// Protocol name, e.g. "tcp", "udp", "tcp6", "udp6", "raw".
31    pub protocol: String,
32    /// Kernel symbol name, e.g. "tcp_seq_afinfo".
33    pub struct_name: String,
34    /// Field path that was checked, e.g. "seq_ops.show".
35    pub field: String,
36    /// Virtual address the function pointer targets.
37    pub hook_address: u64,
38    /// Expected module (should be kernel text).
39    pub expected_module: String,
40    /// Where the hook actually points.
41    pub actual_module: String,
42    /// Whether this function pointer is considered hooked.
43    pub is_hooked: bool,
44}
45
46/// Classify whether a function pointer in a `seq_afinfo` struct is hooked.
47///
48/// - Address of `0` is not considered hooked (null/unset pointer).
49/// - Address within `[kernel_start, kernel_end]` is benign (kernel text).
50/// - Address outside that range is suspicious (hooked).
51pub use crate::heuristics::classify_afinfo_hook;
52
53/// Walk network protocol handler structs and check for hooks.
54///
55/// Looks up `tcp_seq_afinfo`, `udp_seq_afinfo`, `tcp6_seq_afinfo`,
56/// `udp6_seq_afinfo`, and `raw_seq_afinfo` symbols. For each, reads
57/// the `seq_ops` function pointers (`show`, `start`, `next`, `stop`)
58/// and compares against the kernel text range.
59///
60/// Returns `Ok(Vec::new())` if no afinfo symbols are found (graceful
61/// degradation).
62pub fn walk_check_afinfo<P: PhysicalMemoryProvider>(
63    reader: &ObjectReader<P>,
64) -> Result<Vec<AfInfoHookInfo>> {
65    let symbols = reader.symbols();
66
67    // Resolve kernel text boundaries; if missing, we cannot classify anything.
68    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        // Graceful degradation: skip symbols that aren't in this profile.
79        let Some(afinfo_addr) = symbols.symbol_address(sym_name) else {
80            continue;
81        };
82
83        // Read the seq_ops pointer from the seq_afinfo struct.
84        let seq_ops_addr: u64 = match reader.read_pointer(afinfo_addr, "seq_afinfo", "seq_ops") {
85            Ok(addr) => addr,
86            Err(_) => continue, // struct layout unavailable, skip
87        };
88
89        if seq_ops_addr == 0 {
90            continue; // No seq_ops set for this protocol
91        }
92
93        // Read each function pointer from the seq_operations struct.
94        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    // -----------------------------------------------------------------------
134    // classify_afinfo_hook unit tests
135    // -----------------------------------------------------------------------
136
137    #[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        // Address in module space, well outside kernel text
143        assert!(classify_afinfo_hook(
144            0xFFFF_C900_DEAD_BEEF,
145            kernel_start,
146            kernel_end
147        ));
148        // Address just below kernel start
149        assert!(classify_afinfo_hook(
150            kernel_start - 1,
151            kernel_start,
152            kernel_end
153        ));
154        // Address just above kernel end
155        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        // Exactly at start
168        assert!(!classify_afinfo_hook(
169            kernel_start,
170            kernel_start,
171            kernel_end
172        ));
173        // In the middle
174        assert!(!classify_afinfo_hook(
175            kernel_start + 0x1000,
176            kernel_start,
177            kernel_end
178        ));
179        // Exactly at end
180        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        // Null pointer is never considered hooked
189        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        // Simulate checking several protocol handler pointers:
200        // tcp show → kernel (benign)
201        assert!(!classify_afinfo_hook(kernel_func, kernel_start, kernel_end));
202        // udp show → module space (hooked)
203        assert!(classify_afinfo_hook(module_func, kernel_start, kernel_end));
204        // tcp6 show → null (benign)
205        assert!(!classify_afinfo_hook(0, kernel_start, kernel_end));
206        // raw show → just past kernel end (hooked)
207        assert!(classify_afinfo_hook(
208            kernel_end + 0x100,
209            kernel_start,
210            kernel_end
211        ));
212    }
213
214    // -----------------------------------------------------------------------
215    // AfInfoHookInfo struct tests
216    // -----------------------------------------------------------------------
217
218    #[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    // -----------------------------------------------------------------------
238    // walk_check_afinfo integration tests
239    // -----------------------------------------------------------------------
240
241    #[test]
242    fn walk_check_afinfo_no_symbols_returns_empty() {
243        // Build a reader with _stext/_etext but no afinfo symbols.
244        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; // Outside kernel text
271
272        // Build seq_afinfo struct: contains a pointer to seq_operations.
273        // seq_operations contains 4 function pointers (show, start, next, stop).
274        // Layout: afinfo has seq_ops at offset 0 (pointer to seq_operations struct).
275        // seq_operations: show=0, start=8, next=16, stop=24.
276        let seq_ops_vaddr: u64 = 0xFFFF_8000_0020_0000;
277        let seq_ops_paddr: u64 = 0x0090_0000;
278
279        // seq_operations data: show is hooked, rest are kernel
280        let mut seq_ops_data = vec![0u8; 4096];
281        seq_ops_data[0..8].copy_from_slice(&hooked_func.to_le_bytes()); // show → hooked
282        seq_ops_data[8..16].copy_from_slice(&kernel_func.to_le_bytes()); // start → kernel
283        seq_ops_data[16..24].copy_from_slice(&kernel_func.to_le_bytes()); // next → kernel
284        seq_ops_data[24..32].copy_from_slice(&kernel_func.to_le_bytes()); // stop → kernel
285
286        // afinfo data: seq_ops pointer at offset 0
287        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        // Should find results for tcp protocol
316        assert!(!results.is_empty(), "expected non-empty results");
317
318        // Find the hooked entry (show)
319        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        // The other 3 (start, next, stop) should not be hooked
326        let benign: Vec<_> = results.iter().filter(|r| !r.is_hooked).collect();
327        assert_eq!(benign.len(), 3, "expected 3 benign entries");
328    }
329}