Skip to main content

ib_hook/inline/
mod.rs

1/*!
2Inline hooking.
3
4- Supported CPU architectures: x86, x64, ARM64.
5- Support system ABI (`system`, `stdcall`/`win64`) only.
6- `no_std` and depend on `Ntdll.dll` only (if `tracing` is not enabled).
7- RAII (drop guard) design.
8
9  To leak the hook, wrap [`InlineHook`] as [`std::mem::ManuallyDrop<InlineHook>`]
10  (or call [`std::mem::forget()`]).
11- Thread unsafe at the moment.
12
13  If you may enable/disable hooks from multiple threads at the same time,
14  use a [`std::sync::Mutex`] lock.
15- To init a (`mut`) `static`, [`InlineHook::new_disabled()`] can be used.
16
17## Examples
18```
19// cargo add ib-hook --features inline
20use ib_hook::inline::InlineHook;
21
22extern "system" fn original(x: u32) -> u32 { x + 1 }
23
24// Hook the function with a detour
25extern "system" fn hooked(x: u32) -> u32 { x + 0o721 }
26let mut hook = InlineHook::<extern "system" fn(u32) -> u32>::new(original, hooked).unwrap();
27assert!(hook.is_enabled());
28
29// Now calls to original are redirected to hooked
30assert_eq!(original(0x100), 721); // redirected to hooked: 0x100 + 0o721 = 721
31
32// Access original via trampoline
33assert_eq!(hook.trampoline()(0x100), 0x101); // 0x100 + 1
34
35// Disable the hook manually (or automatically on drop)
36hook.disable().unwrap();
37assert!(!hook.is_enabled());
38assert_eq!(original(0x100), 0x101); // back to original
39```
40
41## Disclaimer
42This is currently implemented as a wrapper of
43[KNSoft.SlimDetours](https://github.com/KNSoft/KNSoft.SlimDetours),
44for type safety and RAII (drop guard).
45
46Ref: https://github.com/Chaoses-Ib/ib-shell/pull/1
47*/
48use core::{ffi::c_void, fmt::Debug, mem::transmute_copy};
49
50use slim_detours_sys::SlimDetoursInlineHook;
51use windows::core::HRESULT;
52
53use crate::{FnPtr, log::*};
54
55/// Type-safe and RAII (drop guard) wrapper of an inline hook.
56///
57/// Manages the lifetime of a detour hook, providing easy enable/disable
58/// and cleanup through RAII principles.
59///
60/// See [`inline`](super::inline) module for details.
61///
62/// ## Type Parameters
63/// - `F`: The function type being hooked.
64#[derive(Debug)]
65pub struct InlineHook<F: FnPtr> {
66    /// Sometimes statically known.
67    target: F,
68    /// The trampoline function (original, before hooking).
69    /// If `target == trampoline`, the hook is not enabled.
70    trampoline: F,
71    /// Hooked function pointer
72    ///
73    /// Detour is usually statically known, but we still need to keep it for RAII.
74    detour: F,
75}
76
77impl<F: FnPtr> InlineHook<F> {
78    /// Creates a new `InlineHookGuard` and immediately applies the hook.
79    ///
80    /// ## Arguments
81    /// - `enable`: Whether to enable the hook immediately (true = enable, false = disable)
82    /// - `target`: Pointer to the target function to hook
83    /// - `detour`: Pointer to the detour/hooked function
84    ///
85    /// ## Returns
86    /// - `Ok(InlineHookGuard)` if hook creation succeeds
87    /// - `HRESULT` error if hook creation fails
88    pub fn with_enabled(target: F, detour: F, enable: bool) -> Result<Self, HRESULT> {
89        let target_ptr: *mut c_void = unsafe { transmute_copy(&target) };
90        let detour_ptr: *mut c_void = unsafe { transmute_copy(&detour) };
91
92        let mut trampoline_ptr: *mut c_void = target_ptr;
93        let res = unsafe { SlimDetoursInlineHook(enable as _, &mut trampoline_ptr, detour_ptr) };
94        let hr = HRESULT(res);
95
96        if hr.is_ok() {
97            let trampoline: F = unsafe { transmute_copy(&trampoline_ptr) };
98            let guard = Self {
99                target,
100                trampoline,
101                detour,
102            };
103            debug!(?target, ?detour, ?trampoline, ?enable, "InlineHook");
104            Ok(guard)
105        } else {
106            Err(hr)
107        }
108    }
109
110    /// Creates a new `InlineHookGuard` without immediately enabling it.
111    ///
112    /// ## Arguments
113    /// - `target`: Pointer to the target function to hook
114    /// - `detour`: Pointer to the detour/hooked function
115    ///
116    /// ## Returns
117    /// `InlineHookGuard` with the hook not yet applied.
118    /// Call `enable()` to apply it.
119    pub const fn new_disabled(target: F, detour: F) -> Self {
120        Self {
121            target,
122            trampoline: target,
123            detour,
124        }
125    }
126
127    /// Creates a new `InlineHookGuard` with the hook enabled.
128    ///
129    /// ## Arguments
130    /// - `target`: Pointer to the target function to hook
131    /// - `detour`: Pointer to the detour/hooked function
132    ///
133    /// ## Returns
134    /// - `Ok(InlineHookGuard)` with the hook created and enabled
135    /// - `HRESULT` error if hook creation fails
136    pub fn new(target: F, detour: F) -> Result<Self, HRESULT> {
137        Self::with_enabled(target, detour, true)
138    }
139
140    /// Enables or disables the hook.
141    ///
142    /// ## Arguments
143    /// - `enable`: `true` to enable, `false` to disable
144    ///
145    /// ## Returns
146    /// - `HRESULT` success or error code
147    pub fn set_enabled(&mut self, enable: bool) -> HRESULT {
148        let detour_ptr: *mut c_void = unsafe { transmute_copy(&self.detour) };
149        let mut trampoline_ptr: *mut c_void = unsafe { transmute_copy(&self.trampoline) };
150
151        let res = unsafe { SlimDetoursInlineHook(enable as _, &mut trampoline_ptr, detour_ptr) };
152        let hr = HRESULT(res);
153
154        if hr.is_ok() {
155            self.trampoline = unsafe { transmute_copy(&trampoline_ptr) };
156        }
157        hr
158    }
159
160    /// Enables the hook.
161    ///
162    /// ## Returns
163    /// - `Ok(())` if the hook is enabled successfully (or already enabled)
164    /// - `HRESULT` error if enabling fails
165    pub fn enable(&mut self) -> HRESULT {
166        // SlimDetoursInlineHook() will report 0xD0190001 for already enabled hook
167        if self.is_enabled() {
168            return HRESULT(0);
169        }
170        self.set_enabled(true)
171    }
172
173    /// Disables the hook.
174    ///
175    /// ## Returns
176    /// - `Ok(())` if the hook is disabled successfully (or not enabled)
177    /// - `HRESULT` error if disabling fails
178    pub fn disable(&mut self) -> HRESULT {
179        // SlimDetoursInlineHook() will report 0xD0000173 for not enabled hook
180        if !self.is_enabled() {
181            return HRESULT(0);
182        }
183        self.set_enabled(false)
184    }
185
186    /// Toggles the hook state (enabled -> disabled, disabled -> enabled).
187    ///
188    /// ## Returns
189    /// - `Ok(())` if toggle succeeds
190    /// - `HRESULT` error if toggle fails
191    pub fn toggle(&mut self) -> HRESULT {
192        if self.is_enabled() {
193            self.disable()
194        } else {
195            self.enable()
196        }
197    }
198
199    /// Returns `true` if the hook is currently enabled.
200    #[inline]
201    pub fn is_enabled(&self) -> bool {
202        self.target != self.trampoline
203    }
204
205    /// Returns the target function being hooked.
206    #[inline]
207    pub const fn target(&self) -> F {
208        self.target
209    }
210
211    /// Returns `true` if `other` is the same target function as this hook.
212    ///
213    /// This is mainly for avoiding the warning if not using [`std::ptr::fn_addr_eq()`].
214    #[inline]
215    pub fn is_target(&self, other: F) -> bool {
216        self.target == other
217    }
218
219    /// Returns the detour function that will be called when the hook is active.
220    #[inline]
221    pub const fn detour(&self) -> F {
222        self.detour
223    }
224
225    /// Returns the trampoline function holding the original target implementation.
226    ///
227    /// When the hook is enabled, calling `target()` redirects to `detour()`,
228    /// while `trampoline()` provides access to the original target functionality.
229    #[inline]
230    pub const fn trampoline(&self) -> F {
231        self.trampoline
232    }
233}
234
235impl<F: FnPtr> Drop for InlineHook<F> {
236    fn drop(&mut self) {
237        let hr = self.disable();
238        if !hr.is_ok() {
239            debug!(?hr, "Failed to disable hook on drop");
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use std::sync::Mutex;
247
248    use super::*;
249
250    // Static mutex to prevent race conditions in slim_detours_sys tests
251    // slim_detours_sys is not thread-safe for concurrent hook operations
252    static TEST_MUTEX: Mutex<()> = Mutex::new(());
253
254    /// Mock target function - represents the function being hooked
255    #[inline(never)]
256    extern "system" fn inc_target(x: u32) -> u32 {
257        x + 1
258    }
259
260    /// Mock detour function - represents the hook handler
261    #[inline(never)]
262    extern "system" fn dec_detour(x: u32) -> u32 {
263        x - 1
264    }
265
266    #[test]
267    fn assert_send_sync() {
268        // Compile-time check that InlineHook is Send + Sync
269        fn assert_send<F: FnPtr>(_: &InlineHook<F>) {}
270        fn assert_sync<F: FnPtr>(_: &InlineHook<F>) {}
271
272        type MyFn = extern "system" fn(u32) -> u32;
273        extern "system" fn dummy(_x: u32) -> u32 {
274            0
275        }
276        let hook = InlineHook::<MyFn>::new_disabled(dummy, dummy);
277
278        assert_send(&hook);
279        assert_sync(&hook);
280
281        {
282            type MyFn = unsafe extern "system" fn(*mut c_void) -> u32;
283            unsafe extern "system" fn dummy(_x: *mut c_void) -> u32 {
284                0
285            }
286            let hook = InlineHook::<MyFn>::new_disabled(dummy, dummy);
287            assert_send(&hook);
288            assert_sync(&hook);
289        }
290    }
291
292    #[test]
293    fn is_target() {
294        let _guard = TEST_MUTEX.lock().unwrap();
295        type MyFn = extern "system" fn(u32) -> u32;
296        let target: MyFn = inc_target;
297        let detour: MyFn = dec_detour;
298
299        let hook = InlineHook::<MyFn>::new_disabled(target, detour);
300
301        assert!(hook.is_target(target));
302        assert!(!hook.is_target(detour));
303    }
304
305    #[test]
306    fn inline_hook_creation() {
307        let _guard = TEST_MUTEX.lock().unwrap();
308        type FnType = extern "system" fn(u32) -> u32;
309        let target = inc_target;
310        let detour = dec_detour;
311
312        // Verify functions work before hooking
313        assert_eq!(target(5), 6); // 5 + 1
314        assert_eq!(detour(5), 4); // 5 - 1
315
316        let hook = InlineHook::<FnType>::new(target, detour).unwrap();
317        assert!(hook.is_enabled());
318        assert_eq!(hook.target() as *const c_void, target as *const c_void);
319        assert_eq!(hook.detour() as *const c_void, detour as *const c_void);
320
321        assert_eq!(hook.target()(5), 4); // 5 - 1 (redirected to detour)
322        assert_eq!(inc_target(5), 4); // 5 - 1 (redirected to detour)
323        assert_eq!(hook.trampoline()(5), 6); // 5 + 1 (original behavior via trampoline)
324        assert_eq!(hook.detour()(5), 4); // 5 - 1
325        assert_eq!(dec_detour(5), 4); // 5 - 1 (redirected to detour)
326    }
327
328    #[test]
329    fn inline_hook_disabled_by_default() {
330        let _guard = TEST_MUTEX.lock().unwrap();
331        type FnType = extern "system" fn(u32) -> u32;
332        let target = inc_target;
333        let detour = dec_detour;
334
335        let hook = InlineHook::<FnType>::new_disabled(target, detour);
336        assert!(!hook.is_enabled());
337        assert_eq!(hook.target() as *const c_void, target as *const c_void);
338        assert_eq!(hook.detour() as *const c_void, detour as *const c_void);
339
340        // Without hooking, target function works directly
341        assert_eq!(target(10), 11); // 10 + 1
342    }
343
344    #[test]
345    fn trampoline_is_true_original() {
346        let _guard = TEST_MUTEX.lock().unwrap();
347        type FnType = extern "system" fn(u32) -> u32;
348        let target = inc_target;
349        let detour = dec_detour;
350
351        let hook = InlineHook::<FnType>::new(target, detour).unwrap();
352
353        // trampoline holds the true original functionality after hooking
354        // Calling through trampoline executes original target behavior
355        assert_eq!(hook.trampoline()(5), 6); // 5 + 1 (mock_target's original behavior)
356    }
357
358    #[test]
359    fn enable_disable() {
360        let _guard = TEST_MUTEX.lock().unwrap();
361        type FnType = extern "system" fn(u32) -> u32;
362        let target = inc_target;
363        let detour = dec_detour;
364
365        let mut hook = InlineHook::<FnType>::new(target, detour).unwrap();
366        assert!(hook.is_enabled());
367
368        hook.disable().unwrap();
369        assert!(!hook.is_enabled());
370
371        hook.enable().unwrap();
372        assert!(hook.is_enabled());
373    }
374
375    #[test]
376    fn toggle() {
377        let _guard = TEST_MUTEX.lock().unwrap();
378        type FnType = extern "system" fn(u32) -> u32;
379        let target = inc_target;
380        let detour = dec_detour;
381
382        let mut hook = InlineHook::<FnType>::new(target, detour).unwrap();
383        assert!(hook.is_enabled());
384
385        hook.toggle().unwrap();
386        assert!(!hook.is_enabled());
387
388        hook.toggle().unwrap();
389        assert!(hook.is_enabled());
390    }
391
392    #[test]
393    fn typed_function_pointers() {
394        let _guard = TEST_MUTEX.lock().unwrap();
395        type FnType = extern "system" fn(u32) -> u32;
396        let target = inc_target;
397        let detour = dec_detour;
398
399        let hook = InlineHook::<FnType>::new(target, detour).unwrap();
400
401        // Verify typed methods return callable function pointers
402        assert_eq!(hook.target() as *const c_void, target as *const c_void);
403        assert_eq!(hook.detour() as *const c_void, detour as *const c_void);
404    }
405
406    #[test]
407    fn doc() {
408        let _guard = TEST_MUTEX.lock().unwrap();
409        // Hook a function with a detour
410        extern "system" fn original(x: u32) -> u32 {
411            x + 1
412        }
413
414        extern "system" fn hooked(x: u32) -> u32 {
415            x + 0o721
416        }
417        let mut hook = InlineHook::<extern "system" fn(u32) -> u32>::new(original, hooked).unwrap();
418        assert!(hook.is_enabled());
419
420        // Now calls to original are redirected to hooked
421        assert_eq!(original(0x100), 721); // redirected to hooked: 0x100 + 0o721 = 721
422
423        // Access original via trampoline
424        assert_eq!(hook.trampoline()(0x100), 0x101); // 0x100 + 1
425
426        // Disable the hook manually (or automatically on drop)
427        hook.disable().unwrap();
428        assert!(!hook.is_enabled());
429        assert_eq!(original(0x100), 0x101); // back to original
430    }
431}