wraith/manipulation/remote/
handle.rs

1//! Handle duplication and stealing operations
2
3use crate::error::{Result, WraithError};
4use crate::manipulation::syscall::{
5    get_syscall_table, nt_close, nt_success, DirectSyscall,
6};
7
8/// options for handle duplication
9#[derive(Debug, Clone, Copy)]
10pub struct HandleDuplicateOptions {
11    /// desired access rights for the duplicated handle
12    pub desired_access: u32,
13    /// handle attributes (e.g., OBJ_INHERIT)
14    pub attributes: u32,
15    /// options (e.g., DUPLICATE_SAME_ACCESS, DUPLICATE_CLOSE_SOURCE)
16    pub options: u32,
17}
18
19impl Default for HandleDuplicateOptions {
20    fn default() -> Self {
21        Self {
22            desired_access: 0,
23            attributes: 0,
24            options: DUPLICATE_SAME_ACCESS,
25        }
26    }
27}
28
29impl HandleDuplicateOptions {
30    /// duplicate with same access rights
31    pub fn same_access() -> Self {
32        Self::default()
33    }
34
35    /// duplicate and close the source handle
36    pub fn close_source() -> Self {
37        Self {
38            desired_access: 0,
39            attributes: 0,
40            options: DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE,
41        }
42    }
43
44    /// duplicate with specific access rights
45    pub fn with_access(access: u32) -> Self {
46        Self {
47            desired_access: access,
48            attributes: 0,
49            options: 0,
50        }
51    }
52}
53
54/// information about a handle in a process
55#[derive(Debug, Clone)]
56pub struct HandleInfo {
57    pub handle_value: usize,
58    pub object_type: u32,
59    pub granted_access: u32,
60    pub object_name: Option<String>,
61}
62
63/// wrapper for a stolen/duplicated handle
64pub struct StolenHandle {
65    handle: usize,
66    owns_handle: bool,
67}
68
69impl StolenHandle {
70    /// get the raw handle value
71    pub fn handle(&self) -> usize {
72        self.handle
73    }
74
75    /// release ownership of the handle (don't close on drop)
76    pub fn leak(mut self) -> usize {
77        self.owns_handle = false;
78        self.handle
79    }
80
81    /// create from raw handle value
82    ///
83    /// # Safety
84    /// caller must ensure handle is valid
85    pub unsafe fn from_raw(handle: usize) -> Self {
86        Self {
87            handle,
88            owns_handle: true,
89        }
90    }
91
92    /// create without ownership
93    ///
94    /// # Safety
95    /// caller must ensure handle is valid
96    pub unsafe fn from_raw_borrowed(handle: usize) -> Self {
97        Self {
98            handle,
99            owns_handle: false,
100        }
101    }
102}
103
104impl Drop for StolenHandle {
105    fn drop(&mut self) {
106        if self.owns_handle && self.handle != 0 {
107            let _ = nt_close(self.handle);
108        }
109    }
110}
111
112/// duplicate a handle from one process to another
113pub fn duplicate_handle(
114    source_process: usize,
115    source_handle: usize,
116    target_process: usize,
117    options: HandleDuplicateOptions,
118) -> Result<StolenHandle> {
119    let table = get_syscall_table()?;
120    let syscall = DirectSyscall::from_table(table, "NtDuplicateObject")?;
121
122    let mut target_handle: usize = 0;
123
124    // SAFETY: all handles are assumed valid by caller
125    let status = unsafe {
126        syscall.call_many(&[
127            source_process,
128            source_handle,
129            target_process,
130            &mut target_handle as *mut usize as usize,
131            options.desired_access as usize,
132            options.attributes as usize,
133            options.options as usize,
134        ])
135    };
136
137    if nt_success(status) {
138        Ok(StolenHandle {
139            handle: target_handle,
140            owns_handle: true,
141        })
142    } else {
143        Err(WraithError::HandleDuplicateFailed {
144            reason: format!("NtDuplicateObject failed: {:#x}", status as u32),
145        })
146    }
147}
148
149/// steal a handle from a remote process to the current process
150pub fn steal_handle(
151    source_process: usize,
152    remote_handle: usize,
153    options: HandleDuplicateOptions,
154) -> Result<StolenHandle> {
155    let current_process: usize = usize::MAX; // pseudo handle
156    duplicate_handle(source_process, remote_handle, current_process, options)
157}
158
159/// enumerate handles in the system
160///
161/// returns handles matching optional filter criteria
162pub fn enumerate_system_handles(
163    process_id_filter: Option<u32>,
164    object_type_filter: Option<u32>,
165) -> Result<Vec<SystemHandleEntry>> {
166    let table = get_syscall_table()?;
167    let syscall = DirectSyscall::from_table(table, "NtQuerySystemInformation")?;
168
169    const SYSTEM_HANDLE_INFORMATION: u32 = 16;
170    const SYSTEM_EXTENDED_HANDLE_INFORMATION: u32 = 64;
171
172    // start with 1MB buffer, grow if needed
173    let mut buffer_size: usize = 1024 * 1024;
174    let mut buffer: Vec<u8>;
175    let mut return_length: u32 = 0;
176
177    loop {
178        buffer = vec![0u8; buffer_size];
179
180        let status = unsafe {
181            syscall.call4(
182                SYSTEM_EXTENDED_HANDLE_INFORMATION as usize,
183                buffer.as_mut_ptr() as usize,
184                buffer.len(),
185                &mut return_length as *mut u32 as usize,
186            )
187        };
188
189        if nt_success(status) {
190            break;
191        }
192
193        // STATUS_INFO_LENGTH_MISMATCH
194        if status == 0xC0000004_u32 as i32 {
195            buffer_size = return_length as usize + 0x10000;
196            if buffer_size > 256 * 1024 * 1024 {
197                return Err(WraithError::HandleDuplicateFailed {
198                    reason: "buffer too large".into(),
199                });
200            }
201            continue;
202        }
203
204        return Err(WraithError::HandleDuplicateFailed {
205            reason: format!("NtQuerySystemInformation failed: {:#x}", status as u32),
206        });
207    }
208
209    // parse the handle information
210    parse_handle_info(&buffer, process_id_filter, object_type_filter)
211}
212
213#[repr(C)]
214struct SystemHandleTableEntryInfoEx {
215    object: usize,
216    unique_process_id: usize,
217    handle_value: usize,
218    granted_access: u32,
219    creator_back_trace_index: u16,
220    object_type_index: u16,
221    handle_attributes: u32,
222    reserved: u32,
223}
224
225/// entry from system handle enumeration
226#[derive(Debug, Clone)]
227pub struct SystemHandleEntry {
228    pub process_id: u32,
229    pub handle_value: usize,
230    pub object_type: u16,
231    pub granted_access: u32,
232    pub object_address: usize,
233}
234
235fn parse_handle_info(
236    buffer: &[u8],
237    process_id_filter: Option<u32>,
238    object_type_filter: Option<u32>,
239) -> Result<Vec<SystemHandleEntry>> {
240    if buffer.len() < 16 {
241        return Ok(Vec::new());
242    }
243
244    // first usize is number of handles
245    let count = unsafe { *(buffer.as_ptr() as *const usize) };
246    if count == 0 || count > 10_000_000 {
247        return Ok(Vec::new());
248    }
249
250    let entry_size = core::mem::size_of::<SystemHandleTableEntryInfoEx>();
251    let entries_start = 2 * core::mem::size_of::<usize>(); // skip count and reserved
252
253    let mut handles = Vec::new();
254
255    for i in 0..count {
256        let offset = entries_start + i * entry_size;
257        if offset + entry_size > buffer.len() {
258            break;
259        }
260
261        let entry = unsafe {
262            &*(buffer.as_ptr().add(offset) as *const SystemHandleTableEntryInfoEx)
263        };
264
265        // apply filters
266        if let Some(pid) = process_id_filter {
267            if entry.unique_process_id != pid as usize {
268                continue;
269            }
270        }
271
272        if let Some(obj_type) = object_type_filter {
273            if entry.object_type_index as u32 != obj_type {
274                continue;
275            }
276        }
277
278        handles.push(SystemHandleEntry {
279            process_id: entry.unique_process_id as u32,
280            handle_value: entry.handle_value,
281            object_type: entry.object_type_index,
282            granted_access: entry.granted_access,
283            object_address: entry.object,
284        });
285    }
286
287    Ok(handles)
288}
289
290/// find handles of a specific type in a target process
291pub fn find_handles_in_process(
292    target_pid: u32,
293    object_type: Option<u32>,
294) -> Result<Vec<SystemHandleEntry>> {
295    enumerate_system_handles(Some(target_pid), object_type)
296}
297
298/// steal a process handle from another process
299///
300/// useful for bypassing process protection by duplicating an existing handle
301pub fn steal_process_handle(
302    source_process_handle: usize,
303    remote_handle: usize,
304) -> Result<StolenHandle> {
305    steal_handle(source_process_handle, remote_handle, HandleDuplicateOptions::same_access())
306}
307
308/// object types for filtering
309pub mod object_types {
310    pub const PROCESS: u32 = 7;
311    pub const THREAD: u32 = 8;
312    pub const FILE: u32 = 31; // varies by Windows version
313    pub const SECTION: u32 = 43; // varies by Windows version
314    pub const KEY: u32 = 19; // registry key
315}
316
317// duplicate options
318const DUPLICATE_CLOSE_SOURCE: u32 = 0x00000001;
319const DUPLICATE_SAME_ACCESS: u32 = 0x00000002;
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_handle_duplicate_options() {
327        let opts = HandleDuplicateOptions::same_access();
328        assert!(opts.options & DUPLICATE_SAME_ACCESS != 0);
329
330        let opts = HandleDuplicateOptions::close_source();
331        assert!(opts.options & DUPLICATE_CLOSE_SOURCE != 0);
332        assert!(opts.options & DUPLICATE_SAME_ACCESS != 0);
333    }
334
335    #[test]
336    fn test_enumerate_own_handles() {
337        let pid = std::process::id();
338        let result = find_handles_in_process(pid, None);
339        assert!(result.is_ok());
340
341        let handles = result.unwrap();
342        // we should have at least some handles
343        assert!(!handles.is_empty(), "should have handles");
344    }
345}