wraith/manipulation/syscall/
direct.rs

1//! Direct syscall invocation (inline syscall instruction)
2//!
3//! Bypasses usermode hooks by using inline `syscall` instructions
4//! instead of calling ntdll functions directly.
5
6use super::table::{SyscallEntry, SyscallTable};
7use crate::error::{Result, WraithError};
8use core::arch::asm;
9
10/// direct syscall invoker
11///
12/// holds the SSN and provides methods to invoke the syscall directly
13/// with varying argument counts
14pub struct DirectSyscall {
15    ssn: u16,
16}
17
18impl DirectSyscall {
19    /// create from SSN
20    pub const fn new(ssn: u16) -> Self {
21        Self { ssn }
22    }
23
24    /// create from syscall entry
25    pub fn from_entry(entry: &SyscallEntry) -> Self {
26        Self { ssn: entry.ssn }
27    }
28
29    /// create from syscall table lookup
30    pub fn from_table(table: &SyscallTable, name: &str) -> Result<Self> {
31        let entry = table.get(name).ok_or_else(|| WraithError::SyscallNotFound {
32            name: name.to_string(),
33        })?;
34        Ok(Self::from_entry(entry))
35    }
36
37    /// get SSN
38    pub const fn ssn(&self) -> u16 {
39        self.ssn
40    }
41}
42
43// x64 direct syscall implementations
44#[cfg(target_arch = "x86_64")]
45impl DirectSyscall {
46    /// invoke syscall with 0 arguments
47    ///
48    /// # Safety
49    /// caller must ensure the syscall is appropriate to call with 0 args
50    #[inline(never)]
51    pub unsafe fn call0(&self) -> i32 {
52        let status: i32;
53        // SAFETY: caller guarantees syscall validity
54        unsafe {
55            asm!(
56                "mov r10, rcx",
57                "mov eax, {ssn:e}",
58                "syscall",
59                ssn = in(reg) self.ssn as u32,
60                out("eax") status,
61                out("rcx") _,
62                out("r10") _,
63                out("r11") _,
64                options(nostack)
65            );
66        }
67        status
68    }
69
70    /// invoke syscall with 1 argument
71    ///
72    /// # Safety
73    /// caller must ensure args are valid for this syscall
74    #[inline(never)]
75    pub unsafe fn call1(&self, arg1: usize) -> i32 {
76        let status: i32;
77        // SAFETY: caller guarantees syscall and argument validity
78        unsafe {
79            asm!(
80                "mov r10, rcx",
81                "mov eax, {ssn:e}",
82                "syscall",
83                ssn = in(reg) self.ssn as u32,
84                in("rcx") arg1,
85                out("eax") status,
86                out("r10") _,
87                out("r11") _,
88                options(nostack)
89            );
90        }
91        status
92    }
93
94    /// invoke syscall with 2 arguments
95    ///
96    /// # Safety
97    /// caller must ensure args are valid for this syscall
98    #[inline(never)]
99    pub unsafe fn call2(&self, arg1: usize, arg2: usize) -> i32 {
100        let status: i32;
101        // SAFETY: caller guarantees syscall and argument validity
102        unsafe {
103            asm!(
104                "mov r10, rcx",
105                "mov eax, {ssn:e}",
106                "syscall",
107                ssn = in(reg) self.ssn as u32,
108                in("rcx") arg1,
109                in("rdx") arg2,
110                out("eax") status,
111                out("r10") _,
112                out("r11") _,
113                options(nostack)
114            );
115        }
116        status
117    }
118
119    /// invoke syscall with 3 arguments
120    ///
121    /// # Safety
122    /// caller must ensure args are valid for this syscall
123    #[inline(never)]
124    pub unsafe fn call3(&self, arg1: usize, arg2: usize, arg3: usize) -> i32 {
125        let status: i32;
126        // SAFETY: caller guarantees syscall and argument validity
127        unsafe {
128            asm!(
129                "mov r10, rcx",
130                "mov eax, {ssn:e}",
131                "syscall",
132                ssn = in(reg) self.ssn as u32,
133                in("rcx") arg1,
134                in("rdx") arg2,
135                in("r8") arg3,
136                out("eax") status,
137                out("r10") _,
138                out("r11") _,
139                options(nostack)
140            );
141        }
142        status
143    }
144
145    /// invoke syscall with 4 arguments
146    ///
147    /// # Safety
148    /// caller must ensure args are valid for this syscall
149    #[inline(never)]
150    pub unsafe fn call4(&self, arg1: usize, arg2: usize, arg3: usize, arg4: usize) -> i32 {
151        let status: i32;
152        // SAFETY: caller guarantees syscall and argument validity
153        unsafe {
154            asm!(
155                "mov r10, rcx",
156                "mov eax, {ssn:e}",
157                "syscall",
158                ssn = in(reg) self.ssn as u32,
159                in("rcx") arg1,
160                in("rdx") arg2,
161                in("r8") arg3,
162                in("r9") arg4,
163                out("eax") status,
164                out("r10") _,
165                out("r11") _,
166                options(nostack)
167            );
168        }
169        status
170    }
171
172    /// invoke syscall with 5 arguments
173    ///
174    /// # Safety
175    /// caller must ensure args are valid for this syscall
176    #[inline(never)]
177    pub unsafe fn call5(
178        &self,
179        arg1: usize,
180        arg2: usize,
181        arg3: usize,
182        arg4: usize,
183        arg5: usize,
184    ) -> i32 {
185        let status: i32;
186        // SAFETY: caller guarantees syscall and argument validity
187        // 5th arg goes on stack at rsp+0x28 (after 32-byte shadow space)
188        unsafe {
189            asm!(
190                "sub rsp, 0x30",         // shadow space + arg5
191                "mov [rsp+0x28], {arg5}",
192                "mov r10, rcx",
193                "mov eax, {ssn:e}",
194                "syscall",
195                "add rsp, 0x30",
196                ssn = in(reg) self.ssn as u32,
197                arg5 = in(reg) arg5,
198                in("rcx") arg1,
199                in("rdx") arg2,
200                in("r8") arg3,
201                in("r9") arg4,
202                out("eax") status,
203                out("r10") _,
204                out("r11") _,
205            );
206        }
207        status
208    }
209
210    /// invoke syscall with 6 arguments
211    ///
212    /// # Safety
213    /// caller must ensure args are valid for this syscall
214    #[inline(never)]
215    pub unsafe fn call6(
216        &self,
217        arg1: usize,
218        arg2: usize,
219        arg3: usize,
220        arg4: usize,
221        arg5: usize,
222        arg6: usize,
223    ) -> i32 {
224        let status: i32;
225        // SAFETY: caller guarantees syscall and argument validity
226        // args 5-6 go on stack after shadow space
227        unsafe {
228            asm!(
229                "sub rsp, 0x38",         // shadow space + arg5 + arg6
230                "mov [rsp+0x28], {arg5}",
231                "mov [rsp+0x30], {arg6}",
232                "mov r10, rcx",
233                "mov eax, {ssn:e}",
234                "syscall",
235                "add rsp, 0x38",
236                ssn = in(reg) self.ssn as u32,
237                arg5 = in(reg) arg5,
238                arg6 = in(reg) arg6,
239                in("rcx") arg1,
240                in("rdx") arg2,
241                in("r8") arg3,
242                in("r9") arg4,
243                out("eax") status,
244                out("r10") _,
245                out("r11") _,
246            );
247        }
248        status
249    }
250
251    /// invoke syscall with variable arguments (uses array)
252    ///
253    /// supports up to 8 arguments
254    ///
255    /// # Safety
256    /// caller must ensure args are valid for this syscall
257    #[inline(never)]
258    pub unsafe fn call_many(&self, args: &[usize]) -> i32 {
259        match args.len() {
260            0 => unsafe { self.call0() },
261            1 => unsafe { self.call1(args[0]) },
262            2 => unsafe { self.call2(args[0], args[1]) },
263            3 => unsafe { self.call3(args[0], args[1], args[2]) },
264            4 => unsafe { self.call4(args[0], args[1], args[2], args[3]) },
265            5 => unsafe { self.call5(args[0], args[1], args[2], args[3], args[4]) },
266            6 => unsafe { self.call6(args[0], args[1], args[2], args[3], args[4], args[5]) },
267            _ => {
268                // for more args, use the 6-arg version and hope it's enough
269                // most syscalls don't need more than 6 args
270                unsafe { self.call6(args[0], args[1], args[2], args[3], args[4], args[5]) }
271            }
272        }
273    }
274}
275
276// x86 direct syscall implementations
277#[cfg(target_arch = "x86")]
278impl DirectSyscall {
279    /// invoke syscall with variable arguments (x86 uses stack for all args)
280    ///
281    /// # Safety
282    /// caller must ensure the syscall and args are valid
283    #[inline(never)]
284    pub unsafe fn call(&self, args: &[usize]) -> i32 {
285        // x86 uses int 0x2e with edx pointing to args on stack
286        let status: i32;
287        let args_ptr = args.as_ptr();
288
289        // SAFETY: caller guarantees syscall and argument validity
290        unsafe {
291            asm!(
292                "mov eax, {ssn:e}",
293                "mov edx, {args}",
294                "int 0x2e",
295                ssn = in(reg) self.ssn as u32,
296                args = in(reg) args_ptr,
297                out("eax") status,
298                options(nostack)
299            );
300        }
301        status
302    }
303
304    pub unsafe fn call0(&self) -> i32 {
305        unsafe { self.call(&[]) }
306    }
307
308    pub unsafe fn call1(&self, arg1: usize) -> i32 {
309        unsafe { self.call(&[arg1]) }
310    }
311
312    pub unsafe fn call2(&self, arg1: usize, arg2: usize) -> i32 {
313        unsafe { self.call(&[arg1, arg2]) }
314    }
315
316    pub unsafe fn call3(&self, arg1: usize, arg2: usize, arg3: usize) -> i32 {
317        unsafe { self.call(&[arg1, arg2, arg3]) }
318    }
319
320    pub unsafe fn call4(&self, arg1: usize, arg2: usize, arg3: usize, arg4: usize) -> i32 {
321        unsafe { self.call(&[arg1, arg2, arg3, arg4]) }
322    }
323
324    pub unsafe fn call5(
325        &self,
326        arg1: usize,
327        arg2: usize,
328        arg3: usize,
329        arg4: usize,
330        arg5: usize,
331    ) -> i32 {
332        unsafe { self.call(&[arg1, arg2, arg3, arg4, arg5]) }
333    }
334
335    pub unsafe fn call6(
336        &self,
337        arg1: usize,
338        arg2: usize,
339        arg3: usize,
340        arg4: usize,
341        arg5: usize,
342        arg6: usize,
343    ) -> i32 {
344        unsafe { self.call(&[arg1, arg2, arg3, arg4, arg5, arg6]) }
345    }
346
347    pub unsafe fn call_many(&self, args: &[usize]) -> i32 {
348        unsafe { self.call(args) }
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_direct_syscall_ntclose() {
358        let table = SyscallTable::enumerate().expect("should enumerate");
359        let syscall = DirectSyscall::from_table(&table, "NtClose").expect("should find NtClose");
360
361        // call with invalid handle - should fail but not crash
362        // SAFETY: NtClose with invalid handle is safe (returns error status)
363        let status = unsafe { syscall.call1(0xDEADBEEF) };
364
365        // STATUS_INVALID_HANDLE = 0xC0000008
366        assert_eq!(
367            status, 0xC0000008_u32 as i32,
368            "should return STATUS_INVALID_HANDLE"
369        );
370    }
371}