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}