1use memf_core::object_reader::ObjectReader;
10use memf_format::PhysicalMemoryProvider;
11
12use crate::Result;
13
14const BPF_MAP_TYPES: &[&str] = &[
16 "hash", "array", "prog_array", "perf_event_array", "percpu_hash", "percpu_array", "stack_trace", "cgroup_array", "lru_hash", "lru_percpu_hash", "lpm_trie", "array_of_maps", "hash_of_maps", "devmap", "sockmap", "cpumap", "xskmap", "sockhash", "cgroup_storage", "reuseport_sockarray", "percpu_cgroup_storage", "queue", "stack", "sk_storage", "devmap_hash", "struct_ops", "ringbuf", "inode_storage", "task_storage", ];
46
47pub fn map_type_name(raw: u32) -> String {
49 BPF_MAP_TYPES
50 .get(raw as usize)
51 .map_or_else(|| format!("unknown({raw})"), |s| (*s).to_string())
52}
53
54#[derive(Debug, Clone, serde::Serialize)]
56pub struct EbpfMapInfo {
57 pub id: u32,
59 pub map_type: u32,
61 pub map_type_name: String,
63 pub key_size: u32,
65 pub value_size: u32,
67 pub max_entries: u32,
69 pub name: String,
71 pub is_suspicious: bool,
73}
74
75pub use crate::heuristics::classify_ebpf_map;
81
82pub fn walk_ebpf_maps<P: PhysicalMemoryProvider>(
89 reader: &ObjectReader<P>,
90) -> Result<Vec<EbpfMapInfo>> {
91 let Some(idr_addr) = reader.symbols().symbol_address("map_idr") else {
92 return Ok(Vec::new());
93 };
94
95 let xa_head: u64 = reader
97 .read_field(idr_addr, "idr", "idr_rt")
98 .or_else(|_| reader.read_field::<u64>(idr_addr, "idr", "top"))
99 .unwrap_or(0);
100
101 if xa_head == 0 {
102 return Ok(Vec::new());
103 }
104
105 let mut maps = Vec::new();
106 walk_map_idr_entries(reader, xa_head, &mut maps)?;
107
108 Ok(maps)
109}
110
111fn walk_map_idr_entries<P: PhysicalMemoryProvider>(
115 reader: &ObjectReader<P>,
116 node_ptr: u64,
117 maps: &mut Vec<EbpfMapInfo>,
118) -> Result<()> {
119 const MAX_SLOTS: usize = 64;
120 const MAX_MAPS: usize = 10_000;
121
122 let is_node = (node_ptr & 0x3) == 0x2;
123
124 if is_node {
125 let real_addr = node_ptr & !0x3;
126 let slots_offset = reader
127 .symbols()
128 .field_offset("xa_node", "slots")
129 .unwrap_or(16);
130
131 for i in 0..MAX_SLOTS {
132 if maps.len() >= MAX_MAPS {
133 break;
134 }
135 let slot_addr = real_addr + slots_offset + (i as u64) * 8;
136 let slot_val = {
137 let mut buf = [0u8; 8];
138 match reader.vas().read_virt(slot_addr, &mut buf) {
139 Ok(()) => u64::from_le_bytes(buf),
140 Err(_) => 0,
141 }
142 };
143 if slot_val == 0 {
144 continue;
145 }
146 walk_map_idr_entries(reader, slot_val, maps)?;
147 }
148 } else if node_ptr.trailing_zeros() >= 2 && node_ptr > 0x1000 {
149 if let Ok(info) = read_bpf_map(reader, node_ptr) {
151 maps.push(info);
152 }
153 }
154
155 Ok(())
156}
157
158fn read_bpf_map<P: PhysicalMemoryProvider>(
160 reader: &ObjectReader<P>,
161 map_addr: u64,
162) -> Result<EbpfMapInfo> {
163 let map_type: u32 = reader.read_field(map_addr, "bpf_map", "map_type")?;
165 let map_type_name_str = map_type_name(map_type);
166
167 let key_size: u32 = reader
169 .read_field(map_addr, "bpf_map", "key_size")
170 .unwrap_or(0);
171
172 let value_size: u32 = reader
174 .read_field(map_addr, "bpf_map", "value_size")
175 .unwrap_or(0);
176
177 let max_entries: u32 = reader
179 .read_field(map_addr, "bpf_map", "max_entries")
180 .unwrap_or(0);
181
182 let name = reader
184 .read_field_string(map_addr, "bpf_map", "name", 16)
185 .unwrap_or_default();
186
187 let id: u32 = reader.read_field(map_addr, "bpf_map", "id").unwrap_or(0);
189
190 let is_suspicious = classify_ebpf_map(map_type, &name, value_size);
191
192 Ok(EbpfMapInfo {
193 id,
194 map_type,
195 map_type_name: map_type_name_str,
196 key_size,
197 value_size,
198 max_entries,
199 name,
200 is_suspicious,
201 })
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
208 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
209 use memf_symbols::isf::IsfResolver;
210 use memf_symbols::test_builders::IsfBuilder;
211
212 fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
213 let isf = IsfBuilder::new().build_json();
214 let resolver = IsfResolver::from_value(&isf).unwrap();
215 let (cr3, mem) = PageTableBuilder::new().build();
216 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
217 ObjectReader::new(vas, Box::new(resolver))
218 }
219
220 #[test]
221 fn no_symbol_returns_empty() {
222 let reader = make_no_symbol_reader();
223 let result = walk_ebpf_maps(&reader).unwrap();
224 assert!(result.is_empty(), "no map_idr symbol → empty vec");
225 }
226
227 #[test]
228 fn classify_suspicious_perf_event_array() {
229 assert!(
231 classify_ebpf_map(3, "events", 8),
232 "perf_event_array should be suspicious"
233 );
234 assert!(
236 classify_ebpf_map(26, "output", 0),
237 "ringbuf should be suspicious"
238 );
239 }
240
241 #[test]
242 fn classify_hash_map_with_suspicious_name() {
243 assert!(
245 classify_ebpf_map(0, "rootkit_map", 8),
246 "hash map named 'rootkit_map' should be suspicious"
247 );
248 assert!(
250 !classify_ebpf_map(0, "connection_count", 8),
251 "hash map with benign name should not be suspicious"
252 );
253 }
254
255 #[test]
256 fn map_type_name_all_known() {
257 assert_eq!(map_type_name(0), "hash");
259 assert_eq!(map_type_name(1), "array");
260 assert_eq!(map_type_name(2), "prog_array");
261 assert_eq!(map_type_name(3), "perf_event_array");
262 assert_eq!(map_type_name(4), "percpu_hash");
263 assert_eq!(map_type_name(5), "percpu_array");
264 assert_eq!(map_type_name(6), "stack_trace");
265 assert_eq!(map_type_name(7), "cgroup_array");
266 assert_eq!(map_type_name(8), "lru_hash");
267 assert_eq!(map_type_name(9), "lru_percpu_hash");
268 assert_eq!(map_type_name(10), "lpm_trie");
269 assert_eq!(map_type_name(11), "array_of_maps");
270 assert_eq!(map_type_name(12), "hash_of_maps");
271 assert_eq!(map_type_name(13), "devmap");
272 assert_eq!(map_type_name(14), "sockmap");
273 assert_eq!(map_type_name(15), "cpumap");
274 assert_eq!(map_type_name(16), "xskmap");
275 assert_eq!(map_type_name(17), "sockhash");
276 assert_eq!(map_type_name(18), "cgroup_storage");
277 assert_eq!(map_type_name(19), "reuseport_sockarray");
278 assert_eq!(map_type_name(20), "percpu_cgroup_storage");
279 assert_eq!(map_type_name(21), "queue");
280 assert_eq!(map_type_name(22), "stack");
281 assert_eq!(map_type_name(23), "sk_storage");
282 assert_eq!(map_type_name(24), "devmap_hash");
283 assert_eq!(map_type_name(25), "struct_ops");
284 assert_eq!(map_type_name(26), "ringbuf");
285 assert_eq!(map_type_name(27), "inode_storage");
286 assert_eq!(map_type_name(28), "task_storage");
287 }
288
289 #[test]
290 fn map_type_name_unknown_index() {
291 let name = map_type_name(999);
293 assert!(
294 name.starts_with("unknown("),
295 "out-of-range index should produce unknown(...): {name}"
296 );
297 }
298
299 #[test]
300 fn classify_ebpf_map_suspicious_name_patterns() {
301 for pattern in &["rootkit", "hide_", "intercept", "keylog", "exfil", "covert"] {
303 let name = format!("{pattern}data");
304 assert!(
305 classify_ebpf_map(0, &name, 8),
306 "pattern '{pattern}' in name should be suspicious"
307 );
308 }
309 }
310
311 #[test]
312 fn classify_ebpf_map_case_insensitive_name() {
313 assert!(classify_ebpf_map(0, "ROOTKIT_MAP", 8));
315 assert!(classify_ebpf_map(0, "KeyLog_events", 8));
316 }
317
318 #[test]
319 fn classify_ebpf_map_benign_high_risk_type_with_benign_name() {
320 assert!(classify_ebpf_map(3, "benign_map", 64));
322 assert!(classify_ebpf_map(26, "my_output", 0));
324 }
325
326 #[test]
328 fn walk_ebpf_maps_with_symbol_returns_entries() {
329 use memf_core::test_builders::flags;
330
331 let idr_vaddr: u64 = 0xFFFF_8000_0040_0000;
339 let idr_paddr: u64 = 0x0085_0000;
340 let map_vaddr: u64 = 0xFFFF_8000_0041_0000;
341 let map_paddr: u64 = 0x0086_0000;
342
343 let map_type_off: u64 = 0x00; let key_size_off: u64 = 0x04; let value_size_off: u64 = 0x08; let max_entries_off: u64 = 0x0C; let name_off: u64 = 0x10; let id_off: u64 = 0x20; let isf = IsfBuilder::new()
351 .add_symbol("map_idr", idr_vaddr)
352 .add_struct("idr", 0x20)
353 .add_field("idr", "idr_rt", 0x00u64, "pointer")
354 .add_struct("bpf_map", 0x100)
355 .add_field("bpf_map", "map_type", map_type_off, "unsigned int")
356 .add_field("bpf_map", "key_size", key_size_off, "unsigned int")
357 .add_field("bpf_map", "value_size", value_size_off, "unsigned int")
358 .add_field("bpf_map", "max_entries", max_entries_off, "unsigned int")
359 .add_field("bpf_map", "name", name_off, "char")
360 .add_field("bpf_map", "id", id_off, "unsigned int")
361 .build_json();
362
363 let resolver = IsfResolver::from_value(&isf).unwrap();
364
365 let mut idr_page = [0u8; 4096];
367 idr_page[0..8].copy_from_slice(&map_vaddr.to_le_bytes());
368
369 let mut map_page = [0u8; 4096];
371 map_page[map_type_off as usize..map_type_off as usize + 4]
372 .copy_from_slice(&1u32.to_le_bytes()); map_page[key_size_off as usize..key_size_off as usize + 4]
374 .copy_from_slice(&4u32.to_le_bytes());
375 map_page[value_size_off as usize..value_size_off as usize + 4]
376 .copy_from_slice(&8u32.to_le_bytes());
377 map_page[max_entries_off as usize..max_entries_off as usize + 4]
378 .copy_from_slice(&1024u32.to_le_bytes());
379 map_page[name_off as usize..name_off as usize + 8].copy_from_slice(b"test_map");
380 map_page[id_off as usize..id_off as usize + 4].copy_from_slice(&7u32.to_le_bytes());
381
382 let (cr3, mem) = PageTableBuilder::new()
383 .map_4k(idr_vaddr, idr_paddr, flags::PRESENT | flags::WRITABLE)
384 .write_phys(idr_paddr, &idr_page)
385 .map_4k(map_vaddr, map_paddr, flags::PRESENT | flags::WRITABLE)
386 .write_phys(map_paddr, &map_page)
387 .build();
388
389 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
390 let reader = ObjectReader::new(vas, Box::new(resolver));
391
392 let result = walk_ebpf_maps(&reader);
393 assert!(result.is_ok(), "walk_ebpf_maps should not error");
394 let maps = result.unwrap();
395 assert_eq!(maps.len(), 1, "should return exactly one map entry");
396 let m = &maps[0];
397 assert_eq!(m.id, 7);
398 assert_eq!(m.map_type, 1);
399 assert_eq!(m.map_type_name, "array");
400 assert_eq!(m.key_size, 4);
401 assert_eq!(m.value_size, 8);
402 assert_eq!(m.max_entries, 1024);
403 assert!(
404 m.name.contains("test_map"),
405 "name should be test_map: {}",
406 m.name
407 );
408 assert!(
409 !m.is_suspicious,
410 "benign array map should not be suspicious"
411 );
412 }
413}