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}