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