Skip to main content

ib_hook/inline/
mod.rs

1/*!
2Inline hooking.
3
4- Supported CPU architectures: x86, x64, ARM64.
5- Support all common ABIs.
6  - On x86/x64, system ABI (`system`, `stdcall`/`win64`) and System V ABI (`sysv64`) are tested.
7- `no_std` and depend on `Ntdll.dll` only (if `tracing` is not enabled).
8- RAII (drop guard) design.
9
10  To leak the hook, wrap [`InlineHook`] as [`std::mem::ManuallyDrop<InlineHook>`]
11  (or call [`std::mem::forget()`]).
12- Thread unsafe at the moment.
13
14  If you may enable/disable hooks from multiple threads at the same time,
15  use a [`std::sync::Mutex`] lock.
16- To init a (`mut`) `static`, [`InlineHook::new()`] can be used.
17
18## Examples
19```
20// cargo add ib-hook --features inline
21use ib_hook::inline::InlineHook;
22
23extern "system" fn original(x: u32) -> u32 { x + 1 }
24
25// Hook the function with a detour
26extern "system" fn hooked(x: u32) -> u32 { x + 0o721 }
27let mut hook = InlineHook::<extern "system" fn(u32) -> u32>::new_enabled(original, hooked).unwrap();
28assert!(hook.is_enabled());
29
30// Now calls to original are redirected to hooked
31assert_eq!(original(0x100), 721); // redirected to hooked: 0x100 + 0o721 = 721
32
33// Access original via trampoline
34assert_eq!(hook.trampoline()(0x100), 0x101); // 0x100 + 1
35
36// Disable the hook manually (or automatically on drop)
37hook.disable().unwrap();
38assert!(!hook.is_enabled());
39assert_eq!(original(0x100), 0x101); // back to original
40```
41
42## Multiple hooks
43There are mainly four ways to storing multiple hooks:
44- Custom `struct`: Store static hooks.
45  - `no_std`
46- [`Vec<InlineHook<F>>`]:
47  Store dynamic hooks of the same function type.
48- [`HashMap<F, InlineHook<F>>`](std::collections::HashMap):
49  Store dynamic hooks of the same function type, indexed by target function.
50- [`InlineHookMap`]:
51  Store dynamic hooks of different function types, indexed by target function.
52
53However, as ID args aren't supported at the moment,
54dynamic hooks aren't quite useful unless you don't need `trampoline`.
55
56### [`InlineHookMap`] example
57```no_run
58// cargo add ib-hook --features inline
59use ib_hook::inline::{InlineHook, InlineHookMap};
60
61type MyFn = extern "system" fn(u32) -> u32;
62
63extern "system" fn original1(x: u32) -> u32 { x + 1 }
64extern "system" fn original2(x: u32) -> u32 { x + 2 }
65
66extern "system" fn hooked1(x: u32) -> u32 { x + 0o721 }
67extern "system" fn hooked2(x: u32) -> u32 { x + 0o722 }
68
69// Create a collection of hooks
70let mut hooks = InlineHookMap::new();
71hooks.insert::<MyFn>(original1, hooked1);
72// Insert and enable a hook
73hooks.insert::<MyFn>(original2, hooked2).enable().unwrap();
74
75// Enable all hooks at once
76hooks.enable().on_error(|target, e| eprintln!("Target {target:?} failed: {e:?}"));
77
78// Verify hooks are enabled
79assert_eq!(original1(0x100), 721); // redirected to hooked1
80assert_eq!(original2(0x100), 722); // redirected to hooked2
81
82// Disable all hooks at once
83hooks.disable().on_error(|target, e| eprintln!("Target {target:?} failed: {e:?}"));
84
85// Verify hooks are disabled
86assert_eq!(original1(0x100), 0x101); // back to original
87assert_eq!(original2(0x100), 0x102); // back to original
88
89// Access individual hooks by target function
90if let Some(hook) = hooks.get::<MyFn>(original1) {
91    println!("Hook is enabled: {}", hook.is_enabled());
92}
93```
94
95## Disclaimer
96This is currently implemented as a wrapper of
97[KNSoft.SlimDetours](https://github.com/KNSoft/KNSoft.SlimDetours),
98for type safety and RAII (drop guard).
99
100Ref: https://github.com/Chaoses-Ib/ib-shell/pull/1
101*/
102use core::{
103    ffi::c_void,
104    fmt::Debug,
105    mem::{self, transmute_copy},
106};
107
108use slim_detours_sys::SlimDetoursInlineHook;
109use windows::core::HRESULT;
110
111use crate::{FnPtr, log::*};
112
113#[cfg(feature = "std")]
114mod map;
115#[cfg(feature = "std")]
116pub use map::InlineHookMap;
117
118/// Type-safe and RAII (drop guard) wrapper of an inline hook.
119///
120/// Manages the lifetime of a detour hook, providing easy enable/disable
121/// and cleanup through RAII principles.
122///
123/// See [`inline`](super::inline) module for details.
124///
125/// ## Type Parameters
126/// - `F`: The function type being hooked.
127#[derive(Debug)]
128pub struct InlineHook<F: FnPtr> {
129    /// Sometimes statically known.
130    target: F,
131    /// The trampoline function (original, before hooking).
132    /// If `target == trampoline`, the hook is not enabled.
133    trampoline: F,
134    /// Hooked function pointer
135    ///
136    /// Detour is usually statically known, but we still need to keep it for RAII.
137    detour: F,
138}
139
140impl<F: FnPtr> InlineHook<F> {
141    /// Creates a new `InlineHookGuard` and immediately applies the hook.
142    ///
143    /// ## Arguments
144    /// - `enable`: Whether to enable the hook immediately (true = enable, false = disable)
145    /// - `target`: Pointer to the target function to hook
146    /// - `detour`: Pointer to the detour/hooked function
147    ///
148    /// ## Returns
149    /// - `Ok(InlineHookGuard)` if hook creation succeeds
150    /// - `HRESULT` error if hook creation fails
151    pub fn with_enabled(target: F, detour: F, enable: bool) -> Result<Self, HRESULT> {
152        let target_ptr: *mut c_void = unsafe { transmute_copy(&target) };
153        let detour_ptr: *mut c_void = unsafe { transmute_copy(&detour) };
154
155        let mut trampoline_ptr: *mut c_void = target_ptr;
156        let res = unsafe { SlimDetoursInlineHook(enable as _, &mut trampoline_ptr, detour_ptr) };
157        let hr = HRESULT(res);
158
159        if hr.is_ok() {
160            let trampoline: F = unsafe { transmute_copy(&trampoline_ptr) };
161            let guard = Self {
162                target,
163                trampoline,
164                detour,
165            };
166            debug!(?target, ?detour, ?trampoline, ?enable, "InlineHook");
167            Ok(guard)
168        } else {
169            Err(hr)
170        }
171    }
172
173    /// Creates a new `InlineHookGuard` without immediately enabling it.
174    ///
175    /// ## Arguments
176    /// - `target`: Pointer to the target function to hook
177    /// - `detour`: Pointer to the detour/hooked function
178    ///
179    /// ## Returns
180    /// `InlineHookGuard` with the hook not yet applied.
181    /// Call `enable()` to apply it.
182    #[doc(alias = "new_disabled")]
183    pub const fn new(target: F, detour: F) -> Self {
184        Self {
185            target,
186            trampoline: target,
187            detour,
188        }
189    }
190
191    /// Creates a new `InlineHookGuard` with the hook enabled.
192    ///
193    /// ## Arguments
194    /// - `target`: Pointer to the target function to hook
195    /// - `detour`: Pointer to the detour/hooked function
196    ///
197    /// ## Returns
198    /// - `Ok(InlineHookGuard)` with the hook created and enabled
199    /// - `HRESULT` error if hook creation fails
200    pub fn new_enabled(target: F, detour: F) -> Result<Self, HRESULT> {
201        Self::with_enabled(target, detour, true)
202    }
203
204    /// Enables or disables the hook.
205    ///
206    /// ## Arguments
207    /// - `enable`: `true` to enable, `false` to disable
208    ///
209    /// ## Returns
210    /// - `HRESULT` success or error code
211    pub fn set_enabled(&mut self, enable: bool) -> HRESULT {
212        let detour_ptr: *mut c_void = unsafe { transmute_copy(&self.detour) };
213        let mut trampoline_ptr: *mut c_void = unsafe { transmute_copy(&self.trampoline) };
214
215        let res = unsafe { SlimDetoursInlineHook(enable as _, &mut trampoline_ptr, detour_ptr) };
216        let hr = HRESULT(res);
217
218        if hr.is_ok() {
219            self.trampoline = unsafe { transmute_copy(&trampoline_ptr) };
220        }
221        hr
222    }
223
224    /// Enables the hook.
225    ///
226    /// ## Returns
227    /// - `Ok(())` if the hook is enabled successfully (or already enabled)
228    /// - `HRESULT` error if enabling fails
229    pub fn enable(&mut self) -> HRESULT {
230        // SlimDetoursInlineHook() will report 0xD0190001 for already enabled hook
231        if self.is_enabled() {
232            return HRESULT(0);
233        }
234        self.set_enabled(true)
235    }
236
237    /// Disables the hook.
238    ///
239    /// ## Returns
240    /// - `Ok(())` if the hook is disabled successfully (or not enabled)
241    /// - `HRESULT` error if disabling fails
242    pub fn disable(&mut self) -> HRESULT {
243        // SlimDetoursInlineHook() will report 0xD0000173 for not enabled hook
244        if !self.is_enabled() {
245            return HRESULT(0);
246        }
247        self.set_enabled(false)
248    }
249
250    /// Toggles the hook state (enabled -> disabled, disabled -> enabled).
251    ///
252    /// ## Returns
253    /// - `Ok(())` if toggle succeeds
254    /// - `HRESULT` error if toggle fails
255    pub fn toggle(&mut self) -> HRESULT {
256        if self.is_enabled() {
257            self.disable()
258        } else {
259            self.enable()
260        }
261    }
262
263    /// Returns `true` if the hook is currently enabled.
264    #[inline]
265    pub fn is_enabled(&self) -> bool {
266        self.target != self.trampoline
267    }
268
269    /// Returns the target function being hooked.
270    #[inline]
271    pub const fn target(&self) -> F {
272        self.target
273    }
274
275    /// Returns `true` if `other` is the same target function as this hook.
276    ///
277    /// This is mainly for avoiding the warning if not using [`std::ptr::fn_addr_eq()`].
278    #[inline]
279    pub fn is_target(&self, other: F) -> bool {
280        self.target == other
281    }
282
283    /// Returns the detour function that will be called when the hook is active.
284    #[inline]
285    pub const fn detour(&self) -> F {
286        self.detour
287    }
288
289    /// Returns the trampoline function holding the original target implementation.
290    ///
291    /// When the hook is enabled, calling `target()` redirects to `detour()`,
292    /// while `trampoline()` provides access to the original target functionality.
293    #[inline]
294    pub const fn trampoline(&self) -> F {
295        self.trampoline
296    }
297
298    pub unsafe fn cast<F2: FnPtr>(&self) -> &InlineHook<F2> {
299        unsafe { transmute_copy(&self) }
300    }
301
302    pub unsafe fn cast_mut<F2: FnPtr>(&mut self) -> &mut InlineHook<F2> {
303        unsafe { transmute_copy(&self) }
304    }
305
306    pub unsafe fn cast_into<F2: FnPtr>(self) -> InlineHook<F2> {
307        let hook = InlineHook {
308            target: unsafe { transmute_copy(&self.target) },
309            trampoline: unsafe { transmute_copy(&self.trampoline) },
310            detour: unsafe { transmute_copy(&self.detour) },
311        };
312        // self.trampoline = self.target;
313        mem::forget(self);
314        hook
315    }
316
317    pub unsafe fn into_type_erased(self) -> InlineHook<fn()> {
318        unsafe { self.cast_into::<fn()>() }
319    }
320}
321
322impl<F: FnPtr> Drop for InlineHook<F> {
323    fn drop(&mut self) {
324        let hr = self.disable();
325        if !hr.is_ok() {
326            debug!(?hr, "Failed to disable hook on drop");
327        }
328    }
329}
330
331#[cfg(test)]
332pub mod tests {
333    use std::sync::Mutex;
334
335    use super::*;
336
337    // Static mutex to prevent race conditions in slim_detours_sys tests
338    // slim_detours_sys is not thread-safe for concurrent hook operations
339    pub static TEST_MUTEX: Mutex<()> = Mutex::new(());
340
341    /// Mock target function - represents the function being hooked
342    #[inline(never)]
343    extern "system" fn inc_target(x: u32) -> u32 {
344        x + 1
345    }
346
347    /// Mock detour function - represents the hook handler
348    #[inline(never)]
349    extern "system" fn dec_detour(x: u32) -> u32 {
350        x - 1
351    }
352
353    #[test]
354    fn assert_send_sync() {
355        // Compile-time check that InlineHook is Send + Sync
356        fn assert_send<F: FnPtr>(_: &InlineHook<F>) {}
357        fn assert_sync<F: FnPtr>(_: &InlineHook<F>) {}
358
359        type MyFn = extern "system" fn(u32) -> u32;
360        extern "system" fn dummy(_x: u32) -> u32 {
361            0
362        }
363        let hook = InlineHook::<MyFn>::new(dummy, dummy);
364
365        assert_send(&hook);
366        assert_sync(&hook);
367
368        {
369            type MyFn = unsafe extern "system" fn(*mut c_void) -> u32;
370            unsafe extern "system" fn dummy(_x: *mut c_void) -> u32 {
371                0
372            }
373            let hook = InlineHook::<MyFn>::new(dummy, dummy);
374            assert_send(&hook);
375            assert_sync(&hook);
376        }
377    }
378
379    #[test]
380    fn is_target() {
381        let _guard = TEST_MUTEX.lock().unwrap();
382        type MyFn = extern "system" fn(u32) -> u32;
383        let target: MyFn = inc_target;
384        let detour: MyFn = dec_detour;
385
386        let hook = InlineHook::<MyFn>::new(target, detour);
387
388        assert!(hook.is_target(target));
389        assert!(!hook.is_target(detour));
390    }
391
392    #[test]
393    fn inline_hook_creation() {
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        // Verify functions work before hooking
400        assert_eq!(target(5), 6); // 5 + 1
401        assert_eq!(detour(5), 4); // 5 - 1
402
403        let hook = InlineHook::<FnType>::new_enabled(target, detour).unwrap();
404        assert!(hook.is_enabled());
405        assert_eq!(hook.target() as *const c_void, target as *const c_void);
406        assert_eq!(hook.detour() as *const c_void, detour as *const c_void);
407
408        assert_eq!(hook.target()(5), 4); // 5 - 1 (redirected to detour)
409        assert_eq!(inc_target(5), 4); // 5 - 1 (redirected to detour)
410        assert_eq!(hook.trampoline()(5), 6); // 5 + 1 (original behavior via trampoline)
411        assert_eq!(hook.detour()(5), 4); // 5 - 1
412        assert_eq!(dec_detour(5), 4); // 5 - 1 (redirected to detour)
413    }
414
415    #[test]
416    fn inline_hook_disabled_by_default() {
417        let _guard = TEST_MUTEX.lock().unwrap();
418        type FnType = extern "system" fn(u32) -> u32;
419        let target = inc_target;
420        let detour = dec_detour;
421
422        let hook = InlineHook::<FnType>::new(target, detour);
423        assert!(!hook.is_enabled());
424        assert_eq!(hook.target() as *const c_void, target as *const c_void);
425        assert_eq!(hook.detour() as *const c_void, detour as *const c_void);
426
427        // Without hooking, target function works directly
428        assert_eq!(target(10), 11); // 10 + 1
429    }
430
431    #[test]
432    fn trampoline_is_true_original() {
433        let _guard = TEST_MUTEX.lock().unwrap();
434        type FnType = extern "system" fn(u32) -> u32;
435        let target = inc_target;
436        let detour = dec_detour;
437
438        let hook = InlineHook::<FnType>::new_enabled(target, detour).unwrap();
439
440        // trampoline holds the true original functionality after hooking
441        // Calling through trampoline executes original target behavior
442        assert_eq!(hook.trampoline()(5), 6); // 5 + 1 (mock_target's original behavior)
443    }
444
445    #[test]
446    fn enable_disable() {
447        let _guard = TEST_MUTEX.lock().unwrap();
448        type FnType = extern "system" fn(u32) -> u32;
449        let target = inc_target;
450        let detour = dec_detour;
451
452        let mut hook = InlineHook::<FnType>::new_enabled(target, detour).unwrap();
453        assert!(hook.is_enabled());
454
455        hook.disable().unwrap();
456        assert!(!hook.is_enabled());
457
458        hook.enable().unwrap();
459        assert!(hook.is_enabled());
460    }
461
462    #[test]
463    fn toggle() {
464        let _guard = TEST_MUTEX.lock().unwrap();
465        type FnType = extern "system" fn(u32) -> u32;
466        let target = inc_target;
467        let detour = dec_detour;
468
469        let mut hook = InlineHook::<FnType>::new_enabled(target, detour).unwrap();
470        assert!(hook.is_enabled());
471
472        hook.toggle().unwrap();
473        assert!(!hook.is_enabled());
474
475        hook.toggle().unwrap();
476        assert!(hook.is_enabled());
477    }
478
479    #[test]
480    fn typed_function_pointers() {
481        let _guard = TEST_MUTEX.lock().unwrap();
482        type FnType = extern "system" fn(u32) -> u32;
483        let target = inc_target;
484        let detour = dec_detour;
485
486        let hook = InlineHook::<FnType>::new_enabled(target, detour).unwrap();
487
488        // Verify typed methods return callable function pointers
489        assert_eq!(hook.target() as *const c_void, target as *const c_void);
490        assert_eq!(hook.detour() as *const c_void, detour as *const c_void);
491    }
492
493    #[test]
494    fn doc() {
495        let _guard = TEST_MUTEX.lock().unwrap();
496        // Hook a function with a detour
497        extern "system" fn original(x: u32) -> u32 {
498            x + 1
499        }
500
501        extern "system" fn hooked(x: u32) -> u32 {
502            x + 0o721
503        }
504        let mut hook =
505            InlineHook::<extern "system" fn(u32) -> u32>::new_enabled(original, hooked).unwrap();
506        assert!(hook.is_enabled());
507
508        // Now calls to original are redirected to hooked
509        assert_eq!(original(0x100), 721); // redirected to hooked: 0x100 + 0o721 = 721
510
511        // Access original via trampoline
512        assert_eq!(hook.trampoline()(0x100), 0x101); // 0x100 + 1
513
514        // Disable the hook manually (or automatically on drop)
515        hook.disable().unwrap();
516        assert!(!hook.is_enabled());
517        assert_eq!(original(0x100), 0x101); // back to original
518    }
519
520    /// First arg is RDI instead of RCX.
521    #[test]
522    #[cfg(target_arch = "x86_64")]
523    fn abi_sysv64() {
524        let _guard = TEST_MUTEX.lock().unwrap();
525        type FnType = extern "sysv64" fn(u32) -> u32;
526
527        #[inline(never)]
528        extern "sysv64" fn target_inc(x: u32) -> u32 {
529            x + 1
530        }
531
532        #[inline(never)]
533        extern "sysv64" fn detour_dec(x: u32) -> u32 {
534            x - 1
535        }
536
537        let target = target_inc;
538        let detour = detour_dec;
539
540        assert_eq!(target(5), 6); // 5 + 1
541        assert_eq!(detour(5), 4); // 5 - 1
542
543        let hook = InlineHook::<FnType>::new_enabled(target, detour).unwrap();
544        assert!(hook.is_enabled());
545
546        assert_eq!(hook.target()(5), 4); // 5 - 1 (redirected to detour)
547        assert_eq!(hook.trampoline()(5), 6); // 5 + 1 (original behavior via trampoline)
548        assert_eq!(hook.detour()(5), 4); // 5 - 1
549    }
550}