wraith/manipulation/syscall/
indirect.rs

1//! Indirect syscall invocation (jump to ntdll's syscall instruction)
2//!
3//! Instead of using an inline syscall instruction, indirect syscalls
4//! jump to the syscall instruction inside ntdll. This leaves a cleaner
5//! call stack that appears to originate from ntdll, evading some
6//! call stack analysis techniques.
7
8use super::table::{SyscallEntry, SyscallTable};
9use crate::error::{Result, WraithError};
10use core::arch::asm;
11
12#[cfg(target_arch = "x86_64")]
13const SYSCALL_BYTES: [u8; 2] = [0x0F, 0x05]; // syscall
14
15#[cfg(target_arch = "x86")]
16const SYSCALL_BYTES: [u8; 2] = [0x0F, 0x34]; // sysenter (or could be int 0x2e)
17
18/// indirect syscall invoker
19///
20/// instead of using inline syscall instruction, jumps to the
21/// syscall instruction inside ntdll for cleaner call stack
22pub struct IndirectSyscall {
23    ssn: u16,
24    syscall_addr: usize,
25}
26
27impl IndirectSyscall {
28    /// create from SSN and syscall instruction address
29    ///
30    /// # Safety
31    /// caller must ensure syscall_addr points to a valid syscall instruction
32    pub const unsafe fn new_unchecked(ssn: u16, syscall_addr: usize) -> Self {
33        Self { ssn, syscall_addr }
34    }
35
36    /// create from SSN and syscall instruction address with validation
37    pub fn new(ssn: u16, syscall_addr: usize) -> Result<Self> {
38        // validate the address actually contains a syscall instruction
39        if !Self::validate_syscall_address(syscall_addr) {
40            return Err(WraithError::SyscallEnumerationFailed {
41                reason: format!(
42                    "address {:#x} does not contain valid syscall instruction",
43                    syscall_addr
44                ),
45            });
46        }
47
48        Ok(Self { ssn, syscall_addr })
49    }
50
51    /// validate that an address contains a syscall instruction
52    fn validate_syscall_address(addr: usize) -> bool {
53        if addr == 0 {
54            return false;
55        }
56
57        // SAFETY: we're reading 2 bytes at the address to verify syscall instruction
58        // this could fault if addr is invalid, but we're checking for null above
59        // and this is called with addresses from ntdll which should be valid
60        let bytes: [u8; 2] = unsafe { *(addr as *const [u8; 2]) };
61        bytes == SYSCALL_BYTES
62    }
63
64    /// create from syscall entry
65    pub fn from_entry(entry: &SyscallEntry) -> Result<Self> {
66        let syscall_addr = entry.syscall_address.ok_or_else(|| {
67            WraithError::SyscallEnumerationFailed {
68                reason: format!("no syscall address for {}", entry.name),
69            }
70        })?;
71
72        Self::new(entry.ssn, syscall_addr)
73    }
74
75    /// create from syscall table lookup
76    pub fn from_table(table: &SyscallTable, name: &str) -> Result<Self> {
77        let entry = table.get(name).ok_or_else(|| WraithError::SyscallNotFound {
78            name: name.to_string(),
79        })?;
80        Self::from_entry(entry)
81    }
82
83    /// get SSN
84    pub const fn ssn(&self) -> u16 {
85        self.ssn
86    }
87
88    /// get syscall instruction address
89    pub const fn syscall_address(&self) -> usize {
90        self.syscall_addr
91    }
92}
93
94#[cfg(target_arch = "x86_64")]
95impl IndirectSyscall {
96    /// invoke indirect syscall with 0 arguments
97    ///
98    /// # Safety
99    /// caller must ensure the syscall is appropriate to call with 0 args
100    #[inline(never)]
101    pub unsafe fn call0(&self) -> i32 {
102        let status: i32;
103        // SAFETY: caller guarantees syscall validity, syscall_addr points to valid syscall instruction
104        // we use `call` instead of `jmp` so that the `ret` after the syscall instruction has a valid return address
105        unsafe {
106            asm!(
107                "sub rsp, 0x28",          // shadow space (32 bytes) + align
108                "mov r10, rcx",
109                "mov eax, {ssn:e}",
110                "call {addr}",
111                "add rsp, 0x28",
112                ssn = in(reg) self.ssn as u32,
113                addr = in(reg) self.syscall_addr,
114                out("eax") status,
115                out("rcx") _,
116                out("r10") _,
117                out("r11") _,
118            );
119        }
120        status
121    }
122
123    /// invoke indirect syscall with 1 argument
124    ///
125    /// # Safety
126    /// caller must ensure args are valid for this syscall
127    #[inline(never)]
128    pub unsafe fn call1(&self, arg1: usize) -> i32 {
129        let status: i32;
130        // SAFETY: caller guarantees syscall and argument validity
131        unsafe {
132            asm!(
133                "sub rsp, 0x28",
134                "mov r10, rcx",
135                "mov eax, {ssn:e}",
136                "call {addr}",
137                "add rsp, 0x28",
138                ssn = in(reg) self.ssn as u32,
139                addr = in(reg) self.syscall_addr,
140                in("rcx") arg1,
141                out("eax") status,
142                out("r10") _,
143                out("r11") _,
144            );
145        }
146        status
147    }
148
149    /// invoke indirect syscall with 2 arguments
150    ///
151    /// # Safety
152    /// caller must ensure args are valid for this syscall
153    #[inline(never)]
154    pub unsafe fn call2(&self, arg1: usize, arg2: usize) -> i32 {
155        let status: i32;
156        // SAFETY: caller guarantees syscall and argument validity
157        unsafe {
158            asm!(
159                "sub rsp, 0x28",
160                "mov r10, rcx",
161                "mov eax, {ssn:e}",
162                "call {addr}",
163                "add rsp, 0x28",
164                ssn = in(reg) self.ssn as u32,
165                addr = in(reg) self.syscall_addr,
166                in("rcx") arg1,
167                in("rdx") arg2,
168                out("eax") status,
169                out("r10") _,
170                out("r11") _,
171            );
172        }
173        status
174    }
175
176    /// invoke indirect syscall with 3 arguments
177    ///
178    /// # Safety
179    /// caller must ensure args are valid for this syscall
180    #[inline(never)]
181    pub unsafe fn call3(&self, arg1: usize, arg2: usize, arg3: usize) -> i32 {
182        let status: i32;
183        // SAFETY: caller guarantees syscall and argument validity
184        unsafe {
185            asm!(
186                "sub rsp, 0x28",
187                "mov r10, rcx",
188                "mov eax, {ssn:e}",
189                "call {addr}",
190                "add rsp, 0x28",
191                ssn = in(reg) self.ssn as u32,
192                addr = in(reg) self.syscall_addr,
193                in("rcx") arg1,
194                in("rdx") arg2,
195                in("r8") arg3,
196                out("eax") status,
197                out("r10") _,
198                out("r11") _,
199            );
200        }
201        status
202    }
203
204    /// invoke indirect syscall with 4 arguments
205    ///
206    /// # Safety
207    /// caller must ensure args are valid for this syscall
208    #[inline(never)]
209    pub unsafe fn call4(&self, arg1: usize, arg2: usize, arg3: usize, arg4: usize) -> i32 {
210        let status: i32;
211        // SAFETY: caller guarantees syscall and argument validity
212        unsafe {
213            asm!(
214                "sub rsp, 0x28",
215                "mov r10, rcx",
216                "mov eax, {ssn:e}",
217                "call {addr}",
218                "add rsp, 0x28",
219                ssn = in(reg) self.ssn as u32,
220                addr = in(reg) self.syscall_addr,
221                in("rcx") arg1,
222                in("rdx") arg2,
223                in("r8") arg3,
224                in("r9") arg4,
225                out("eax") status,
226                out("r10") _,
227                out("r11") _,
228            );
229        }
230        status
231    }
232
233    /// invoke indirect syscall with 5 arguments
234    ///
235    /// # Safety
236    /// caller must ensure args are valid for this syscall
237    #[inline(never)]
238    pub unsafe fn call5(
239        &self,
240        arg1: usize,
241        arg2: usize,
242        arg3: usize,
243        arg4: usize,
244        arg5: usize,
245    ) -> i32 {
246        let status: i32;
247        // SAFETY: caller guarantees syscall and argument validity
248        // note: we put arg5 at [rsp+0x20] so that after `call` pushes return address,
249        // the kernel sees it at [rsp+0x28] as expected by Windows x64 calling convention
250        unsafe {
251            asm!(
252                "sub rsp, 0x28",
253                "mov [rsp+0x20], {arg5}",
254                "mov r10, rcx",
255                "mov eax, {ssn:e}",
256                "call {addr}",
257                "add rsp, 0x28",
258                ssn = in(reg) self.ssn as u32,
259                addr = in(reg) self.syscall_addr,
260                arg5 = in(reg) arg5,
261                in("rcx") arg1,
262                in("rdx") arg2,
263                in("r8") arg3,
264                in("r9") arg4,
265                out("eax") status,
266                out("r10") _,
267                out("r11") _,
268            );
269        }
270        status
271    }
272
273    /// invoke indirect syscall with 6 arguments
274    ///
275    /// # Safety
276    /// caller must ensure args are valid for this syscall
277    #[inline(never)]
278    pub unsafe fn call6(
279        &self,
280        arg1: usize,
281        arg2: usize,
282        arg3: usize,
283        arg4: usize,
284        arg5: usize,
285        arg6: usize,
286    ) -> i32 {
287        let status: i32;
288        // SAFETY: caller guarantees syscall and argument validity
289        // note: we put args at [rsp+0x20] and [rsp+0x28] so that after `call` pushes return address,
290        // the kernel sees them at [rsp+0x28] and [rsp+0x30] as expected
291        unsafe {
292            asm!(
293                "sub rsp, 0x30",
294                "mov [rsp+0x20], {arg5}",
295                "mov [rsp+0x28], {arg6}",
296                "mov r10, rcx",
297                "mov eax, {ssn:e}",
298                "call {addr}",
299                "add rsp, 0x30",
300                ssn = in(reg) self.ssn as u32,
301                addr = in(reg) self.syscall_addr,
302                arg5 = in(reg) arg5,
303                arg6 = in(reg) arg6,
304                in("rcx") arg1,
305                in("rdx") arg2,
306                in("r8") arg3,
307                in("r9") arg4,
308                out("eax") status,
309                out("r10") _,
310                out("r11") _,
311            );
312        }
313        status
314    }
315
316    /// invoke indirect syscall with variable arguments
317    ///
318    /// # Safety
319    /// caller must ensure args are valid for this syscall
320    #[inline(never)]
321    pub unsafe fn call_many(&self, args: &[usize]) -> i32 {
322        match args.len() {
323            0 => unsafe { self.call0() },
324            1 => unsafe { self.call1(args[0]) },
325            2 => unsafe { self.call2(args[0], args[1]) },
326            3 => unsafe { self.call3(args[0], args[1], args[2]) },
327            4 => unsafe { self.call4(args[0], args[1], args[2], args[3]) },
328            5 => unsafe { self.call5(args[0], args[1], args[2], args[3], args[4]) },
329            6 => unsafe { self.call6(args[0], args[1], args[2], args[3], args[4], args[5]) },
330            _ => unsafe { self.call6(args[0], args[1], args[2], args[3], args[4], args[5]) },
331        }
332    }
333}
334
335#[cfg(target_arch = "x86")]
336impl IndirectSyscall {
337    /// invoke indirect syscall (x86)
338    ///
339    /// # Safety
340    /// caller must ensure args are valid for this syscall
341    #[inline(never)]
342    pub unsafe fn call(&self, args: &[usize]) -> i32 {
343        let status: i32;
344        let args_ptr = args.as_ptr();
345
346        // SAFETY: caller guarantees syscall and argument validity
347        unsafe {
348            asm!(
349                "mov eax, {ssn:e}",
350                "mov edx, {args}",
351                "call {addr}",
352                ssn = in(reg) self.ssn as u32,
353                args = in(reg) args_ptr,
354                addr = in(reg) self.syscall_addr,
355                out("eax") status,
356                options(nostack)
357            );
358        }
359        status
360    }
361
362    pub unsafe fn call0(&self) -> i32 {
363        unsafe { self.call(&[]) }
364    }
365
366    pub unsafe fn call1(&self, arg1: usize) -> i32 {
367        unsafe { self.call(&[arg1]) }
368    }
369
370    pub unsafe fn call2(&self, arg1: usize, arg2: usize) -> i32 {
371        unsafe { self.call(&[arg1, arg2]) }
372    }
373
374    pub unsafe fn call3(&self, arg1: usize, arg2: usize, arg3: usize) -> i32 {
375        unsafe { self.call(&[arg1, arg2, arg3]) }
376    }
377
378    pub unsafe fn call4(&self, arg1: usize, arg2: usize, arg3: usize, arg4: usize) -> i32 {
379        unsafe { self.call(&[arg1, arg2, arg3, arg4]) }
380    }
381
382    pub unsafe fn call5(
383        &self,
384        arg1: usize,
385        arg2: usize,
386        arg3: usize,
387        arg4: usize,
388        arg5: usize,
389    ) -> i32 {
390        unsafe { self.call(&[arg1, arg2, arg3, arg4, arg5]) }
391    }
392
393    pub unsafe fn call6(
394        &self,
395        arg1: usize,
396        arg2: usize,
397        arg3: usize,
398        arg4: usize,
399        arg5: usize,
400        arg6: usize,
401    ) -> i32 {
402        unsafe { self.call(&[arg1, arg2, arg3, arg4, arg5, arg6]) }
403    }
404
405    pub unsafe fn call_many(&self, args: &[usize]) -> i32 {
406        unsafe { self.call(args) }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn test_indirect_syscall_ntclose() {
416        let table = SyscallTable::enumerate().expect("should enumerate");
417
418        if let Ok(syscall) = IndirectSyscall::from_table(&table, "NtClose") {
419            // SAFETY: NtClose with invalid handle is safe (returns error status)
420            let status = unsafe { syscall.call1(0xDEADBEEF) };
421            assert_eq!(status, 0xC0000008_u32 as i32);
422        }
423    }
424
425    #[test]
426    fn test_syscall_address_in_ntdll() {
427        let table = SyscallTable::enumerate().expect("should enumerate");
428
429        if let Some(entry) = table.get("NtClose") {
430            if let Some(addr) = entry.syscall_address {
431                // should be within ntdll's address range
432                assert!(addr > entry.address, "syscall should be after function start");
433                assert!(
434                    addr < entry.address + 32,
435                    "syscall should be within stub"
436                );
437            }
438        }
439    }
440}