wraith/manipulation/inline_hook/hook/
chain.rs

1//! Hook chaining support
2//!
3//! Allows multiple hooks on the same target function, organized by priority.
4//! Each hook in the chain can call the next via its trampoline.
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::trampoline::ExecutableMemory;
16use core::marker::PhantomData;
17
18const PAGE_EXECUTE_READWRITE: u32 = 0x40;
19
20/// entry in the hook chain
21struct ChainEntry {
22    /// detour function address
23    detour: usize,
24    /// trampoline to call next in chain (or original)
25    trampoline: ExecutableMemory,
26    /// priority (lower = called first)
27    priority: i32,
28}
29
30/// hook chain for multiple hooks on one target
31///
32/// manages a chain of hooks on a single function. hooks are called
33/// in priority order (lower priority first). each hook receives a
34/// trampoline to call the next hook in the chain.
35pub struct HookChain<A: Architecture> {
36    /// target function address
37    target: usize,
38    /// chain entries sorted by priority
39    entries: Vec<ChainEntry>,
40    /// original function bytes
41    original_bytes: Vec<u8>,
42    /// prologue size
43    prologue_size: usize,
44    /// current hook bytes at target
45    current_hook: Vec<u8>,
46    _arch: PhantomData<A>,
47}
48
49impl<A: Architecture> HookChain<A> {
50    /// create a new hook chain on target function
51    ///
52    /// this analyzes the target but does not install any hooks yet.
53    pub fn new(target: usize) -> Result<Self> {
54        let min_size = A::MIN_HOOK_SIZE;
55
56        // analyze target function
57        let target_bytes = unsafe {
58            core::slice::from_raw_parts(target as *const u8, 64)
59        };
60
61        let boundary = A::find_instruction_boundary(target_bytes, min_size)
62            .ok_or_else(|| WraithError::HookDetectionFailed {
63                function: format!("{:#x}", target),
64                reason: "failed to find instruction boundary".into(),
65            })?;
66
67        let original_bytes = target_bytes[..boundary].to_vec();
68
69        Ok(Self {
70            target,
71            entries: Vec::new(),
72            original_bytes,
73            prologue_size: boundary,
74            current_hook: Vec::new(),
75            _arch: PhantomData,
76        })
77    }
78
79    /// add a hook to the chain
80    ///
81    /// returns the trampoline address for calling the next hook in chain.
82    /// lower priority values are called first.
83    pub fn add(&mut self, detour: usize, priority: i32) -> Result<usize> {
84        // find insertion position (sorted by priority)
85        let pos = self.entries
86            .iter()
87            .position(|e| e.priority > priority)
88            .unwrap_or(self.entries.len());
89
90        // build trampoline for this entry
91        // it will call the next entry (or original if last)
92        let next_target = if pos < self.entries.len() {
93            // there's a next entry - call its detour
94            self.entries[pos].detour
95        } else {
96            // this is the last entry - build trampoline to original
97            self.target
98        };
99
100        let trampoline = self.build_trampoline_to(next_target)?;
101        let trampoline_addr = trampoline.base();
102
103        // insert new entry
104        self.entries.insert(pos, ChainEntry {
105            detour,
106            trampoline,
107            priority,
108        });
109
110        // update trampolines for entries before this one
111        self.rebuild_trampolines_before(pos)?;
112
113        // update the hook at target to call first entry
114        self.update_target_hook()?;
115
116        Ok(trampoline_addr)
117    }
118
119    /// remove a hook from the chain by detour address
120    ///
121    /// returns true if the hook was found and removed.
122    pub fn remove(&mut self, detour: usize) -> Result<bool> {
123        let pos = match self.entries.iter().position(|e| e.detour == detour) {
124            Some(p) => p,
125            None => return Ok(false),
126        };
127
128        self.entries.remove(pos);
129
130        if self.entries.is_empty() {
131            // no more hooks, restore original
132            self.restore_original()?;
133        } else {
134            // rebuild trampolines and update target
135            self.rebuild_all_trampolines()?;
136            self.update_target_hook()?;
137        }
138
139        Ok(true)
140    }
141
142    /// get the trampoline for calling the original function
143    ///
144    /// this is the trampoline of the last entry in the chain.
145    pub fn original(&self) -> Option<usize> {
146        self.entries.last().map(|e| e.trampoline.base())
147    }
148
149    /// get number of hooks in the chain
150    pub fn len(&self) -> usize {
151        self.entries.len()
152    }
153
154    /// check if chain is empty
155    pub fn is_empty(&self) -> bool {
156        self.entries.is_empty()
157    }
158
159    /// get target address
160    pub fn target(&self) -> usize {
161        self.target
162    }
163
164    /// restore original function and drop all hooks
165    pub fn restore(mut self) -> Result<()> {
166        self.restore_original()?;
167        Ok(())
168    }
169
170    /// build a trampoline that jumps to given address
171    fn build_trampoline_to(&self, target: usize) -> Result<ExecutableMemory> {
172        let mut memory = ExecutableMemory::allocate_near(self.target, 64)?;
173
174        // if target is the original function, we need to copy prologue
175        if target == self.target {
176            let mut code = Vec::with_capacity(self.prologue_size + A::JMP_ABS_SIZE);
177
178            // copy and relocate original bytes
179            let mut src_offset = 0;
180            while src_offset < self.prologue_size {
181                let remaining = &self.original_bytes[src_offset..];
182                let insn_len = A::find_instruction_boundary(remaining, 1).unwrap_or(1);
183                let instruction = &self.original_bytes[src_offset..src_offset + insn_len];
184
185                if A::needs_relocation(instruction) {
186                    let old_addr = self.target + src_offset;
187                    let new_addr = memory.base() + code.len();
188                    if let Some(relocated) = A::relocate_instruction(instruction, old_addr, new_addr) {
189                        code.extend_from_slice(&relocated);
190                    } else {
191                        code.extend_from_slice(instruction);
192                    }
193                } else {
194                    code.extend_from_slice(instruction);
195                }
196
197                src_offset += insn_len;
198            }
199
200            // jump to continuation
201            let continuation = self.target + self.prologue_size;
202            let jmp_location = memory.base() + code.len();
203
204            if let Some(jmp) = A::encode_jmp_rel(jmp_location, continuation) {
205                code.extend_from_slice(&jmp);
206            } else {
207                code.extend_from_slice(&A::encode_jmp_abs(continuation));
208            }
209
210            memory.write(&code)?;
211        } else {
212            // just jump to the target detour
213            let jmp = A::encode_jmp_rel(memory.base(), target)
214                .unwrap_or_else(|| A::encode_jmp_abs(target));
215            memory.write(&jmp)?;
216        }
217
218        memory.flush_icache()?;
219
220        Ok(memory)
221    }
222
223    /// rebuild trampolines before position
224    fn rebuild_trampolines_before(&mut self, pos: usize) -> Result<()> {
225        // entries before `pos` need their trampolines updated
226        // to call the new entry at `pos`
227        if pos > 0 {
228            let new_target = self.entries[pos].detour;
229            let prev = &mut self.entries[pos - 1];
230
231            // rebuild trampoline
232            let mut new_tramp = ExecutableMemory::allocate_near(self.target, 64)?;
233            let jmp = A::encode_jmp_rel(new_tramp.base(), new_target)
234                .unwrap_or_else(|| A::encode_jmp_abs(new_target));
235            new_tramp.write(&jmp)?;
236            new_tramp.flush_icache()?;
237
238            prev.trampoline = new_tramp;
239        }
240
241        Ok(())
242    }
243
244    /// rebuild all trampolines in the chain
245    fn rebuild_all_trampolines(&mut self) -> Result<()> {
246        let len = self.entries.len();
247
248        for i in 0..len {
249            let next_target = if i + 1 < len {
250                self.entries[i + 1].detour
251            } else {
252                self.target // original
253            };
254
255            let new_tramp = self.build_trampoline_to(next_target)?;
256            self.entries[i].trampoline = new_tramp;
257        }
258
259        Ok(())
260    }
261
262    /// update the hook at target to call first entry
263    fn update_target_hook(&mut self) -> Result<()> {
264        if self.entries.is_empty() {
265            return self.restore_original();
266        }
267
268        let first_detour = self.entries[0].detour;
269
270        // generate hook stub
271        let hook_stub = A::encode_jmp_rel(self.target, first_detour)
272            .unwrap_or_else(|| A::encode_jmp_abs(first_detour));
273
274        let mut padded = hook_stub.clone();
275        if padded.len() < self.prologue_size {
276            let padding = A::encode_nop_sled(self.prologue_size - padded.len());
277            padded.extend_from_slice(&padding);
278        }
279
280        // write to target
281        {
282            let _guard = ProtectionGuard::new(
283                self.target,
284                self.prologue_size,
285                PAGE_EXECUTE_READWRITE,
286            )?;
287
288            unsafe {
289                core::ptr::copy_nonoverlapping(
290                    padded.as_ptr(),
291                    self.target as *mut u8,
292                    self.prologue_size,
293                );
294            }
295        }
296
297        flush_icache(self.target, self.prologue_size)?;
298        self.current_hook = padded;
299
300        Ok(())
301    }
302
303    /// restore original function bytes
304    fn restore_original(&mut self) -> Result<()> {
305        let _guard = ProtectionGuard::new(
306            self.target,
307            self.prologue_size,
308            PAGE_EXECUTE_READWRITE,
309        )?;
310
311        unsafe {
312            core::ptr::copy_nonoverlapping(
313                self.original_bytes.as_ptr(),
314                self.target as *mut u8,
315                self.prologue_size,
316            );
317        }
318
319        flush_icache(self.target, self.prologue_size)?;
320        self.current_hook.clear();
321
322        Ok(())
323    }
324}
325
326impl<A: Architecture> Drop for HookChain<A> {
327    fn drop(&mut self) {
328        // restore original on drop
329        let _ = self.restore_original();
330    }
331}
332
333fn flush_icache(address: usize, size: usize) -> Result<()> {
334    let result = unsafe {
335        FlushInstructionCache(
336            GetCurrentProcess(),
337            address as *const _,
338            size,
339        )
340    };
341
342    if result == 0 {
343        Err(WraithError::from_last_error("FlushInstructionCache"))
344    } else {
345        Ok(())
346    }
347}
348
349#[link(name = "kernel32")]
350extern "system" {
351    fn FlushInstructionCache(
352        hProcess: *mut core::ffi::c_void,
353        lpBaseAddress: *const core::ffi::c_void,
354        dwSize: usize,
355    ) -> i32;
356
357    fn GetCurrentProcess() -> *mut core::ffi::c_void;
358}