1use std::collections::HashSet;
10
11use memf_core::object_reader::ObjectReader;
12use memf_format::PhysicalMemoryProvider;
13
14use crate::Result;
15
16const MAX_MODULES: usize = 4096;
18
19#[allow(clippy::struct_excessive_bools)]
25#[derive(Debug, Clone, serde::Serialize)]
26pub struct ModXviewEntry {
27 pub name: String,
29 pub base_addr: u64,
31 pub size: u32,
33 pub in_module_list: bool,
35 pub in_kobj_list: bool,
37 pub in_memory_map: bool,
39 pub is_hidden: bool,
42}
43
44pub use crate::heuristics::classify_module_visibility;
50
51pub fn walk_modxview<P: PhysicalMemoryProvider>(
62 reader: &ObjectReader<P>,
63) -> Result<Vec<ModXviewEntry>> {
64 let modules_addr = match reader.symbols().symbol_address("modules") {
66 Some(addr) => addr,
67 None => return Ok(Vec::new()),
68 };
69
70 let module_addrs = reader.walk_list(modules_addr, "module", "list")?;
72
73 let mut seen = HashSet::new();
74 let mut entries = Vec::new();
75
76 for &mod_addr in module_addrs.iter().take(MAX_MODULES) {
77 if !seen.insert(mod_addr) {
78 break; }
80
81 let name = reader
82 .read_field_string(mod_addr, "module", "name", 56)
83 .unwrap_or_else(|_| "<unknown>".to_string());
84
85 let base_addr: u64 = reader
86 .read_field(mod_addr, "module", "module_core")
87 .unwrap_or(0);
88
89 let size: u32 = reader
90 .read_field(mod_addr, "module", "core_size")
91 .unwrap_or(0);
92
93 let in_module_list = true;
95
96 let in_kobj_list = check_kobj_linkage(reader, mod_addr);
99
100 let in_memory_map = check_memory_mapped(reader, base_addr, size);
103
104 let is_hidden = classify_module_visibility(in_module_list, in_kobj_list, in_memory_map);
105
106 entries.push(ModXviewEntry {
107 name,
108 base_addr,
109 size,
110 in_module_list,
111 in_kobj_list,
112 in_memory_map,
113 is_hidden,
114 });
115 }
116
117 Ok(entries)
118}
119
120fn check_kobj_linkage<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>, mod_addr: u64) -> bool {
127 let mkobj_offset = match reader.symbols().field_offset("module", "mkobj") {
129 Some(off) => off,
130 None => return true, };
132
133 let kobj_offset = match reader.symbols().field_offset("module_kobject", "kobj") {
135 Some(off) => off,
136 None => return true,
137 };
138
139 let entry_offset = match reader.symbols().field_offset("kobject", "entry") {
141 Some(off) => off,
142 None => return true,
143 };
144
145 let entry_addr = mod_addr + mkobj_offset + kobj_offset + entry_offset;
147 let next_ptr: u64 = match reader.read_field(entry_addr, "list_head", "next") {
148 Ok(v) => v,
149 Err(_) => return true, };
151
152 next_ptr != 0
154}
155
156fn check_memory_mapped<P: PhysicalMemoryProvider>(
163 reader: &ObjectReader<P>,
164 base_addr: u64,
165 size: u32,
166) -> bool {
167 if base_addr == 0 || size == 0 {
168 return true; }
170
171 reader.read_bytes(base_addr, 1).is_ok()
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn classify_all_visible_benign() {
181 assert!(!classify_module_visibility(true, true, true));
182 }
183
184 #[test]
185 fn classify_missing_from_list_suspicious() {
186 assert!(classify_module_visibility(false, true, true));
188 }
189
190 #[test]
191 fn classify_missing_from_kobj_suspicious() {
192 assert!(classify_module_visibility(true, false, true));
194 }
195
196 #[test]
197 fn classify_all_missing_not_suspicious() {
198 assert!(!classify_module_visibility(false, false, false));
200 }
201
202 #[test]
203 fn walk_no_symbol_returns_empty() {
204 use memf_core::test_builders::PageTableBuilder;
205 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
206 use memf_symbols::isf::IsfResolver;
207 use memf_symbols::test_builders::IsfBuilder;
208
209 let isf = IsfBuilder::new()
211 .add_struct("module", 64)
212 .add_field("module", "name", 0, "char")
213 .add_field("module", "list", 8, "list_head")
214 .add_struct("list_head", 16)
215 .add_field("list_head", "next", 0, "pointer")
216 .add_field("list_head", "prev", 8, "pointer")
217 .build_json();
218
219 let resolver = IsfResolver::from_value(&isf).unwrap();
220 let (cr3, mem) = PageTableBuilder::new().build();
221 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
222 let reader = ObjectReader::new(vas, Box::new(resolver));
223
224 let result = walk_modxview(&reader);
225 assert!(result.is_ok());
226 assert!(result.unwrap().is_empty());
227 }
228
229 #[test]
230 fn modxview_entry_serializes() {
231 let entry = ModXviewEntry {
232 name: "test_module".to_string(),
233 base_addr: 0xFFFF_8000_0000_1000,
234 size: 4096,
235 in_module_list: true,
236 in_kobj_list: true,
237 in_memory_map: false,
238 is_hidden: true,
239 };
240 let json = serde_json::to_string(&entry).unwrap();
241 assert!(json.contains("test_module"));
242 assert!(json.contains("\"is_hidden\":true"));
243 }
244
245 #[test]
246 fn classify_missing_from_memory_suspicious() {
247 assert!(classify_module_visibility(true, true, false));
249 }
250
251 #[test]
252 fn classify_only_in_memory_suspicious() {
253 assert!(classify_module_visibility(false, false, true));
255 }
256
257 #[test]
258 fn classify_only_in_module_list_suspicious() {
259 assert!(classify_module_visibility(true, false, false));
261 }
262
263 #[test]
264 fn classify_only_in_kobj_suspicious() {
265 assert!(classify_module_visibility(false, true, false));
267 }
268
269 #[test]
270 fn check_memory_mapped_zero_base_returns_true() {
271 use memf_core::test_builders::PageTableBuilder;
273 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
274 use memf_symbols::isf::IsfResolver;
275 use memf_symbols::test_builders::IsfBuilder;
276
277 let isf = IsfBuilder::new().build_json();
278 let resolver = IsfResolver::from_value(&isf).unwrap();
279 let (cr3, mem) = PageTableBuilder::new().build();
280 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
281 let reader = ObjectReader::new(vas, Box::new(resolver));
282
283 assert!(check_memory_mapped(&reader, 0, 4096));
284 }
285
286 #[test]
287 fn check_memory_mapped_zero_size_returns_true() {
288 use memf_core::test_builders::PageTableBuilder;
290 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
291 use memf_symbols::isf::IsfResolver;
292 use memf_symbols::test_builders::IsfBuilder;
293
294 let isf = IsfBuilder::new().build_json();
295 let resolver = IsfResolver::from_value(&isf).unwrap();
296 let (cr3, mem) = PageTableBuilder::new().build();
297 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
298 let reader = ObjectReader::new(vas, Box::new(resolver));
299
300 assert!(check_memory_mapped(&reader, 0xFFFF_8000_0000_1000, 0));
301 }
302
303 #[test]
304 fn check_memory_mapped_unreadable_returns_false() {
305 use memf_core::test_builders::PageTableBuilder;
307 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
308 use memf_symbols::isf::IsfResolver;
309 use memf_symbols::test_builders::IsfBuilder;
310
311 let isf = IsfBuilder::new().build_json();
312 let resolver = IsfResolver::from_value(&isf).unwrap();
313 let (cr3, mem) = PageTableBuilder::new().build();
314 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
315 let reader = ObjectReader::new(vas, Box::new(resolver));
316
317 assert!(!check_memory_mapped(&reader, 0xDEAD_BEEF_0000_1000, 4096));
319 }
320
321 #[test]
322 fn check_kobj_linkage_missing_mkobj_offset_returns_true() {
323 use memf_core::test_builders::PageTableBuilder;
325 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
326 use memf_symbols::isf::IsfResolver;
327 use memf_symbols::test_builders::IsfBuilder;
328
329 let isf = IsfBuilder::new().build_json();
331 let resolver = IsfResolver::from_value(&isf).unwrap();
332 let (cr3, mem) = PageTableBuilder::new().build();
333 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
334 let reader = ObjectReader::new(vas, Box::new(resolver));
335
336 assert!(check_kobj_linkage(&reader, 0xFFFF_8000_0000_0000));
337 }
338
339 #[test]
340 fn check_kobj_linkage_missing_kobj_offset_returns_true() {
341 use memf_core::test_builders::PageTableBuilder;
343 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
344 use memf_symbols::isf::IsfResolver;
345 use memf_symbols::test_builders::IsfBuilder;
346
347 let isf = IsfBuilder::new()
348 .add_struct("module", 128)
349 .add_field("module", "mkobj", 0, "pointer")
350 .build_json();
352
353 let resolver = IsfResolver::from_value(&isf).unwrap();
354 let (cr3, mem) = PageTableBuilder::new().build();
355 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
356 let reader = ObjectReader::new(vas, Box::new(resolver));
357
358 assert!(check_kobj_linkage(&reader, 0xFFFF_8000_0000_0000));
359 }
360
361 #[test]
362 fn check_kobj_linkage_missing_entry_offset_returns_true() {
363 use memf_core::test_builders::PageTableBuilder;
365 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
366 use memf_symbols::isf::IsfResolver;
367 use memf_symbols::test_builders::IsfBuilder;
368
369 let isf = IsfBuilder::new()
370 .add_struct("module", 128)
371 .add_field("module", "mkobj", 0, "pointer")
372 .add_struct("module_kobject", 64)
373 .add_field("module_kobject", "kobj", 0, "pointer")
374 .build_json();
376
377 let resolver = IsfResolver::from_value(&isf).unwrap();
378 let (cr3, mem) = PageTableBuilder::new().build();
379 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
380 let reader = ObjectReader::new(vas, Box::new(resolver));
381
382 assert!(check_kobj_linkage(&reader, 0xFFFF_8000_0000_0000));
383 }
384
385 #[test]
386 fn walk_modxview_with_one_module_entry() {
387 use memf_core::test_builders::{flags as ptf, PageTableBuilder};
389 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
390 use memf_symbols::isf::IsfResolver;
391 use memf_symbols::test_builders::IsfBuilder;
392
393 let head_vaddr: u64 = 0xFFFF_8800_00E0_0000;
404 let head_paddr: u64 = 0x00E0_0000;
405 let mod_a_vaddr: u64 = 0xFFFF_8800_00E1_0000;
406 let mod_a_paddr: u64 = 0x00E1_0000;
407
408 let mut head_page = [0u8; 4096];
409 head_page[0..8].copy_from_slice(&mod_a_vaddr.to_le_bytes());
411 head_page[8..16].copy_from_slice(&mod_a_vaddr.to_le_bytes());
412
413 let mut mod_a_page = [0u8; 4096];
414 mod_a_page[0..8].copy_from_slice(&head_vaddr.to_le_bytes());
416 mod_a_page[8..16].copy_from_slice(&head_vaddr.to_le_bytes());
417 mod_a_page[0x10..0x15].copy_from_slice(b"dummy");
419 mod_a_page[0x48..0x50].copy_from_slice(&0xFFFF_A000_0000u64.to_le_bytes());
421 mod_a_page[0x50..0x54].copy_from_slice(&0x4000u32.to_le_bytes());
423
424 let isf = IsfBuilder::new()
425 .add_struct("module", 256)
426 .add_field("module", "list", 0x00u64, "list_head")
427 .add_field("module", "name", 0x10u64, "char")
428 .add_field("module", "module_core", 0x48u64, "pointer")
429 .add_field("module", "core_size", 0x50u64, "unsigned int")
430 .add_struct("list_head", 16)
431 .add_field("list_head", "next", 0x00u64, "pointer")
432 .add_field("list_head", "prev", 0x08u64, "pointer")
433 .add_symbol("modules", head_vaddr)
434 .build_json();
435 let resolver = IsfResolver::from_value(&isf).unwrap();
436
437 let (cr3, mem) = PageTableBuilder::new()
438 .map_4k(head_vaddr, head_paddr, ptf::WRITABLE)
439 .write_phys(head_paddr, &head_page)
440 .map_4k(mod_a_vaddr, mod_a_paddr, ptf::WRITABLE)
441 .write_phys(mod_a_paddr, &mod_a_page)
442 .build();
443 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
444 let reader = ObjectReader::new(vas, Box::new(resolver));
445
446 let result = walk_modxview(&reader).unwrap_or_default();
447 assert_eq!(result.len(), 1, "should find exactly one module entry");
448 assert_eq!(result[0].name, "dummy");
449 assert_eq!(result[0].base_addr, 0xFFFF_A000_0000);
450 assert_eq!(result[0].size, 0x4000);
451 assert!(result[0].in_module_list, "module found in list");
452 }
453
454 #[test]
455 fn check_kobj_linkage_unreadable_memory_returns_true() {
456 use memf_core::test_builders::PageTableBuilder;
458 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
459 use memf_symbols::isf::IsfResolver;
460 use memf_symbols::test_builders::IsfBuilder;
461
462 let isf = IsfBuilder::new()
463 .add_struct("module", 128)
464 .add_field("module", "mkobj", 0, "pointer")
465 .add_struct("module_kobject", 64)
466 .add_field("module_kobject", "kobj", 0, "pointer")
467 .add_struct("kobject", 64)
468 .add_field("kobject", "entry", 0, "pointer")
469 .add_struct("list_head", 16)
470 .add_field("list_head", "next", 0, "pointer")
471 .add_field("list_head", "prev", 8, "pointer")
472 .build_json();
473
474 let resolver = IsfResolver::from_value(&isf).unwrap();
475 let (cr3, mem) = PageTableBuilder::new().build();
476 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
477 let reader = ObjectReader::new(vas, Box::new(resolver));
478
479 assert!(check_kobj_linkage(&reader, 0xDEAD_BEEF_0000_0000));
481 }
482}