wraith/manipulation/inline_hook/hook/
mid.rs

1//! Mid-function hooks
2//!
3//! Hooks at arbitrary locations within a function, not just the prologue.
4//! Requires saving and restoring CPU context around the detour call.
5
6#[cfg(all(not(feature = "std"), feature = "alloc"))]
7use alloc::{format, string::String, vec::Vec};
8
9#[cfg(feature = "std")]
10use std::{format, string::String, vec::Vec};
11
12use crate::error::{Result, WraithError};
13use crate::util::memory::ProtectionGuard;
14use crate::manipulation::inline_hook::arch::Architecture;
15use crate::manipulation::inline_hook::guard::HookGuard;
16use crate::manipulation::inline_hook::trampoline::ExecutableMemory;
17use super::Hook;
18use core::marker::PhantomData;
19
20const PAGE_EXECUTE_READWRITE: u32 = 0x40;
21
22/// CPU context passed to mid-function hook handlers (x64)
23#[cfg(target_arch = "x86_64")]
24#[repr(C)]
25#[derive(Debug, Clone, Copy)]
26pub struct HookContext {
27    pub rflags: u64,
28    pub r15: u64,
29    pub r14: u64,
30    pub r13: u64,
31    pub r12: u64,
32    pub r11: u64,
33    pub r10: u64,
34    pub r9: u64,
35    pub r8: u64,
36    pub rdi: u64,
37    pub rsi: u64,
38    pub rbp: u64,
39    pub rsp: u64,
40    pub rbx: u64,
41    pub rdx: u64,
42    pub rcx: u64,
43    pub rax: u64,
44}
45
46/// CPU context passed to mid-function hook handlers (x86)
47#[cfg(target_arch = "x86")]
48#[repr(C)]
49#[derive(Debug, Clone, Copy)]
50pub struct HookContext {
51    pub eflags: u32,
52    pub edi: u32,
53    pub esi: u32,
54    pub ebp: u32,
55    pub esp: u32,
56    pub ebx: u32,
57    pub edx: u32,
58    pub ecx: u32,
59    pub eax: u32,
60}
61
62/// mid-function hook type signature
63#[cfg(target_arch = "x86_64")]
64pub type MidHookFn = extern "C" fn(ctx: *mut HookContext);
65
66#[cfg(target_arch = "x86")]
67pub type MidHookFn = extern "cdecl" fn(ctx: *mut HookContext);
68
69/// mid-function hook
70///
71/// hooks at an arbitrary location within a function.
72/// saves all registers, calls the detour with a context pointer,
73/// then restores registers and continues execution.
74pub struct MidFunctionHook<A: Architecture> {
75    /// address to hook (not necessarily function start)
76    address: usize,
77    /// detour function receiving context pointer
78    detour: usize,
79    _arch: PhantomData<A>,
80}
81
82impl<A: Architecture> MidFunctionHook<A> {
83    /// create a new mid-function hook
84    ///
85    /// # Arguments
86    /// * `address` - the exact address to place the hook
87    /// * `detour` - function pointer of type `MidHookFn`
88    pub fn new(address: usize, detour: MidHookFn) -> Self {
89        Self {
90            address,
91            detour: detour as usize,
92            _arch: PhantomData,
93        }
94    }
95
96    /// create from raw address
97    pub fn from_raw(address: usize, detour: usize) -> Self {
98        Self {
99            address,
100            detour,
101            _arch: PhantomData,
102        }
103    }
104
105    /// install the mid-function hook
106    #[cfg(target_arch = "x86_64")]
107    pub fn install(self) -> Result<HookGuard<A>> {
108        let min_hook_size = A::MIN_HOOK_SIZE;
109
110        // read original bytes at hook location
111        let original_bytes = unsafe {
112            let target_bytes = core::slice::from_raw_parts(self.address as *const u8, 64);
113            let boundary = A::find_instruction_boundary(target_bytes, min_hook_size)
114                .ok_or_else(|| WraithError::HookDetectionFailed {
115                    function: format!("{:#x}", self.address),
116                    reason: "failed to find instruction boundary".into(),
117                })?;
118            core::slice::from_raw_parts(self.address as *const u8, boundary).to_vec()
119        };
120
121        let prologue_size = original_bytes.len();
122
123        // allocate memory for the wrapper stub
124        let mut stub_memory = ExecutableMemory::allocate_near(self.address, 256)?;
125        let stub_base = stub_memory.base();
126
127        // build the context-saving wrapper stub
128        let wrapper_code = build_x64_wrapper(
129            stub_base,
130            self.detour,
131            self.address,
132            &original_bytes,
133        )?;
134
135        stub_memory.write(&wrapper_code)?;
136        stub_memory.flush_icache()?;
137
138        // write jump to wrapper at hook location
139        let hook_stub = A::encode_jmp_rel(self.address, stub_base)
140            .or_else(|| Some(A::encode_jmp_abs(stub_base)))
141            .unwrap();
142
143        let mut padded_stub = hook_stub;
144        if padded_stub.len() < prologue_size {
145            let padding = A::encode_nop_sled(prologue_size - padded_stub.len());
146            padded_stub.extend_from_slice(&padding);
147        }
148
149        // write the hook
150        {
151            let _guard = ProtectionGuard::new(
152                self.address,
153                prologue_size,
154                PAGE_EXECUTE_READWRITE,
155            )?;
156
157            unsafe {
158                core::ptr::copy_nonoverlapping(
159                    padded_stub.as_ptr(),
160                    self.address as *mut u8,
161                    prologue_size,
162                );
163            }
164        }
165
166        flush_icache(self.address, prologue_size)?;
167
168        Ok(HookGuard::new(
169            self.address,
170            self.detour,
171            original_bytes,
172            Some(stub_memory),
173        ))
174    }
175
176    #[cfg(target_arch = "x86")]
177    pub fn install(self) -> Result<HookGuard<A>> {
178        let min_hook_size = A::MIN_HOOK_SIZE;
179
180        let original_bytes = unsafe {
181            let target_bytes = core::slice::from_raw_parts(self.address as *const u8, 32);
182            let boundary = A::find_instruction_boundary(target_bytes, min_hook_size)
183                .ok_or_else(|| WraithError::HookDetectionFailed {
184                    function: format!("{:#x}", self.address),
185                    reason: "failed to find instruction boundary".into(),
186                })?;
187            core::slice::from_raw_parts(self.address as *const u8, boundary).to_vec()
188        };
189
190        let prologue_size = original_bytes.len();
191
192        let mut stub_memory = ExecutableMemory::allocate_near(self.address, 128)?;
193        let stub_base = stub_memory.base();
194
195        let wrapper_code = build_x86_wrapper(
196            stub_base,
197            self.detour,
198            self.address,
199            &original_bytes,
200        )?;
201
202        stub_memory.write(&wrapper_code)?;
203        stub_memory.flush_icache()?;
204
205        let hook_stub = A::encode_jmp_rel(self.address, stub_base)
206            .or_else(|| Some(A::encode_jmp_abs(stub_base)))
207            .unwrap();
208
209        let mut padded_stub = hook_stub;
210        if padded_stub.len() < prologue_size {
211            let padding = A::encode_nop_sled(prologue_size - padded_stub.len());
212            padded_stub.extend_from_slice(&padding);
213        }
214
215        {
216            let _guard = ProtectionGuard::new(
217                self.address,
218                prologue_size,
219                PAGE_EXECUTE_READWRITE,
220            )?;
221
222            unsafe {
223                core::ptr::copy_nonoverlapping(
224                    padded_stub.as_ptr(),
225                    self.address as *mut u8,
226                    prologue_size,
227                );
228            }
229        }
230
231        flush_icache(self.address, prologue_size)?;
232
233        Ok(HookGuard::new(
234            self.address,
235            self.detour,
236            original_bytes,
237            Some(stub_memory),
238        ))
239    }
240}
241
242impl<A: Architecture> Hook for MidFunctionHook<A> {
243    type Guard = HookGuard<A>;
244
245    fn install(self) -> Result<Self::Guard> {
246        MidFunctionHook::install(self)
247    }
248
249    fn target(&self) -> usize {
250        self.address
251    }
252
253    fn detour(&self) -> usize {
254        self.detour
255    }
256}
257
258/// build x64 context-saving wrapper
259#[cfg(target_arch = "x86_64")]
260fn build_x64_wrapper(
261    stub_base: usize,
262    detour: usize,
263    original_addr: usize,
264    original_bytes: &[u8],
265) -> Result<Vec<u8>> {
266    use crate::manipulation::inline_hook::arch::X64;
267
268    let mut code = Vec::with_capacity(256);
269
270    // save all registers (context structure)
271    // pushfq
272    code.push(0x9C);
273
274    // push r15-r8, rdi, rsi, rbp, rsp placeholder, rbx, rdx, rcx, rax
275    code.extend_from_slice(&[0x41, 0x57]); // push r15
276    code.extend_from_slice(&[0x41, 0x56]); // push r14
277    code.extend_from_slice(&[0x41, 0x55]); // push r13
278    code.extend_from_slice(&[0x41, 0x54]); // push r12
279    code.extend_from_slice(&[0x41, 0x53]); // push r11
280    code.extend_from_slice(&[0x41, 0x52]); // push r10
281    code.extend_from_slice(&[0x41, 0x51]); // push r9
282    code.extend_from_slice(&[0x41, 0x50]); // push r8
283    code.push(0x57); // push rdi
284    code.push(0x56); // push rsi
285    code.push(0x55); // push rbp
286    code.push(0x54); // push rsp (placeholder, will be fixed)
287    code.push(0x53); // push rbx
288    code.push(0x52); // push rdx
289    code.push(0x51); // push rcx
290    code.push(0x50); // push rax
291
292    // fix rsp in context (add offset for pushes)
293    // mov [rsp + 0x38], rsp; add qword ptr [rsp+0x38], 0x90
294    code.extend_from_slice(&[0x48, 0x89, 0x64, 0x24, 0x38]); // mov [rsp+0x38], rsp
295    code.extend_from_slice(&[0x48, 0x83, 0x44, 0x24, 0x38, 0x90]); // add [rsp+0x38], 0x90
296
297    // mov rcx, rsp (first arg = context pointer)
298    code.extend_from_slice(&[0x48, 0x89, 0xE1]);
299
300    // sub rsp, 0x28 (shadow space + alignment)
301    code.extend_from_slice(&[0x48, 0x83, 0xEC, 0x28]);
302
303    // call detour
304    // we'll use mov rax, imm64; call rax for simplicity
305    code.extend_from_slice(&[0x48, 0xB8]); // mov rax, imm64
306    code.extend_from_slice(&detour.to_le_bytes());
307    code.extend_from_slice(&[0xFF, 0xD0]); // call rax
308
309    // add rsp, 0x28
310    code.extend_from_slice(&[0x48, 0x83, 0xC4, 0x28]);
311
312    // restore registers
313    code.push(0x58); // pop rax
314    code.push(0x59); // pop rcx
315    code.push(0x5A); // pop rdx
316    code.push(0x5B); // pop rbx
317    code.extend_from_slice(&[0x48, 0x83, 0xC4, 0x08]); // add rsp, 8 (skip rsp)
318    code.push(0x5D); // pop rbp
319    code.push(0x5E); // pop rsi
320    code.push(0x5F); // pop rdi
321    code.extend_from_slice(&[0x41, 0x58]); // pop r8
322    code.extend_from_slice(&[0x41, 0x59]); // pop r9
323    code.extend_from_slice(&[0x41, 0x5A]); // pop r10
324    code.extend_from_slice(&[0x41, 0x5B]); // pop r11
325    code.extend_from_slice(&[0x41, 0x5C]); // pop r12
326    code.extend_from_slice(&[0x41, 0x5D]); // pop r13
327    code.extend_from_slice(&[0x41, 0x5E]); // pop r14
328    code.extend_from_slice(&[0x41, 0x5F]); // pop r15
329    code.push(0x9D); // popfq
330
331    // relocate and copy original instructions
332    // we need to track the current position in the stub for relocation calculations
333    let mut src_offset = 0;
334
335    while src_offset < original_bytes.len() {
336        let remaining = &original_bytes[src_offset..];
337
338        // find instruction length
339        let insn_len = X64::find_instruction_boundary(remaining, 1)
340            .ok_or_else(|| WraithError::HookDetectionFailed {
341                function: format!("{:#x}", original_addr + src_offset),
342                reason: "failed to decode instruction during relocation".into(),
343            })?;
344
345        let instruction = &original_bytes[src_offset..src_offset + insn_len];
346        let old_addr = original_addr + src_offset;
347        let new_addr = stub_base + code.len();
348
349        // check if instruction needs relocation
350        if X64::needs_relocation(instruction) {
351            match X64::relocate_instruction(instruction, old_addr, new_addr) {
352                Some(relocated) => {
353                    code.extend_from_slice(&relocated);
354                }
355                None => {
356                    // relocation failed - instruction target is too far
357                    // fall back to indirect: push target; ret pattern
358                    return Err(WraithError::RelocationFailed {
359                        rva: src_offset as u32,
360                        reason: format!(
361                            "instruction at {:#x} cannot be relocated to {:#x}",
362                            old_addr, new_addr
363                        ),
364                    });
365                }
366            }
367        } else {
368            // no relocation needed, copy as-is
369            code.extend_from_slice(instruction);
370        }
371
372        src_offset += insn_len;
373    }
374
375    // jump to continuation (original_addr + original_bytes.len())
376    let continuation = original_addr + original_bytes.len();
377    let jmp_location = stub_base + code.len();
378
379    if let Some(jmp) = X64::encode_jmp_rel(jmp_location, continuation) {
380        code.extend_from_slice(&jmp);
381    } else {
382        code.extend_from_slice(&X64::encode_jmp_abs(continuation));
383    }
384
385    Ok(code)
386}
387
388/// build x86 context-saving wrapper
389#[cfg(target_arch = "x86")]
390fn build_x86_wrapper(
391    stub_base: usize,
392    detour: usize,
393    original_addr: usize,
394    original_bytes: &[u8],
395) -> Result<Vec<u8>> {
396    use crate::manipulation::inline_hook::arch::X86;
397
398    let mut code = Vec::with_capacity(128);
399
400    // pushfd
401    code.push(0x9C);
402    // pushad (edi, esi, ebp, esp, ebx, edx, ecx, eax)
403    code.push(0x60);
404
405    // push esp (context pointer as argument)
406    code.push(0x54);
407
408    // call detour
409    code.push(0xB8); // mov eax, imm32
410    code.extend_from_slice(&(detour as u32).to_le_bytes());
411    code.extend_from_slice(&[0xFF, 0xD0]); // call eax
412
413    // add esp, 4 (pop argument)
414    code.extend_from_slice(&[0x83, 0xC4, 0x04]);
415
416    // popad
417    code.push(0x61);
418    // popfd
419    code.push(0x9D);
420
421    // relocate and copy original instructions
422    let mut src_offset = 0;
423
424    while src_offset < original_bytes.len() {
425        let remaining = &original_bytes[src_offset..];
426
427        // find instruction length
428        let insn_len = X86::find_instruction_boundary(remaining, 1)
429            .ok_or_else(|| WraithError::HookDetectionFailed {
430                function: format!("{:#x}", original_addr + src_offset),
431                reason: "failed to decode instruction during relocation".into(),
432            })?;
433
434        let instruction = &original_bytes[src_offset..src_offset + insn_len];
435        let old_addr = original_addr + src_offset;
436        let new_addr = stub_base + code.len();
437
438        // check if instruction needs relocation
439        if X86::needs_relocation(instruction) {
440            match X86::relocate_instruction(instruction, old_addr, new_addr) {
441                Some(relocated) => {
442                    code.extend_from_slice(&relocated);
443                }
444                None => {
445                    return Err(WraithError::RelocationFailed {
446                        rva: src_offset as u32,
447                        reason: format!(
448                            "instruction at {:#x} cannot be relocated to {:#x}",
449                            old_addr, new_addr
450                        ),
451                    });
452                }
453            }
454        } else {
455            // no relocation needed, copy as-is
456            code.extend_from_slice(instruction);
457        }
458
459        src_offset += insn_len;
460    }
461
462    // jump to continuation
463    let continuation = original_addr + original_bytes.len();
464    let jmp_location = stub_base + code.len();
465
466    if let Some(jmp) = X86::encode_jmp_rel(jmp_location, continuation) {
467        code.extend_from_slice(&jmp);
468    } else {
469        code.extend_from_slice(&X86::encode_jmp_abs(continuation as usize));
470    }
471
472    Ok(code)
473}
474
475fn flush_icache(address: usize, size: usize) -> Result<()> {
476    let result = unsafe {
477        FlushInstructionCache(
478            GetCurrentProcess(),
479            address as *const _,
480            size,
481        )
482    };
483
484    if result == 0 {
485        Err(WraithError::from_last_error("FlushInstructionCache"))
486    } else {
487        Ok(())
488    }
489}
490
491#[link(name = "kernel32")]
492extern "system" {
493    fn FlushInstructionCache(
494        hProcess: *mut core::ffi::c_void,
495        lpBaseAddress: *const core::ffi::c_void,
496        dwSize: usize,
497    ) -> i32;
498
499    fn GetCurrentProcess() -> *mut core::ffi::c_void;
500}