dyncvoke-core 0.1.1

PEB walking, dynamic invoke, and Tartarus Gate indirect syscalls for Dyncvoke
Documentation
// End-to-end smoke test for the entire post-rewrite stack:
//
//   syscall! macro
//     -> Tartarus Gate resolve_syscall (Phase 2)
//        -> extract_ssn(stub) and get_syscall_address(stub)
//     -> Hell's Hall variadic do_syscall (Phase 3)
//        -> jump into ntdll's `syscall` instruction
//
// Three real syscalls in sequence:
//
//   1. NtAllocateVirtualMemory  -> get a writable page
//   2. NtProtectVirtualMemory   -> flip it to PAGE_EXECUTE_READ
//   3. NtFreeVirtualMemory      -> release it
//
// If any of these returns a non-zero NTSTATUS, something in the path is
// wrong: a bad SSN, a misaligned stack at the syscall, a register mix-up,
// or the wrong syscall instruction address.

use dyncvoke_core::syscall;
use std::ffi::c_void;
use std::ptr::null_mut;

#[test]
fn smoke_alloc_protect_free_round_trip() {
    let mut addr: *mut c_void = null_mut();
    let mut size: usize = 0x1000;

    // 1. allocate writable
    let alloc_status = syscall!(
        "NtAllocateVirtualMemory",
        -1isize as *mut c_void,
        &mut addr as *mut *mut c_void,
        0usize,
        &mut size as *mut usize,
        0x3000u32, // MEM_COMMIT | MEM_RESERVE
        0x04u32    // PAGE_READWRITE
    )
    .expect("resolve NtAllocateVirtualMemory") as i32;
    assert_eq!(
        alloc_status, 0,
        "NtAllocateVirtualMemory NTSTATUS=0x{:08X}",
        alloc_status as u32
    );
    assert!(!addr.is_null());
    assert!(size >= 0x1000, "kernel rounded size down to {}", size);

    // touch the page so we know the protection actually took effect
    unsafe {
        *(addr as *mut u8) = 0x42;
        assert_eq!(*(addr as *mut u8), 0x42);
    }

    // 2. reprotect to executable-read. The protect syscall takes a *mut PVOID
    //    for the base and a *mut SIZE_T for the size — both inout.
    let mut old_prot: u32 = 0;
    let prot_status = syscall!(
        "NtProtectVirtualMemory",
        -1isize as *mut c_void,
        &mut addr as *mut *mut c_void,
        &mut size as *mut usize,
        0x20u32,                          // PAGE_EXECUTE_READ
        &mut old_prot as *mut u32
    )
    .expect("resolve NtProtectVirtualMemory") as i32;
    assert_eq!(
        prot_status, 0,
        "NtProtectVirtualMemory NTSTATUS=0x{:08X}",
        prot_status as u32
    );
    assert_eq!(
        old_prot, 0x04,
        "old protection should have been PAGE_READWRITE, got 0x{:X}",
        old_prot
    );

    // 3. free
    let mut zero_size: usize = 0;
    let free_status = syscall!(
        "NtFreeVirtualMemory",
        -1isize as *mut c_void,
        &mut addr as *mut *mut c_void,
        &mut zero_size as *mut usize,
        0x8000u32   // MEM_RELEASE
    )
    .expect("resolve NtFreeVirtualMemory") as i32;
    assert_eq!(
        free_status, 0,
        "NtFreeVirtualMemory NTSTATUS=0x{:08X}",
        free_status as u32
    );
}

#[test]
fn smoke_do_syscall_macro_with_cached_resolution() {
    // Same dance, but resolve once and use the do_syscall! macro for the
    // hot path — confirms the bypass macro lines up bit-for-bit with the
    // top-level syscall! macro.
    use dyncvoke_core::{do_syscall, resolve_syscall};

    let (alloc_ssn, alloc_addr) = resolve_syscall("NtAllocateVirtualMemory").unwrap();
    let (free_ssn,  free_addr)  = resolve_syscall("NtFreeVirtualMemory").unwrap();

    let mut addr: *mut c_void = null_mut();
    let mut size: usize = 0x1000;
    let status = do_syscall!(
        alloc_ssn, alloc_addr,
        -1isize as *mut c_void,
        &mut addr as *mut *mut c_void,
        0usize,
        &mut size as *mut usize,
        0x3000u32,
        0x04u32
    ) as i32;
    assert_eq!(status, 0);
    assert!(!addr.is_null());

    let mut z: usize = 0;
    let status = do_syscall!(
        free_ssn, free_addr,
        -1isize as *mut c_void,
        &mut addr as *mut *mut c_void,
        &mut z as *mut usize,
        0x8000u32
    ) as i32;
    assert_eq!(status, 0);
}