Skip to main content

memf_linux/
tty_check.rs

1//! Linux TTY operations hook detector.
2//!
3//! Walks the `tty_drivers` list and checks each driver's
4//! `tty_operations` function pointers against the kernel text
5//! region (`_stext`..`_etext`). Handlers pointing outside this
6//! range indicate potential rootkit hooks on TTY devices.
7
8use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::{Error, Result, TtyCheckInfo};
12
13/// Check TTY driver operations for hooks.
14///
15/// Walks the `tty_drivers` linked list, reads each driver's
16/// `tty_operations` struct, and checks function pointers against
17/// the kernel text region.
18pub fn check_tty_hooks<P: PhysicalMemoryProvider>(
19    reader: &ObjectReader<P>,
20) -> Result<Vec<TtyCheckInfo>> {
21    let tty_drivers_addr = reader
22        .symbols()
23        .symbol_address("tty_drivers")
24        .ok_or_else(|| Error::MissingKernelSymbol {
25            name: "tty_drivers".into(),
26        })?;
27
28    let stext =
29        reader
30            .symbols()
31            .symbol_address("_stext")
32            .ok_or_else(|| Error::MissingKernelSymbol {
33                name: "_stext".into(),
34            })?;
35
36    let etext =
37        reader
38            .symbols()
39            .symbol_address("_etext")
40            .ok_or_else(|| Error::MissingKernelSymbol {
41                name: "_etext".into(),
42            })?;
43
44    let _tty_drivers_offset = reader
45        .symbols()
46        .field_offset("tty_driver", "tty_drivers")
47        .ok_or_else(|| Error::MissingField {
48            struct_name: "tty_driver".into(),
49            field_name: "tty_drivers".into(),
50        })?;
51
52    // Walk the tty_drivers linked list
53    let driver_addrs = reader.walk_list(tty_drivers_addr, "tty_driver", "tty_drivers")?;
54
55    let mut results = Vec::new();
56
57    for &driver_addr in &driver_addrs {
58        let name = reader
59            .read_field_string(driver_addr, "tty_driver", "name", 64)
60            .unwrap_or_else(|_| "<unknown>".to_string());
61
62        let ops_ptr: u64 = match reader.read_field(driver_addr, "tty_driver", "ops") {
63            Ok(v) if v != 0 => v,
64            _ => continue,
65        };
66
67        // Check each operation function pointer
68        let ops_fields = ["open", "close", "write", "ioctl"];
69        for &op_name in &ops_fields {
70            let handler: u64 = match reader.read_field(ops_ptr, "tty_operations", op_name) {
71                Ok(v) => v,
72                Err(_) => continue,
73            };
74
75            if handler == 0 {
76                continue;
77            }
78
79            let hooked = handler < stext || handler > etext;
80
81            results.push(TtyCheckInfo {
82                name: name.clone(),
83                operation: op_name.to_string(),
84                handler,
85                hooked,
86            });
87        }
88    }
89
90    Ok(results)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
97    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
98    use memf_symbols::isf::IsfResolver;
99    use memf_symbols::test_builders::IsfBuilder;
100
101    fn make_test_reader(
102        data: &[u8],
103        vaddr: u64,
104        paddr: u64,
105        stext: u64,
106        etext: u64,
107    ) -> ObjectReader<SyntheticPhysMem> {
108        let isf = IsfBuilder::new()
109            .add_struct("tty_driver", 128)
110            .add_field("tty_driver", "name", 0, "pointer")
111            .add_field("tty_driver", "ops", 16, "pointer")
112            .add_field("tty_driver", "tty_drivers", 24, "list_head")
113            .add_struct("tty_operations", 128)
114            .add_field("tty_operations", "open", 0, "pointer")
115            .add_field("tty_operations", "close", 8, "pointer")
116            .add_field("tty_operations", "write", 16, "pointer")
117            .add_field("tty_operations", "ioctl", 48, "pointer")
118            .add_struct("list_head", 16)
119            .add_field("list_head", "next", 0, "pointer")
120            .add_field("list_head", "prev", 8, "pointer")
121            .add_symbol("tty_drivers", vaddr + 0x800)
122            .add_symbol("_stext", stext)
123            .add_symbol("_etext", etext)
124            .build_json();
125
126        let resolver = IsfResolver::from_value(&isf).unwrap();
127        let (cr3, mem) = PageTableBuilder::new()
128            .map_4k(vaddr, paddr, ptflags::WRITABLE)
129            .write_phys(paddr, data)
130            .build();
131        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
132        ObjectReader::new(vas, Box::new(resolver))
133    }
134
135    #[test]
136    fn clean_tty_ops_not_hooked() {
137        let vaddr: u64 = 0xFFFF_8000_0010_0000;
138        let paddr: u64 = 0x0080_0000;
139        let stext: u64 = 0xFFFF_8000_0000_0000;
140        let etext: u64 = 0xFFFF_8000_00FF_FFFF;
141        let mut data = vec![0u8; 4096];
142
143        // tty_drivers list_head at +0x800 (points to self = empty list → just test setup)
144        let drivers_head = vaddr + 0x800;
145        data[0x800..0x808].copy_from_slice(&drivers_head.to_le_bytes()); // next = self
146        data[0x808..0x810].copy_from_slice(&drivers_head.to_le_bytes()); // prev = self
147
148        let reader = make_test_reader(&data, vaddr, paddr, stext, etext);
149        let results = check_tty_hooks(&reader).unwrap();
150
151        // Empty list → no results (but no error either)
152        assert!(results.is_empty());
153    }
154
155    #[test]
156    fn missing_tty_drivers_symbol() {
157        let isf = IsfBuilder::new()
158            .add_struct("tty_driver", 64)
159            .add_field("tty_driver", "name", 0, "pointer")
160            .add_struct("list_head", 16)
161            .add_field("list_head", "next", 0, "pointer")
162            .add_field("list_head", "prev", 8, "pointer")
163            .add_symbol("_stext", 0xFFFF_8000_0000_0000)
164            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
165            .build_json();
166
167        let resolver = IsfResolver::from_value(&isf).unwrap();
168        let (cr3, mem) = PageTableBuilder::new().build();
169        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
170        let reader = ObjectReader::new(vas, Box::new(resolver));
171
172        let result = check_tty_hooks(&reader);
173        assert!(
174            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "tty_drivers"),
175            "expected MissingKernelSymbol {{name: \"tty_drivers\"}}, got {result:?}"
176        );
177    }
178
179    #[test]
180    fn missing_stext_symbol_returns_missing_kernel_symbol() {
181        // tty_drivers present but _stext absent → Error
182        let isf = IsfBuilder::new()
183            .add_struct("tty_driver", 64)
184            .add_field("tty_driver", "name", 0, "pointer")
185            .add_field("tty_driver", "tty_drivers", 8, "list_head")
186            .add_struct("list_head", 16)
187            .add_field("list_head", "next", 0, "pointer")
188            .add_field("list_head", "prev", 8, "pointer")
189            .add_symbol("tty_drivers", 0xFFFF_8000_0010_0000)
190            // _stext intentionally omitted
191            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
192            .build_json();
193
194        let resolver = IsfResolver::from_value(&isf).unwrap();
195        let (cr3, mem) = PageTableBuilder::new().build();
196        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
197        let reader = ObjectReader::new(vas, Box::new(resolver));
198
199        let result = check_tty_hooks(&reader);
200        assert!(
201            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "_stext"),
202            "expected MissingKernelSymbol {{name: \"_stext\"}}, got {result:?}"
203        );
204    }
205
206    #[test]
207    fn missing_etext_symbol_returns_missing_kernel_symbol() {
208        // tty_drivers + _stext present but _etext absent → Error
209        let isf = IsfBuilder::new()
210            .add_struct("tty_driver", 64)
211            .add_field("tty_driver", "name", 0, "pointer")
212            .add_field("tty_driver", "tty_drivers", 8, "list_head")
213            .add_struct("list_head", 16)
214            .add_field("list_head", "next", 0, "pointer")
215            .add_field("list_head", "prev", 8, "pointer")
216            .add_symbol("tty_drivers", 0xFFFF_8000_0010_0000)
217            .add_symbol("_stext", 0xFFFF_8000_0000_0000)
218            // _etext intentionally omitted
219            .build_json();
220
221        let resolver = IsfResolver::from_value(&isf).unwrap();
222        let (cr3, mem) = PageTableBuilder::new().build();
223        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
224        let reader = ObjectReader::new(vas, Box::new(resolver));
225
226        let result = check_tty_hooks(&reader);
227        assert!(
228            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "_etext"),
229            "expected MissingKernelSymbol {{name: \"_etext\"}}, got {result:?}"
230        );
231    }
232
233    #[test]
234    fn missing_tty_drivers_field_offset_returns_missing_field() {
235        // tty_drivers symbol present but tty_driver.tty_drivers field absent → Error
236        let isf = IsfBuilder::new()
237            .add_struct("tty_driver", 64)
238            .add_field("tty_driver", "name", 0, "pointer")
239            // tty_drivers field intentionally omitted from tty_driver struct
240            .add_struct("list_head", 16)
241            .add_field("list_head", "next", 0, "pointer")
242            .add_field("list_head", "prev", 8, "pointer")
243            .add_symbol("tty_drivers", 0xFFFF_8000_0010_0000)
244            .add_symbol("_stext", 0xFFFF_8000_0000_0000)
245            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
246            .build_json();
247
248        let resolver = IsfResolver::from_value(&isf).unwrap();
249        let (cr3, mem) = PageTableBuilder::new().build();
250        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
251        let reader = ObjectReader::new(vas, Box::new(resolver));
252
253        let result = check_tty_hooks(&reader);
254        assert!(
255            matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "tty_driver" && field_name == "tty_drivers"),
256            "expected MissingField tty_driver.tty_drivers, got {result:?}"
257        );
258    }
259
260    // --- check_tty_hooks: list with a real driver entry, ops non-zero, handler in text range ---
261    // Exercises the driver-loop body (lines 46-77): ops_ptr != 0, read handler, hooked=false.
262    #[test]
263    fn check_tty_hooks_driver_with_clean_ops() {
264        // Layout:
265        //   page at vaddr (paddr):
266        //     +0x000: tty_driver struct
267        //       +0x000: name ptr (points to name string at +0xE00)
268        //       +0x010: ops ptr  (points to tty_operations at +0xC00)
269        //       +0x018: tty_drivers list_head
270        //     +0x800: tty_drivers global list_head (next=driver_entry, prev=driver_entry)
271        //     +0xC00: tty_operations struct
272        //       +0x000: open handler
273        //       +0x008: close handler
274        //       +0x010: write handler
275        //       +0x030: ioctl handler
276        //     +0xE00: name string "ttyS\0"
277        //
278        //   stext = 0xFFFF_8000_0000_0000, etext = 0xFFFF_8000_00FF_FFFF
279        //   All handlers inside [stext, etext] → hooked = false.
280
281        let vaddr: u64 = 0xFFFF_8000_0020_0000;
282        let paddr: u64 = 0x0020_0000;
283
284        let stext: u64 = 0xFFFF_8000_0000_0000;
285        let etext: u64 = 0xFFFF_8000_00FF_FFFF;
286
287        // A handler value inside the text region.
288        let clean_handler: u64 = 0xFFFF_8000_0001_0000;
289
290        // tty_drivers list head sits at vaddr + 0x800 (as per make_test_reader).
291        let drivers_head: u64 = vaddr + 0x800;
292        // Driver entry's tty_drivers list_head is at driver_base + 0x018.
293        let driver_list_entry: u64 = vaddr + 0x018;
294        // ops pointer stored at driver_base + 0x010.
295        let ops_ptr: u64 = vaddr + 0xC00;
296        // Name pointer stored at driver_base + 0x000 (name field at offset 0).
297        let name_ptr: u64 = vaddr + 0xE00;
298
299        let mut data = vec![0u8; 4096];
300
301        // --- tty_driver at base 0x000 ---
302        // name ptr (field offset=0): points to name string
303        data[0x000..0x008].copy_from_slice(&name_ptr.to_le_bytes());
304        // ops ptr (field offset=16=0x010)
305        data[0x010..0x018].copy_from_slice(&ops_ptr.to_le_bytes());
306        // tty_drivers list_head (field offset=24=0x018):
307        //   next = drivers_head  (so walk_list returns this one driver)
308        //   prev = drivers_head
309        data[0x018..0x020].copy_from_slice(&drivers_head.to_le_bytes()); // next
310        data[0x020..0x028].copy_from_slice(&drivers_head.to_le_bytes()); // prev
311
312        // --- tty_drivers global list_head at 0x800 ---
313        // next = driver_list_entry (the driver's embedded list_head)
314        // prev = driver_list_entry
315        data[0x800..0x808].copy_from_slice(&driver_list_entry.to_le_bytes());
316        data[0x808..0x810].copy_from_slice(&driver_list_entry.to_le_bytes());
317
318        // --- tty_operations at 0xC00 ---
319        data[0xC00..0xC08].copy_from_slice(&clean_handler.to_le_bytes()); // open
320        data[0xC08..0xC10].copy_from_slice(&clean_handler.to_le_bytes()); // close
321        data[0xC10..0xC18].copy_from_slice(&clean_handler.to_le_bytes()); // write
322        data[0xC30..0xC38].copy_from_slice(&clean_handler.to_le_bytes()); // ioctl
323
324        // --- name string at 0xE00 ---
325        data[0xE00..0xE05].copy_from_slice(b"ttyS\0");
326
327        let reader = make_test_reader(&data, vaddr, paddr, stext, etext);
328        let results = check_tty_hooks(&reader).expect("should not error");
329
330        // 4 ops checked, all within text region → hooked=false for all.
331        assert!(
332            !results.is_empty(),
333            "expected at least one ops entry from the driver"
334        );
335        for r in &results {
336            assert!(
337                !r.hooked,
338                "clean handler inside text region must not be flagged"
339            );
340        }
341    }
342
343    // --- check_tty_hooks: driver with ops outside text region → hooked = true ---
344    #[test]
345    fn check_tty_hooks_driver_with_hooked_ops() {
346        let vaddr: u64 = 0xFFFF_8000_0021_0000;
347        let paddr: u64 = 0x0021_0000;
348
349        let stext: u64 = 0xFFFF_8000_0000_0000;
350        let etext: u64 = 0xFFFF_8000_00FF_FFFF;
351
352        // A handler value OUTSIDE the text region (suspicious).
353        let hooked_handler: u64 = 0xFFFF_CAFE_DEAD_0001;
354
355        let drivers_head: u64 = vaddr + 0x800;
356        let driver_list_entry: u64 = vaddr + 0x018;
357        let ops_ptr: u64 = vaddr + 0xC00;
358        let name_ptr: u64 = vaddr + 0xE00;
359
360        let mut data = vec![0u8; 4096];
361
362        data[0x000..0x008].copy_from_slice(&name_ptr.to_le_bytes());
363        data[0x010..0x018].copy_from_slice(&ops_ptr.to_le_bytes());
364        data[0x018..0x020].copy_from_slice(&drivers_head.to_le_bytes());
365        data[0x020..0x028].copy_from_slice(&drivers_head.to_le_bytes());
366
367        data[0x800..0x808].copy_from_slice(&driver_list_entry.to_le_bytes());
368        data[0x808..0x810].copy_from_slice(&driver_list_entry.to_le_bytes());
369
370        // All ops point outside text region.
371        data[0xC00..0xC08].copy_from_slice(&hooked_handler.to_le_bytes());
372        data[0xC08..0xC10].copy_from_slice(&hooked_handler.to_le_bytes());
373        data[0xC10..0xC18].copy_from_slice(&hooked_handler.to_le_bytes());
374        data[0xC30..0xC38].copy_from_slice(&hooked_handler.to_le_bytes());
375
376        data[0xE00..0xE09].copy_from_slice(b"rootkit0\0");
377
378        let reader = make_test_reader(&data, vaddr, paddr, stext, etext);
379        let results = check_tty_hooks(&reader).expect("should not error");
380
381        assert!(!results.is_empty(), "hooked ops must produce entries");
382        for r in &results {
383            assert!(
384                r.hooked,
385                "handler outside text region must be flagged as hooked"
386            );
387        }
388    }
389
390    // --- check_tty_hooks: driver with ops == 0 → skipped (continue branch) ---
391    #[test]
392    fn check_tty_hooks_driver_ops_null_skipped() {
393        let vaddr: u64 = 0xFFFF_8000_0022_0000;
394        let paddr: u64 = 0x0022_0000;
395
396        let stext: u64 = 0xFFFF_8000_0000_0000;
397        let etext: u64 = 0xFFFF_8000_00FF_FFFF;
398
399        let drivers_head: u64 = vaddr + 0x800;
400        let driver_list_entry: u64 = vaddr + 0x018;
401        let name_ptr: u64 = vaddr + 0xE00;
402
403        let mut data = vec![0u8; 4096];
404
405        data[0x000..0x008].copy_from_slice(&name_ptr.to_le_bytes());
406        // ops at 0x010 = 0 (null) → ops branch: `Ok(v) if v != 0` fails → continue
407        data[0x010..0x018].copy_from_slice(&0u64.to_le_bytes());
408        data[0x018..0x020].copy_from_slice(&drivers_head.to_le_bytes());
409        data[0x020..0x028].copy_from_slice(&drivers_head.to_le_bytes());
410
411        data[0x800..0x808].copy_from_slice(&driver_list_entry.to_le_bytes());
412        data[0x808..0x810].copy_from_slice(&driver_list_entry.to_le_bytes());
413
414        data[0xE00..0xE09].copy_from_slice(b"nullops\0\0");
415
416        let reader = make_test_reader(&data, vaddr, paddr, stext, etext);
417        let results = check_tty_hooks(&reader).expect("should not error");
418
419        assert!(
420            results.is_empty(),
421            "null ops_ptr → driver skipped → no results"
422        );
423    }
424}