1use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::{Error, Result, TtyCheckInfo};
12
13pub 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 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 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 let drivers_head = vaddr + 0x800;
145 data[0x800..0x808].copy_from_slice(&drivers_head.to_le_bytes()); data[0x808..0x810].copy_from_slice(&drivers_head.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr, stext, etext);
149 let results = check_tty_hooks(&reader).unwrap();
150
151 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 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 .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 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 .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 let isf = IsfBuilder::new()
237 .add_struct("tty_driver", 64)
238 .add_field("tty_driver", "name", 0, "pointer")
239 .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 #[test]
263 fn check_tty_hooks_driver_with_clean_ops() {
264 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 let clean_handler: u64 = 0xFFFF_8000_0001_0000;
289
290 let drivers_head: u64 = vaddr + 0x800;
292 let driver_list_entry: u64 = vaddr + 0x018;
294 let ops_ptr: u64 = vaddr + 0xC00;
296 let name_ptr: u64 = vaddr + 0xE00;
298
299 let mut data = vec![0u8; 4096];
300
301 data[0x000..0x008].copy_from_slice(&name_ptr.to_le_bytes());
304 data[0x010..0x018].copy_from_slice(&ops_ptr.to_le_bytes());
306 data[0x018..0x020].copy_from_slice(&drivers_head.to_le_bytes()); data[0x020..0x028].copy_from_slice(&drivers_head.to_le_bytes()); 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 data[0xC00..0xC08].copy_from_slice(&clean_handler.to_le_bytes()); data[0xC08..0xC10].copy_from_slice(&clean_handler.to_le_bytes()); data[0xC10..0xC18].copy_from_slice(&clean_handler.to_le_bytes()); data[0xC30..0xC38].copy_from_slice(&clean_handler.to_le_bytes()); 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 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 #[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 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 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 #[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 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}