dyncvoke-core 0.1.0

PEB walking, dynamic invoke, and Tartarus Gate indirect syscalls for Dyncvoke
//! In-process PEB / LDR walk. Avoids EnumProcessModules / GetModuleBaseNameW.

use core::ffi::c_void;

/// LIST_ENTRY — Windows doubly-linked list head.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ListEntry {
    pub flink: *mut ListEntry,
    pub blink: *mut ListEntry,
}

/// UNICODE_STRING — counted UTF-16 string. Matches ntdef layout.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct UnicodeString {
    pub length: u16,
    pub maximum_length: u16,
    pub buffer: *mut u16,
}

/// LDR_DATA_TABLE_ENTRY prefix used by the load-order walk.
///
/// x64 offsets (Geoff Chappell, LDR_DATA_TABLE_ENTRY):
///   0x00 InLoadOrderLinks, 0x10 InMemoryOrderLinks,
///   0x20 InInitializationOrderLinks, 0x30 DllBase,
///   0x38 EntryPoint, 0x40 SizeOfImage,
///   0x48 FullDllName, 0x58 BaseDllName.
#[repr(C)]
pub struct LdrDataTableEntry {
    pub in_load_order_links: ListEntry,
    pub in_memory_order_links: ListEntry,
    pub in_initialization_order_links: ListEntry,
    pub dll_base: *mut c_void,
    pub entry_point: *mut c_void,
    pub size_of_image: u32,
    pub full_dll_name: UnicodeString,
    pub base_dll_name: UnicodeString,
}

/// PEB_LDR_DATA prefix.
///
/// phnt / NtDoc: ULONG Length, BOOLEAN Initialized, HANDLE SsHandle,
/// LIST_ENTRY InLoadOrderModuleList. On x64 that list head is at 0x10.
#[repr(C)]
pub struct PebLdrData {
    pub _length_and_init: [u8; 8],
    pub _ss_handle: *mut c_void,
    pub in_load_order_module_list: ListEntry,
}

/// PEB prefix through Ldr.
///
/// winternl.h: `BYTE Reserved1[2]`, `BYTE BeingDebugged`, `BYTE Reserved2[1]`,
/// `PVOID Reserved3[2]`, `PPEB_LDR_DATA Ldr`.
/// `repr(C)` supplies the 4-byte x64 alignment pad after Reserved2, so Ldr
/// lands at 0x18 on x64 and 0x0C on x86.
/// Source: <https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb>
#[repr(C)]
pub struct Peb {
    pub _reserved1: [u8; 2],
    pub being_debugged: u8,
    pub _reserved2: [u8; 1],
    pub _reserved3: [*mut c_void; 2],
    pub ldr: *mut PebLdrData,
}

/// Current process PEB. `gs:[0x60]` on x64, `fs:[0x30]` on x86 (`TEB.ProcessEnvironmentBlock`).
#[inline(always)]
pub fn current_peb() -> *mut Peb {
    let peb: *mut Peb;
    #[cfg(target_arch = "x86_64")]
    unsafe {
        core::arch::asm!(
            "mov {peb}, gs:[0x60]",
            peb = out(reg) peb,
            options(nostack, preserves_flags, readonly)
        );
    }
    #[cfg(target_arch = "x86")]
    unsafe {
        core::arch::asm!(
            "mov {peb}, fs:[0x30]",
            peb = out(reg) peb,
            options(nostack, preserves_flags, readonly)
        );
    }
    #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
    compile_error!("dyncvoke PEB walker only supports x86 / x86_64");
    peb
}

/// djb2 over ascii-lowercased input. Const so call sites can embed a u32 hash.
pub const fn hash_name(bytes: &[u8]) -> u32 {
    let mut hash: u32 = 5381;
    let mut i = 0;
    while i < bytes.len() {
        let mut c = bytes[i];
        if c >= b'A' && c <= b'Z' {
            c += 32;
        }
        hash = hash.wrapping_mul(33).wrapping_add(c as u32);
        i += 1;
    }
    hash
}

unsafe fn unicode_eq_ascii_ci(us: &UnicodeString, target: &[u8]) -> bool {
    if us.buffer.is_null() {
        return target.is_empty() && us.length == 0;
    }
    let len = (us.length / 2) as usize;
    if len != target.len() {
        return false;
    }
    for i in 0..len {
        let wide = *us.buffer.add(i);
        if wide >= 0x80 {
            return false;
        }
        let mut byte = wide as u8;
        if byte >= b'A' && byte <= b'Z' {
            byte += 32;
        }
        let mut tgt = target[i];
        if tgt >= b'A' && tgt <= b'Z' {
            tgt += 32;
        }
        if byte != tgt {
            return false;
        }
    }
    true
}

unsafe fn hash_unicode_string(us: &UnicodeString) -> u32 {
    if us.buffer.is_null() {
        return 0;
    }
    let len = (us.length / 2) as usize;
    let mut hash: u32 = 5381;
    for i in 0..len {
        let wide = *us.buffer.add(i);
        if wide >= 0x80 {
            return 0;
        }
        let mut byte = wide as u8;
        if byte >= b'A' && byte <= b'Z' {
            byte += 32;
        }
        hash = hash.wrapping_mul(33).wrapping_add(byte as u32);
    }
    hash
}

unsafe fn walk_modules<F>(mut visit: F) -> usize
where
    F: FnMut(&LdrDataTableEntry) -> bool,
{
    let peb = current_peb();
    if peb.is_null() {
        return 0;
    }
    let ldr = (*peb).ldr;
    if ldr.is_null() {
        return 0;
    }
    let head = &(*ldr).in_load_order_module_list as *const ListEntry as *mut ListEntry;
    let mut cursor = (*head).flink;
    while !cursor.is_null() && cursor != head {
        let entry = cursor as *mut LdrDataTableEntry;
        if visit(&*entry) {
            return (*entry).dll_base as usize;
        }
        cursor = (*cursor).flink;
    }
    0
}

/// Look up a loaded module by base name (e.g. `"ntdll.dll"`). ASCII, case-insensitive.
pub fn get_module_by_name(name: &str) -> usize {
    unsafe { walk_modules(|entry| unicode_eq_ascii_ci(&entry.base_dll_name, name.as_bytes())) }
}

/// Look up a loaded module by djb2 of its lowercased ascii base name.
pub fn get_module_by_hash(hash: u32) -> usize {
    unsafe { walk_modules(|entry| hash_unicode_string(&entry.base_dll_name) == hash) }
}