wraith/manipulation/syscall/
wrappers.rs

1//! Typed wrappers for common syscalls
2//!
3//! Provides safe-ish Rust interfaces for frequently used NT syscalls,
4//! handling argument marshaling and error checking.
5//!
6//! Note: These wrappers require the `std` feature for the global syscall table.
7
8#[cfg(feature = "std")]
9use super::get_syscall_table;
10use super::{nt_success, DirectSyscall};
11use crate::error::{Result, WraithError};
12
13// NT structures for syscall arguments
14
15/// OBJECT_ATTRIBUTES structure
16#[repr(C)]
17pub struct ObjectAttributes {
18    pub length: u32,
19    pub root_directory: usize,
20    pub object_name: *const UnicodeString,
21    pub attributes: u32,
22    pub security_descriptor: *const core::ffi::c_void,
23    pub security_quality_of_service: *const core::ffi::c_void,
24}
25
26impl Default for ObjectAttributes {
27    fn default() -> Self {
28        Self {
29            length: core::mem::size_of::<Self>() as u32,
30            root_directory: 0,
31            object_name: core::ptr::null(),
32            attributes: 0,
33            security_descriptor: core::ptr::null(),
34            security_quality_of_service: core::ptr::null(),
35        }
36    }
37}
38
39impl ObjectAttributes {
40    /// create empty OBJECT_ATTRIBUTES
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// create with OBJ_CASE_INSENSITIVE
46    pub fn case_insensitive() -> Self {
47        Self {
48            attributes: OBJ_CASE_INSENSITIVE,
49            ..Self::default()
50        }
51    }
52}
53
54/// CLIENT_ID structure
55#[repr(C)]
56#[derive(Debug, Clone, Copy, Default)]
57pub struct ClientId {
58    pub unique_process: usize,
59    pub unique_thread: usize,
60}
61
62impl ClientId {
63    /// create for a process ID
64    pub fn for_process(pid: u32) -> Self {
65        Self {
66            unique_process: pid as usize,
67            unique_thread: 0,
68        }
69    }
70
71    /// create for a thread ID
72    pub fn for_thread(tid: u32) -> Self {
73        Self {
74            unique_process: 0,
75            unique_thread: tid as usize,
76        }
77    }
78}
79
80/// UNICODE_STRING structure
81#[repr(C)]
82pub struct UnicodeString {
83    pub length: u16,
84    pub maximum_length: u16,
85    pub buffer: *const u16,
86}
87
88// object attribute flags
89pub const OBJ_CASE_INSENSITIVE: u32 = 0x00000040;
90pub const OBJ_INHERIT: u32 = 0x00000002;
91
92// process access rights
93pub const PROCESS_ALL_ACCESS: u32 = 0x1F0FFF;
94pub const PROCESS_VM_READ: u32 = 0x0010;
95pub const PROCESS_VM_WRITE: u32 = 0x0020;
96pub const PROCESS_VM_OPERATION: u32 = 0x0008;
97pub const PROCESS_QUERY_INFORMATION: u32 = 0x0400;
98pub const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
99
100// thread access rights
101pub const THREAD_ALL_ACCESS: u32 = 0x1F03FF;
102pub const THREAD_SET_INFORMATION: u32 = 0x0020;
103pub const THREAD_QUERY_INFORMATION: u32 = 0x0040;
104
105// thread information classes
106pub const THREAD_HIDE_FROM_DEBUGGER: u32 = 17;
107
108// allocation types
109pub const MEM_COMMIT: u32 = 0x1000;
110pub const MEM_RESERVE: u32 = 0x2000;
111pub const MEM_RELEASE: u32 = 0x8000;
112
113// protection flags
114pub const PAGE_NOACCESS: u32 = 0x01;
115pub const PAGE_READONLY: u32 = 0x02;
116pub const PAGE_READWRITE: u32 = 0x04;
117pub const PAGE_WRITECOPY: u32 = 0x08;
118pub const PAGE_EXECUTE: u32 = 0x10;
119pub const PAGE_EXECUTE_READ: u32 = 0x20;
120pub const PAGE_EXECUTE_READWRITE: u32 = 0x40;
121pub const PAGE_EXECUTE_WRITECOPY: u32 = 0x80;
122pub const PAGE_GUARD: u32 = 0x100;
123
124// pseudo handles
125pub const CURRENT_PROCESS: usize = usize::MAX; // -1
126pub const CURRENT_THREAD: usize = usize::MAX - 1; // -2
127
128// NtClose - close a handle
129pub fn nt_close(handle: usize) -> Result<()> {
130    let table = get_syscall_table()?;
131    let syscall = DirectSyscall::from_table(table, "NtClose")?;
132
133    // SAFETY: NtClose is safe to call with any handle value
134    let status = unsafe { syscall.call1(handle) };
135
136    if nt_success(status) {
137        Ok(())
138    } else {
139        Err(WraithError::SyscallFailed {
140            name: "NtClose".into(),
141            status,
142        })
143    }
144}
145
146/// NtOpenProcess - open a process handle
147pub fn nt_open_process(
148    desired_access: u32,
149    object_attributes: &ObjectAttributes,
150    client_id: &ClientId,
151) -> Result<usize> {
152    let mut handle: usize = 0;
153
154    let table = get_syscall_table()?;
155    let syscall = DirectSyscall::from_table(table, "NtOpenProcess")?;
156
157    // SAFETY: all pointers point to valid stack data
158    let status = unsafe {
159        syscall.call4(
160            &mut handle as *mut usize as usize,
161            desired_access as usize,
162            object_attributes as *const _ as usize,
163            client_id as *const _ as usize,
164        )
165    };
166
167    if nt_success(status) {
168        Ok(handle)
169    } else {
170        Err(WraithError::SyscallFailed {
171            name: "NtOpenProcess".into(),
172            status,
173        })
174    }
175}
176
177/// NtReadVirtualMemory - read memory from a process
178pub fn nt_read_virtual_memory(
179    process_handle: usize,
180    base_address: usize,
181    buffer: &mut [u8],
182) -> Result<usize> {
183    let mut bytes_read: usize = 0;
184
185    let table = get_syscall_table()?;
186    let syscall = DirectSyscall::from_table(table, "NtReadVirtualMemory")?;
187
188    // SAFETY: buffer is valid and properly sized
189    let status = unsafe {
190        syscall.call5(
191            process_handle,
192            base_address,
193            buffer.as_mut_ptr() as usize,
194            buffer.len(),
195            &mut bytes_read as *mut usize as usize,
196        )
197    };
198
199    if nt_success(status) {
200        Ok(bytes_read)
201    } else {
202        Err(WraithError::SyscallFailed {
203            name: "NtReadVirtualMemory".into(),
204            status,
205        })
206    }
207}
208
209/// NtWriteVirtualMemory - write memory to a process
210pub fn nt_write_virtual_memory(
211    process_handle: usize,
212    base_address: usize,
213    buffer: &[u8],
214) -> Result<usize> {
215    let mut bytes_written: usize = 0;
216
217    let table = get_syscall_table()?;
218    let syscall = DirectSyscall::from_table(table, "NtWriteVirtualMemory")?;
219
220    // SAFETY: buffer is valid and properly sized
221    let status = unsafe {
222        syscall.call5(
223            process_handle,
224            base_address,
225            buffer.as_ptr() as usize,
226            buffer.len(),
227            &mut bytes_written as *mut usize as usize,
228        )
229    };
230
231    if nt_success(status) {
232        Ok(bytes_written)
233    } else {
234        Err(WraithError::SyscallFailed {
235            name: "NtWriteVirtualMemory".into(),
236            status,
237        })
238    }
239}
240
241/// NtAllocateVirtualMemory - allocate memory in a process
242pub fn nt_allocate_virtual_memory(
243    process_handle: usize,
244    preferred_base: usize,
245    size: usize,
246    allocation_type: u32,
247    protect: u32,
248) -> Result<(usize, usize)> {
249    let mut base_address = preferred_base;
250    let mut region_size = size;
251
252    let table = get_syscall_table()?;
253    let syscall = DirectSyscall::from_table(table, "NtAllocateVirtualMemory")?;
254
255    // SAFETY: pointers are valid stack addresses
256    let status = unsafe {
257        syscall.call6(
258            process_handle,
259            &mut base_address as *mut usize as usize,
260            0, // zero_bits
261            &mut region_size as *mut usize as usize,
262            allocation_type as usize,
263            protect as usize,
264        )
265    };
266
267    if nt_success(status) {
268        Ok((base_address, region_size))
269    } else {
270        Err(WraithError::SyscallFailed {
271            name: "NtAllocateVirtualMemory".into(),
272            status,
273        })
274    }
275}
276
277/// NtFreeVirtualMemory - free memory in a process
278pub fn nt_free_virtual_memory(
279    process_handle: usize,
280    base_address: usize,
281    free_type: u32,
282) -> Result<()> {
283    let mut base = base_address;
284    let mut size: usize = 0;
285
286    let table = get_syscall_table()?;
287    let syscall = DirectSyscall::from_table(table, "NtFreeVirtualMemory")?;
288
289    // SAFETY: pointers are valid stack addresses
290    let status = unsafe {
291        syscall.call4(
292            process_handle,
293            &mut base as *mut usize as usize,
294            &mut size as *mut usize as usize,
295            free_type as usize,
296        )
297    };
298
299    if nt_success(status) {
300        Ok(())
301    } else {
302        Err(WraithError::SyscallFailed {
303            name: "NtFreeVirtualMemory".into(),
304            status,
305        })
306    }
307}
308
309/// NtProtectVirtualMemory - change memory protection
310pub fn nt_protect_virtual_memory(
311    process_handle: usize,
312    base_address: usize,
313    size: usize,
314    new_protect: u32,
315) -> Result<u32> {
316    let mut base = base_address;
317    let mut region_size = size;
318    let mut old_protect: u32 = 0;
319
320    let table = get_syscall_table()?;
321    let syscall = DirectSyscall::from_table(table, "NtProtectVirtualMemory")?;
322
323    // SAFETY: pointers are valid stack addresses
324    let status = unsafe {
325        syscall.call5(
326            process_handle,
327            &mut base as *mut usize as usize,
328            &mut region_size as *mut usize as usize,
329            new_protect as usize,
330            &mut old_protect as *mut u32 as usize,
331        )
332    };
333
334    if nt_success(status) {
335        Ok(old_protect)
336    } else {
337        Err(WraithError::SyscallFailed {
338            name: "NtProtectVirtualMemory".into(),
339            status,
340        })
341    }
342}
343
344/// NtSetInformationThread - set thread information
345///
346/// commonly used with ThreadHideFromDebugger (17) to hide threads from debuggers
347pub fn nt_set_information_thread(
348    thread_handle: usize,
349    information_class: u32,
350    thread_information: *const core::ffi::c_void,
351    thread_information_length: u32,
352) -> Result<()> {
353    let table = get_syscall_table()?;
354    let syscall = DirectSyscall::from_table(table, "NtSetInformationThread")?;
355
356    // SAFETY: caller is responsible for valid thread_information pointer
357    let status = unsafe {
358        syscall.call4(
359            thread_handle,
360            information_class as usize,
361            thread_information as usize,
362            thread_information_length as usize,
363        )
364    };
365
366    if nt_success(status) {
367        Ok(())
368    } else {
369        Err(WraithError::SyscallFailed {
370            name: "NtSetInformationThread".into(),
371            status,
372        })
373    }
374}
375
376/// hide current thread from debugger
377pub fn hide_thread_from_debugger() -> Result<()> {
378    nt_set_information_thread(
379        CURRENT_THREAD,
380        THREAD_HIDE_FROM_DEBUGGER,
381        core::ptr::null(),
382        0,
383    )
384}
385
386/// NtQuerySystemInformation - query system information
387pub fn nt_query_system_information(
388    information_class: u32,
389    buffer: &mut [u8],
390) -> Result<u32> {
391    let mut return_length: u32 = 0;
392
393    let table = get_syscall_table()?;
394    let syscall = DirectSyscall::from_table(table, "NtQuerySystemInformation")?;
395
396    // SAFETY: buffer is valid and properly sized
397    let status = unsafe {
398        syscall.call4(
399            information_class as usize,
400            buffer.as_mut_ptr() as usize,
401            buffer.len(),
402            &mut return_length as *mut u32 as usize,
403        )
404    };
405
406    if nt_success(status) {
407        Ok(return_length)
408    } else {
409        Err(WraithError::SyscallFailed {
410            name: "NtQuerySystemInformation".into(),
411            status,
412        })
413    }
414}
415
416/// NtQueryInformationProcess - query process information
417pub fn nt_query_information_process(
418    process_handle: usize,
419    information_class: u32,
420    buffer: &mut [u8],
421) -> Result<u32> {
422    let mut return_length: u32 = 0;
423
424    let table = get_syscall_table()?;
425    let syscall = DirectSyscall::from_table(table, "NtQueryInformationProcess")?;
426
427    // SAFETY: buffer is valid and properly sized
428    let status = unsafe {
429        syscall.call5(
430            process_handle,
431            information_class as usize,
432            buffer.as_mut_ptr() as usize,
433            buffer.len(),
434            &mut return_length as *mut u32 as usize,
435        )
436    };
437
438    if nt_success(status) {
439        Ok(return_length)
440    } else {
441        Err(WraithError::SyscallFailed {
442            name: "NtQueryInformationProcess".into(),
443            status,
444        })
445    }
446}
447
448/// NtQueryVirtualMemory - query virtual memory information
449pub fn nt_query_virtual_memory(
450    process_handle: usize,
451    base_address: usize,
452    information_class: u32,
453    buffer: &mut [u8],
454) -> Result<usize> {
455    let mut return_length: usize = 0;
456
457    let table = get_syscall_table()?;
458    let syscall = DirectSyscall::from_table(table, "NtQueryVirtualMemory")?;
459
460    // SAFETY: buffer is valid and properly sized
461    let status = unsafe {
462        syscall.call6(
463            process_handle,
464            base_address,
465            information_class as usize,
466            buffer.as_mut_ptr() as usize,
467            buffer.len(),
468            &mut return_length as *mut usize as usize,
469        )
470    };
471
472    if nt_success(status) {
473        Ok(return_length)
474    } else {
475        Err(WraithError::SyscallFailed {
476            name: "NtQueryVirtualMemory".into(),
477            status,
478        })
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    #[test]
487    fn test_nt_close_invalid() {
488        let result = nt_close(0xDEADBEEF);
489        assert!(result.is_err());
490    }
491
492    #[test]
493    fn test_allocate_and_free() {
494        // allocate some memory in current process
495        let result = nt_allocate_virtual_memory(
496            CURRENT_PROCESS,
497            0, // let OS choose address
498            4096,
499            MEM_COMMIT | MEM_RESERVE,
500            PAGE_READWRITE,
501        );
502
503        if let Ok((base, _size)) = result {
504            assert!(base != 0, "should have allocated memory");
505
506            // free it
507            let free_result = nt_free_virtual_memory(CURRENT_PROCESS, base, MEM_RELEASE);
508            assert!(free_result.is_ok(), "should free memory");
509        }
510    }
511
512    #[test]
513    fn test_protect_memory() {
514        // allocate memory
515        let (base, _) = nt_allocate_virtual_memory(
516            CURRENT_PROCESS,
517            0,
518            4096,
519            MEM_COMMIT | MEM_RESERVE,
520            PAGE_READWRITE,
521        )
522        .expect("should allocate");
523
524        // change protection
525        let old = nt_protect_virtual_memory(CURRENT_PROCESS, base, 4096, PAGE_READONLY)
526            .expect("should change protection");
527
528        assert_eq!(old, PAGE_READWRITE);
529
530        // cleanup
531        let _ = nt_free_virtual_memory(CURRENT_PROCESS, base, MEM_RELEASE);
532    }
533}