Skip to main content

memf_linux/
check_modules.rs

1//! Linux hidden kernel module detector.
2//!
3//! Cross-references kernel modules found via the `modules` linked list
4//! against the kernel's `kset` hierarchy (sysfs). Modules present in
5//! one view but not the other may have been hidden by a rootkit.
6
7use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::{Error, HiddenModuleInfo, Result};
11
12/// Check whether a module is linked into the sysfs kobj tree.
13///
14/// Walks `module.mkobj.kobj.entry.next` — a non-null pointer indicates the
15/// module is present in the kobj/sysfs hierarchy. Returns `true` (assume
16/// present) if any required field offset is unavailable in the symbol table
17/// or if the memory is unreadable.
18fn check_module_in_sysfs<P: PhysicalMemoryProvider>(
19    reader: &ObjectReader<P>,
20    mod_addr: u64,
21) -> bool {
22    let mkobj_offset = match reader.symbols().field_offset("module", "mkobj") {
23        Some(off) => off,
24        None => return true, // Can't verify — assume present
25    };
26    let kobj_offset = match reader.symbols().field_offset("module_kobject", "kobj") {
27        Some(off) => off,
28        None => return true,
29    };
30    let entry_offset = match reader.symbols().field_offset("kobject", "entry") {
31        Some(off) => off,
32        None => return true,
33    };
34
35    let entry_addr = mod_addr + mkobj_offset + kobj_offset + entry_offset;
36    let next_ptr: u64 = match reader.read_field(entry_addr, "list_head", "next") {
37        Ok(v) => v,
38        Err(_) => return true, // Can't read — assume present
39    };
40
41    // Non-null next pointer means linked into kobj tree
42    next_ptr != 0
43}
44
45/// Cross-reference kernel modules for hidden module detection.
46///
47/// Walks the `modules` linked list and the `module_kset` kobj tree,
48/// then merges results. Modules visible in one but not both are
49/// flagged as potentially hidden.
50pub fn check_hidden_modules<P: PhysicalMemoryProvider>(
51    reader: &ObjectReader<P>,
52) -> Result<Vec<HiddenModuleInfo>> {
53    let modules_addr =
54        reader
55            .symbols()
56            .symbol_address("modules")
57            .ok_or_else(|| Error::MissingKernelSymbol {
58                name: "modules".into(),
59            })?;
60
61    let _list_offset = reader
62        .symbols()
63        .field_offset("module", "list")
64        .ok_or_else(|| Error::MissingField {
65            struct_name: "module".into(),
66            field_name: "list".into(),
67        })?;
68
69    // Walk the modules linked list
70    let module_addrs = reader.walk_list(modules_addr, "module", "list")?;
71
72    let mut results = Vec::new();
73
74    for &mod_addr in &module_addrs {
75        let name = reader
76            .read_field_string(mod_addr, "module", "name", 56)
77            .unwrap_or_else(|_| "<unknown>".to_string());
78
79        let base_addr: u64 = reader
80            .read_field(mod_addr, "module", "module_core")
81            .unwrap_or(0);
82
83        let size: u32 = reader
84            .read_field(mod_addr, "module", "core_size")
85            .unwrap_or(0);
86
87        // Present in modules list by definition (we found it there).
88        // Check sysfs linkage via kobj entry: module.mkobj.kobj.entry.next
89        // must be non-null to indicate the module is linked into the sysfs
90        // kobj tree. Returns true (assume present) if fields are missing.
91        let in_sysfs = check_module_in_sysfs(reader, mod_addr);
92
93        results.push(HiddenModuleInfo {
94            name,
95            base_addr,
96            size: u64::from(size),
97            in_modules_list: true,
98            in_sysfs,
99        });
100    }
101
102    Ok(results)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
109    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
110    use memf_symbols::isf::IsfResolver;
111    use memf_symbols::test_builders::IsfBuilder;
112
113    fn make_test_reader(data: &[u8], vaddr: u64, paddr: u64) -> ObjectReader<SyntheticPhysMem> {
114        let isf = IsfBuilder::new()
115            .add_struct("module", 256)
116            .add_field("module", "name", 0, "char")
117            .add_field("module", "list", 56, "list_head")
118            .add_field("module", "module_core", 128, "pointer")
119            .add_field("module", "core_size", 136, "unsigned int")
120            .add_field("module", "mkobj", 160, "module_kobject")
121            .add_struct("module_kobject", 64)
122            .add_field("module_kobject", "kobj", 0, "kobject")
123            .add_struct("kobject", 64)
124            .add_field("kobject", "name", 0, "pointer")
125            .add_field("kobject", "entry", 16, "list_head")
126            .add_struct("list_head", 16)
127            .add_field("list_head", "next", 0, "pointer")
128            .add_field("list_head", "prev", 8, "pointer")
129            .add_symbol("modules", vaddr + 0x800)
130            .add_symbol("module_kset", vaddr + 0x900)
131            .build_json();
132
133        let resolver = IsfResolver::from_value(&isf).unwrap();
134        let (cr3, mem) = PageTableBuilder::new()
135            .map_4k(vaddr, paddr, ptflags::WRITABLE)
136            .write_phys(paddr, data)
137            .build();
138        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
139        ObjectReader::new(vas, Box::new(resolver))
140    }
141
142    #[test]
143    fn empty_module_list() {
144        let vaddr: u64 = 0xFFFF_8000_0010_0000;
145        let paddr: u64 = 0x0080_0000;
146        let mut data = vec![0u8; 4096];
147
148        // modules list_head at +0x800 (self-referencing = empty)
149        let modules_head = vaddr + 0x800;
150        data[0x800..0x808].copy_from_slice(&modules_head.to_le_bytes());
151        data[0x808..0x810].copy_from_slice(&modules_head.to_le_bytes());
152
153        let reader = make_test_reader(&data, vaddr, paddr);
154        let results = check_hidden_modules(&reader).unwrap();
155
156        assert!(results.is_empty());
157    }
158
159    #[test]
160    fn missing_modules_symbol() {
161        let isf = IsfBuilder::new()
162            .add_struct("module", 64)
163            .add_field("module", "name", 0, "char")
164            .add_field("module", "list", 8, "list_head")
165            .add_struct("list_head", 16)
166            .add_field("list_head", "next", 0, "pointer")
167            .add_field("list_head", "prev", 8, "pointer")
168            .build_json();
169
170        let resolver = IsfResolver::from_value(&isf).unwrap();
171        let (cr3, mem) = PageTableBuilder::new().build();
172        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
173        let reader = ObjectReader::new(vas, Box::new(resolver));
174
175        let result = check_hidden_modules(&reader);
176        assert!(
177            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "modules"),
178            "expected MissingKernelSymbol {{name: \"modules\"}}, got {result:?}"
179        );
180    }
181
182    #[test]
183    fn missing_module_list_field_returns_error() {
184        // modules symbol present but module.list field absent → Error
185        let isf = IsfBuilder::new()
186            .add_struct("module", 64)
187            .add_field("module", "name", 0, "char")
188            // list field intentionally omitted
189            .add_struct("list_head", 16)
190            .add_field("list_head", "next", 0, "pointer")
191            .add_field("list_head", "prev", 8, "pointer")
192            .add_symbol("modules", 0xFFFF_8000_0010_0800)
193            .build_json();
194
195        let resolver = IsfResolver::from_value(&isf).unwrap();
196        let (cr3, mem) = PageTableBuilder::new().build();
197        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
198        let reader = ObjectReader::new(vas, Box::new(resolver));
199
200        let result = check_hidden_modules(&reader);
201        assert!(
202            matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "module" && field_name == "list"),
203            "expected MissingField module.list, got {result:?}"
204        );
205    }
206
207    #[test]
208    fn single_module_in_list_with_sysfs() {
209        // Set up a modules list with one real module entry that IS linked in sysfs.
210        // module.mkobj at offset 160; module_kobject.kobj at offset 0;
211        // kobject.entry at offset 16 → entry_addr = mod_addr + 160 + 0 + 16 = mod_addr + 176
212        // list_head.next at entry_addr offset 0 → set to a non-zero sentinel to indicate linked.
213        let vaddr: u64 = 0xFFFF_8000_0010_0000;
214        let paddr: u64 = 0x0080_0000;
215        let mut data = vec![0u8; 4096];
216
217        let module_list_vaddr = vaddr; // module is at the page start
218        let module_list_field_vaddr = module_list_vaddr + 56;
219
220        let modules_head = vaddr + 0x800;
221        data[0x800..0x808].copy_from_slice(&module_list_field_vaddr.to_le_bytes());
222        data[0x808..0x810].copy_from_slice(&modules_head.to_le_bytes());
223
224        // module.name at offset 0
225        data[0..8].copy_from_slice(b"rootkit\0");
226        // module.list at offset 56
227        data[56..64].copy_from_slice(&modules_head.to_le_bytes());
228        data[64..72].copy_from_slice(&modules_head.to_le_bytes());
229        // module.module_core at offset 128
230        let base: u64 = 0xFFFF_C000_0000_0000;
231        data[128..136].copy_from_slice(&base.to_le_bytes());
232        // module.core_size at offset 136
233        data[136..140].copy_from_slice(&4096u32.to_le_bytes());
234        // kobj.entry.next at offset 176 (mod_addr+160+0+16): set to non-zero → linked in sysfs
235        let kobj_entry_next_sentinel: u64 = 0xFFFF_8000_DEAD_0001;
236        data[176..184].copy_from_slice(&kobj_entry_next_sentinel.to_le_bytes());
237
238        let reader = make_test_reader(&data, vaddr, paddr);
239        let results = check_hidden_modules(&reader).unwrap();
240
241        assert_eq!(results.len(), 1, "should find one module");
242        assert!(
243            results[0].name.starts_with("rootkit"),
244            "name should match: {}",
245            results[0].name
246        );
247        assert_eq!(results[0].base_addr, base);
248        assert_eq!(results[0].size, 4096);
249        assert!(results[0].in_modules_list);
250        assert!(
251            results[0].in_sysfs,
252            "module with non-zero kobj entry.next should be in sysfs"
253        );
254    }
255
256    #[test]
257    fn single_module_not_in_sysfs() {
258        use memf_core::test_builders::{flags as ptflags, PageTableBuilder};
259        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
260        use memf_symbols::isf::IsfResolver;
261
262        // Module in the modules list but NOT linked in sysfs (kobj entry.next == 0).
263        let vaddr: u64 = 0xFFFF_8000_0011_0000;
264        let paddr: u64 = 0x0081_0000;
265        let mut data = vec![0u8; 4096];
266
267        let module_list_field_vaddr = vaddr + 56;
268        let modules_head = vaddr + 0x800;
269        data[0x800..0x808].copy_from_slice(&module_list_field_vaddr.to_le_bytes());
270        data[0x808..0x810].copy_from_slice(&modules_head.to_le_bytes());
271
272        data[0..8].copy_from_slice(b"hidden\0\0");
273        data[56..64].copy_from_slice(&modules_head.to_le_bytes());
274        data[64..72].copy_from_slice(&modules_head.to_le_bytes());
275        let base: u64 = 0xFFFF_C001_0000_0000;
276        data[128..136].copy_from_slice(&base.to_le_bytes());
277        data[136..140].copy_from_slice(&4096u32.to_le_bytes());
278        // kobj.entry.next at offset 176 = 0 → not linked in sysfs
279        // (data is zero-initialized, so nothing to set)
280
281        let isf = memf_symbols::test_builders::IsfBuilder::new()
282            .add_struct("module", 256)
283            .add_field("module", "name", 0, "char")
284            .add_field("module", "list", 56, "list_head")
285            .add_field("module", "module_core", 128, "pointer")
286            .add_field("module", "core_size", 136, "unsigned int")
287            .add_field("module", "mkobj", 160, "module_kobject")
288            .add_struct("module_kobject", 64)
289            .add_field("module_kobject", "kobj", 0, "kobject")
290            .add_struct("kobject", 64)
291            .add_field("kobject", "name", 0, "pointer")
292            .add_field("kobject", "entry", 16, "list_head")
293            .add_struct("list_head", 16)
294            .add_field("list_head", "next", 0, "pointer")
295            .add_field("list_head", "prev", 8, "pointer")
296            .add_symbol("modules", vaddr + 0x800)
297            .add_symbol("module_kset", vaddr + 0x900)
298            .build_json();
299
300        let resolver = IsfResolver::from_value(&isf).unwrap();
301        let (cr3, mem) = PageTableBuilder::new()
302            .map_4k(vaddr, paddr, ptflags::WRITABLE)
303            .write_phys(paddr, &data)
304            .build();
305        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
306        let reader = ObjectReader::new(vas, Box::new(resolver));
307
308        let results = check_hidden_modules(&reader).unwrap();
309        assert_eq!(results.len(), 1, "should find one module");
310        assert!(results[0].name.starts_with("hidden"));
311        assert!(results[0].in_modules_list);
312        assert!(
313            !results[0].in_sysfs,
314            "module with kobj entry.next==0 should not be in sysfs"
315        );
316    }
317}