Skip to main content

memf_linux/
keyboard_notifiers.rs

1//! Keyboard notifier chain forensics — keylogger detection.
2//!
3//! Walks the `keyboard_notifier_list` (`raw_notifier_head`) linked list of
4//! `notifier_block` structures.  Each entry records a `notifier_call`
5//! function pointer.  A pointer outside the kernel text range indicates a
6//! potential keylogger injected by a rootkit.
7
8use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::Result;
12
13/// Information about a single `notifier_block` on the keyboard notifier chain.
14#[derive(Debug, Clone, serde::Serialize)]
15pub struct KeyboardNotifierInfo {
16    /// Virtual address of the `notifier_block`.
17    pub address: u64,
18    /// `notifier_block.notifier_call` — function pointer.
19    pub notifier_call: u64,
20    /// `notifier_block.priority`.
21    pub priority: i32,
22    /// True when `notifier_call` lies outside `_stext`..`_etext`.
23    pub is_suspicious: bool,
24}
25
26/// Walk `keyboard_notifier_list` and return all registered notifier blocks.
27///
28/// Returns `Ok(Vec::new())` when the `keyboard_notifier_list` symbol is absent.
29///
30/// `raw_notifier_head` layout:
31///   +0: head (pointer to first `notifier_block`, or NULL)
32///
33/// `notifier_block` layout:
34///   +0:  notifier_call (pointer)
35///   +8:  next (pointer to next notifier_block, or NULL)
36///   +16: priority (i32)
37pub fn walk_keyboard_notifiers<P: PhysicalMemoryProvider>(
38    reader: &ObjectReader<P>,
39) -> Result<Vec<KeyboardNotifierInfo>> {
40    const MAX_NOTIFIERS: usize = 1_000;
41    let Some(head_addr) = reader.symbols().symbol_address("keyboard_notifier_list") else {
42        return Ok(Vec::new());
43    };
44
45    let stext = reader.symbols().symbol_address("_stext").unwrap_or(0);
46    let etext = reader
47        .symbols()
48        .symbol_address("_etext")
49        .unwrap_or(u64::MAX);
50
51    // Read raw_notifier_head.head pointer (offset 0).
52    let first_nb = match reader.read_bytes(head_addr, 8) {
53        Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
54        _ => return Ok(Vec::new()),
55    };
56
57    let mut notifiers = Vec::new();
58    let mut current = first_nb;
59
60    for _ in 0..MAX_NOTIFIERS {
61        if current == 0 {
62            break;
63        }
64
65        // notifier_call at offset 0
66        let notifier_call = match reader.read_bytes(current, 8) {
67            Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
68            _ => break,
69        };
70
71        // next at offset 8
72        let next = match reader.read_bytes(current + 8, 8) {
73            Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
74            _ => 0,
75        };
76
77        // priority at offset 16 (i32)
78        let priority = match reader.read_bytes(current + 16, 4) {
79            Ok(b) if b.len() == 4 => b.try_into().map_or(0, i32::from_le_bytes),
80            _ => 0,
81        };
82
83        let is_suspicious = classify_notifier(notifier_call, stext, etext);
84
85        notifiers.push(KeyboardNotifierInfo {
86            address: current,
87            notifier_call,
88            priority,
89            is_suspicious,
90        });
91
92        current = next;
93    }
94
95    Ok(notifiers)
96}
97
98/// Classify a notifier_call pointer as suspicious if outside kernel text.
99pub use crate::heuristics::classify_notifier;
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
105    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
106    use memf_symbols::isf::IsfResolver;
107    use memf_symbols::test_builders::IsfBuilder;
108
109    fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
110        let isf = IsfBuilder::new().build_json();
111        let resolver = IsfResolver::from_value(&isf).unwrap();
112        let (cr3, mem) = PageTableBuilder::new().build();
113        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
114        ObjectReader::new(vas, Box::new(resolver))
115    }
116
117    #[test]
118    fn no_symbol_returns_empty() {
119        let reader = make_no_symbol_reader();
120        let result = walk_keyboard_notifiers(&reader).unwrap();
121        assert!(
122            result.is_empty(),
123            "no keyboard_notifier_list symbol → empty vec"
124        );
125    }
126
127    #[test]
128    fn classify_in_kernel_benign() {
129        let stext = 0xFFFF_FFFF_8100_0000_u64;
130        let etext = 0xFFFF_FFFF_8200_0000_u64;
131        let call = 0xFFFF_FFFF_8150_0000_u64;
132        assert!(
133            !classify_notifier(call, stext, etext),
134            "in-kernel notifier_call should be benign"
135        );
136    }
137
138    #[test]
139    fn classify_out_of_kernel_suspicious() {
140        let stext = 0xFFFF_FFFF_8100_0000_u64;
141        let etext = 0xFFFF_FFFF_8200_0000_u64;
142        let call = 0x0000_7FFF_1234_5678_u64; // userspace range → suspicious
143        assert!(
144            classify_notifier(call, stext, etext),
145            "out-of-kernel notifier_call should be suspicious"
146        );
147    }
148
149    // RED test: walk with a symbol and one notifier_block in memory returns an entry.
150    #[test]
151    fn walk_keyboard_notifiers_with_symbol_returns_entry() {
152        // notifier_block layout:
153        //   offset 0:  notifier_call (pointer, 8 bytes)
154        //   offset 8:  next (pointer, 8 bytes) — NULL = end of chain
155        //   offset 16: priority (i32, 4 bytes)
156        //
157        // raw_notifier_head:
158        //   offset 0: head (pointer to first notifier_block, or NULL)
159
160        let head_vaddr: u64 = 0xFFFF_8000_0010_0000; // raw_notifier_head.head
161        let head_paddr: u64 = 0x0080_0000;
162        let nb_vaddr: u64 = 0xFFFF_8000_0010_1000;
163        let nb_paddr: u64 = 0x0081_0000;
164
165        let notifier_call: u64 = 0xFFFF_FFFF_8155_0000; // in-kernel
166        let priority: i32 = 10;
167
168        // raw_notifier_head page: head ptr at offset 0 → nb_vaddr
169        let mut head_data = [0u8; 0x1000];
170        head_data[0..8].copy_from_slice(&nb_vaddr.to_le_bytes());
171
172        // notifier_block page:
173        //   +0: notifier_call
174        //   +8: next = 0 (end of chain)
175        //   +16: priority
176        let mut nb_data = [0u8; 0x1000];
177        nb_data[0..8].copy_from_slice(&notifier_call.to_le_bytes());
178        nb_data[8..16].copy_from_slice(&0u64.to_le_bytes()); // next = NULL
179        nb_data[16..20].copy_from_slice(&priority.to_le_bytes());
180
181        let stext: u64 = 0xFFFF_FFFF_8100_0000;
182        let etext: u64 = 0xFFFF_FFFF_8200_0000;
183
184        let isf = IsfBuilder::new()
185            .add_symbol("keyboard_notifier_list", head_vaddr)
186            .add_symbol("_stext", stext)
187            .add_symbol("_etext", etext)
188            .build_json();
189
190        let resolver = IsfResolver::from_value(&isf).unwrap();
191        let (cr3, mut mem) = PageTableBuilder::new()
192            .map_4k(head_vaddr, head_paddr, flags::PRESENT | flags::WRITABLE)
193            .map_4k(nb_vaddr, nb_paddr, flags::PRESENT | flags::WRITABLE)
194            .build();
195        mem.write_bytes(head_paddr, &head_data);
196        mem.write_bytes(nb_paddr, &nb_data);
197
198        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
199        let reader = ObjectReader::new(vas, Box::new(resolver));
200
201        let notifiers = walk_keyboard_notifiers(&reader).unwrap();
202        assert_eq!(notifiers.len(), 1, "should find one notifier_block");
203        assert_eq!(notifiers[0].notifier_call, notifier_call);
204        assert_eq!(notifiers[0].priority, priority);
205        assert!(
206            !notifiers[0].is_suspicious,
207            "in-kernel call should be benign"
208        );
209    }
210}