Skip to main content

spoof/
lib.rs

1//! Call-stack spoofing for Dyncvoke.
2//!
3//! Public API is [`spoof!`] (direct call) and [`spoof_syscall!`] (indirect
4//! syscall). Mode is a compile-time choice:
5//!
6//! - **Synthetic** (default, `dyncvoke` feature `spoof`). Builds
7//!   `RtlUserThreadStart -> BaseThreadInitThunk -> gadget frames -> target`.
8//!   Works from any thread, including pool threads.
9//! - **Desync** (`dyncvoke` feature `spoof-desync`, crate feature `desync`).
10//!   Splices spoofed frames onto a live `BaseThreadInitThunk` return on the
11//!   current thread. Looks closer to a normal user thread. Fails on pool
12//!   threads that never went through that path.
13//!
14//! ```ignore
15//! use spoof::{spoof_syscall, AsPointer};
16//! use core::ffi::c_void;
17//! use core::ptr::null_mut;
18//!
19//! let mut addr: *mut c_void = null_mut();
20//! let mut size: usize = 0x1000;
21//! let status = spoof_syscall!(
22//!     "NtAllocateVirtualMemory",
23//!     -1isize,
24//!     addr.as_ptr_mut(),
25//!     0usize,
26//!     size.as_ptr_mut(),
27//!     0x3000u32,
28//!     0x04u32,
29//! ).unwrap() as i32;
30//! ```
31//!
32//! The trampoline lives in `src/asm/{msvc,gnu}/*.asm` and reads
33//! [`types::Config`] by field order. Do not reorder those fields.
34//! Syscall SSNs come from [`dyncvoke_core::resolve_syscall`].
35
36#![no_std]
37#![cfg_attr(docsrs, feature(doc_cfg))]
38extern crate alloc;
39
40#[cfg(not(windows))]
41compile_error!("dyncvoke-spoof is Windows-only");
42
43use alloc::string::String;
44use alloc::vec::Vec;
45use core::ffi::c_void;
46
47use data::lc;
48
49pub mod pe;
50pub mod types;
51pub mod unwind;
52pub mod util;
53
54use crate::pe::{function_by_rva, runtime_function_table};
55use crate::types::{Config, ImageRuntimeFunction};
56use crate::unwind::{rbp_offset, stack_frame};
57#[cfg(not(feature = "desync"))]
58use crate::unwind::ignoring_set_fpreg;
59use crate::util::{find_gadget, find_valid_instruction_offset, shuffle};
60
61#[cfg(feature = "desync")]
62use crate::util::find_base_thread_return_address;
63
64// Compile-time-selected asm trampoline. Synthetic mode calls SpoofSynthetic,
65// desync calls Spoof. The asm files compile both — the linker drops the
66// unused symbol.
67#[cfg(feature = "desync")]
68unsafe extern "C" {
69    fn Spoof(config: &mut Config) -> *mut c_void;
70}
71
72#[cfg(not(feature = "desync"))]
73unsafe extern "C" {
74    fn SpoofSynthetic(config: &mut Config) -> *mut c_void;
75}
76
77/// Spoof a direct function call. Resolves the spoofing scaffolding once
78/// and then jumps to `addr` with the supplied args under a fake stack.
79///
80/// # Examples
81///
82/// ```ignore
83/// let kernel32 = dyncvoke_core::get_module_base_address("kernel32.dll");
84/// let virtual_alloc = dyncvoke_core::get_function_address(kernel32, "VirtualAlloc");
85/// let addr = spoof::spoof!(
86///     virtual_alloc,
87///     core::ptr::null_mut::<core::ffi::c_void>(),
88///     1 << 12,
89///     0x3000u32,
90///     0x04u32
91/// )?;
92/// ```
93#[macro_export]
94macro_rules! spoof {
95    ($addr:expr, $($arg:expr),+ $(,)?) => {
96        unsafe {
97            $crate::__private::spoof(
98                $addr as *mut ::core::ffi::c_void,
99                &[$(::core::mem::transmute($arg as usize)),*],
100                $crate::SpoofKind::Function,
101            )
102        }
103    };
104}
105
106/// Spoof an indirect syscall. SSN is resolved via dyncvoke_core's Tartarus
107/// Gate, then the syscall instruction inside ntdll is dispatched under a
108/// fake stack.
109///
110/// # Examples
111///
112/// ```ignore
113/// use spoof::{spoof_syscall, AsPointer};
114/// let mut addr = core::ptr::null_mut::<core::ffi::c_void>();
115/// let mut size = (1 << 12) as usize;
116/// let status = spoof_syscall!(
117///     "NtAllocateVirtualMemory",
118///     -1isize,
119///     addr.as_ptr_mut(),
120///     0usize,
121///     size.as_ptr_mut(),
122///     0x3000u32,
123///     0x04u32
124/// )? as i32;
125/// ```
126#[macro_export]
127macro_rules! spoof_syscall {
128    ($name:expr, $($arg:expr),* $(,)?) => {
129        unsafe {
130            $crate::__private::spoof(
131                ::core::ptr::null_mut(),
132                &[$(::core::mem::transmute($arg as usize)),*],
133                $crate::SpoofKind::Syscall($name),
134            )
135        }
136    };
137}
138
139/// Selects which mode the public entry uses.
140pub enum SpoofKind<'a> {
141    /// Spoof a direct function call.
142    Function,
143    /// Spoof an indirect syscall by ntdll export name.
144    Syscall(&'a str),
145}
146
147/// Where a failure happened. Carried by SpoofError variants as a non-string
148/// tag so the error path doesn't leak plaintext module/function names into
149/// `.rdata`. Display impl decrypts via obfstr at format time only.
150#[derive(Debug, Clone, Copy)]
151pub enum SpoofTag {
152    Kernelbase,
153    Kernel32,
154    Ntdll,
155    RtlUserThreadStart,
156    BaseThreadInitThunk,
157    AddRspGadget,
158    JmpRbxGadget,
159}
160
161/// Spoofing errors. Variants carry [`SpoofTag`] rather than literal strings.
162#[derive(Debug)]
163pub enum SpoofError {
164    TooManyArguments,
165    NullFunctionAddress,
166    ModuleNotLoaded(SpoofTag),
167    FunctionNotFound(SpoofTag),
168    PdataMissing(SpoofTag),
169    UnwindInfoMissing(SpoofTag),
170    PrologueNotFound,
171    PushRbpPrologueNotFound,
172    GadgetNotFound(SpoofTag),
173    BaseThreadReturnNotFound,
174    SsnResolutionFailed,
175}
176
177impl core::fmt::Display for SpoofError {
178    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
179        let tag_str = |t: SpoofTag| match t {
180            SpoofTag::Kernelbase => lc!("kernelbase.dll"),
181            SpoofTag::Kernel32 => lc!("kernel32.dll"),
182            SpoofTag::Ntdll => lc!("ntdll.dll"),
183            SpoofTag::RtlUserThreadStart => lc!("RtlUserThreadStart"),
184            SpoofTag::BaseThreadInitThunk => lc!("BaseThreadInitThunk"),
185            SpoofTag::AddRspGadget => lc!("add rsp,0x58; ret"),
186            SpoofTag::JmpRbxGadget => lc!("jmp [rbx]"),
187        };
188        match self {
189            SpoofError::TooManyArguments => write!(f, "{}", lc!("too many arguments")),
190            SpoofError::NullFunctionAddress => write!(f, "{}", lc!("null function address")),
191            SpoofError::ModuleNotLoaded(t) => write!(f, "{}: {}", lc!("module"), tag_str(*t)),
192            SpoofError::FunctionNotFound(t) => write!(f, "{}: {}", lc!("export"), tag_str(*t)),
193            SpoofError::PdataMissing(t) => write!(f, "{}: {}", lc!("pdata"), tag_str(*t)),
194            SpoofError::UnwindInfoMissing(t) => write!(f, "{}: {}", lc!("unwind"), tag_str(*t)),
195            SpoofError::PrologueNotFound => write!(f, "{}", lc!("no prologue")),
196            SpoofError::PushRbpPrologueNotFound => write!(f, "{}", lc!("no rbp prologue")),
197            SpoofError::GadgetNotFound(t) => write!(f, "{}: {}", lc!("gadget"), tag_str(*t)),
198            SpoofError::BaseThreadReturnNotFound => write!(f, "{}", lc!("no thread return")),
199            SpoofError::SsnResolutionFailed => write!(f, "{}", lc!("ssn fail")),
200        }
201    }
202}
203
204impl core::error::Error for SpoofError {}
205
206/// Ergonomic trait that lets call sites write `addr.as_ptr_mut()` in macro
207/// args instead of `&mut addr as *mut _ as *mut c_void`.
208pub trait AsPointer {
209    fn as_ptr_const(&self) -> *const c_void;
210    fn as_ptr_mut(&mut self) -> *mut c_void;
211}
212
213impl<T> AsPointer for T {
214    #[inline(always)]
215    fn as_ptr_const(&self) -> *const c_void {
216        self as *const _ as *const c_void
217    }
218    #[inline(always)]
219    fn as_ptr_mut(&mut self) -> *mut c_void {
220        self as *mut _ as *mut c_void
221    }
222}
223
224#[doc(hidden)]
225pub mod __private {
226    use super::*;
227    #[allow(unused_imports)]
228    use alloc::string::String;
229
230    /// Synthetic-mode entry. Builds the full scaffolding from scratch and
231    /// hands it to the asm trampoline. Works on any thread.
232    #[cfg(not(feature = "desync"))]
233    pub unsafe fn spoof(
234        addr: *mut c_void,
235        args: &[*const c_void],
236        kind: SpoofKind,
237    ) -> Result<*mut c_void, SpoofError> {
238        if args.len() > 11 {
239            return Err(SpoofError::TooManyArguments);
240        }
241        if matches!(kind, SpoofKind::Function) && addr.is_null() {
242            return Err(SpoofError::NullFunctionAddress);
243        }
244
245        let mut config = Config::default();
246
247        let kernelbase = dyncvoke_core::get_module_base_address(&lc!("kernelbase.dll"));
248        if kernelbase == 0 {
249            return Err(SpoofError::ModuleNotLoaded(SpoofTag::Kernelbase));
250        }
251        let ntdll = dyncvoke_core::get_module_base_address(&lc!("ntdll.dll"));
252        if ntdll == 0 {
253            return Err(SpoofError::ModuleNotLoaded(SpoofTag::Ntdll));
254        }
255        let kernel32 = dyncvoke_core::get_module_base_address(&lc!("kernel32.dll"));
256        if kernel32 == 0 {
257            return Err(SpoofError::ModuleNotLoaded(SpoofTag::Kernel32));
258        }
259
260        let rtl_user_addr =
261            dyncvoke_core::get_function_address(ntdll, &lc!("RtlUserThreadStart"));
262        if rtl_user_addr == 0 {
263            return Err(SpoofError::FunctionNotFound(SpoofTag::RtlUserThreadStart));
264        }
265        let base_thread_addr =
266            dyncvoke_core::get_function_address(kernel32, &lc!("BaseThreadInitThunk"));
267        if base_thread_addr == 0 {
268            return Err(SpoofError::FunctionNotFound(SpoofTag::BaseThreadInitThunk));
269        }
270
271        config.rtl_user_addr = rtl_user_addr as *const c_void;
272        config.base_thread_addr = base_thread_addr as *const c_void;
273
274        let kb_table = runtime_function_table(kernelbase as *mut c_void)
275            .ok_or(SpoofError::PdataMissing(SpoofTag::Kernelbase))?;
276        let ntdll_table = runtime_function_table(ntdll as *mut c_void)
277            .ok_or(SpoofError::PdataMissing(SpoofTag::Ntdll))?;
278        let k32_table = runtime_function_table(kernel32 as *mut c_void)
279            .ok_or(SpoofError::PdataMissing(SpoofTag::Kernel32))?;
280
281        let rtl_user_runtime =
282            function_by_rva(ntdll_table, (rtl_user_addr - ntdll) as u32)
283                .ok_or(SpoofError::UnwindInfoMissing(SpoofTag::RtlUserThreadStart))?;
284        let base_thread_runtime =
285            function_by_rva(k32_table, (base_thread_addr - kernel32) as u32)
286                .ok_or(SpoofError::UnwindInfoMissing(SpoofTag::BaseThreadInitThunk))?;
287
288        config.rtl_user_thread_size =
289            ignoring_set_fpreg(ntdll as *mut c_void, rtl_user_runtime)
290                .ok_or(SpoofError::UnwindInfoMissing(SpoofTag::RtlUserThreadStart))? as u64;
291        config.base_thread_size =
292            ignoring_set_fpreg(kernel32 as *mut c_void, base_thread_runtime)
293                .ok_or(SpoofError::UnwindInfoMissing(SpoofTag::BaseThreadInitThunk))? as u64;
294
295        let first_prolog = find_prolog(kernelbase as *mut c_void, kb_table)
296            .ok_or(SpoofError::PrologueNotFound)?;
297        config.first_frame_fp =
298            (first_prolog.frame + first_prolog.offset as u64) as *const c_void;
299        config.first_frame_size = first_prolog.stack_size as u64;
300
301        let second_prolog = find_push_rbp(kernelbase as *mut c_void, kb_table)
302            .ok_or(SpoofError::PushRbpPrologueNotFound)?;
303        config.second_frame_fp =
304            (second_prolog.frame + second_prolog.offset as u64) as *const c_void;
305        config.second_frame_size = second_prolog.stack_size as u64;
306        config.rbp_stack_offset = second_prolog.rbp_offset as u64;
307
308        let (add_rsp_addr, size) = find_gadget(
309            kernelbase as *mut c_void,
310            &[0x48, 0x83, 0xC4, 0x58, 0xC3],
311            kb_table,
312            Some(0x58),
313        )
314        .ok_or(SpoofError::GadgetNotFound(SpoofTag::AddRspGadget))?;
315        config.add_rsp_gadget = add_rsp_addr as *const c_void;
316        config.add_rsp_frame_size = size as u64;
317
318        let (jmp_rbx_addr, size) =
319            find_gadget(kernelbase as *mut c_void, &[0xFF, 0x23], kb_table, None)
320                .ok_or(SpoofError::GadgetNotFound(SpoofTag::JmpRbxGadget))?;
321        config.jmp_rbx_gadget = jmp_rbx_addr as *const c_void;
322        config.jmp_rbx_frame_size = size as u64;
323
324        config.number_args = args.len() as u64;
325        write_args(&mut config, args);
326
327        match kind {
328            SpoofKind::Function => config.spoof_function = addr as *const c_void,
329            SpoofKind::Syscall(name) => {
330                let (ssn, syscall_addr) = dyncvoke_core::resolve_syscall(name)
331                    .map_err(|_| SpoofError::SsnResolutionFailed)?;
332                config.is_syscall = 1;
333                config.ssn = ssn as u32;
334                config.spoof_function = syscall_addr as *const c_void;
335            }
336        }
337
338        Ok(SpoofSynthetic(&mut config))
339    }
340
341    /// Desync-mode entry. Reuses the current thread's real
342    /// BaseThreadInitThunk return record. Won't work on pool threads.
343    #[cfg(feature = "desync")]
344    pub unsafe fn spoof(
345        addr: *mut c_void,
346        args: &[*const c_void],
347        kind: SpoofKind,
348    ) -> Result<*mut c_void, SpoofError> {
349        if args.len() > 11 {
350            return Err(SpoofError::TooManyArguments);
351        }
352        if matches!(kind, SpoofKind::Function) && addr.is_null() {
353            return Err(SpoofError::NullFunctionAddress);
354        }
355
356        let mut config = Config::default();
357
358        let kernelbase = dyncvoke_core::get_module_base_address(&lc!("kernelbase.dll"));
359        if kernelbase == 0 {
360            return Err(SpoofError::ModuleNotLoaded(SpoofTag::Kernelbase));
361        }
362        let kernel32 = dyncvoke_core::get_module_base_address(&lc!("kernel32.dll"));
363        if kernel32 == 0 {
364            return Err(SpoofError::ModuleNotLoaded(SpoofTag::Kernel32));
365        }
366
367        let base_thread_addr =
368            dyncvoke_core::get_function_address(kernel32, &lc!("BaseThreadInitThunk"));
369        if base_thread_addr == 0 {
370            return Err(SpoofError::FunctionNotFound(SpoofTag::BaseThreadInitThunk));
371        }
372
373        let k32_table = runtime_function_table(kernel32 as *mut c_void)
374            .ok_or(SpoofError::PdataMissing(SpoofTag::Kernel32))?;
375        let base_thread_runtime =
376            function_by_rva(k32_table, (base_thread_addr - kernel32) as u32)
377                .ok_or(SpoofError::UnwindInfoMissing(SpoofTag::BaseThreadInitThunk))?;
378
379        let base_thread_size =
380            (base_thread_runtime.EndAddress - base_thread_runtime.BeginAddress) as usize;
381        let stack_slot = find_base_thread_return_address(
382            kernel32 as *mut c_void,
383            base_thread_addr as *mut c_void,
384            base_thread_size,
385        )
386        .ok_or(SpoofError::BaseThreadReturnNotFound)?;
387        config.return_address = stack_slot as *const c_void;
388
389        let kb_table = runtime_function_table(kernelbase as *mut c_void)
390            .ok_or(SpoofError::PdataMissing(SpoofTag::Kernelbase))?;
391
392        let first_prolog = find_prolog(kernelbase as *mut c_void, kb_table)
393            .ok_or(SpoofError::PrologueNotFound)?;
394        config.first_frame_fp =
395            (first_prolog.frame + first_prolog.offset as u64) as *const c_void;
396        config.first_frame_size = first_prolog.stack_size as u64;
397
398        let second_prolog = find_push_rbp(kernelbase as *mut c_void, kb_table)
399            .ok_or(SpoofError::PushRbpPrologueNotFound)?;
400        config.second_frame_fp =
401            (second_prolog.frame + second_prolog.offset as u64) as *const c_void;
402        config.second_frame_size = second_prolog.stack_size as u64;
403        config.rbp_stack_offset = second_prolog.rbp_offset as u64;
404
405        let (add_rsp_addr, size) = find_gadget(
406            kernelbase as *mut c_void,
407            &[0x48, 0x83, 0xC4, 0x58, 0xC3],
408            kb_table,
409            Some(0x58),
410        )
411        .ok_or(SpoofError::GadgetNotFound(SpoofTag::AddRspGadget))?;
412        config.add_rsp_gadget = add_rsp_addr as *const c_void;
413        config.add_rsp_frame_size = size as u64;
414
415        let (jmp_rbx_addr, size) =
416            find_gadget(kernelbase as *mut c_void, &[0xFF, 0x23], kb_table, None)
417                .ok_or(SpoofError::GadgetNotFound(SpoofTag::JmpRbxGadget))?;
418        config.jmp_rbx_gadget = jmp_rbx_addr as *const c_void;
419        config.jmp_rbx_frame_size = size as u64;
420
421        config.number_args = args.len() as u64;
422        write_args(&mut config, args);
423
424        match kind {
425            SpoofKind::Function => config.spoof_function = addr as *const c_void,
426            SpoofKind::Syscall(name) => {
427                let (ssn, syscall_addr) = dyncvoke_core::resolve_syscall(name)
428                    .map_err(|_| SpoofError::SsnResolutionFailed)?;
429                config.is_syscall = 1;
430                config.ssn = ssn as u32;
431                config.spoof_function = syscall_addr as *const c_void;
432            }
433        }
434
435        Ok(Spoof(&mut config))
436    }
437
438    #[inline]
439    fn write_args(cfg: &mut Config, args: &[*const c_void]) {
440        for (i, &a) in args.iter().enumerate() {
441            match i {
442                0 => cfg.arg01 = a,
443                1 => cfg.arg02 = a,
444                2 => cfg.arg03 = a,
445                3 => cfg.arg04 = a,
446                4 => cfg.arg05 = a,
447                5 => cfg.arg06 = a,
448                6 => cfg.arg07 = a,
449                7 => cfg.arg08 = a,
450                8 => cfg.arg09 = a,
451                9 => cfg.arg10 = a,
452                10 => cfg.arg11 = a,
453                _ => break,
454            }
455        }
456    }
457}
458
459/// Selected decoy frame metadata.
460#[derive(Copy, Clone, Default)]
461struct Prolog {
462    frame: u64,
463    stack_size: u32,
464    offset: u32,
465    rbp_offset: u32,
466}
467
468/// Scan kernelbase's runtime function table for a function whose unwind
469/// info describes a spoof-compatible RSP-based frame. Shuffles the survivors
470/// and returns the first one — picks a different decoy each run.
471fn find_prolog(module_base: *mut c_void, runtime_table: &[ImageRuntimeFunction]) -> Option<Prolog> {
472    let mut prologs: Vec<Prolog> = runtime_table
473        .iter()
474        .filter_map(|runtime| {
475            let (is_valid, stack_size) = unsafe { stack_frame(module_base, runtime) }?;
476            if !is_valid {
477                return None;
478            }
479            let offset = find_valid_instruction_offset(module_base, runtime)?;
480            let frame = module_base as u64 + runtime.BeginAddress as u64;
481            Some(Prolog {
482                frame,
483                stack_size,
484                offset,
485                ..Default::default()
486            })
487        })
488        .collect();
489
490    if prologs.is_empty() {
491        return None;
492    }
493    shuffle(&mut prologs);
494    prologs.first().copied()
495}
496
497/// Same idea but for an RBP-pushing prologue — these are needed for the
498/// inner spoofed frame where the unwinder will reconstruct rbp from the
499/// stored slot.
500fn find_push_rbp(
501    module_base: *mut c_void,
502    runtime_table: &[ImageRuntimeFunction],
503) -> Option<Prolog> {
504    let mut prologs: Vec<Prolog> = runtime_table
505        .iter()
506        .filter_map(|runtime| {
507            let (rbp_off, stack_size) = unsafe { rbp_offset(module_base, runtime) }?;
508            if rbp_off == 0 || stack_size == 0 || stack_size <= rbp_off {
509                return None;
510            }
511            let offset = find_valid_instruction_offset(module_base, runtime)?;
512            let frame = module_base as u64 + runtime.BeginAddress as u64;
513            Some(Prolog {
514                frame,
515                stack_size,
516                offset,
517                rbp_offset: rbp_off,
518            })
519        })
520        .collect();
521
522    if prologs.is_empty() {
523        return None;
524    }
525    // First match is consistently unsuitable on most Windows builds; drop it.
526    prologs.remove(0);
527    if prologs.is_empty() {
528        return None;
529    }
530    shuffle(&mut prologs);
531    prologs.first().copied()
532}