dyncvoke-spoof 0.1.1

Call stack spoofing primitives for Dyncvoke (synthetic and desync)
Documentation
//! Minimal PE walker — just enough to locate the .pdata RUNTIME_FUNCTION
//! table for a loaded module. We deliberately don't reuse dyncvoke_core's PE
//! parser because that one was built for manual mapping (full optional-header
//! deserialization, etc.). For spoofing we only need the data-directory at
//! IMAGE_DIRECTORY_ENTRY_EXCEPTION.

use core::ffi::c_void;
use core::mem::size_of;
use core::slice;

use crate::types::{ImageDataDirectory, ImageRuntimeFunction, IMAGE_DIRECTORY_ENTRY_EXCEPTION};

/// Read the exception data directory's runtime function table for a loaded
/// module. Returns None if the headers are malformed or the directory is
/// empty.
pub unsafe fn runtime_function_table<'a>(
    module_base: *mut c_void,
) -> Option<&'a [ImageRuntimeFunction]> {
    let base = module_base as *const u8;

    // PE signature offset lives at DOS header offset 0x3C.
    let nt_offset = *(base.add(0x3C) as *const i32);
    if nt_offset <= 0 {
        return None;
    }
    let nt_header = base.add(nt_offset as usize);

    // "PE\0\0" signature check.
    if *(nt_header as *const u32) != 0x0000_4550 {
        return None;
    }

    // Magic byte at NT+0x18 distinguishes PE32 (0x10b) from PE32+ (0x20b).
    // We only support x64 / PE32+ here.
    let optional_header = nt_header.add(0x18);
    let magic = *(optional_header as *const u16);
    if magic != 0x020b {
        return None;
    }

    // DataDirectory[] sits at optional_header+0x70 on PE32+. Each entry is 8
    // bytes (VA + Size).
    let data_dirs_base = optional_header.add(0x70);
    let dir_ptr = data_dirs_base
        .add(IMAGE_DIRECTORY_ENTRY_EXCEPTION * size_of::<ImageDataDirectory>())
        as *const ImageDataDirectory;
    let dir = *dir_ptr;

    if dir.VirtualAddress == 0 || dir.Size == 0 {
        return None;
    }

    let table_addr = (module_base as usize + dir.VirtualAddress as usize) as *const ImageRuntimeFunction;
    let count = dir.Size as usize / size_of::<ImageRuntimeFunction>();

    Some(slice::from_raw_parts(table_addr, count))
}

/// Find the [`ImageRuntimeFunction`] whose BeginAddress matches `rva`.
pub fn function_by_rva<'a>(
    table: &'a [ImageRuntimeFunction],
    rva: u32,
) -> Option<&'a ImageRuntimeFunction> {
    table.iter().find(|f| f.BeginAddress == rva)
}