Skip to main content

dyncvoke_core/sys/
mod.rs

1//! Tartarus Gate SSN resolution and syscall-instruction location.
2//!
3//! 1. Hell's Gate: clean `MOV R10, RCX; MOV EAX, <ssn>` prologue.
4//! 2. Halo's Gate: `E9` at stub entry; recover SSN from ±32-byte neighbors.
5//! 3. Tartarus Gate: `E9` at offset 3 (after `MOV R10, RCX`); same neighbor walk.
6
7use alloc::string::String;
8use core::ptr::read;
9
10use data::lc;
11
12#[doc(hidden)]
13pub mod asm;
14
15/// Maximum neighbor search range for hooked syscalls.
16const RANGE: usize = 255;
17
18/// Stub size for downward neighbor search.
19const DOWN: usize = 32;
20
21/// Stub size for upward neighbor search.
22const UP: isize = -32;
23
24/// Extract the SSN from a syscall stub. Returns `None` if the stub is not
25/// recognizable as one of the three Tartarus Gate forms.
26pub fn extract_ssn(address: *const u8) -> Option<u16> {
27    unsafe {
28        // Hell's Gate: 4C 8B D1 B8 <lo> <hi> 00 00 ...
29        if read(address) == 0x4C
30            && read(address.add(1)) == 0x8B
31            && read(address.add(2)) == 0xD1
32            && read(address.add(3)) == 0xB8
33            && read(address.add(6)) == 0x00
34            && read(address.add(7)) == 0x00
35        {
36            let high = read(address.add(5)) as u16;
37            let low = read(address.add(4)) as u16;
38            return Some((high << 8) | low);
39        }
40
41        // Halo's Gate: hook at entry (E9 = JMP rel32).
42        if read(address) == 0xE9 {
43            return search_neighbors(address);
44        }
45
46        // Tartarus Gate: hook after MOV R10, RCX (E9 at offset 3).
47        if read(address.add(3)) == 0xE9 {
48            return search_neighbors(address);
49        }
50    }
51    None
52}
53
54/// Walk ±32-byte neighbors looking for a clean Hell's Gate stub, then
55/// back-derive the requested SSN by the offset distance.
56fn search_neighbors(address: *const u8) -> Option<u16> {
57    unsafe {
58        for idx in 1..RANGE {
59            // Search DOWN (toward higher addresses). The neighbor's SSN is
60            // higher than ours by `idx` because syscall IDs increase with
61            // address on ntdll's stub layout, so we subtract.
62            if read(address.add(idx * DOWN)) == 0x4C
63                && read(address.add(1 + idx * DOWN)) == 0x8B
64                && read(address.add(2 + idx * DOWN)) == 0xD1
65                && read(address.add(3 + idx * DOWN)) == 0xB8
66                && read(address.add(6 + idx * DOWN)) == 0x00
67                && read(address.add(7 + idx * DOWN)) == 0x00
68            {
69                let high = read(address.add(5 + idx * DOWN)) as u16;
70                let low = read(address.add(4 + idx * DOWN)) as u16;
71                let neighbor_ssn = (high << 8) | low;
72                return Some(neighbor_ssn.wrapping_sub(idx as u16));
73            }
74
75            // Search UP (toward lower addresses). Neighbor's SSN is lower
76            // than ours, so add.
77            if read(address.offset(idx as isize * UP)) == 0x4C
78                && read(address.offset(1 + idx as isize * UP)) == 0x8B
79                && read(address.offset(2 + idx as isize * UP)) == 0xD1
80                && read(address.offset(3 + idx as isize * UP)) == 0xB8
81                && read(address.offset(6 + idx as isize * UP)) == 0x00
82                && read(address.offset(7 + idx as isize * UP)) == 0x00
83            {
84                let high = read(address.offset(5 + idx as isize * UP)) as u16;
85                let low = read(address.offset(4 + idx as isize * UP)) as u16;
86                let neighbor_ssn = (high << 8) | low;
87                return Some(neighbor_ssn.wrapping_add(idx as u16));
88            }
89        }
90    }
91    None
92}
93
94/// Locate the `syscall; ret` (0F 05 C3) sequence within a stub. The trailing
95/// C3 distinguishes the direct-return syscall path from the
96/// SSDT-side-check branch, which is what we want for stack-discipline
97/// reasons (after the syscall, ntdll's `ret` pops back to our caller).
98pub fn get_syscall_address(address: *mut core::ffi::c_void) -> Option<usize> {
99    unsafe {
100        let p = address.cast::<u8>();
101        (1..RANGE).find_map(|i| {
102            if read(p.add(i)) == 0x0F
103                && read(p.add(i + 1)) == 0x05
104                && read(p.add(i + 2)) == 0xC3
105            {
106                Some(p.add(i) as usize)
107            } else {
108                None
109            }
110        })
111    }
112}
113
114/// Errors surfaced by [`resolve_syscall`].
115#[derive(Debug)]
116pub enum SyscallError {
117    NtdllMissing,
118    FunctionNotFound(String),
119    SsnNotFound(String),
120    SyscallAddrNotFound(String),
121}
122
123impl core::fmt::Display for SyscallError {
124    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
125        match self {
126            SyscallError::NtdllMissing => write!(f, "ntdll.dll not loaded"),
127            SyscallError::FunctionNotFound(n) => write!(f, "export not found: {}", n),
128            SyscallError::SsnNotFound(n) => write!(f, "SSN not extractable for {}", n),
129            SyscallError::SyscallAddrNotFound(n) => write!(f, "syscall instr not found for {}", n),
130        }
131    }
132}
133
134impl core::error::Error for SyscallError {}
135
136/// Resolve `(SSN, syscall_instr_addr)` for a Zw/Nt export of ntdll.
137pub fn resolve_syscall(name: &str) -> Result<(u16, usize), SyscallError> {
138    let ntdll = crate::get_module_base_address(&lc!("ntdll.dll"));
139    if ntdll == 0 {
140        return Err(SyscallError::NtdllMissing);
141    }
142    let stub = crate::get_function_address(ntdll, name);
143    if stub == 0 {
144        return Err(SyscallError::FunctionNotFound(name.into()));
145    }
146    let ssn = extract_ssn(stub as *const u8)
147        .ok_or_else(|| SyscallError::SsnNotFound(name.into()))?;
148    let addr = get_syscall_address(stub as *mut _)
149        .ok_or_else(|| SyscallError::SyscallAddrNotFound(name.into()))?;
150    Ok((ssn, addr))
151}