dyncvoke-core 0.1.0

PEB walking, dynamic invoke, and Tartarus Gate indirect syscalls for Dyncvoke
//! Tartarus Gate SSN resolution and syscall-instruction location.
//!
//! 1. Hell's Gate: clean `MOV R10, RCX; MOV EAX, <ssn>` prologue.
//! 2. Halo's Gate: `E9` at stub entry; recover SSN from ±32-byte neighbors.
//! 3. Tartarus Gate: `E9` at offset 3 (after `MOV R10, RCX`); same neighbor walk.

use alloc::string::String;
use core::ptr::read;

use data::lc;

#[doc(hidden)]
pub mod asm;

/// Maximum neighbor search range for hooked syscalls.
const RANGE: usize = 255;

/// Stub size for downward neighbor search.
const DOWN: usize = 32;

/// Stub size for upward neighbor search.
const UP: isize = -32;

/// Extract the SSN from a syscall stub. Returns `None` if the stub is not
/// recognizable as one of the three Tartarus Gate forms.
pub fn extract_ssn(address: *const u8) -> Option<u16> {
    unsafe {
        // Hell's Gate: 4C 8B D1 B8 <lo> <hi> 00 00 ...
        if read(address) == 0x4C
            && read(address.add(1)) == 0x8B
            && read(address.add(2)) == 0xD1
            && read(address.add(3)) == 0xB8
            && read(address.add(6)) == 0x00
            && read(address.add(7)) == 0x00
        {
            let high = read(address.add(5)) as u16;
            let low = read(address.add(4)) as u16;
            return Some((high << 8) | low);
        }

        // Halo's Gate: hook at entry (E9 = JMP rel32).
        if read(address) == 0xE9 {
            return search_neighbors(address);
        }

        // Tartarus Gate: hook after MOV R10, RCX (E9 at offset 3).
        if read(address.add(3)) == 0xE9 {
            return search_neighbors(address);
        }
    }
    None
}

/// Walk ±32-byte neighbors looking for a clean Hell's Gate stub, then
/// back-derive the requested SSN by the offset distance.
fn search_neighbors(address: *const u8) -> Option<u16> {
    unsafe {
        for idx in 1..RANGE {
            // Search DOWN (toward higher addresses). The neighbor's SSN is
            // higher than ours by `idx` because syscall IDs increase with
            // address on ntdll's stub layout, so we subtract.
            if read(address.add(idx * DOWN)) == 0x4C
                && read(address.add(1 + idx * DOWN)) == 0x8B
                && read(address.add(2 + idx * DOWN)) == 0xD1
                && read(address.add(3 + idx * DOWN)) == 0xB8
                && read(address.add(6 + idx * DOWN)) == 0x00
                && read(address.add(7 + idx * DOWN)) == 0x00
            {
                let high = read(address.add(5 + idx * DOWN)) as u16;
                let low = read(address.add(4 + idx * DOWN)) as u16;
                let neighbor_ssn = (high << 8) | low;
                return Some(neighbor_ssn.wrapping_sub(idx as u16));
            }

            // Search UP (toward lower addresses). Neighbor's SSN is lower
            // than ours, so add.
            if read(address.offset(idx as isize * UP)) == 0x4C
                && read(address.offset(1 + idx as isize * UP)) == 0x8B
                && read(address.offset(2 + idx as isize * UP)) == 0xD1
                && read(address.offset(3 + idx as isize * UP)) == 0xB8
                && read(address.offset(6 + idx as isize * UP)) == 0x00
                && read(address.offset(7 + idx as isize * UP)) == 0x00
            {
                let high = read(address.offset(5 + idx as isize * UP)) as u16;
                let low = read(address.offset(4 + idx as isize * UP)) as u16;
                let neighbor_ssn = (high << 8) | low;
                return Some(neighbor_ssn.wrapping_add(idx as u16));
            }
        }
    }
    None
}

/// Locate the `syscall; ret` (0F 05 C3) sequence within a stub. The trailing
/// C3 distinguishes the direct-return syscall path from the
/// SSDT-side-check branch, which is what we want for stack-discipline
/// reasons (after the syscall, ntdll's `ret` pops back to our caller).
pub fn get_syscall_address(address: *mut core::ffi::c_void) -> Option<usize> {
    unsafe {
        let p = address.cast::<u8>();
        (1..RANGE).find_map(|i| {
            if read(p.add(i)) == 0x0F
                && read(p.add(i + 1)) == 0x05
                && read(p.add(i + 2)) == 0xC3
            {
                Some(p.add(i) as usize)
            } else {
                None
            }
        })
    }
}

/// Errors surfaced by [`resolve_syscall`].
#[derive(Debug)]
pub enum SyscallError {
    NtdllMissing,
    FunctionNotFound(String),
    SsnNotFound(String),
    SyscallAddrNotFound(String),
}

impl core::fmt::Display for SyscallError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            SyscallError::NtdllMissing => write!(f, "ntdll.dll not loaded"),
            SyscallError::FunctionNotFound(n) => write!(f, "export not found: {}", n),
            SyscallError::SsnNotFound(n) => write!(f, "SSN not extractable for {}", n),
            SyscallError::SyscallAddrNotFound(n) => write!(f, "syscall instr not found for {}", n),
        }
    }
}

impl core::error::Error for SyscallError {}

/// Resolve `(SSN, syscall_instr_addr)` for a Zw/Nt export of ntdll.
pub fn resolve_syscall(name: &str) -> Result<(u16, usize), SyscallError> {
    let ntdll = crate::get_module_base_address(&lc!("ntdll.dll"));
    if ntdll == 0 {
        return Err(SyscallError::NtdllMissing);
    }
    let stub = crate::get_function_address(ntdll, name);
    if stub == 0 {
        return Err(SyscallError::FunctionNotFound(name.into()));
    }
    let ssn = extract_ssn(stub as *const u8)
        .ok_or_else(|| SyscallError::SsnNotFound(name.into()))?;
    let addr = get_syscall_address(stub as *mut _)
        .ok_or_else(|| SyscallError::SyscallAddrNotFound(name.into()))?;
    Ok((ssn, addr))
}