AlienDwarf/neohook

GitHub: AlienDwarf/neohook

一个基于 Rust 编写的 Windows 函数 Hook 库,通过事务性和线程安全机制实现可靠的运行时函数拦截。

Stars: 11 | Forks: 0

# NeoHook 🪝🦀 [![Crates.io](https://img.shields.io/crates/v/neohook.svg)](https://crates.io/crates/neohook) [![License: MIT / Apache-2.0](https://img.shields.io/badge/license-MIT%20%2F%20Apache--2.0-blue.svg)](#license) [![Platform: Windows](https://img.shields.io/badge/platform-Windows-0078D6?logo=windows)](https://www.microsoft.com/windows) [![Arch: x86 / x86_64](https://img.shields.io/badge/arch-x86%20%7C%20x86__64-lightgrey)](https://en.wikipedia.org/wiki/X86) [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/5c27361813193117.svg)](https://github.com/aliendwarf/neohook/actions/workflows/ci.yml) 只需一行代码即可 Hook 任何函数 —— 支持事务性和线程安全。告别指针到指针的混乱。 NeoHook 让运行时函数 Hook 变得简单且可靠:Win32 API、游戏引擎函数、第三方 DLL 导出,任何带有代码指针的对象均可。它将底层二进制补丁的精确性与 Rust 的内存安全、类型系统以及 RAII 所有权模型完美结合。 ## 为什么选择 NeoHook? 函数 Hook 想要做好非常困难。编写一个 `JMP` 补丁只需要几行汇编代码——但在一个实时的、多线程的进程中安全地执行它,需要同时解决多个问题: | 问题 | 朴素方法 | NeoHook | | :------------------------------------------------- | :------------------------------: | :--------------------------------------: | | 另一个线程正在执行你正在打补丁的字节 | 访问冲突 | ✅ 打补丁期间挂起线程 | | 指令指针指向被覆盖的字节 | 崩溃 | ✅ IP 重定向 | | 栈上的返回地址指向补丁区域 | 返回时崩溃 | ✅ 栈重定向 | | JMP/CALL 指令在重定位后中断 | 错误的目标 | ✅ 通过 `iced-x86` 进行指令重定位 | | 批处理中的一个 Hook 失败 | 不稳定 | ✅ 原子回滚 - 要么全部成功要么什么都不做 | | 代码退出作用域后 Hook 泄漏 | 永久补丁,卸载时崩溃 | ✅ RAII:在 `Drop` 时自动 unhook | ## 功能 - **原子事务** - 排队多个 Hook 并在一个步骤中提交。如果任何一个 Hook 失败,同一事务中之前所有已应用的更改都会自动回滚,使进程保持在已知良好的状态。 - **完全线程安全** - 在应用补丁之前,枚举并挂起进程中的所有线程。 - **RIP / EIP 重定向** - 如果线程的指令指针落在正在被覆盖的字节内,它将被重定位。 - **栈扫描** - 扫描每个线程的前 512 个栈槽,查找指向补丁区域的返回地址,并将它们重写为等效的跳板(trampoline)。 - **指令重定位** - 使用 [`iced-x86`](https://github.com/icedland/iced) 进行精确解码、重定位和重新编码。 - **智能跳板分配** - 在 x64 上,在目标地址 ±2 GB 范围内分配跳板内存,以便使用紧凑的 5 字节相对跳转即可满足需求。如果没有合适的内存,则退回使用 14 字节绝对跳转 `(FF 25)`。 - **IAT Hooking** - 重写导入地址表(Import Address Table)条目,将调用重定向到整个 DLL 导出,而无需触及函数前导码。 - **VTable Hooking** - 重写选定的 VTable 槽以劫持(detour)虚拟调用,并在 unhook 时恢复原始槽位。 - **实例级 VTable Hooking** - 克隆对象的 VTable,对克隆进行补丁,并仅重定向该实例。 - **Hook 链式调用** - 劫持已安装 Hook 的跳板,以定义的顺序叠加多个拦截器。 - **RAII 所有权** - `commit()` 返回的 `Vec` 在被 drop 时会自动 unhook 并恢复原始内存。 - **零样板宏** - `detour_inline!` 和 `detour_helper!` 仅需单个表达式即可安装完整的 Hook。 - **C FFI** - 暴露带有自动生成头文件(`cbindgen`)的 C ABI,可从 C、C++、Python (`ctypes`) 或任何支持 FFI 的语言调用。 ## 路线图 | 版本 | 状态 | 功能 | | ------- | ---------: | ------------------------------------------------------ | | v0.1.0 | ✅ 完成 | 初始发布版本 | | v0.1.0 | ✅ 完成 | Inline hooking | | v0.1.0 | ✅ 完成 | IAT hooking | | v0.1.0 | ✅ 完成 | 事务 API(`begin`, `attach`, `commit`, `abort`) | | v0.1.0 | ✅ 完成 | 线程更新(`update_thread`, `update_all_threads`) | | v0.1.0 | ✅ 完成 | 跳板分配 + 重定位 | | v0.1.0 | ✅ 完成 | 托管网关 / Hook 链式调用 | | v0.1.0 | ✅ 完成 | 提交失败时回滚 | | v0.1.0 | ✅ 完成 | drop 时 RAII unhook | | v0.1.0 | ✅ 完成 | C FFI 事务入口点 | | v0.2.0 | ✅ 完成 | VTable hooking | | v0.2.0 | ✅ 完成 | 实例级 VTable Hook | | v0.2.0 | ✅ 完成 | 共享 VTable 补丁 | | v0.2.0 | ✅ 完成 | C FFI 中的 VTable Hook 支持 | | v0.2.0 | ✅ 完成 | 针对 C++ / COM 目标的额外测试和示例 | | v0.3.0 | ⬜ 计划中 | 在不完全 unhook 的情况下启用 / 禁用 Hook | | v0.3.0 | ⬜ 计划中 | 递归 / 重入保护 | | v0.3.0 | ⬜ 计划中 | 改进的诊断 / 调试输出 | | v0.4.0 | ⬜ 计划中 | Export / EAT hooking | | v0.5.0 | ⬜ 计划中 | VEH hooking | -- ## 安装 将此 crate 添加到你的 `Cargo.toml` 中: ``` [dependencies] neohook = "0.1.0" ``` ## 快速开始 ### 单行 Hook - `detour_inline!` 当你想要完全替换一个函数且不需要调用原始函数时,请使用此方法。 ``` use neohook::detour_inline; #[inline(never)] fn target(x: i32) -> i32 { std::hint::black_box(x) * 2 } // returns x * 2 fn detour(x: i32) -> i32 { x + 100 } fn main() { let _hook = detour_inline!(target, detour).expect("hook failed"); // One line: suspend threads, patch, resume. assert_eq!(target(5), 105); // intercepted // _hook drops here => original bytes restored automatically } ``` ## 用法示例 ### 调用原函数 - `detour_helper!` `detour_helper!` 将跳板指针存储在 `OnceLock` 中,以便你可以从你的 detour 内部将调用转发到原始函数。 ``` use std::sync::OnceLock; use neohook::detour_helper; type AddFn = fn(i32, i32) -> i32; // Storage for the original function pointer (generated by the macro) static ORG_ADD: OnceLock = OnceLock::new(); #[inline(never)] fn add(a: i32, b: i32) -> i32 { a + b } fn detour_add(a: i32, b: i32) -> i32 { // Call the original, then multiply the result let original = ORG_ADD.get().expect("original not set"); original(a, b) * 10 } fn main() { // Args: (static name, target, detour, function type) let _hook = detour_helper!(ORG_ADD, add, detour_add, AddFn) .expect("hook failed"); assert_eq!(add(2, 3), 50); // (2 + 3) * 10 } ``` ### 完全控制 - 事务 API 当你需要原子化地安装多个 Hook,或者需要细粒度控制时,请直接使用 `DetourTransaction` API。 ``` use neohook::DetourTransaction; fn main() { let mut session = DetourTransaction::begin(); // Suspend all threads in the process before the commit session.update_all_threads(); // Queue hooks - none are applied yet session.attach(fn_a as *mut u8, detour_a as *const u8).unwrap(); session.attach(fn_b as *mut u8, detour_b as *const u8).unwrap(); // Atomically apply all queued hooks. // If fn_b fails, fn_a is automatically rolled back. let hooks = session.commit().expect("transaction failed"); } ``` ### IAT Hooking 通过重写导入地址表条目而不是修补函数前导码,将整个模块中对导入函数的调用重定向。当你只想拦截来自特定模块的调用时,这非常有用。 ``` use neohook::DetourTransaction; use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW; type SleepFn = unsafe extern "system" fn(u32); static ORIG_SLEEP: OnceLock = OnceLock::new(); unsafe extern "system" fn hooked_sleep(ms: u32) { if let Some(orig) = ORIG_SLEEP.get() { orig(ms / 2); } } fn main() { unsafe { let h_module = GetModuleHandleW(std::ptr::null()); // current module let mut orig_ptr: *mut u8 = std::ptr::null_mut(); let mut session = DetourTransaction::begin(); session.update_all_threads(); session .attach_iat( h_module, "KERNEL32.dll", "Sleep", hooked_sleep as *const u8, ) .expect("IAT hook failed"); let hooks = session.commit().expect("transaction failed"); let original_ptr = hooks[0].original_ptr(); let original: SleepFn = std::mem::transmute(original_ptr); let _ = ORIG_SLEEP.set(original); // Sleep is now intercepted for this module windows_sys::Win32::System::Threading::Sleep(1000); // returns immediately } } ``` ### VTable Hooking 通过在同一事务 API 中排队 VTable Hook,重定向特定的虚拟槽。 ``` use neohook::DetourTransaction; type SlotFn = extern "system" fn() -> i32; extern "system" fn original_method() -> i32 { 1 } extern "system" fn detour_method() -> i32 { 2 } fn main() { // Demonstration with a synthetic VTable array. // In real usage, this is usually an object's vtable pointer. let mut vtable = [original_method as *mut u8]; let mut tx = DetourTransaction::begin(); let original_ptr = tx .attach_vtable(vtable.as_mut_ptr(), 0, detour_method as *const u8) .expect("VTable attach failed"); let _hooks = tx.commit().expect("transaction failed"); let original: SlotFn = unsafe { std::mem::transmute(original_ptr) }; let current: SlotFn = unsafe { std::mem::transmute(vtable[0]) }; assert_eq!(current(), 2); assert_eq!(original(), 1); } ``` 有关对象作用域的变体,请参阅 [`examples/vtable_instance_hook.rs`](examples/vtable_instance_hook.rs)。 有关 Hook COM 风格接口(`IUnknown` 的 `QueryInterface`/`AddRef`/`Release` 布局), 请参阅 [`examples/com_vtable_hook.rs`](examples/com_vtable_hook.rs)。 ### 保持 Hook 存活(DLL 注入 / DllMain) 在 Rust 中,当值离开作用域时,它们会被 drop(并且 Hook 会被卸载)。在注入到运行中进程的 DLL 内部,你的初始化线程最终会结束——除非你显式延长其生命周期,否则它会带走你的 Hook。 正确的模式是将 Hook 守卫移动到 `OnceLock>` 全局变量中: ``` use std::sync::OnceLock; use neohook::{DetourTransaction, Hook}; static ACTIVE_HOOKS: OnceLock> = OnceLock::new(); unsafe extern "system" fn target_present(/* ... */) { /* ... */ } unsafe extern "system" fn hooked_present(/* ... */) { /* ... */ } fn install_hooks() { let mut session = DetourTransaction::begin(); session.update_all_threads(); session .attach(target_present as *mut u8, hooked_present as *const u8) .unwrap(); let guards = session.commit().expect("hook install failed"); // Transfer ownership into the global - hooks stay alive for the process lifetime if ACTIVE_HOOKS.set(guards).is_err() { // Already initialised (e.g. called twice) - new guards drop and unhook safely } } ``` ### C / C++ FFI NeoHook 暴露了 C ABI。使用以下命令生成头文件: ``` cargo build --features generate-headers ``` 头文件将被写入 `include` 目录。 **关于 FFI 所有权的说明:** - `detours_transaction_commit` 获取事务指针的所有权并将其释放。 - 返回的句柄会保持 Hook 存活,直到你调用 `detours_handle_unhook_and_free`。 - 所有线程安全保证(挂起、RIP 重定向、栈扫描)在从 C/C++ 调用时同样适用。 **VTable FFI API:** - `detours_transaction_attach_vtable(tx, vtable, index, detour)` 成功时返回先前的槽位指针。 ## 工作原理 - 底层机制 ### 朴素补丁的问题 编写一个 `JMP` 需要多个字节。在实时系统中,当你覆盖这些字节时,另一个 CPU 核心可能正在执行这些确切的字节——从而导致立即崩溃。即使你很幸运地避免了竞态,相对跳转指令(`E9 xx xx xx xx`)编码的是_距离其自身地址的偏移量_。将其原封不动地复制到新位置会导致它跳转到错误的地方。 ### NeoHook 提交序列 对于每个 inline hook,NeoHook 都会在目标函数附近构建一个跳板。 该跳板包含两部分: 1. 一个托管网关 2. 一个重定位的函数体 托管网关是一个由 NeoHook 拥有的小型跳转存根,它充当稳定的 original_ptr(),并且稍后可以再次被 Hook。 ``` Original target │ ├── patched to detour │ └── trampoline ├── managed gateway └── relocated original instructions + jump back ``` 从概念上讲,跳板看起来像这样: ``` trampoline: +-------------------------------+ | managed gateway | -> jumps to relocated body +-------------------------------+ | relocated stolen instructions | | jump back to target+stolen | +-------------------------------+ ``` ``` DetourTransaction::commit() │ ├─ 1. FREEZE ──── SuspendThread() on every tracked process thread │ (except the calling thread) │ ├─ 2. SCAN ──── For each suspended thread: │ a. Read thread context with GetThreadContext() │ b. If RIP/EIP points into the bytes that will be overwritten: │ redirect it to trampoline/body + offset │ c. Scan the top part of the stack for stale return addresses │ that still point into the soon-to-be-patched range │ d. Rewrite those return addresses to the relocated body │ ├─ 3. PATCH ──── For each queued hook: │ a. If needed, build or prepare the trampoline │ b. Write the detour jump into the original target │ c. Register the trampoline gateway as the new managed gateway │ d. If the patched target was itself a managed gateway: │ remove the old gateway from the registry │ If any step fails: │ rollback applied hooks and restore redirected threads │ └─ 4. THAW ──── ResumeThread() on every suspended thread ``` ### 指令重定位 Hook 点被覆盖的字节会被复制到跳板缓冲区。因为这些指令通常包含依赖于位置的编码(RIP 相对加载、短分支、`CALL` 目标),所以它们不能简单地原样复制。`iced-x86` 会解码每条被窃取的指令,重新计算相对于新跳板地址的所有相对偏移量,然后对结果进行重新编码。 跳板末尾会追加一个回跳,以将执行返回到原函数中第一条未被窃取的指令。因此,通过跳板进行调用等同于调用原函数。 ### 智能跳板分配 在 x86_64 上,5 字节的 `E9 rel32` 跳转只能达到 ±2 GB 的范围。`TrampolineAlloc::alloc_nearby` 使用 `VirtualQuery` 从目标地址向外扫描空闲内存区域,并在该窗口内进行分配。如果在 ±2 GB 内没有合适的区域,引擎将升级为 14 字节的间接绝对跳转(`FF 25 00000000 `)。 ### Hook 链式调用 托管网关本身可以作为另一个 inline hook 的目标。托管网关本身可以作为另一个 inline hook 的目标。这就是 Hook 链式调用的工作原理。 假设我们用 detour_A Hook 了 target。 ``` Before: target | v [ original function ] ``` 在第一次 Hook 之后: ``` target -----------------------> detour_A gateway_A --------------------> trampoline_body_A | v relocated stolen bytes | v target + stolen_len ``` 现在假设 `target` 又被 `detour_B` Hook。这意味着新目标不再是真正的函数入口。新目标是 `gateway_A`。NeoHook 读取 gateway_A 的目的地,然后创建一个新的网关: ``` gateway_B --------------------> previous target of gateway_A = trampoline_body_A ``` ## 架构概述 ``` neohook/ ├── src/ │ ├── lib.rs - Public API surface, macros (detour_inline!, detour_helper!) │ ├── api.rs - DetourTransaction: high-level Rust API + C FFI entry points │ ├── transaction.rs - TransactionCore: commit/rollback engine │ ├── alloc.rs - TrampolineAlloc: near-memory allocation (x86 + x86_64) │ ├── disasm.rs - Disassembler: instruction length, relocation via iced-x86 │ ├── iat.rs - IatHook: IAT parsing and pointer rewriting │ ├── mem.rs - Memory helpers: VirtualProtect wrapper, atomic write │ ├── module.rs - Module utilities: find_function, get_module_handle │ └── threads.rs - ThreadEnumerator: toolhelp32 snapshot, open/suspend threads └── include/ ├── neohook.h - Auto-generated C header (cbindgen) └── neohook.hpp - C++ header ``` ## 错误处理 所有可能失败的操作都会返回 `Result`: | 变体 | 发生时间 | | :------------------------------ | :------------------------------------------------------------------------------------------------------- | | `DetourError::NotStarted` | 对已经提交或中止的事务调用了方法 | | `DetourError::AllocationFailed` | 在目标地址附近未找到合适的空闲内存区域 | | `DetourError::RelocationFailed` | `iced-x86` 无法重定位被窃取的指令(例如,RIP 相对目标距离跳板 > 2 GB) | | `DetourError::InvalidParameter` | 传入了空指针,或者在模块中未找到请求的 IAT 导入 | `DetourError` 实现了 `std::error::Error` 和 `Display`,因此它可以与 `?`、`anyhow`、`thiserror` 等配合使用。 ## 开发 ### 运行测试 ``` cargo test -- --test-threads=1 ``` 你必须确保只使用单个线程,否则会有发生竞态条件的风险。 ## 免责声明 本库仅供**调试、合法的游戏模组开发、教育目的,以及对您拥有或获得明确授权分析的软件进行逆向工程**使用。 作者不认可将本库用于开发违反条款、未经授权绕过安全措施或对他人造成损害的软件。 ## License 根据以下任一协议授权: - **MIT License** ([LICENSE-MIT](LICENSE-MIT) 或 https://opensource.org/licenses/MIT) - **Apache License, Version 2.0** ([LICENSE-APACHE](LICENSE-APACHE) 或 https://www.apache.org/licenses/LICENSE-2.0) 由你自行选择。
标签:Rust, 二进制重写, 函数钩子, 可视化界面, 底层开发, 端点可见性, 网络流量审计, 通知系统