Skip to main content

memf_linux/
ftrace.rs

1//! Ftrace hook detection from kernel memory.
2//!
3//! Detects malicious ftrace hooks by walking the `ftrace_ops_list` global
4//! linked list.  Each `ftrace_ops` entry records a `func` function pointer
5//! that is called for every instrumented kernel function.  A `func` pointer
6//! that lies outside the kernel text range (`_stext`..`_etext`) is a strong
7//! indicator of a rootkit hook.
8
9use memf_core::object_reader::ObjectReader;
10use memf_format::PhysicalMemoryProvider;
11
12use crate::Result;
13
14/// Information about a single ftrace_ops entry.
15#[derive(Debug, Clone, serde::Serialize)]
16pub struct FtraceHookInfo {
17    /// Virtual address of the `ftrace_ops` struct.
18    pub address: u64,
19    /// `ftrace_ops.func` — the hook function pointer.
20    pub func: u64,
21    /// Resolved symbol name if available, otherwise hex string.
22    pub func_name: String,
23    /// `ftrace_ops.flags` field.
24    pub flags: u32,
25    /// True when `func` lies outside `_stext`..`_etext`.
26    pub is_suspicious: bool,
27}
28
29/// Walk `ftrace_ops_list` and return all registered ftrace hooks.
30///
31/// Returns `Ok(Vec::new())` when the `ftrace_ops_list` symbol is absent.
32///
33/// `ftrace_ops` layout (simplified, x86-64):
34///   +0x00: func (pointer) — the hook callback
35///   +0x08: list (list_head) — embedded linked list
36///   +0x18: flags (u32)
37pub fn walk_ftrace_hooks<P: PhysicalMemoryProvider>(
38    reader: &ObjectReader<P>,
39) -> Result<Vec<FtraceHookInfo>> {
40    const MAX_HOOKS: usize = 1_000;
41    let Some(list_head_addr) = reader.symbols().symbol_address("ftrace_ops_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    // ftrace_ops.list is a list_head at offset 8.
52    // Walk the list: list_head.next points to ftrace_ops.list (i.e. ops+8).
53    // container_of: ops = (ops.list ptr) - 8.
54    let list_offset: u64 = reader
55        .symbols()
56        .field_offset("ftrace_ops", "list")
57        .unwrap_or(8);
58    let func_offset: u64 = reader
59        .symbols()
60        .field_offset("ftrace_ops", "func")
61        .unwrap_or(0);
62    let flags_offset: u64 = reader
63        .symbols()
64        .field_offset("ftrace_ops", "flags")
65        .unwrap_or(0x18);
66
67    // Read the next pointer from the list head sentinel.
68    let next_field_offset: u64 = reader
69        .symbols()
70        .field_offset("list_head", "next")
71        .unwrap_or(0);
72
73    let mut hooks = Vec::new();
74
75    let first_ptr = match reader.read_bytes(list_head_addr + next_field_offset, 8) {
76        Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
77        _ => return Ok(Vec::new()),
78    };
79
80    let mut current_list_ptr = first_ptr;
81
82    for _ in 0..MAX_HOOKS {
83        // current_list_ptr points to ops.list; sentinel = list_head_addr
84        if current_list_ptr == list_head_addr || current_list_ptr == 0 {
85            break;
86        }
87
88        // container_of: ops base = current_list_ptr - list_offset
89        let ops_addr = current_list_ptr.wrapping_sub(list_offset);
90
91        let func = match reader.read_bytes(ops_addr + func_offset, 8) {
92            Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
93            _ => 0,
94        };
95
96        let flags = match reader.read_bytes(ops_addr + flags_offset, 4) {
97            Ok(b) if b.len() == 4 => b.try_into().map_or(0, u32::from_le_bytes),
98            _ => 0,
99        };
100
101        let is_suspicious = classify_ftrace_hook(func, stext, etext);
102        let func_name = format!("{func:#018x}");
103
104        hooks.push(FtraceHookInfo {
105            address: ops_addr,
106            func,
107            func_name,
108            flags,
109            is_suspicious,
110        });
111
112        // Advance: read ops.list.next
113        current_list_ptr = match reader.read_bytes(current_list_ptr + next_field_offset, 8) {
114            Ok(b) if b.len() == 8 => b.try_into().map_or(0, u64::from_le_bytes),
115            _ => break,
116        };
117    }
118
119    Ok(hooks)
120}
121
122/// Classify whether a `func` pointer is suspicious given the kernel text range.
123pub use crate::heuristics::classify_ftrace_hook;
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
129    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
130    use memf_symbols::isf::IsfResolver;
131    use memf_symbols::test_builders::IsfBuilder;
132
133    fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
134        let isf = IsfBuilder::new().build_json();
135        let resolver = IsfResolver::from_value(&isf).unwrap();
136        let (cr3, mem) = PageTableBuilder::new().build();
137        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
138        ObjectReader::new(vas, Box::new(resolver))
139    }
140
141    #[test]
142    fn no_symbol_returns_empty() {
143        let reader = make_no_symbol_reader();
144        let result = walk_ftrace_hooks(&reader).unwrap();
145        assert!(result.is_empty(), "no ftrace_ops_list symbol → empty vec");
146    }
147
148    #[test]
149    fn classify_in_kernel_benign() {
150        let stext = 0xFFFF_FFFF_8100_0000_u64;
151        let etext = 0xFFFF_FFFF_8200_0000_u64;
152        let func = 0xFFFF_FFFF_8150_0000_u64; // inside kernel text
153        assert!(
154            !classify_ftrace_hook(func, stext, etext),
155            "in-kernel func should be benign"
156        );
157    }
158
159    #[test]
160    fn classify_out_of_kernel_suspicious() {
161        let stext = 0xFFFF_FFFF_8100_0000_u64;
162        let etext = 0xFFFF_FFFF_8200_0000_u64;
163        let func = 0xFFFF_C900_0000_0000_u64; // outside kernel text → suspicious
164        assert!(
165            classify_ftrace_hook(func, stext, etext),
166            "out-of-kernel func should be suspicious"
167        );
168    }
169
170    // RED test: walk_ftrace_hooks with a real symbol and mapped ops should return entries.
171    #[test]
172    fn walk_ftrace_hooks_with_symbol_returns_entries() {
173        use memf_core::test_builders::flags;
174
175        // Layout of ftrace_ops (simplified):
176        //   0x00: func (pointer, 8 bytes)
177        //   0x08: list.next (pointer, 8 bytes) — points to next ops or back to list head
178        //   0x10: list.prev (pointer, 8 bytes)
179        //   0x18: flags (u32, 4 bytes)
180        //
181        // We create one ops entry.  ftrace_ops_list symbol points to the list head
182        // (a list_head whose .next points to our ops.list).
183        //
184        // For simplicity we make a self-referential list: ops.list.next = list_head_addr
185        // so the walk terminates after one entry.
186
187        let list_head_vaddr: u64 = 0xFFFF_8000_0010_0000;
188        let list_head_paddr: u64 = 0x0080_0000;
189        let ops_vaddr: u64 = 0xFFFF_8000_0010_1000;
190        let ops_paddr: u64 = 0x0081_0000;
191
192        // ftrace_ops.list is at offset 8 within ftrace_ops.
193        // ftrace_ops_list (list_head) .next points to &ops.list = ops_vaddr + 8.
194        // ops.list.next points back to list_head_vaddr (sentinel) so walk stops.
195
196        let func_ptr: u64 = 0xFFFF_FFFF_8150_0000; // in-kernel
197        let ops_flags: u32 = 0x0001;
198
199        // Build list head page: [next=ops_vaddr+8, prev=ops_vaddr+8]
200        let mut list_head_data = [0u8; 0x1000];
201        list_head_data[0..8].copy_from_slice(&(ops_vaddr + 8).to_le_bytes());
202        list_head_data[8..16].copy_from_slice(&(ops_vaddr + 8).to_le_bytes());
203
204        // Build ops page:
205        //   +0x00: func ptr
206        //   +0x08: list.next = list_head_vaddr (sentinel → stop)
207        //   +0x10: list.prev
208        //   +0x18: flags
209        let mut ops_data = [0u8; 0x1000];
210        ops_data[0x00..0x08].copy_from_slice(&func_ptr.to_le_bytes());
211        ops_data[0x08..0x10].copy_from_slice(&list_head_vaddr.to_le_bytes());
212        ops_data[0x10..0x18].copy_from_slice(&list_head_vaddr.to_le_bytes());
213        ops_data[0x18..0x1C].copy_from_slice(&ops_flags.to_le_bytes());
214
215        let stext: u64 = 0xFFFF_FFFF_8100_0000;
216        let etext: u64 = 0xFFFF_FFFF_8200_0000;
217
218        let isf = IsfBuilder::new()
219            .add_struct("ftrace_ops", 64)
220            .add_field("ftrace_ops", "func", 0, "pointer")
221            .add_field("ftrace_ops", "list", 8, "list_head")
222            .add_field("ftrace_ops", "flags", 0x18, "unsigned int")
223            .add_struct("list_head", 16)
224            .add_field("list_head", "next", 0, "pointer")
225            .add_field("list_head", "prev", 8, "pointer")
226            .add_symbol("ftrace_ops_list", list_head_vaddr)
227            .add_symbol("_stext", stext)
228            .add_symbol("_etext", etext)
229            .build_json();
230
231        let resolver = IsfResolver::from_value(&isf).unwrap();
232        let (cr3, mut mem) = PageTableBuilder::new()
233            .map_4k(
234                list_head_vaddr,
235                list_head_paddr,
236                flags::PRESENT | flags::WRITABLE,
237            )
238            .map_4k(ops_vaddr, ops_paddr, flags::PRESENT | flags::WRITABLE)
239            .build();
240        mem.write_bytes(list_head_paddr, &list_head_data);
241        mem.write_bytes(ops_paddr, &ops_data);
242
243        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
244        let reader = ObjectReader::new(vas, Box::new(resolver));
245
246        let hooks = walk_ftrace_hooks(&reader).unwrap();
247        assert_eq!(hooks.len(), 1, "should find one ftrace hook");
248        assert_eq!(hooks[0].func, func_ptr);
249        assert!(!hooks[0].is_suspicious, "in-kernel func should be benign");
250    }
251}