Skip to main content

memf_linux/
syscalls.rs

1//! Linux syscall table integrity checker.
2//!
3//! Reads the `sys_call_table` kernel symbol and checks each handler
4//! address against the kernel text region (`_stext`..`_etext`).
5//! Entries pointing outside this range are flagged as potentially hooked.
6
7use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::{Error, Result, SyscallInfo};
11
12/// Default number of syscall table entries for x86_64.
13const DEFAULT_NR_SYSCALLS: u64 = 450;
14
15/// Check the syscall table for hooks.
16///
17/// Reads `sys_call_table` entries and compares each handler against the
18/// `_stext`..`_etext` kernel text range. Returns info for each entry,
19/// marking entries outside the text region as potentially hooked.
20pub fn check_syscall_table<P: PhysicalMemoryProvider>(
21    reader: &ObjectReader<P>,
22) -> Result<Vec<SyscallInfo>> {
23    let table_addr = reader
24        .symbols()
25        .symbol_address("sys_call_table")
26        .ok_or_else(|| Error::MissingKernelSymbol {
27            name: "sys_call_table".into(),
28        })?;
29
30    let stext =
31        reader
32            .symbols()
33            .symbol_address("_stext")
34            .ok_or_else(|| Error::MissingKernelSymbol {
35                name: "_stext".into(),
36            })?;
37
38    let etext =
39        reader
40            .symbols()
41            .symbol_address("_etext")
42            .ok_or_else(|| Error::MissingKernelSymbol {
43                name: "_etext".into(),
44            })?;
45
46    // Determine number of syscalls: prefer __NR_syscall_max + 1, else default
47    let nr_syscalls = reader
48        .symbols()
49        .symbol_address("__NR_syscall_max")
50        .map_or(DEFAULT_NR_SYSCALLS, |max| max + 1);
51
52    // Read the entire table as raw bytes (each entry is 8 bytes / u64 pointer)
53    let table_size = usize::try_from(nr_syscalls).unwrap_or(0) * 8;
54    let table_raw = reader.read_bytes(table_addr, table_size)?;
55
56    let mut entries = Vec::with_capacity(nr_syscalls as usize);
57
58    for i in 0..nr_syscalls {
59        let off = (i as usize) * 8;
60        let handler = table_raw[off..off + 8]
61            .try_into()
62            .map_or(0, u64::from_le_bytes);
63
64        let hooked = handler < stext || handler > etext;
65
66        entries.push(SyscallInfo {
67            number: i,
68            handler,
69            hooked,
70            expected_name: None,
71        });
72    }
73
74    Ok(entries)
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
81    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
82    use memf_symbols::isf::IsfResolver;
83    use memf_symbols::test_builders::IsfBuilder;
84
85    fn make_test_reader(
86        data: &[u8],
87        vaddr: u64,
88        paddr: u64,
89        nr_syscalls: u64,
90        stext: u64,
91        etext: u64,
92    ) -> ObjectReader<SyntheticPhysMem> {
93        let mut builder = IsfBuilder::new()
94            .add_struct("task_struct", 64)
95            .add_field("task_struct", "pid", 0, "int")
96            .add_symbol("sys_call_table", vaddr)
97            .add_symbol("_stext", stext)
98            .add_symbol("_etext", etext);
99
100        if nr_syscalls > 0 {
101            builder = builder.add_symbol("__NR_syscall_max", nr_syscalls - 1);
102        }
103
104        let isf = builder.build_json();
105        let resolver = IsfResolver::from_value(&isf).unwrap();
106        let (cr3, mem) = PageTableBuilder::new()
107            .map_4k(vaddr, paddr, flags::WRITABLE)
108            .write_phys(paddr, data)
109            .build();
110        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
111        ObjectReader::new(vas, Box::new(resolver))
112    }
113
114    #[test]
115    fn all_handlers_in_text_region() {
116        let vaddr: u64 = 0xFFFF_8000_0010_0000;
117        let paddr: u64 = 0x0080_0000;
118        let stext: u64 = 0xFFFF_8000_0000_0000;
119        let etext: u64 = 0xFFFF_8000_00FF_FFFF;
120        let mut data = vec![0u8; 4096];
121
122        // 3 syscall entries, all within text region
123        let handler0: u64 = 0xFFFF_8000_0001_0000;
124        let handler1: u64 = 0xFFFF_8000_0002_0000;
125        let handler2: u64 = 0xFFFF_8000_0003_0000;
126        data[0..8].copy_from_slice(&handler0.to_le_bytes());
127        data[8..16].copy_from_slice(&handler1.to_le_bytes());
128        data[16..24].copy_from_slice(&handler2.to_le_bytes());
129
130        let reader = make_test_reader(&data, vaddr, paddr, 3, stext, etext);
131        let entries = check_syscall_table(&reader).unwrap();
132
133        assert_eq!(entries.len(), 3);
134        assert!(!entries[0].hooked);
135        assert!(!entries[1].hooked);
136        assert!(!entries[2].hooked);
137        assert_eq!(entries[0].number, 0);
138        assert_eq!(entries[1].number, 1);
139        assert_eq!(entries[2].number, 2);
140        assert_eq!(entries[0].handler, handler0);
141    }
142
143    #[test]
144    fn hooked_syscall_detected() {
145        let vaddr: u64 = 0xFFFF_8000_0010_0000;
146        let paddr: u64 = 0x0080_0000;
147        let stext: u64 = 0xFFFF_8000_0000_0000;
148        let etext: u64 = 0xFFFF_8000_00FF_FFFF;
149        let mut data = vec![0u8; 4096];
150
151        // Handler 0: normal (in text)
152        let normal: u64 = 0xFFFF_8000_0001_0000;
153        data[0..8].copy_from_slice(&normal.to_le_bytes());
154
155        // Handler 1: hooked! (outside text, points to a module)
156        let hooked: u64 = 0xFFFF_C900_1234_5678;
157        data[8..16].copy_from_slice(&hooked.to_le_bytes());
158
159        // Handler 2: normal
160        data[16..24].copy_from_slice(&normal.to_le_bytes());
161
162        let reader = make_test_reader(&data, vaddr, paddr, 3, stext, etext);
163        let entries = check_syscall_table(&reader).unwrap();
164
165        assert_eq!(entries.len(), 3);
166        assert!(!entries[0].hooked);
167        assert!(entries[1].hooked);
168        assert!(!entries[2].hooked);
169        assert_eq!(entries[1].handler, hooked);
170    }
171
172    #[test]
173    fn missing_sys_call_table_symbol() {
174        let isf = IsfBuilder::new()
175            .add_struct("task_struct", 64)
176            .add_field("task_struct", "pid", 0, "int")
177            .add_symbol("_stext", 0xFFFF_8000_0000_0000)
178            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
179            // No sys_call_table symbol
180            .build_json();
181
182        let resolver = IsfResolver::from_value(&isf).unwrap();
183        let (cr3, mem) = PageTableBuilder::new().build();
184        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
185        let reader = ObjectReader::new(vas, Box::new(resolver));
186
187        let result = check_syscall_table(&reader);
188        assert!(
189            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "sys_call_table"),
190            "expected MissingKernelSymbol {{name: \"sys_call_table\"}}, got {result:?}"
191        );
192    }
193
194    #[test]
195    fn missing_stext_symbol_returns_missing_kernel_symbol() {
196        let isf = IsfBuilder::new()
197            .add_symbol("sys_call_table", 0xFFFF_8000_0010_0000)
198            // _stext intentionally omitted
199            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
200            .build_json();
201        let resolver = IsfResolver::from_value(&isf).unwrap();
202        let (cr3, mem) = PageTableBuilder::new().build();
203        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
204        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
205        let result = check_syscall_table(&reader);
206        assert!(
207            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "_stext"),
208            "expected MissingKernelSymbol {{name: \"_stext\"}}, got {result:?}"
209        );
210    }
211
212    #[test]
213    fn uses_default_count_without_nr_syscall_max() {
214        let vaddr: u64 = 0xFFFF_8000_0010_0000;
215        let paddr: u64 = 0x0080_0000;
216        let stext: u64 = 0xFFFF_8000_0000_0000;
217        let etext: u64 = 0xFFFF_8000_00FF_FFFF;
218
219        // Fill page with a valid handler repeated
220        let mut data = vec![0u8; 4096];
221        let handler: u64 = 0xFFFF_8000_0001_0000;
222        for i in 0..512 {
223            let off = i * 8;
224            data[off..off + 8].copy_from_slice(&handler.to_le_bytes());
225        }
226
227        // nr_syscalls = 0 means no __NR_syscall_max symbol
228        let reader = make_test_reader(&data, vaddr, paddr, 0, stext, etext);
229        let entries = check_syscall_table(&reader).unwrap();
230
231        // Should use DEFAULT_NR_SYSCALLS but clamp to what fits in one page
232        assert_eq!(entries.len(), DEFAULT_NR_SYSCALLS as usize);
233    }
234}