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