use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::{Error, Result, TtyCheckInfo};
pub fn check_tty_hooks<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<TtyCheckInfo>> {
let tty_drivers_addr = reader
.symbols()
.symbol_address("tty_drivers")
.ok_or_else(|| Error::MissingKernelSymbol {
name: "tty_drivers".into(),
})?;
let stext =
reader
.symbols()
.symbol_address("_stext")
.ok_or_else(|| Error::MissingKernelSymbol {
name: "_stext".into(),
})?;
let etext =
reader
.symbols()
.symbol_address("_etext")
.ok_or_else(|| Error::MissingKernelSymbol {
name: "_etext".into(),
})?;
let _tty_drivers_offset = reader
.symbols()
.field_offset("tty_driver", "tty_drivers")
.ok_or_else(|| Error::MissingField {
struct_name: "tty_driver".into(),
field_name: "tty_drivers".into(),
})?;
let driver_addrs = reader.walk_list(tty_drivers_addr, "tty_driver", "tty_drivers")?;
let mut results = Vec::new();
for &driver_addr in &driver_addrs {
let name = reader
.read_field_string(driver_addr, "tty_driver", "name", 64)
.unwrap_or_else(|_| "<unknown>".to_string());
let ops_ptr: u64 = match reader.read_field(driver_addr, "tty_driver", "ops") {
Ok(v) if v != 0 => v,
_ => continue,
};
let ops_fields = ["open", "close", "write", "ioctl"];
for &op_name in &ops_fields {
let handler: u64 = match reader.read_field(ops_ptr, "tty_operations", op_name) {
Ok(v) => v,
Err(_) => continue,
};
if handler == 0 {
continue;
}
let hooked = handler < stext || handler > etext;
results.push(TtyCheckInfo {
name: name.clone(),
operation: op_name.to_string(),
handler,
hooked,
});
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
fn make_test_reader(
data: &[u8],
vaddr: u64,
paddr: u64,
stext: u64,
etext: u64,
) -> ObjectReader<SyntheticPhysMem> {
let isf = IsfBuilder::new()
.add_struct("tty_driver", 128)
.add_field("tty_driver", "name", 0, "pointer")
.add_field("tty_driver", "ops", 16, "pointer")
.add_field("tty_driver", "tty_drivers", 24, "list_head")
.add_struct("tty_operations", 128)
.add_field("tty_operations", "open", 0, "pointer")
.add_field("tty_operations", "close", 8, "pointer")
.add_field("tty_operations", "write", 16, "pointer")
.add_field("tty_operations", "ioctl", 48, "pointer")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_symbol("tty_drivers", vaddr + 0x800)
.add_symbol("_stext", stext)
.add_symbol("_etext", etext)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(vaddr, paddr, ptflags::WRITABLE)
.write_phys(paddr, data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn clean_tty_ops_not_hooked() {
let vaddr: u64 = 0xFFFF_8000_0010_0000;
let paddr: u64 = 0x0080_0000;
let stext: u64 = 0xFFFF_8000_0000_0000;
let etext: u64 = 0xFFFF_8000_00FF_FFFF;
let mut data = vec![0u8; 4096];
let drivers_head = vaddr + 0x800;
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);
let results = check_tty_hooks(&reader).unwrap();
assert!(results.is_empty());
}
#[test]
fn missing_tty_drivers_symbol() {
let isf = IsfBuilder::new()
.add_struct("tty_driver", 64)
.add_field("tty_driver", "name", 0, "pointer")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_symbol("_stext", 0xFFFF_8000_0000_0000)
.add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = check_tty_hooks(&reader);
assert!(
matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "tty_drivers"),
"expected MissingKernelSymbol {{name: \"tty_drivers\"}}, got {result:?}"
);
}
#[test]
fn missing_stext_symbol_returns_missing_kernel_symbol() {
let isf = IsfBuilder::new()
.add_struct("tty_driver", 64)
.add_field("tty_driver", "name", 0, "pointer")
.add_field("tty_driver", "tty_drivers", 8, "list_head")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_symbol("tty_drivers", 0xFFFF_8000_0010_0000)
.add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = check_tty_hooks(&reader);
assert!(
matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "_stext"),
"expected MissingKernelSymbol {{name: \"_stext\"}}, got {result:?}"
);
}
#[test]
fn missing_etext_symbol_returns_missing_kernel_symbol() {
let isf = IsfBuilder::new()
.add_struct("tty_driver", 64)
.add_field("tty_driver", "name", 0, "pointer")
.add_field("tty_driver", "tty_drivers", 8, "list_head")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_symbol("tty_drivers", 0xFFFF_8000_0010_0000)
.add_symbol("_stext", 0xFFFF_8000_0000_0000)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = check_tty_hooks(&reader);
assert!(
matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "_etext"),
"expected MissingKernelSymbol {{name: \"_etext\"}}, got {result:?}"
);
}
#[test]
fn missing_tty_drivers_field_offset_returns_missing_field() {
let isf = IsfBuilder::new()
.add_struct("tty_driver", 64)
.add_field("tty_driver", "name", 0, "pointer")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_symbol("tty_drivers", 0xFFFF_8000_0010_0000)
.add_symbol("_stext", 0xFFFF_8000_0000_0000)
.add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = check_tty_hooks(&reader);
assert!(
matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "tty_driver" && field_name == "tty_drivers"),
"expected MissingField tty_driver.tty_drivers, got {result:?}"
);
}
#[test]
fn check_tty_hooks_driver_with_clean_ops() {
let vaddr: u64 = 0xFFFF_8000_0020_0000;
let paddr: u64 = 0x0020_0000;
let stext: u64 = 0xFFFF_8000_0000_0000;
let etext: u64 = 0xFFFF_8000_00FF_FFFF;
let clean_handler: u64 = 0xFFFF_8000_0001_0000;
let drivers_head: u64 = vaddr + 0x800;
let driver_list_entry: u64 = vaddr + 0x018;
let ops_ptr: u64 = vaddr + 0xC00;
let name_ptr: u64 = vaddr + 0xE00;
let mut data = vec![0u8; 4096];
data[0x000..0x008].copy_from_slice(&name_ptr.to_le_bytes());
data[0x010..0x018].copy_from_slice(&ops_ptr.to_le_bytes());
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());
data[0x808..0x810].copy_from_slice(&driver_list_entry.to_le_bytes());
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");
let reader = make_test_reader(&data, vaddr, paddr, stext, etext);
let results = check_tty_hooks(&reader).expect("should not error");
assert!(
!results.is_empty(),
"expected at least one ops entry from the driver"
);
for r in &results {
assert!(
!r.hooked,
"clean handler inside text region must not be flagged"
);
}
}
#[test]
fn check_tty_hooks_driver_with_hooked_ops() {
let vaddr: u64 = 0xFFFF_8000_0021_0000;
let paddr: u64 = 0x0021_0000;
let stext: u64 = 0xFFFF_8000_0000_0000;
let etext: u64 = 0xFFFF_8000_00FF_FFFF;
let hooked_handler: u64 = 0xFFFF_CAFE_DEAD_0001;
let drivers_head: u64 = vaddr + 0x800;
let driver_list_entry: u64 = vaddr + 0x018;
let ops_ptr: u64 = vaddr + 0xC00;
let name_ptr: u64 = vaddr + 0xE00;
let mut data = vec![0u8; 4096];
data[0x000..0x008].copy_from_slice(&name_ptr.to_le_bytes());
data[0x010..0x018].copy_from_slice(&ops_ptr.to_le_bytes());
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());
data[0x808..0x810].copy_from_slice(&driver_list_entry.to_le_bytes());
data[0xC00..0xC08].copy_from_slice(&hooked_handler.to_le_bytes());
data[0xC08..0xC10].copy_from_slice(&hooked_handler.to_le_bytes());
data[0xC10..0xC18].copy_from_slice(&hooked_handler.to_le_bytes());
data[0xC30..0xC38].copy_from_slice(&hooked_handler.to_le_bytes());
data[0xE00..0xE09].copy_from_slice(b"rootkit0\0");
let reader = make_test_reader(&data, vaddr, paddr, stext, etext);
let results = check_tty_hooks(&reader).expect("should not error");
assert!(!results.is_empty(), "hooked ops must produce entries");
for r in &results {
assert!(
r.hooked,
"handler outside text region must be flagged as hooked"
);
}
}
#[test]
fn check_tty_hooks_driver_ops_null_skipped() {
let vaddr: u64 = 0xFFFF_8000_0022_0000;
let paddr: u64 = 0x0022_0000;
let stext: u64 = 0xFFFF_8000_0000_0000;
let etext: u64 = 0xFFFF_8000_00FF_FFFF;
let drivers_head: u64 = vaddr + 0x800;
let driver_list_entry: u64 = vaddr + 0x018;
let name_ptr: u64 = vaddr + 0xE00;
let mut data = vec![0u8; 4096];
data[0x000..0x008].copy_from_slice(&name_ptr.to_le_bytes());
data[0x010..0x018].copy_from_slice(&0u64.to_le_bytes());
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());
data[0x808..0x810].copy_from_slice(&driver_list_entry.to_le_bytes());
data[0xE00..0xE09].copy_from_slice(b"nullops\0\0");
let reader = make_test_reader(&data, vaddr, paddr, stext, etext);
let results = check_tty_hooks(&reader).expect("should not error");
assert!(
results.is_empty(),
"null ops_ptr → driver skipped → no results"
);
}
}