Skip to main content

memf_linux/
ebpf_progs.rs

1//! eBPF map enumeration from kernel memory.
2//!
3//! The existing `bpf.rs` enumerates eBPF programs via `bpf_prog_idr`.
4//! This module enumerates eBPF **maps** via `map_idr`, which are separate
5//! kernel objects used for data sharing between eBPF programs and userspace.
6//! Rootkits often use PERF_EVENT_ARRAY or RINGBUF maps for stealthy data
7//! exfiltration.
8
9use memf_core::object_reader::ObjectReader;
10use memf_format::PhysicalMemoryProvider;
11
12use crate::Result;
13
14/// BPF map type strings indexed by their kernel enum value.
15const BPF_MAP_TYPES: &[&str] = &[
16    "hash",                  // 0
17    "array",                 // 1
18    "prog_array",            // 2
19    "perf_event_array",      // 3
20    "percpu_hash",           // 4
21    "percpu_array",          // 5
22    "stack_trace",           // 6
23    "cgroup_array",          // 7
24    "lru_hash",              // 8
25    "lru_percpu_hash",       // 9
26    "lpm_trie",              // 10
27    "array_of_maps",         // 11
28    "hash_of_maps",          // 12
29    "devmap",                // 13
30    "sockmap",               // 14
31    "cpumap",                // 15
32    "xskmap",                // 16
33    "sockhash",              // 17
34    "cgroup_storage",        // 18
35    "reuseport_sockarray",   // 19
36    "percpu_cgroup_storage", // 20
37    "queue",                 // 21
38    "stack",                 // 22
39    "sk_storage",            // 23
40    "devmap_hash",           // 24
41    "struct_ops",            // 25
42    "ringbuf",               // 26
43    "inode_storage",         // 27
44    "task_storage",          // 28
45];
46
47/// Convert a raw map type integer to its string name.
48pub 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/// Information about a loaded eBPF map.
55#[derive(Debug, Clone, serde::Serialize)]
56pub struct EbpfMapInfo {
57    /// Unique map ID.
58    pub id: u32,
59    /// Raw map type integer.
60    pub map_type: u32,
61    /// Human-readable map type name.
62    pub map_type_name: String,
63    /// Key size in bytes.
64    pub key_size: u32,
65    /// Value size in bytes.
66    pub value_size: u32,
67    /// Maximum number of entries.
68    pub max_entries: u32,
69    /// Map name (BPF_OBJ_NAME_LEN = 16 bytes, null-terminated).
70    pub name: String,
71    /// True when the map is classified as suspicious.
72    pub is_suspicious: bool,
73}
74
75/// Classify whether an eBPF map is suspicious.
76///
77/// Suspicious criteria:
78/// - Map type is `perf_event_array` or `ringbuf` AND name matches known rootkit patterns
79/// - Any map type AND name exactly matches a known suspicious name
80pub use crate::heuristics::classify_ebpf_map;
81
82/// Walk `map_idr` and return all loaded eBPF maps.
83///
84/// Uses the same xarray/IDR traversal pattern as `bpf.rs` for `bpf_prog_idr`,
85/// applied to `map_idr` (the kernel's IDR for `bpf_map` objects).
86///
87/// Returns `Ok(Vec::new())` when `map_idr` symbol is absent.
88pub 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    // Read idr.idr_rt.xa_head (or legacy idr.top) to get the xarray/radix root.
96    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
111/// Recursively walk xarray/radix-tree nodes to find `bpf_map` leaf pointers.
112///
113/// Mirrors the logic in `bpf.rs`'s `walk_idr_entries` for `bpf_prog`.
114fn 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        // Leaf pointer — attempt to read a bpf_map struct.
150        if let Ok(info) = read_bpf_map(reader, node_ptr) {
151            maps.push(info);
152        }
153    }
154
155    Ok(())
156}
157
158/// Read a single `bpf_map` struct and populate `EbpfMapInfo`.
159fn read_bpf_map<P: PhysicalMemoryProvider>(
160    reader: &ObjectReader<P>,
161    map_addr: u64,
162) -> Result<EbpfMapInfo> {
163    // bpf_map.map_type (u32)
164    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    // bpf_map.key_size (u32)
168    let key_size: u32 = reader
169        .read_field(map_addr, "bpf_map", "key_size")
170        .unwrap_or(0);
171
172    // bpf_map.value_size (u32)
173    let value_size: u32 = reader
174        .read_field(map_addr, "bpf_map", "value_size")
175        .unwrap_or(0);
176
177    // bpf_map.max_entries (u32)
178    let max_entries: u32 = reader
179        .read_field(map_addr, "bpf_map", "max_entries")
180        .unwrap_or(0);
181
182    // bpf_map.name (BPF_OBJ_NAME_LEN = 16 bytes, null-terminated)
183    let name = reader
184        .read_field_string(map_addr, "bpf_map", "name", 16)
185        .unwrap_or_default();
186
187    // bpf_map.id — stored in the map's aux or directly; try direct first.
188    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        // perf_event_array (type 3) is always suspicious per spec
230        assert!(
231            classify_ebpf_map(3, "events", 8),
232            "perf_event_array should be suspicious"
233        );
234        // ringbuf (type 26) is always suspicious
235        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        // hash map (type 0) with a rootkit name is suspicious
244        assert!(
245            classify_ebpf_map(0, "rootkit_map", 8),
246            "hash map named 'rootkit_map' should be suspicious"
247        );
248        // hash map with benign name is not suspicious
249        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        // Verify every known type string for indices 0–28
258        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        // Index beyond the known range → "unknown(N)"
292        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        // All SUSPICIOUS_MAP_NAMES patterns should flag any map type
302        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        // Names are lowercased before matching
314        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        // perf_event_array (3) is always suspicious regardless of name
321        assert!(classify_ebpf_map(3, "benign_map", 64));
322        // ringbuf (26) is always suspicious
323        assert!(classify_ebpf_map(26, "my_output", 0));
324    }
325
326    // Walk with a fully-constructed IDR → returns real EbpfMapInfo entries.
327    #[test]
328    fn walk_ebpf_maps_with_symbol_returns_entries() {
329        use memf_core::test_builders::flags;
330
331        // Memory layout:
332        //   idr page  @ paddr 0x0085_0000 (vaddr 0xFFFF_8000_0040_0000)
333        //   map page  @ paddr 0x0086_0000 (vaddr 0xFFFF_8000_0041_0000)
334        //
335        // idr.idr_rt at offset 0 = map_vaddr (clean leaf: low bits 0x0, > 0x1000)
336        // bpf_map.map_type at offset 0 = 1 (array)
337
338        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; // u32
344        let key_size_off: u64 = 0x04; // u32
345        let value_size_off: u64 = 0x08; // u32
346        let max_entries_off: u64 = 0x0C; // u32
347        let name_off: u64 = 0x10; // char[16]
348        let id_off: u64 = 0x20; // u32
349
350        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        // idr page: idr_rt = map_vaddr (leaf pointer)
366        let mut idr_page = [0u8; 4096];
367        idr_page[0..8].copy_from_slice(&map_vaddr.to_le_bytes());
368
369        // bpf_map page
370        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()); // array
373        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}