wraith/manipulation/spoof/
spoofed.rs

1//! Spoofed syscall invocation
2//!
3//! Combines gadget finding, stack spoofing, and trampolines to invoke
4//! syscalls with spoofed return addresses that appear legitimate.
5
6use super::gadget::GadgetFinder;
7use super::trampoline::{SpoofTrampoline, TrampolineAllocator};
8use crate::error::{Result, WraithError};
9use crate::manipulation::syscall::SyscallEntry;
10use core::arch::asm;
11use std::sync::OnceLock;
12
13/// global trampoline allocator
14static TRAMPOLINE_ALLOC: OnceLock<Result<TrampolineAllocator>> = OnceLock::new();
15
16fn get_trampoline_allocator() -> Result<&'static TrampolineAllocator> {
17    let result = TRAMPOLINE_ALLOC.get_or_init(TrampolineAllocator::new);
18    match result {
19        Ok(alloc) => Ok(alloc),
20        Err(_e) => Err(WraithError::TrampolineAllocationFailed {
21            near: 0,
22            size: 0,
23        }),
24    }
25}
26
27/// mode of return address spoofing
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum SpoofMode {
30    /// use a gadget (jmp rbx/rax) for clean indirect jump
31    Gadget,
32    /// synthesize a fake stack frame chain
33    SyntheticStack,
34    /// simple return address replacement
35    SimpleSpoof,
36    /// no spoofing (falls back to indirect syscall)
37    None,
38}
39
40impl Default for SpoofMode {
41    fn default() -> Self {
42        Self::Gadget
43    }
44}
45
46/// configuration for spoofed syscalls
47#[derive(Debug, Clone)]
48pub struct SpoofConfig {
49    /// spoofing mode to use
50    pub mode: SpoofMode,
51    /// prefer ntdll gadgets (most legitimate looking)
52    pub prefer_ntdll: bool,
53    /// custom spoof address (for SimpleSpoof mode)
54    pub custom_spoof_addr: Option<usize>,
55    /// stack pattern to synthesize (for SyntheticStack mode)
56    pub stack_pattern: Option<Vec<&'static str>>,
57}
58
59impl Default for SpoofConfig {
60    fn default() -> Self {
61        Self {
62            mode: SpoofMode::Gadget,
63            prefer_ntdll: true,
64            custom_spoof_addr: None,
65            stack_pattern: None,
66        }
67    }
68}
69
70impl SpoofConfig {
71    /// create config for gadget-based spoofing
72    pub fn gadget() -> Self {
73        Self {
74            mode: SpoofMode::Gadget,
75            ..Default::default()
76        }
77    }
78
79    /// create config for simple address spoofing
80    pub fn simple(spoof_addr: usize) -> Self {
81        Self {
82            mode: SpoofMode::SimpleSpoof,
83            custom_spoof_addr: Some(spoof_addr),
84            ..Default::default()
85        }
86    }
87
88    /// create config for synthetic stack
89    pub fn synthetic(pattern: Vec<&'static str>) -> Self {
90        Self {
91            mode: SpoofMode::SyntheticStack,
92            stack_pattern: Some(pattern),
93            ..Default::default()
94        }
95    }
96}
97
98/// spoofed syscall invoker
99///
100/// wraps a syscall with return address spoofing to evade call stack analysis
101pub struct SpoofedSyscall {
102    /// syscall number
103    ssn: u16,
104    /// address of syscall instruction in ntdll
105    syscall_addr: usize,
106    /// gadget address for return
107    gadget_addr: usize,
108    /// spoof address for simple mode
109    spoof_addr: usize,
110    /// name of the syscall
111    name: String,
112    /// allocated trampoline (if using trampoline mode)
113    trampoline: Option<SpoofTrampoline>,
114    /// spoofing mode
115    mode: SpoofMode,
116}
117
118impl SpoofedSyscall {
119    /// create spoofed syscall from name with default config
120    pub fn new(name: &str) -> Result<Self> {
121        Self::with_config(name, SpoofConfig::default())
122    }
123
124    /// create spoofed syscall with custom configuration
125    pub fn with_config(name: &str, config: SpoofConfig) -> Result<Self> {
126        let table = crate::manipulation::syscall::get_syscall_table()?;
127        let entry = table.get(name).ok_or_else(|| WraithError::SyscallNotFound {
128            name: name.to_string(),
129        })?;
130
131        Self::from_entry_with_config(entry, config)
132    }
133
134    /// create from syscall entry with config
135    pub fn from_entry_with_config(entry: &SyscallEntry, config: SpoofConfig) -> Result<Self> {
136        let syscall_addr = entry.syscall_address.ok_or_else(|| {
137            WraithError::SyscallEnumerationFailed {
138                reason: format!("no syscall address for {}", entry.name),
139            }
140        })?;
141
142        // find gadget based on mode
143        let (gadget_addr, spoof_addr) = match config.mode {
144            SpoofMode::Gadget => {
145                let finder = GadgetFinder::new()?;
146                let gadget = finder.find_best_jmp_gadget()?;
147                (gadget.address(), 0)
148            }
149            SpoofMode::SimpleSpoof => {
150                let spoof = config.custom_spoof_addr.unwrap_or_else(|| {
151                    // default: use a kernel32 address
152                    let finder = GadgetFinder::new().ok();
153                    finder
154                        .and_then(|f| f.find_ret("kernel32.dll").ok())
155                        .and_then(|r| r.into_iter().next())
156                        .map(|g| g.address())
157                        .unwrap_or(0)
158                });
159                (0, spoof)
160            }
161            SpoofMode::SyntheticStack => {
162                // for synthetic stack, we need both a gadget and the stack setup
163                let finder = GadgetFinder::new()?;
164                let gadget = finder.find_best_jmp_gadget()?;
165                (gadget.address(), 0)
166            }
167            SpoofMode::None => (0, 0),
168        };
169
170        // allocate trampoline if needed
171        let trampoline = if config.mode != SpoofMode::None {
172            let alloc = get_trampoline_allocator()?;
173            let tramp = alloc.allocate()?;
174
175            // write appropriate trampoline code
176            match config.mode {
177                SpoofMode::Gadget => {
178                    tramp.write_spoofed_syscall(entry.ssn, syscall_addr, gadget_addr)?;
179                }
180                SpoofMode::SimpleSpoof => {
181                    tramp.write_simple_spoofed_syscall(entry.ssn, syscall_addr, spoof_addr)?;
182                }
183                SpoofMode::SyntheticStack => {
184                    tramp.write_spoofed_syscall(entry.ssn, syscall_addr, gadget_addr)?;
185                }
186                SpoofMode::None => {}
187            }
188
189            Some(tramp)
190        } else {
191            None
192        };
193
194        Ok(Self {
195            ssn: entry.ssn,
196            syscall_addr,
197            gadget_addr,
198            spoof_addr,
199            name: entry.name.clone(),
200            trampoline,
201            mode: config.mode,
202        })
203    }
204
205    /// create from syscall entry with default config
206    pub fn from_entry(entry: &SyscallEntry) -> Result<Self> {
207        Self::from_entry_with_config(entry, SpoofConfig::default())
208    }
209
210    /// get syscall number
211    pub fn ssn(&self) -> u16 {
212        self.ssn
213    }
214
215    /// get syscall name
216    pub fn name(&self) -> &str {
217        &self.name
218    }
219
220    /// get the spoofing mode
221    pub fn mode(&self) -> SpoofMode {
222        self.mode
223    }
224
225    /// get gadget address (if using gadget mode)
226    pub fn gadget_addr(&self) -> Option<usize> {
227        if self.gadget_addr != 0 {
228            Some(self.gadget_addr)
229        } else {
230            None
231        }
232    }
233}
234
235// x86_64 syscall implementations with spoofing
236#[cfg(target_arch = "x86_64")]
237impl SpoofedSyscall {
238    /// invoke spoofed syscall with 0 arguments
239    ///
240    /// # Safety
241    /// caller must ensure the syscall is appropriate to call with 0 args
242    #[inline(never)]
243    pub unsafe fn call0(&self) -> i32 {
244        if let Some(ref tramp) = self.trampoline {
245            type Fn0 = unsafe extern "system" fn() -> i32;
246            let f: Fn0 = unsafe { tramp.as_fn_ptr() };
247            unsafe { f() }
248        } else {
249            unsafe { self.call0_direct() }
250        }
251    }
252
253    /// invoke spoofed syscall with 1 argument
254    ///
255    /// # Safety
256    /// caller must ensure args are valid for this syscall
257    #[inline(never)]
258    pub unsafe fn call1(&self, arg1: usize) -> i32 {
259        if let Some(ref tramp) = self.trampoline {
260            type Fn1 = unsafe extern "system" fn(usize) -> i32;
261            let f: Fn1 = unsafe { tramp.as_fn_ptr() };
262            unsafe { f(arg1) }
263        } else {
264            unsafe { self.call1_direct(arg1) }
265        }
266    }
267
268    /// invoke spoofed syscall with 2 arguments
269    ///
270    /// # Safety
271    /// caller must ensure args are valid for this syscall
272    #[inline(never)]
273    pub unsafe fn call2(&self, arg1: usize, arg2: usize) -> i32 {
274        if let Some(ref tramp) = self.trampoline {
275            type Fn2 = unsafe extern "system" fn(usize, usize) -> i32;
276            let f: Fn2 = unsafe { tramp.as_fn_ptr() };
277            unsafe { f(arg1, arg2) }
278        } else {
279            unsafe { self.call2_direct(arg1, arg2) }
280        }
281    }
282
283    /// invoke spoofed syscall with 3 arguments
284    ///
285    /// # Safety
286    /// caller must ensure args are valid for this syscall
287    #[inline(never)]
288    pub unsafe fn call3(&self, arg1: usize, arg2: usize, arg3: usize) -> i32 {
289        if let Some(ref tramp) = self.trampoline {
290            type Fn3 = unsafe extern "system" fn(usize, usize, usize) -> i32;
291            let f: Fn3 = unsafe { tramp.as_fn_ptr() };
292            unsafe { f(arg1, arg2, arg3) }
293        } else {
294            unsafe { self.call3_direct(arg1, arg2, arg3) }
295        }
296    }
297
298    /// invoke spoofed syscall with 4 arguments
299    ///
300    /// # Safety
301    /// caller must ensure args are valid for this syscall
302    #[inline(never)]
303    pub unsafe fn call4(&self, arg1: usize, arg2: usize, arg3: usize, arg4: usize) -> i32 {
304        if let Some(ref tramp) = self.trampoline {
305            type Fn4 = unsafe extern "system" fn(usize, usize, usize, usize) -> i32;
306            let f: Fn4 = unsafe { tramp.as_fn_ptr() };
307            unsafe { f(arg1, arg2, arg3, arg4) }
308        } else {
309            unsafe { self.call4_direct(arg1, arg2, arg3, arg4) }
310        }
311    }
312
313    /// invoke spoofed syscall with 5 arguments
314    ///
315    /// # Safety
316    /// caller must ensure args are valid for this syscall
317    #[inline(never)]
318    pub unsafe fn call5(
319        &self,
320        arg1: usize,
321        arg2: usize,
322        arg3: usize,
323        arg4: usize,
324        arg5: usize,
325    ) -> i32 {
326        if let Some(ref tramp) = self.trampoline {
327            type Fn5 = unsafe extern "system" fn(usize, usize, usize, usize, usize) -> i32;
328            let f: Fn5 = unsafe { tramp.as_fn_ptr() };
329            unsafe { f(arg1, arg2, arg3, arg4, arg5) }
330        } else {
331            unsafe { self.call5_direct(arg1, arg2, arg3, arg4, arg5) }
332        }
333    }
334
335    /// invoke spoofed syscall with 6 arguments
336    ///
337    /// # Safety
338    /// caller must ensure args are valid for this syscall
339    #[inline(never)]
340    pub unsafe fn call6(
341        &self,
342        arg1: usize,
343        arg2: usize,
344        arg3: usize,
345        arg4: usize,
346        arg5: usize,
347        arg6: usize,
348    ) -> i32 {
349        if let Some(ref tramp) = self.trampoline {
350            type Fn6 = unsafe extern "system" fn(usize, usize, usize, usize, usize, usize) -> i32;
351            let f: Fn6 = unsafe { tramp.as_fn_ptr() };
352            unsafe { f(arg1, arg2, arg3, arg4, arg5, arg6) }
353        } else {
354            unsafe { self.call6_direct(arg1, arg2, arg3, arg4, arg5, arg6) }
355        }
356    }
357
358    /// invoke with variable arguments
359    ///
360    /// # Safety
361    /// caller must ensure args are valid for this syscall
362    #[inline(never)]
363    pub unsafe fn call_many(&self, args: &[usize]) -> i32 {
364        match args.len() {
365            0 => unsafe { self.call0() },
366            1 => unsafe { self.call1(args[0]) },
367            2 => unsafe { self.call2(args[0], args[1]) },
368            3 => unsafe { self.call3(args[0], args[1], args[2]) },
369            4 => unsafe { self.call4(args[0], args[1], args[2], args[3]) },
370            5 => unsafe { self.call5(args[0], args[1], args[2], args[3], args[4]) },
371            6 => unsafe { self.call6(args[0], args[1], args[2], args[3], args[4], args[5]) },
372            _ => unsafe { self.call6(args[0], args[1], args[2], args[3], args[4], args[5]) },
373        }
374    }
375
376    // direct fallback implementations (non-spoofed)
377    #[inline(never)]
378    unsafe fn call0_direct(&self) -> i32 {
379        let status: i32;
380        unsafe {
381            asm!(
382                "sub rsp, 0x28",
383                "mov r10, rcx",
384                "mov eax, {ssn:e}",
385                "call {addr}",
386                "add rsp, 0x28",
387                ssn = in(reg) self.ssn as u32,
388                addr = in(reg) self.syscall_addr,
389                out("eax") status,
390                out("rcx") _,
391                out("r10") _,
392                out("r11") _,
393            );
394        }
395        status
396    }
397
398    #[inline(never)]
399    unsafe fn call1_direct(&self, arg1: usize) -> i32 {
400        let status: i32;
401        unsafe {
402            asm!(
403                "sub rsp, 0x28",
404                "mov r10, rcx",
405                "mov eax, {ssn:e}",
406                "call {addr}",
407                "add rsp, 0x28",
408                ssn = in(reg) self.ssn as u32,
409                addr = in(reg) self.syscall_addr,
410                in("rcx") arg1,
411                out("eax") status,
412                out("r10") _,
413                out("r11") _,
414            );
415        }
416        status
417    }
418
419    #[inline(never)]
420    unsafe fn call2_direct(&self, arg1: usize, arg2: usize) -> i32 {
421        let status: i32;
422        unsafe {
423            asm!(
424                "sub rsp, 0x28",
425                "mov r10, rcx",
426                "mov eax, {ssn:e}",
427                "call {addr}",
428                "add rsp, 0x28",
429                ssn = in(reg) self.ssn as u32,
430                addr = in(reg) self.syscall_addr,
431                in("rcx") arg1,
432                in("rdx") arg2,
433                out("eax") status,
434                out("r10") _,
435                out("r11") _,
436            );
437        }
438        status
439    }
440
441    #[inline(never)]
442    unsafe fn call3_direct(&self, arg1: usize, arg2: usize, arg3: usize) -> i32 {
443        let status: i32;
444        unsafe {
445            asm!(
446                "sub rsp, 0x28",
447                "mov r10, rcx",
448                "mov eax, {ssn:e}",
449                "call {addr}",
450                "add rsp, 0x28",
451                ssn = in(reg) self.ssn as u32,
452                addr = in(reg) self.syscall_addr,
453                in("rcx") arg1,
454                in("rdx") arg2,
455                in("r8") arg3,
456                out("eax") status,
457                out("r10") _,
458                out("r11") _,
459            );
460        }
461        status
462    }
463
464    #[inline(never)]
465    unsafe fn call4_direct(&self, arg1: usize, arg2: usize, arg3: usize, arg4: usize) -> i32 {
466        let status: i32;
467        unsafe {
468            asm!(
469                "sub rsp, 0x28",
470                "mov r10, rcx",
471                "mov eax, {ssn:e}",
472                "call {addr}",
473                "add rsp, 0x28",
474                ssn = in(reg) self.ssn as u32,
475                addr = in(reg) self.syscall_addr,
476                in("rcx") arg1,
477                in("rdx") arg2,
478                in("r8") arg3,
479                in("r9") arg4,
480                out("eax") status,
481                out("r10") _,
482                out("r11") _,
483            );
484        }
485        status
486    }
487
488    #[inline(never)]
489    unsafe fn call5_direct(
490        &self,
491        arg1: usize,
492        arg2: usize,
493        arg3: usize,
494        arg4: usize,
495        arg5: usize,
496    ) -> i32 {
497        let status: i32;
498        unsafe {
499            asm!(
500                "sub rsp, 0x28",
501                "mov [rsp+0x20], {arg5}",
502                "mov r10, rcx",
503                "mov eax, {ssn:e}",
504                "call {addr}",
505                "add rsp, 0x28",
506                ssn = in(reg) self.ssn as u32,
507                addr = in(reg) self.syscall_addr,
508                arg5 = in(reg) arg5,
509                in("rcx") arg1,
510                in("rdx") arg2,
511                in("r8") arg3,
512                in("r9") arg4,
513                out("eax") status,
514                out("r10") _,
515                out("r11") _,
516            );
517        }
518        status
519    }
520
521    #[inline(never)]
522    unsafe fn call6_direct(
523        &self,
524        arg1: usize,
525        arg2: usize,
526        arg3: usize,
527        arg4: usize,
528        arg5: usize,
529        arg6: usize,
530    ) -> i32 {
531        let status: i32;
532        unsafe {
533            asm!(
534                "sub rsp, 0x30",
535                "mov [rsp+0x20], {arg5}",
536                "mov [rsp+0x28], {arg6}",
537                "mov r10, rcx",
538                "mov eax, {ssn:e}",
539                "call {addr}",
540                "add rsp, 0x30",
541                ssn = in(reg) self.ssn as u32,
542                addr = in(reg) self.syscall_addr,
543                arg5 = in(reg) arg5,
544                arg6 = in(reg) arg6,
545                in("rcx") arg1,
546                in("rdx") arg2,
547                in("r8") arg3,
548                in("r9") arg4,
549                out("eax") status,
550                out("r10") _,
551                out("r11") _,
552            );
553        }
554        status
555    }
556}
557
558// x86 implementation (32-bit)
559#[cfg(target_arch = "x86")]
560impl SpoofedSyscall {
561    /// invoke spoofed syscall (x86)
562    ///
563    /// # Safety
564    /// caller must ensure args are valid for this syscall
565    #[inline(never)]
566    pub unsafe fn call(&self, args: &[usize]) -> i32 {
567        // x86 spoofing is simpler - just manipulate the stack
568        let status: i32;
569        let args_ptr = args.as_ptr();
570
571        unsafe {
572            asm!(
573                "mov eax, {ssn:e}",
574                "mov edx, {args}",
575                "call {addr}",
576                ssn = in(reg) self.ssn as u32,
577                args = in(reg) args_ptr,
578                addr = in(reg) self.syscall_addr,
579                out("eax") status,
580                options(nostack)
581            );
582        }
583        status
584    }
585
586    pub unsafe fn call0(&self) -> i32 {
587        unsafe { self.call(&[]) }
588    }
589
590    pub unsafe fn call1(&self, arg1: usize) -> i32 {
591        unsafe { self.call(&[arg1]) }
592    }
593
594    pub unsafe fn call2(&self, arg1: usize, arg2: usize) -> i32 {
595        unsafe { self.call(&[arg1, arg2]) }
596    }
597
598    pub unsafe fn call3(&self, arg1: usize, arg2: usize, arg3: usize) -> i32 {
599        unsafe { self.call(&[arg1, arg2, arg3]) }
600    }
601
602    pub unsafe fn call4(&self, arg1: usize, arg2: usize, arg3: usize, arg4: usize) -> i32 {
603        unsafe { self.call(&[arg1, arg2, arg3, arg4]) }
604    }
605
606    pub unsafe fn call5(
607        &self,
608        arg1: usize,
609        arg2: usize,
610        arg3: usize,
611        arg4: usize,
612        arg5: usize,
613    ) -> i32 {
614        unsafe { self.call(&[arg1, arg2, arg3, arg4, arg5]) }
615    }
616
617    pub unsafe fn call6(
618        &self,
619        arg1: usize,
620        arg2: usize,
621        arg3: usize,
622        arg4: usize,
623        arg5: usize,
624        arg6: usize,
625    ) -> i32 {
626        unsafe { self.call(&[arg1, arg2, arg3, arg4, arg5, arg6]) }
627    }
628
629    pub unsafe fn call_many(&self, args: &[usize]) -> i32 {
630        unsafe { self.call(args) }
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637
638    #[test]
639    fn test_spoofed_syscall_creation() {
640        match SpoofedSyscall::new("NtClose") {
641            Ok(syscall) => {
642                assert!(syscall.ssn() > 0);
643                assert!(syscall.gadget_addr().is_some());
644            }
645            Err(_) => {
646                // might fail in some test environments
647            }
648        }
649    }
650
651    #[test]
652    fn test_spoofed_syscall_ntclose() {
653        if let Ok(syscall) = SpoofedSyscall::new("NtClose") {
654            // call with invalid handle - should fail but not crash
655            let status = unsafe { syscall.call1(0xDEADBEEF) };
656            assert_eq!(
657                status, 0xC0000008_u32 as i32,
658                "should return STATUS_INVALID_HANDLE"
659            );
660        }
661    }
662
663    #[test]
664    fn test_simple_spoof_mode() {
665        if let Ok(syscall) = SpoofedSyscall::with_config("NtClose", SpoofConfig {
666            mode: SpoofMode::SimpleSpoof,
667            custom_spoof_addr: Some(0x7FFE0000), // arbitrary address
668            ..Default::default()
669        }) {
670            let status = unsafe { syscall.call1(0xDEADBEEF) };
671            assert_eq!(status, 0xC0000008_u32 as i32);
672        }
673    }
674
675    #[test]
676    fn test_none_mode_fallback() {
677        if let Ok(syscall) = SpoofedSyscall::with_config("NtClose", SpoofConfig {
678            mode: SpoofMode::None,
679            ..Default::default()
680        }) {
681            let status = unsafe { syscall.call1(0xDEADBEEF) };
682            assert_eq!(status, 0xC0000008_u32 as i32);
683        }
684    }
685}