Skip to main content

memf_linux/
check_idt.rs

1//! Linux IDT (Interrupt Descriptor Table) hook detector.
2//!
3//! Rootkits can hook the IDT to intercept system calls and hardware
4//! interrupts (MITRE ATT&CK T1014). This module reads the IDT entries
5//! from memory and checks if handler addresses point outside the kernel
6//! text segment (`_stext`..`_etext`), which indicates potential hooking.
7//!
8//! On x86_64, the IDT has 256 entries, each a 16-byte `gate_descriptor`:
9//!   - offset_low:  u16 at +0
10//!   - segment:     u16 at +2
11//!   - ist:         u8  at +4
12//!   - type_attr:   u8  at +5
13//!   - offset_mid:  u16 at +6
14//!   - offset_high: u32 at +8
15//!   - reserved:    u32 at +12
16//!
17//! The handler address is reconstructed as:
18//!   `(offset_high << 32) | (offset_mid << 16) | offset_low`
19
20use memf_core::object_reader::ObjectReader;
21use memf_format::PhysicalMemoryProvider;
22
23use crate::Result;
24
25/// Maximum number of IDT entries on x86_64.
26const MAX_IDT_ENTRIES: usize = 256;
27
28/// Size of each IDT gate descriptor in bytes.
29const GATE_DESC_SIZE: usize = 16;
30
31/// Information about a single IDT entry with hook classification.
32#[derive(Debug, Clone, serde::Serialize)]
33pub struct IdtEntryInfo {
34    /// Interrupt vector number (0-255).
35    pub vector: u8,
36    /// Reconstructed handler virtual address.
37    pub handler_addr: u64,
38    /// Code segment selector from the gate descriptor.
39    pub segment: u16,
40    /// Human-readable gate type name.
41    pub gate_type: String,
42    /// Whether the handler points outside the kernel text section.
43    pub is_hooked: bool,
44}
45
46/// Classify whether an IDT handler address is hooked.
47///
48/// - Address of `0` is not considered hooked (null/unused entry).
49/// - Address within `[kernel_start, kernel_end]` is benign (kernel text).
50/// - Address outside that range is suspicious (hooked).
51pub use crate::heuristics::classify_idt_entry;
52
53/// Map a gate type nibble to a human-readable name.
54///
55/// The lower 4 bits of the `type_attr` byte encode the gate type:
56/// - `0xE` → Interrupt Gate (interrupts disabled on entry)
57/// - `0xF` → Trap Gate (interrupts remain enabled)
58/// - Other values are uncommon/reserved.
59pub fn gate_type_name(type_attr: u8) -> String {
60    let gate_nibble = type_attr & 0x0F;
61    match gate_nibble {
62        0xE => "Interrupt Gate".to_string(),
63        0xF => "Trap Gate".to_string(),
64        other => format!("Unknown(0x{other:02X})"),
65    }
66}
67
68/// Walk the IDT and classify each entry against the kernel text range.
69///
70/// Looks up `_stext`, `_etext`, and `idt_table` symbols. If any are
71/// missing, returns `Ok(Vec::new())` for graceful degradation. For each
72/// of the 256 IDT entries, reconstructs the handler address and checks
73/// whether it falls within the kernel text section.
74pub fn walk_check_idt<P: PhysicalMemoryProvider>(
75    reader: &ObjectReader<P>,
76) -> Result<Vec<IdtEntryInfo>> {
77    let symbols = reader.symbols();
78
79    // Resolve kernel text boundaries; if missing, we cannot classify.
80    let Some(kernel_start) = symbols.symbol_address("_stext") else {
81        return Ok(Vec::new());
82    };
83    let Some(kernel_end) = symbols.symbol_address("_etext") else {
84        return Ok(Vec::new());
85    };
86
87    // Resolve the IDT base address.
88    let Some(idt_base) = symbols.symbol_address("idt_table") else {
89        return Ok(Vec::new());
90    };
91
92    let mut results = Vec::new();
93
94    for vector in 0..MAX_IDT_ENTRIES {
95        let entry_addr = idt_base + (vector as u64) * (GATE_DESC_SIZE as u64);
96
97        // Read the full 16-byte gate descriptor.
98        let raw = match reader.read_bytes(entry_addr, GATE_DESC_SIZE) {
99            Ok(bytes) => bytes,
100            Err(_) => continue,
101        };
102
103        // Parse fields from the gate descriptor:
104        //   offset_low:  u16 at +0
105        //   segment:     u16 at +2
106        //   _ist:        u8  at +4
107        //   type_attr:   u8  at +5
108        //   offset_mid:  u16 at +6
109        //   offset_high: u32 at +8
110        //   _reserved:   u32 at +12
111        let offset_low = u16::from_le_bytes([raw[0], raw[1]]);
112        let segment = u16::from_le_bytes([raw[2], raw[3]]);
113        let type_attr = raw[5];
114        let offset_mid = u16::from_le_bytes([raw[6], raw[7]]);
115        let offset_high = u32::from_le_bytes([raw[8], raw[9], raw[10], raw[11]]);
116
117        let handler_addr =
118            u64::from(offset_high) << 32 | u64::from(offset_mid) << 16 | u64::from(offset_low);
119
120        // Skip unused entries (handler == 0).
121        if handler_addr == 0 {
122            continue;
123        }
124
125        let is_hooked = classify_idt_entry(handler_addr, kernel_start, kernel_end);
126        let gate_type = gate_type_name(type_attr);
127
128        results.push(IdtEntryInfo {
129            vector: vector as u8,
130            handler_addr,
131            segment,
132            gate_type,
133            is_hooked,
134        });
135    }
136
137    Ok(results)
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use memf_core::test_builders::PageTableBuilder;
144    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
145    use memf_symbols::isf::IsfResolver;
146    use memf_symbols::test_builders::IsfBuilder;
147
148    // -----------------------------------------------------------------------
149    // classify_idt_entry unit tests
150    // -----------------------------------------------------------------------
151
152    #[test]
153    fn classify_hooked_outside_kernel() {
154        let kernel_start = 0xFFFF_8000_0000_0000u64;
155        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
156
157        // Address in module space, well outside kernel text
158        assert!(classify_idt_entry(
159            0xFFFF_C900_DEAD_BEEF,
160            kernel_start,
161            kernel_end
162        ));
163        // Address just below kernel start
164        assert!(classify_idt_entry(
165            kernel_start - 1,
166            kernel_start,
167            kernel_end
168        ));
169        // Address just above kernel end
170        assert!(classify_idt_entry(kernel_end + 1, kernel_start, kernel_end));
171    }
172
173    #[test]
174    fn classify_benign_inside_kernel() {
175        let kernel_start = 0xFFFF_8000_0000_0000u64;
176        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
177
178        // Exactly at start
179        assert!(!classify_idt_entry(kernel_start, kernel_start, kernel_end));
180        // In the middle
181        assert!(!classify_idt_entry(
182            kernel_start + 0x1000,
183            kernel_start,
184            kernel_end
185        ));
186        // Exactly at end
187        assert!(!classify_idt_entry(kernel_end, kernel_start, kernel_end));
188    }
189
190    #[test]
191    fn classify_null_benign() {
192        let kernel_start = 0xFFFF_8000_0000_0000u64;
193        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
194
195        // Null pointer is never considered hooked
196        assert!(!classify_idt_entry(0, kernel_start, kernel_end));
197    }
198
199    #[test]
200    fn gate_type_interrupt() {
201        assert_eq!(gate_type_name(0x8E), "Interrupt Gate");
202        // Also works with just the nibble
203        assert_eq!(gate_type_name(0x0E), "Interrupt Gate");
204    }
205
206    #[test]
207    fn gate_type_trap() {
208        assert_eq!(gate_type_name(0x8F), "Trap Gate");
209        assert_eq!(gate_type_name(0x0F), "Trap Gate");
210    }
211
212    #[test]
213    fn gate_type_unknown() {
214        assert_eq!(gate_type_name(0x00), "Unknown(0x00)");
215        assert_eq!(gate_type_name(0x85), "Unknown(0x05)");
216        assert_eq!(gate_type_name(0xAC), "Unknown(0x0C)");
217    }
218
219    #[test]
220    fn walk_no_symbol_returns_empty() {
221        // Build a reader with no idt_table symbol — walker should gracefully
222        // return an empty vector instead of erroring.
223        let isf = IsfBuilder::new()
224            .add_struct("task_struct", 64)
225            .add_field("task_struct", "pid", 0, "int")
226            .add_symbol("_stext", 0xFFFF_8000_0000_0000)
227            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
228            .build_json();
229
230        let resolver = IsfResolver::from_value(&isf).unwrap();
231        let (cr3, mem) = PageTableBuilder::new().build();
232        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
233        let reader = ObjectReader::new(vas, Box::new(resolver));
234
235        let results = walk_check_idt(&reader).unwrap();
236        assert!(
237            results.is_empty(),
238            "expected empty results when idt_table symbol is missing"
239        );
240    }
241
242    #[test]
243    fn walk_missing_stext_returns_empty() {
244        // _stext absent → graceful empty
245        let isf = IsfBuilder::new()
246            // _stext intentionally omitted
247            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
248            .add_symbol("idt_table", 0xFFFF_8000_0020_0000)
249            .build_json();
250
251        let resolver = IsfResolver::from_value(&isf).unwrap();
252        let (cr3, mem) = PageTableBuilder::new().build();
253        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
254        let reader = ObjectReader::new(vas, Box::new(resolver));
255
256        let results = walk_check_idt(&reader).unwrap();
257        assert!(results.is_empty(), "missing _stext should yield empty vec");
258    }
259
260    #[test]
261    fn walk_missing_etext_returns_empty() {
262        // _etext absent → graceful empty
263        let isf = IsfBuilder::new()
264            .add_symbol("_stext", 0xFFFF_8000_0000_0000)
265            // _etext intentionally omitted
266            .add_symbol("idt_table", 0xFFFF_8000_0020_0000)
267            .build_json();
268
269        let resolver = IsfResolver::from_value(&isf).unwrap();
270        let (cr3, mem) = PageTableBuilder::new().build();
271        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
272        let reader = ObjectReader::new(vas, Box::new(resolver));
273
274        let results = walk_check_idt(&reader).unwrap();
275        assert!(results.is_empty(), "missing _etext should yield empty vec");
276    }
277
278    // -----------------------------------------------------------------------
279    // walk_check_idt: all symbols present, IDT all zeros → all entries skipped
280    // -----------------------------------------------------------------------
281
282    #[test]
283    fn walk_check_idt_symbol_present_all_zero_entries() {
284        // idt_table, _stext, _etext all present. IDT memory is all zeros:
285        // every gate's reconstructed handler_addr == 0 → skipped → empty result.
286        let idt_vaddr: u64 = 0xFFFF_8800_0070_0000;
287        let idt_paddr: u64 = 0x0080_0000;
288        let kernel_start: u64 = 0xFFFF_8000_0000_0000;
289        let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
290
291        // 256 * 16 = 4096 bytes, all zeros → every handler_addr == 0 → skipped
292        let page = [0u8; 4096];
293
294        let isf = IsfBuilder::new()
295            .add_symbol("_stext", kernel_start)
296            .add_symbol("_etext", kernel_end)
297            .add_symbol("idt_table", idt_vaddr)
298            .build_json();
299
300        let resolver = IsfResolver::from_value(&isf).unwrap();
301        let (cr3, mem) = PageTableBuilder::new()
302            .map_4k(
303                idt_vaddr,
304                idt_paddr,
305                memf_core::test_builders::flags::WRITABLE,
306            )
307            .write_phys(idt_paddr, &page)
308            .build();
309        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
310        let reader = ObjectReader::new(vas, Box::new(resolver));
311
312        let results = walk_check_idt(&reader).unwrap_or_default();
313        assert!(
314            results.is_empty(),
315            "all-zero IDT entries (handler==0) should all be skipped"
316        );
317    }
318
319    #[test]
320    fn gate_type_all_nibbles_covered() {
321        // Verify every nibble value yields a consistent string
322        for nibble in 0u8..=0x0F {
323            let name = gate_type_name(nibble);
324            match nibble {
325                0xE => assert_eq!(name, "Interrupt Gate"),
326                0xF => assert_eq!(name, "Trap Gate"),
327                _ => assert!(name.starts_with("Unknown("), "nibble {nibble:#x}: {name}"),
328            }
329        }
330    }
331
332    #[test]
333    fn classify_idt_entry_at_boundaries() {
334        let ks: u64 = 0xFFFF_8000_0000_0000;
335        let ke: u64 = 0xFFFF_8000_00FF_FFFF;
336        // Exactly at start — benign
337        assert!(!classify_idt_entry(ks, ks, ke));
338        // Exactly at end — benign
339        assert!(!classify_idt_entry(ke, ks, ke));
340        // One below start — suspicious
341        assert!(classify_idt_entry(ks - 1, ks, ke));
342        // One above end — suspicious
343        assert!(classify_idt_entry(ke + 1, ks, ke));
344    }
345
346    // Walk with one benign and one hooked IDT entry (exercises lines 99-143).
347    #[test]
348    fn walk_check_idt_benign_and_hooked_entries() {
349        use memf_core::test_builders::flags as ptf;
350
351        let kernel_start: u64 = 0xFFFF_8000_0000_0000;
352        let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
353
354        // IDT table: 256 entries × 16 bytes = 4096 bytes.
355        let idt_vaddr: u64 = 0xFFFF_8800_0072_0000;
356        let idt_paddr: u64 = 0x0072_0000;
357
358        // Build an IDT page: entry 0 = benign handler inside kernel text,
359        // entry 1 = hooked handler outside kernel text, rest = 0.
360        let mut idt_page = [0u8; 4096];
361
362        // Entry 0: handler = kernel_start + 0x1000 (benign), Interrupt Gate (0x8E).
363        let h0: u64 = kernel_start + 0x1000;
364        let h0_low = (h0 & 0xFFFF) as u16;
365        let h0_mid = ((h0 >> 16) & 0xFFFF) as u16;
366        let h0_high = ((h0 >> 32) & 0xFFFF_FFFF) as u32;
367        let off0 = 0usize;
368        idt_page[off0..off0 + 2].copy_from_slice(&h0_low.to_le_bytes());
369        idt_page[off0 + 2..off0 + 4].copy_from_slice(&0x0010u16.to_le_bytes()); // segment
370        idt_page[off0 + 5] = 0x8E; // type_attr: Interrupt Gate
371        idt_page[off0 + 6..off0 + 8].copy_from_slice(&h0_mid.to_le_bytes());
372        idt_page[off0 + 8..off0 + 12].copy_from_slice(&h0_high.to_le_bytes());
373
374        // Entry 1: handler = 0xFFFF_C000_DEAD_0000 (outside kernel text = hooked), Trap Gate (0x8F).
375        let h1: u64 = 0xFFFF_C000_DEAD_0000;
376        let h1_low = (h1 & 0xFFFF) as u16;
377        let h1_mid = ((h1 >> 16) & 0xFFFF) as u16;
378        let h1_high = ((h1 >> 32) & 0xFFFF_FFFF) as u32;
379        let off1 = 16usize;
380        idt_page[off1..off1 + 2].copy_from_slice(&h1_low.to_le_bytes());
381        idt_page[off1 + 2..off1 + 4].copy_from_slice(&0x0010u16.to_le_bytes());
382        idt_page[off1 + 5] = 0x8F; // type_attr: Trap Gate
383        idt_page[off1 + 6..off1 + 8].copy_from_slice(&h1_mid.to_le_bytes());
384        idt_page[off1 + 8..off1 + 12].copy_from_slice(&h1_high.to_le_bytes());
385
386        let isf = IsfBuilder::new()
387            .add_symbol("_stext", kernel_start)
388            .add_symbol("_etext", kernel_end)
389            .add_symbol("idt_table", idt_vaddr)
390            .build_json();
391
392        let resolver = IsfResolver::from_value(&isf).unwrap();
393        let (cr3, mem) = PageTableBuilder::new()
394            .map_4k(idt_vaddr, idt_paddr, ptf::WRITABLE)
395            .write_phys(idt_paddr, &idt_page)
396            .build();
397        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
398        let reader = ObjectReader::new(vas, Box::new(resolver));
399
400        let results = walk_check_idt(&reader).unwrap();
401        assert_eq!(results.len(), 2, "two non-zero entries expected");
402
403        // Entry 0: benign
404        assert_eq!(results[0].vector, 0);
405        assert_eq!(results[0].handler_addr, h0);
406        assert_eq!(results[0].gate_type, "Interrupt Gate");
407        assert!(
408            !results[0].is_hooked,
409            "entry 0 is inside kernel text → not hooked"
410        );
411
412        // Entry 1: hooked
413        assert_eq!(results[1].vector, 1);
414        assert_eq!(results[1].handler_addr, h1);
415        assert_eq!(results[1].gate_type, "Trap Gate");
416        assert!(
417            results[1].is_hooked,
418            "entry 1 is outside kernel text → hooked"
419        );
420    }
421
422    // IdtEntryInfo struct coverage: Debug, Clone, Serialize.
423    #[test]
424    fn idt_entry_info_debug_clone_serialize() {
425        use super::IdtEntryInfo;
426        let entry = IdtEntryInfo {
427            vector: 0x21,
428            handler_addr: 0xFFFF_8000_0001_0000,
429            segment: 0x10,
430            gate_type: "Interrupt Gate".to_string(),
431            is_hooked: false,
432        };
433        let cloned = entry.clone();
434        let dbg = format!("{cloned:?}");
435        assert!(dbg.contains("Interrupt Gate"));
436        let json = serde_json::to_string(&entry).unwrap();
437        assert!(json.contains("\"vector\":33"));
438        assert!(json.contains("\"is_hooked\":false"));
439    }
440}