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