dyncvoke-spoof 0.1.0

Call stack spoofing primitives for Dyncvoke (synthetic and desync)
use alloc::vec::Vec;
use core::{ffi::c_void, slice};

use crate::types::ImageRuntimeFunction;
use crate::unwind::ignoring_set_fpreg;

/// Scan a function body for `call qword ptr [rip+0]` (48 FF 15 00 00 00 00).
/// Returns the offset *past* the gadget — that's the address the unwinder
/// will treat as the post-call site when the spoofed frame is on the stack.
pub fn find_valid_instruction_offset(
    module: *mut c_void,
    runtime: &ImageRuntimeFunction,
) -> Option<u32> {
    let start = module as u64 + runtime.BeginAddress as u64;
    let end = module as u64 + runtime.EndAddress as u64;
    let size = end - start;
    let pattern = &[0x48u8, 0xFFu8, 0x15u8];

    unsafe {
        let bytes = slice::from_raw_parts(start as *const u8, size as usize);
        memchr::memmem::find(bytes, pattern).map(|pos| (pos + 7) as u32)
    }
}

/// Find the first occurrence of `pattern` in any function body of `module`
/// (restricted to ranges from the RUNTIME_FUNCTION table — anything else
/// might be data or alignment padding). Returns the address and the size of
/// the function that contains it.
///
/// `required_frame_size` constrains matches to functions whose unwind frame
/// size equals the given value. The ROP chain in synthetic/desync mode needs
/// the AddRspXGadget to sit in a function whose own frame size is exactly
/// 0x58 so that the post-syscall `add rsp,0x58; ret` lands on the
/// JmpRbxGadget push slot. Without this constraint, shuffle can pick a
/// gadget whose containing function has a different frame size (jump tables
/// or alignment padding spelling the same bytes) and the chain silently
/// breaks at run time.
pub fn find_gadget(
    module: *mut c_void,
    pattern: &[u8],
    runtime_table: &[ImageRuntimeFunction],
    required_frame_size: Option<u32>,
) -> Option<(*mut u8, u32)> {
    unsafe {
        let mut gadgets: Vec<(*mut u8, u32)> = runtime_table
            .iter()
            .filter_map(|runtime| {
                let start = module as u64 + runtime.BeginAddress as u64;
                let end = module as u64 + runtime.EndAddress as u64;
                let size = end.saturating_sub(start);

                let bytes = slice::from_raw_parts(start as *const u8, size as usize);
                let pos = memchr::memmem::find(bytes, pattern)?;
                let addr = (start as *mut u8).wrapping_add(pos);
                let frame_size = ignoring_set_fpreg(module, runtime)?;
                if frame_size == 0 {
                    return None;
                }
                if let Some(req) = required_frame_size {
                    if frame_size != req {
                        return None;
                    }
                }
                Some((addr, frame_size))
            })
            .collect();

        if gadgets.is_empty() {
            return None;
        }

        shuffle(&mut gadgets);
        gadgets.first().copied()
    }
}

/// Fisher-Yates shuffle using rdtsc as a cheap entropy source. The goal is
/// not cryptographic randomness, it is reducing the chance that an EDR
/// signature pinned to a specific gadget address survives across runs.
pub fn shuffle<T>(list: &mut [T]) {
    let mut seed = unsafe { core::arch::x86_64::_rdtsc() };
    for i in (1..list.len()).rev() {
        seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
        let j = seed as usize % (i + 1);
        list.swap(i, j);
    }
}

/// Walk the current thread's stack looking for a return address that lands
/// inside the BaseThreadInitThunk function body. Used by desync mode to
/// splice our spoofed frames on top of a legitimately-resident kernel32
/// return record.
///
/// Returns the stack address holding the return, not the return value
/// itself — the asm trampoline rewrites the slot in place.
#[cfg(feature = "desync")]
pub unsafe fn find_base_thread_return_address(
    kernel32: *mut c_void,
    base_thread_addr: *mut c_void,
    base_thread_size: usize,
) -> Option<usize> {
    let _ = kernel32;
    let base_addr = base_thread_addr as usize;

    // Read TEB.StackBase (gs:[0x08]) and TEB.StackLimit (gs:[0x10]) directly
    // — same OPSEC reason we use the PEB walker in dyncvoke_core: no
    // kernel32/psapi calls, nothing to hook.
    let stack_base: usize;
    let stack_limit: usize;
    core::arch::asm!(
        "mov {sb}, gs:[0x08]",
        "mov {sl}, gs:[0x10]",
        sb = out(reg) stack_base,
        sl = out(reg) stack_limit,
        options(nostack, preserves_flags, readonly),
    );

    let mut rsp = stack_base.saturating_sub(8);
    while rsp >= stack_limit {
        let val = *(rsp as *const usize);
        if val >= base_addr && val < base_addr + base_thread_size {
            return Some(rsp);
        }
        rsp = rsp.saturating_sub(8);
        if rsp == 0 {
            break;
        }
    }
    None
}