Skip to main content

imping_hook/
hook.rs

1//! Typed function detours.
2
3use crate::error::{Error, Result};
4use crate::function::Function;
5use crate::registry;
6use crate::scan::{Pattern, Target};
7
8/// A typed function detour, and the RAII guard that owns it.
9///
10/// While enabled, calls to the target run a detour of the same signature `F`
11/// instead; the original stays callable through [`original`](Self::original).
12/// Build one with [`Hook::at`], or [`Hook::scan`] to find the target by a byte
13/// pattern.
14///
15/// # Threads
16///
17/// [`enable`](Self::enable), [`disable`](Self::disable) and drop suspend every
18/// other thread while patching and move any thread stopped inside the patched
19/// bytes, so hooks can be toggled in a running, multi-threaded process. Don't
20/// toggle or drop a hook from inside its own target on the same thread. The
21/// `unsafe` `*_unsuspended` methods skip the suspension.
22///
23/// # Batches
24///
25/// Inside [`batch`](crate::batch), `enable` and `disable` are queued and
26/// applied together when the batch ends. The `*_unsuspended` methods apply at
27/// once even there, and replace any change queued for the same hook.
28///
29/// # Dropping
30///
31/// Dropping a hook disables it and frees its generated code. If it can't be
32/// disabled, say because a third party rewrote the target, the code is leaked
33/// instead so the target never jumps into freed memory. [`leak`](Self::leak)
34/// keeps a hook installed on purpose.
35///
36/// # Examples
37///
38/// ```no_run
39/// use imping_hook::Hook;
40///
41/// #[inline(never)]
42/// extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
43/// extern "C" fn sub(a: i32, b: i32) -> i32 { a - b }
44///
45/// # fn main() -> imping_hook::Result<()> {
46/// // SAFETY: `add` is a real function with the signature below, and `sub`
47/// // shares it.
48/// let hook = unsafe {
49///     Hook::at(add as *const ())
50///         .redirect(sub as extern "C" fn(i32, i32) -> i32)
51///         .install()?
52/// };
53///
54/// assert_eq!(add(3, 2), 5);
55/// hook.enable()?;
56/// assert_eq!(add(3, 2), 1);
57/// assert_eq!(hook.original().call(3, 2), 5);
58/// hook.disable()?;
59/// assert_eq!(add(3, 2), 5);
60/// # Ok(())
61/// # }
62/// ```
63pub struct Hook<F: Function = fn()> {
64    // Boxed so its address stays put while a `batch` has a change queued.
65    inner: Box<detour::TypedDetour<F>>,
66    target: usize,
67}
68
69// Non-generic so `Hook::at(..)` needs no turbofish; `redirect` fixes `F`.
70impl Hook {
71    /// Starts building a hook of the function at `target`.
72    ///
73    /// Nothing is patched until [`install`](HookWith::install).
74    pub fn at(target: *const ()) -> HookBuilder {
75        HookBuilder { target }
76    }
77
78    /// Starts building a hook of the function a byte pattern finds.
79    ///
80    /// The pattern is searched for in the main module's code when the hook is
81    /// installed, and must match exactly once; see [`scan`](crate::scan) for
82    /// the syntax.
83    ///
84    /// # Examples
85    ///
86    /// ```no_run
87    /// # use imping_hook::Hook;
88    /// # extern "C" fn my_detour(a: i32) -> i32 { a }
89    /// # fn main() -> imping_hook::Result<()> {
90    /// // SAFETY: the pattern finds a function with the detour's signature.
91    /// let hook = unsafe {
92    ///     Hook::scan("48 89 5C 24 ?? 57 48 83 EC 20")
93    ///         .in_module("engine.dll")
94    ///         .redirect(my_detour as extern "C" fn(i32) -> i32)
95    ///         .install()?
96    /// };
97    /// # Ok(())
98    /// # }
99    /// ```
100    pub fn scan(pattern: &str) -> HookScan {
101        HookScan {
102            pattern: Pattern::new(pattern),
103        }
104    }
105}
106
107// Send + Sync come for free: TypedDetour is both and the rest is an address.
108impl<F: Function> Hook<F> {
109    /// Enables the hook, redirecting the target to the detour.
110    ///
111    /// Does nothing if already enabled. Suspends other threads while patching;
112    /// see [`Hook`] for threads and batches.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error naming the target if patching fails, such as
117    /// [`Error::TargetModified`] when a third party changed its code.
118    pub fn enable(&self) -> Result<()> {
119        self.toggle(true)
120    }
121
122    /// Disables the hook, restoring the original function.
123    ///
124    /// Does nothing if already disabled. Suspends other threads while patching;
125    /// see [`Hook`] for threads and batches.
126    ///
127    /// # Errors
128    ///
129    /// As for [`enable`](Self::enable).
130    pub fn disable(&self) -> Result<()> {
131        self.toggle(false)
132    }
133
134    /// Enables the hook without suspending other threads.
135    ///
136    /// Faster than [`enable`](Self::enable), and applied at once even inside a
137    /// batch; see [`Hook`].
138    ///
139    /// # Errors
140    ///
141    /// As for [`enable`](Self::enable).
142    ///
143    /// # Safety
144    ///
145    /// No other thread may run the target's first bytes while they are
146    /// written, or it may execute a half-written instruction.
147    pub unsafe fn enable_unsuspended(&self) -> Result<()> {
148        crate::batch::forget(self.target);
149        // SAFETY: forwarded from the caller.
150        unsafe { self.toggle_now(true, detour::Threads::None) }
151    }
152
153    /// Disables the hook without suspending other threads.
154    ///
155    /// Applied at once like [`enable_unsuspended`](Self::enable_unsuspended).
156    ///
157    /// # Errors
158    ///
159    /// As for [`enable`](Self::enable).
160    ///
161    /// # Safety
162    ///
163    /// No other thread may run the target's first bytes while the original
164    /// ones are written back.
165    pub unsafe fn disable_unsuspended(&self) -> Result<()> {
166        crate::batch::forget(self.target);
167        // SAFETY: forwarded from the caller.
168        unsafe { self.toggle_now(false, detour::Threads::None) }
169    }
170
171    /// Returns whether the hook is enabled.
172    pub fn is_enabled(&self) -> bool {
173        self.inner.is_enabled()
174    }
175
176    /// Returns the original, unhooked function, borrowed from the hook.
177    ///
178    /// Callable whether or not the hook is enabled; see [`Original`](crate::Original).
179    pub fn original(&self) -> F::Original<'_> {
180        self.inner.original()
181    }
182
183    /// Returns the original function as a pointer not tied to the hook's
184    /// lifetime.
185    ///
186    /// Prefer [`original`](Self::original) unless the pointer has to outlive a
187    /// borrow.
188    ///
189    /// # Safety
190    ///
191    /// Don't call the returned function after the hook is dropped. Use
192    /// [`leak`](Self::leak) if it must stay callable.
193    pub unsafe fn trampoline(&self) -> F {
194        // SAFETY: forwarded from the caller.
195        unsafe { self.inner.trampoline() }
196    }
197
198    /// Keeps the hook installed for the rest of the process and returns the
199    /// original function.
200    ///
201    /// The hook stays in its current state, so enable it first if it should
202    /// stay active. The target address stays reserved.
203    pub fn leak(self) -> F {
204        // SAFETY: `self` is forgotten right away, so the trampoline is never
205        // freed and stays valid for `'static`.
206        let original = unsafe { self.inner.trampoline() };
207        core::mem::forget(self);
208        original
209    }
210
211    /// Applies the change with every other thread suspended, or queues it
212    /// inside a batch.
213    fn toggle(&self, enable: bool) -> Result<()> {
214        if crate::batch::queue(&*self.inner, self.target, enable) {
215            return Ok(());
216        }
217        // SAFETY: `Threads::All` suspends every other thread and moves any that
218        // stopped inside the patch; the caller isn't running the target's prolog.
219        unsafe { self.toggle_now(enable, detour::Threads::All) }
220    }
221
222    /// Applies the change now, in its own transaction.
223    ///
224    /// # Safety
225    ///
226    /// Threads not suspended by `threads` must not run the patched bytes while
227    /// they are written.
228    unsafe fn toggle_now(&self, enable: bool, threads: detour::Threads<'_>) -> Result<()> {
229        let mut transaction = detour::Transaction::new();
230        if enable {
231            transaction.enable(&*self.inner);
232        } else {
233            transaction.disable(&*self.inner);
234        }
235        // SAFETY: forwarded from the caller.
236        unsafe { transaction.commit(threads) }
237            .map_err(|error| Error::from_detour(error, self.target))
238    }
239}
240
241impl<F: Function> Drop for Hook<F> {
242    fn drop(&mut self) {
243        // Release the address only once the target is restored. If it can't
244        // be, it's still patched (detour leaks its code) and stays reserved.
245        crate::batch::forget(self.target);
246        // SAFETY: as in `toggle`; the caller isn't running the target's prolog.
247        if unsafe { self.toggle_now(false, detour::Threads::All) }.is_ok() {
248            registry::release(self.target);
249        }
250    }
251}
252
253impl<F: Function> core::fmt::Debug for Hook<F> {
254    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
255        f.debug_struct("Hook")
256            .field("target", &(self.target as *const ()))
257            .field("enabled", &self.is_enabled())
258            .finish()
259    }
260}
261
262/// A hook builder holding the target address, returned by [`Hook::at`].
263#[must_use = "a hook builder does nothing until `redirect` and `install` are called"]
264pub struct HookBuilder {
265    target: *const (),
266}
267
268impl HookBuilder {
269    /// Sets the detour to redirect the target to, which fixes the hook's
270    /// signature `F`.
271    pub fn redirect<F: Function>(self, detour: F) -> HookWith<F> {
272        HookWith {
273            target: Target::Address(self.target),
274            detour,
275        }
276    }
277}
278
279/// A hook builder holding a byte pattern to find the target by, returned by
280/// [`Hook::scan`].
281#[must_use = "a hook builder does nothing until `redirect` and `install` are called"]
282pub struct HookScan {
283    pattern: Pattern,
284}
285
286impl HookScan {
287    /// Searches a loaded module such as `"engine.dll"` instead of the main
288    /// module.
289    pub fn in_module(self, module: &str) -> Self {
290        HookScan {
291            pattern: self.pattern.in_module(module),
292        }
293    }
294
295    /// Moves the target this many bytes on from the start of the match, or back
296    /// for a negative count.
297    pub fn offset(self, bytes: isize) -> Self {
298        HookScan {
299            pattern: self.pattern.offset(bytes),
300        }
301    }
302
303    /// Follows the `call` or `jmp` at the target to the function it goes to;
304    /// see [`Pattern::follow_call`].
305    pub fn follow_call(self) -> Self {
306        HookScan {
307            pattern: self.pattern.follow_call(),
308        }
309    }
310
311    /// Sets the detour to redirect the target to, which fixes the hook's
312    /// signature `F`.
313    pub fn redirect<F: Function>(self, detour: F) -> HookWith<F> {
314        HookWith {
315            target: Target::Pattern(self.pattern),
316            detour,
317        }
318    }
319}
320
321/// A hook builder holding the target and detour, returned by
322/// [`HookBuilder::redirect`] and [`HookScan::redirect`].
323#[must_use = "a hook builder does nothing until `install` is called"]
324pub struct HookWith<F: Function> {
325    target: Target,
326    detour: F,
327}
328
329impl<F: Function> HookWith<F> {
330    /// Installs the hook, disabled; enable it with [`Hook::enable`].
331    ///
332    /// Only calls that reach the target are redirected; copies inlined into
333    /// callers are not.
334    ///
335    /// # Errors
336    ///
337    /// - The [`scan`](crate::scan) errors, for a hook built with
338    ///   [`Hook::scan`].
339    /// - [`Error::AlreadyHooked`] if a detour or injection already holds the
340    ///   address.
341    /// - Otherwise an error naming the target if it can't be patched.
342    ///
343    /// # Safety
344    ///
345    /// - The target (the address given to [`Hook::at`], or what
346    ///   [`Hook::scan`] finds) must be a function with signature `F`, not one
347    ///   the compiler may have inlined into all its callers.
348    /// - The detour must match that signature and calling convention.
349    pub unsafe fn install(self) -> Result<Hook<F>> {
350        let target = self.target.resolve()?;
351        let address = target as usize;
352
353        // Claim the address up front so a second hook on it is refused.
354        if !registry::claim(address) {
355            return Err(Error::AlreadyHooked { address });
356        }
357
358        // SAFETY: the caller guarantees `target` is a function of signature `F`.
359        let target_fn: F = unsafe { F::from_ptr(target) };
360
361        // SAFETY: the target and detour share the signature `F`.
362        match unsafe { detour::TypedDetour::<F>::new(target_fn, self.detour) } {
363            Ok(inner) => Ok(Hook {
364                inner: Box::new(inner),
365                target: address,
366            }),
367            Err(error) => {
368                registry::release(address);
369                Err(Error::from_detour(error, address))
370            }
371        }
372    }
373}