wraith/manipulation/inline_hook/
builder.rs

1//! Type-state hook builder
2//!
3//! Provides a compile-time safe builder pattern for creating hooks,
4//! similar to ManualMapper in the manual_map module.
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::manipulation::inline_hook::arch::Architecture;
14use crate::manipulation::inline_hook::guard::HookGuard;
15use crate::manipulation::inline_hook::trampoline::ExecutableMemory;
16use crate::util::memory::ProtectionGuard;
17use core::marker::PhantomData;
18
19const PAGE_EXECUTE_READWRITE: u32 = 0x40;
20
21/// type-state markers for hook building stages
22pub mod state {
23    /// initial state - no target set
24    pub struct Uninitialized;
25    /// target function has been set
26    pub struct TargetSet;
27    /// detour function has been set
28    pub struct DetourSet;
29    /// trampoline has been allocated
30    pub struct TrampolineAllocated;
31    /// trampoline has been built
32    pub struct TrampolineBuilt;
33    /// ready to install
34    pub struct Ready;
35}
36
37/// type-state hook builder
38///
39/// ensures hook creation follows the correct sequence:
40/// `Uninitialized -> TargetSet -> DetourSet -> TrampolineAllocated -> TrampolineBuilt -> Ready`
41pub struct HookBuilder<A: Architecture, S> {
42    target: Option<usize>,
43    detour: Option<usize>,
44    prologue_bytes: Option<Vec<u8>>,
45    prologue_size: Option<usize>,
46    trampoline_memory: Option<ExecutableMemory>,
47    hook_stub: Option<Vec<u8>>,
48    _arch: PhantomData<A>,
49    _state: PhantomData<S>,
50}
51
52impl<A: Architecture> HookBuilder<A, state::Uninitialized> {
53    /// create a new hook builder
54    pub fn new() -> Self {
55        Self {
56            target: None,
57            detour: None,
58            prologue_bytes: None,
59            prologue_size: None,
60            trampoline_memory: None,
61            hook_stub: None,
62            _arch: PhantomData,
63            _state: PhantomData,
64        }
65    }
66
67    /// set the target function address
68    pub fn target(self, addr: usize) -> Result<HookBuilder<A, state::TargetSet>> {
69        if addr == 0 {
70            return Err(WraithError::NullPointer {
71                context: "hook target",
72            });
73        }
74
75        Ok(HookBuilder {
76            target: Some(addr),
77            detour: None,
78            prologue_bytes: None,
79            prologue_size: None,
80            trampoline_memory: None,
81            hook_stub: None,
82            _arch: PhantomData,
83            _state: PhantomData,
84        })
85    }
86}
87
88impl<A: Architecture> Default for HookBuilder<A, state::Uninitialized> {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl<A: Architecture> HookBuilder<A, state::TargetSet> {
95    /// get the target address
96    pub fn get_target(&self) -> usize {
97        self.target.unwrap()
98    }
99
100    /// set the detour function address
101    pub fn detour(self, addr: usize) -> Result<HookBuilder<A, state::DetourSet>> {
102        if addr == 0 {
103            return Err(WraithError::NullPointer {
104                context: "hook detour",
105            });
106        }
107
108        Ok(HookBuilder {
109            target: self.target,
110            detour: Some(addr),
111            prologue_bytes: None,
112            prologue_size: None,
113            trampoline_memory: None,
114            hook_stub: None,
115            _arch: PhantomData,
116            _state: PhantomData,
117        })
118    }
119}
120
121impl<A: Architecture> HookBuilder<A, state::DetourSet> {
122    /// get the target address
123    pub fn get_target(&self) -> usize {
124        self.target.unwrap()
125    }
126
127    /// get the detour address
128    pub fn get_detour(&self) -> usize {
129        self.detour.unwrap()
130    }
131
132    /// analyze target and allocate trampoline
133    pub fn allocate_trampoline(self) -> Result<HookBuilder<A, state::TrampolineAllocated>> {
134        let target = self.target.unwrap();
135        let detour = self.detour.unwrap();
136
137        // calculate required hook size
138        let hook_size = A::preferred_hook_size(target, detour);
139
140        // analyze target function
141        let target_bytes = unsafe {
142            core::slice::from_raw_parts(target as *const u8, 64)
143        };
144
145        let boundary = A::find_instruction_boundary(target_bytes, hook_size)
146            .ok_or_else(|| WraithError::HookDetectionFailed {
147                function: format!("{:#x}", target),
148                reason: "failed to find instruction boundary".into(),
149            })?;
150
151        let prologue_bytes = target_bytes[..boundary].to_vec();
152
153        // allocate trampoline memory
154        let trampoline_size = boundary + A::JMP_ABS_SIZE + 16;
155        let trampoline_memory = ExecutableMemory::allocate_near(target, trampoline_size)?;
156
157        Ok(HookBuilder {
158            target: self.target,
159            detour: self.detour,
160            prologue_bytes: Some(prologue_bytes),
161            prologue_size: Some(boundary),
162            trampoline_memory: Some(trampoline_memory),
163            hook_stub: None,
164            _arch: PhantomData,
165            _state: PhantomData,
166        })
167    }
168}
169
170impl<A: Architecture> HookBuilder<A, state::TrampolineAllocated> {
171    /// get the target address
172    pub fn get_target(&self) -> usize {
173        self.target.unwrap()
174    }
175
176    /// get prologue size
177    pub fn prologue_size(&self) -> usize {
178        self.prologue_size.unwrap()
179    }
180
181    /// build the trampoline
182    pub fn build_trampoline(mut self) -> Result<HookBuilder<A, state::TrampolineBuilt>> {
183        let target = self.target.unwrap();
184        let prologue_bytes = self.prologue_bytes.as_ref().unwrap();
185        let prologue_size = self.prologue_size.unwrap();
186        let trampoline = self.trampoline_memory.as_mut().unwrap();
187
188        let trampoline_base = trampoline.base();
189
190        // build trampoline code
191        let mut trampoline_code = Vec::with_capacity(prologue_size + A::JMP_ABS_SIZE);
192
193        // relocate prologue bytes
194        let mut src_offset = 0;
195        let mut dst_offset = 0;
196
197        while src_offset < prologue_size {
198            let remaining = &prologue_bytes[src_offset..];
199            if remaining.is_empty() {
200                break;
201            }
202
203            let insn_len = A::find_instruction_boundary(remaining, 1)
204                .ok_or_else(|| WraithError::HookDetectionFailed {
205                    function: format!("{:#x}", target + src_offset),
206                    reason: "failed to decode instruction".into(),
207                })?;
208
209            let instruction = &prologue_bytes[src_offset..src_offset + insn_len];
210
211            if A::needs_relocation(instruction) {
212                let old_addr = target + src_offset;
213                let new_addr = trampoline_base + dst_offset;
214
215                let relocated = A::relocate_instruction(instruction, old_addr, new_addr)
216                    .ok_or_else(|| WraithError::RelocationFailed {
217                        rva: src_offset as u32,
218                        reason: "instruction cannot be relocated".into(),
219                    })?;
220
221                trampoline_code.extend_from_slice(&relocated);
222                dst_offset += relocated.len();
223            } else {
224                trampoline_code.extend_from_slice(instruction);
225                dst_offset += insn_len;
226            }
227
228            src_offset += insn_len;
229        }
230
231        // append jump back
232        let continuation = target + prologue_size;
233        let jmp_location = trampoline_base + dst_offset;
234
235        if let Some(jmp_bytes) = A::encode_jmp_rel(jmp_location, continuation) {
236            trampoline_code.extend_from_slice(&jmp_bytes);
237        } else {
238            trampoline_code.extend_from_slice(&A::encode_jmp_abs(continuation));
239        }
240
241        // write to memory
242        trampoline.write(&trampoline_code)?;
243        trampoline.flush_icache()?;
244
245        Ok(HookBuilder {
246            target: self.target,
247            detour: self.detour,
248            prologue_bytes: self.prologue_bytes,
249            prologue_size: self.prologue_size,
250            trampoline_memory: self.trampoline_memory,
251            hook_stub: None,
252            _arch: PhantomData,
253            _state: PhantomData,
254        })
255    }
256}
257
258impl<A: Architecture> HookBuilder<A, state::TrampolineBuilt> {
259    /// get the target address
260    pub fn get_target(&self) -> usize {
261        self.target.unwrap()
262    }
263
264    /// get the trampoline address
265    pub fn trampoline(&self) -> usize {
266        self.trampoline_memory.as_ref().unwrap().base()
267    }
268
269    /// generate hook stub and prepare for installation
270    pub fn prepare(mut self) -> Result<HookBuilder<A, state::Ready>> {
271        let target = self.target.unwrap();
272        let detour = self.detour.unwrap();
273        let prologue_size = self.prologue_size.unwrap();
274
275        // generate hook stub
276        let hook_stub = A::encode_jmp_rel(target, detour)
277            .unwrap_or_else(|| A::encode_jmp_abs(detour));
278
279        // pad with NOPs if needed
280        let mut padded_stub = hook_stub;
281        if padded_stub.len() < prologue_size {
282            let padding = A::encode_nop_sled(prologue_size - padded_stub.len());
283            padded_stub.extend_from_slice(&padding);
284        }
285
286        Ok(HookBuilder {
287            target: self.target,
288            detour: self.detour,
289            prologue_bytes: self.prologue_bytes,
290            prologue_size: self.prologue_size,
291            trampoline_memory: self.trampoline_memory,
292            hook_stub: Some(padded_stub),
293            _arch: PhantomData,
294            _state: PhantomData,
295        })
296    }
297}
298
299impl<A: Architecture> HookBuilder<A, state::Ready> {
300    /// get the target address
301    pub fn get_target(&self) -> usize {
302        self.target.unwrap()
303    }
304
305    /// get the trampoline address
306    pub fn trampoline(&self) -> usize {
307        self.trampoline_memory.as_ref().unwrap().base()
308    }
309
310    /// install the hook
311    pub fn install(mut self) -> Result<HookGuard<A>> {
312        let target = self.target.unwrap();
313        let detour = self.detour.unwrap();
314        let prologue_bytes = self.prologue_bytes.take().unwrap();
315        let prologue_size = self.prologue_size.unwrap();
316        let hook_stub = self.hook_stub.as_ref().unwrap();
317
318        // write hook stub to target
319        {
320            let _guard = ProtectionGuard::new(
321                target,
322                prologue_size,
323                PAGE_EXECUTE_READWRITE,
324            )?;
325
326            unsafe {
327                core::ptr::copy_nonoverlapping(
328                    hook_stub.as_ptr(),
329                    target as *mut u8,
330                    prologue_size,
331                );
332            }
333        }
334
335        // flush instruction cache
336        flush_icache(target, prologue_size)?;
337
338        Ok(HookGuard::new(
339            target,
340            detour,
341            prologue_bytes,
342            self.trampoline_memory.take(),
343        ))
344    }
345}
346
347/// convenience function to build a hook step-by-step
348///
349/// # Example
350/// ```ignore
351/// let guard = HookBuilder::<NativeArch, _>::new()
352///     .target(target_addr)?
353///     .detour(detour_addr)?
354///     .allocate_trampoline()?
355///     .build_trampoline()?
356///     .prepare()?
357///     .install()?;
358/// ```
359pub fn build<A: Architecture>() -> HookBuilder<A, state::Uninitialized> {
360    HookBuilder::new()
361}
362
363fn flush_icache(address: usize, size: usize) -> Result<()> {
364    let result = unsafe {
365        FlushInstructionCache(
366            GetCurrentProcess(),
367            address as *const _,
368            size,
369        )
370    };
371
372    if result == 0 {
373        Err(WraithError::from_last_error("FlushInstructionCache"))
374    } else {
375        Ok(())
376    }
377}
378
379#[link(name = "kernel32")]
380extern "system" {
381    fn FlushInstructionCache(
382        hProcess: *mut core::ffi::c_void,
383        lpBaseAddress: *const core::ffi::c_void,
384        dwSize: usize,
385    ) -> i32;
386
387    fn GetCurrentProcess() -> *mut core::ffi::c_void;
388}