3 releases
Uses new Rust 2024
| 0.1.2 | Dec 30, 2025 |
|---|---|
| 0.1.1 | Nov 15, 2025 |
| 0.1.0 | Nov 15, 2025 |
#248 in Concurrency
130KB
2.5K
SLoC
WinHook
A next generation function hooking library for x86_64 Windows and Wine.
Pros
- First of its kind, fully atomic and threadsafe hook installation without IP relocation
- Constant time hooking without ever "stopping the world" (suspending all threads in a process)
- Support for stateful closures as hooks via closure-ffi
- No instruction boundary splitting (which can otherwise cause crashes when a branch target is clobbered with a jump instruction)
Cons
- Windows and Wine on x86_64 only (for now)
- Function addresses not aligned to 16 bytes are more likely to fail to be hooked, see
InstallerResultExt::retry_unchecked - Hooked functions must be a part of a module like an executable or a DLL
- Limited to the Microsoft x64 calling convention ABI
Examples
WinHook can be used with "C", "system" and "win64" ABI functions when targeting x86_64-pc-windows. The "Rust" calling convention is inherently unstable and unsupported.
Aside from Fn closures with internal state, WinHook provides a method for installing FnMut hooks, i.e. using closures with mutable internal state. Under the hood, they are wrapped in a Mutex (note that recursive calls are not allowed, as they would mutably borrow the state a second time). FnOnce hooks are likewise supported.
Let's take a simple "add two numbers" function and instrument it to notify a callback whenever a new highest sum is returned.
#[inline(never)]
extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
let new_highest = |num: i32| println!("new highest sum: {}", num);
use winhook::HookInstaller;
// Note how we added "unsafe" to the function signature.
// Intercepting a function call must preserve all invariants of the original
// and it is not possible to broadly claim for it to be memory safe.
let hook_handle = HookInstaller::<unsafe extern "C" fn(_, _) -> _>::for_function(add)
.install_mut({
let mut max = i32::MIN;
move |original| move |a, b| {
// SAFETY: it's definitely safe to call the original function once
// with the original arguments.
let sum = unsafe { original(a, b) };
if sum > max {
// Notify our callback and update the maximum value.
new_highest(sum);
max = sum;
}
// Return the original result.
sum
}
})
.unwrap();
// NOTE: always assign the returned `HookHandle` to a variable.
// The hook will be uninstalled when the handle goes out of scope.
// Installing a hook *is* safe, enabling it *is NOT*.
// You guarantee all invariants of the original function are preserved.
unsafe {
hook_handle.enable(true);
}
// Should print 4, 8, 15, 16, 23, 42.
let sequence = [
add(3, 1),
add(1, 2),
add(6, 2),
add(6, 9),
add(10, 2),
add(14, 2),
add(3, 20),
add(34, 8),
];
// The original output should not be affected.
assert_eq!(sequence, [4, 3, 8, 15, 12, 16, 23, 42]);
Broadly speaking, the return value of the outer closure passed to HookInstaller::install_* methods is the function to be invoked in place of the hooked one. If you plan to completely detour the original function, it does not have to be a closure:
#[inline(never)]
#[allow(improper_ctypes_definitions)]
extern "system" fn hello(name: String) {
println!("Hello, {name}");
}
// The hook is using the "Rust" calling convention as it must coerce to a `Fn`.
fn goodbye(name: String) {
println!("Goodbye, {name}");
}
use winhook::HookInstaller;
// `enable` can be used when installing the hook directly,
// which is more efficient (and still *unsafe*, see above example).
let _hook_handle = unsafe {
HookInstaller::<unsafe extern "system" fn(String)>::for_function(hello)
.enable(true)
.install(|_original| goodbye)
.unwrap()
};
// Should print "Goodbye, Mario".
hello("Mario".to_owned());
In cases where you do not wish to store a hook handle directly, you can leak it with std::mem::forget or use the associated HookHandle::into_raw method. Do not store a handle past its module's lifetime (after the module is unloaded).
Note about iced-x86
This crate currently depends on closure-ffi-iced-x86 due to an upstream dependency on it to prevent duplicating the iced-x86 dependency in its own dependency tree.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Dependencies
~28MB
~562K SLoC