CyberSnakeH/SmileHook
GitHub: CyberSnakeH/SmileHook
SmileHook:Linux x86-64平台下的Rust内联函数钩子工具。
Stars: 0 | Forks: 0
# SmileHook



SmileHook 通过用跳转到 *绕道* 的方式覆盖函数的前置代码,同时通过 *跳板* 保持原始函数的可访问性。它是一个 **进程内** 库——它修补当前进程运行的代码——并且是用默认安全的 Rust 编写的,每个 `unsafe` 块都有正当的理由。将代码注入另一个进程是一个单独的问题(参见 [路线图](#roadmap))。
- **14字节绝对跳转** (`jmp [rip+0]` + 地址) —— 没有±2 GiB的范围限制。
- **忠实的跳板**:使用 `[iced-x86](https://github.com/icedland/iced)` 将窃取的指令重新定位,包括重写不再到达其目标的相对分支。
- **线程安全的修补**:每次启用/禁用都会冻结其他线程,并重新定位任何捕获的指令指针 *和* 栈返回地址,因此并发调用者永远不会观察到半写的前置代码。
- **原子批量应用**:在单个冻结下翻转多个钩子。
- **按地址或按符号名称钩子**(通过 `dlsym`)。
- **拒绝不安全的目标**:如果函数的前置代码比跳转短,则拒绝而不是破坏下一个函数。
- **C / C++ ABI**:链接静态库和钩子来自原生有效负载。
## 快速开始
```
use std::sync::atomic::{AtomicPtr, Ordering};
// Holds the trampoline so the detour can call the original.
static ORIGINAL: AtomicPtr = AtomicPtr::new(std::ptr::null_mut());
extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
extern "C" fn add_detour(a: i32, b: i32) -> i32 {
let original: extern "C" fn(i32, i32) -> i32 =
unsafe { std::mem::transmute(ORIGINAL.load(Ordering::SeqCst)) };
original(a, b) + 100
}
fn main() -> smilehook::Result<()> {
let registry = smilehook::global();
let target = add as *const () as *mut u8;
// SAFETY: `add_detour` is ABI-compatible with `add`.
let trampoline = unsafe { registry.create(target, add_detour as *const () as *const u8)? };
ORIGINAL.store(trampoline as *mut u8, Ordering::SeqCst);
unsafe { registry.enable(target)? }; // calls to `add` now route through the detour
assert_eq!(add(1, 2), 103);
unsafe { registry.disable(target)? }; // restore the original
assert_eq!(add(1, 2), 3);
Ok(())
}
```
绕道 **必须** 与其目标具有 ABI 兼容性。原始函数总是通过 `create` 返回的跳板来访问,而不是再次调用目标(这将重新进入绕道)。
### 两层
* **`Hook`** —— 一个单独的 RAII 钩子。在释放时自动禁用和释放。用于范围限定、一次性的钩子。
* **`Registry`** —— 一个通过目标地址键索引的线程安全钩子集合。它拒绝重复项并支持批量操作。通过 `smilehook::global()` 可用全局实例。
## 工作原理
1. **解码** —— `iced-x86` 解码目标的前置代码,以便只窃取 *整个* 指令(至少 14 字节)。
2. **重新定位** —— 这些指令被重新编码到可执行页面(`mmap`),然后跳转到 `target + N`。调用跳板相当于调用原始函数。
3. **修补** —— 前置代码被覆盖为 14 字节绝对间接跳转:`FF 25 00 00 00 00` (`jmp qword ptr [rip + 0]`) 后跟 8 字节绕道地址。与 `E9 rel32` 跳转不同,这种形式没有范围限制。
4. **冻结** —— 每次启用、禁用和释放都在 *停止世界* 冻结下发生(参见 [安全和并发](#safety-and-concurrency)),因此并发调用者永远不会执行部分修补的前置代码。
`iced-x86` 的 `BlockEncoder` 处理微妙的部分:如果窃取的相对 `call`/`jmp` 无法从跳板到达其目标,则将其重写为带有绝对地址的间接跳转,该地址存储在行外。跳回由编码器作为 *块指令*(而不是手动附加)传递,以保持布局正确。由于编码器不报告这种重写方式的可用偏移量,SmileHook 通过解码编码的跳板重新推导每个重新定位的偏移量,并记录一个 **边界映射**(原始偏移量 → 跳板偏移量),用于修复任何在前置代码中冻结的线程。
## 按符号名称钩子
`symbol_address` 通过 `dlsym(RTLD_DEFAULT, …)` 解析导出的函数,并通过 `Registry::create_by_symbol` 在一步中将其钩子——对于拦截库函数(例如 `glXSwapBuffers` 或 `vkQueuePresentKHR`)来说很方便:
```
let trampoline = unsafe {
smilehook::global().create_by_symbol("strlen", my_strlen as *const () as *const u8)?
};
```
## 原子批量应用
`enable_all` / `disable_all` 在每个钩子上冻结进程一次。要一起翻转多个钩子并支付 **一个** 停止世界的费用而不是 N,请排队更改并在单个冻结中应用它们(MinHook 的 `MH_ApplyQueued` 的类似物):
```
let reg = smilehook::global();
reg.queue_enable(target_a)?;
reg.queue_disable(target_b)?; // mixed enables and disables are fine
unsafe { reg.apply_queued()? }; // all take effect under one freeze
```
`queue_*` 只更新簿记(因此是安全的);实际的修补发生在 `apply_queued` 中。已处于请求状态的钩子将被跳过。
## C/C++ API
SmileHook 还构建为静态库,公开一个与 MinHook 相似的 C ABI,因此原生有效负载(例如 `LD_PRELOAD` 到目标中的 C++ 覆盖)可以安装钩子而无需编写任何 Rust。该包的 `[lib]` 类型包括 `staticlib`,因此发布构建会生成 `target/release/libsmilehook.a`;匹配的声明在 `include/smilehook.h` 中,一个可运行的示例在 `examples/c_api.c` 中。
```
#include "smilehook.h"
typedef int (*add_fn)(int, int);
static add_fn original;
int add_detour(int a, int b) { return original(a, b) + 100; }
void install(void *add) {
sh_create_hook(add, (const void *)add_detour, (void **)&original);
sh_enable_hook(add); /* calls to `add` now route through the detour */
}
```
```
cargo build --release # -> target/release/libsmilehook.a
cc -O0 -Iinclude examples/c_api.c target/release/libsmilehook.a \
-lpthread -ldl -lm -o c_api && ./c_api
```
每个 `sh_*` 调用都返回一个状态码(`SH_OK` 或负数 `SH_ERR_*`);`sh_strerror` 将其转换为静态消息。入口点捕获任何内部的 Rust 紧急情况并返回 `SH_ERR_OTHER` 而不是在边界处展开,因此钩子引擎的故障不会终止宿主进程。
## 架构
```
src/
├── lib.rs crate root, docs, public re-exports
├── error.rs Error + Result
├── memory.rs ExecBuffer (executable mmap) and patch_code (mprotect)
├── arch/
│ ├── mod.rs ISA dispatch (x86-64 only; compile error otherwise)
│ └── x86_64.rs prologue decoding/relocation, jump generation, boundary map
├── freeze.rs stop-the-world thread suspension during a patch
├── symbol.rs symbol_address — resolve a function by name via dlsym
├── ffi.rs C ABI bridge (sh_* functions) for non-Rust payloads
├── hook.rs Hook — a single RAII hook
└── registry.rs Registry — thread-safe multi-hook registry + global()
include/
└── smilehook.h C/C++ header matching the ffi.rs bridge
```
特定于 ISA 的逻辑(解码、重新定位、跳转发射)被隔离在 `arch::x86_64` 中;该包的其余部分与架构无关。
## 安全性和并发
安装钩子修补正在运行的、可执行的代码,因此绕道 **必须** 与目标具有 ABI 兼容性。
14 字节写入不是原子的,Linux 没有进程内的 "挂起线程" 原语,因此 SmileHook 使用 **停止世界的冻结** 来确保对并发执行的修补安全。在每次启用、禁用和释放修补线程时:
1. 通过 `/proc/self/task` 列举其他线程并向每个线程发送实时信号(`tgkill` 与 `SIGRTMIN+4`);
2. 等待直到所有线程都停在异步信号安全的处理程序中(在停车之前退出的线程将从等待集中删除);
3. 使用重新定位边界映射重新定位任何在受影响字节中捕获的线程——其保存的指令指针 **和** 栈上的返回地址,该地址指向窃取的前置代码(前置代码中的 `call` 会返回到修补的字节);
4. 应用修补并释放线程。
如果线程阻塞冻结信号,则操作将失败并返回 `Error::FreezeTimeout` 而不是挂起。`Registry` 操作还通过互斥锁进行序列化。
调用者的一个义务仍然存在:**在其绕道可能仍在执行时,不要删除或释放钩子**,因为这样会释放它调用的跳板。
## 健壮性
两个测试套件在许多前置代码上测试重新定位引擎,而不仅仅是少数几个:
* **语料库健全性** 在 ~130 个 *真实* glibc/pthread 函数上运行重新定位器——向量化的字符串/内存操作、stdio、数学、线程——并断言每个重新定位在结构上和语义上都是健全的(良好的边界映射、没有哨兵偏移量、按字节逐字复制的普通指令、有效的跳回)或被干净地拒绝。它永远不会修补实时函数,因此可以在任意代码上安全地运行。
* **差异模糊测试** 使用成千上万的随机输入将具有不同前置代码形状的合成函数(叶、早期 `call`、大框架、分支、SSE、许多局部变量)钩子,检查透明绕道是否可以精确地复制原始函数,以及转换绕道是否正确组合。
如果优化后的前置代码比 14 字节跳转短(例如,`endbr64; mov; syscall; ret` 系统调用包装器),则使用 `Error::PrologueTooShort` 拒绝而不是允许覆盖相邻的函数。
## 构建和测试
不需要注入器——测试在测试进程中钩子函数。
```
cargo build --release # also emits target/release/libsmilehook.a
cargo test # hook, registry, symbol, ffi, thread-safety, fuzz
cargo test --release # the same, with optimized codegen
cargo run --example basic # multi-hook registry demo
cargo run --example diag # disassemble a target prologue and its trampoline
```
`tests/thread_safety.rs` 是一个压力测试:工作线程在紧密循环中锤击目标,而主线程在数百次(单独和原子批量)切换钩子。每个观察到的返回值必须是原始的或钩子的——永远不会是撕裂的结果。
## 状态
- [x] x86-64 内联钩子(14 字节绝对跳转)
- [x] 带有完整指令重新定位和指令指针/返回地址修复边界映射的跳板
- [x] RAII `Hook`:在释放时启用/禁用/恢复
- [x] 线程安全的多钩子 `Registry` + 全局 `global()`
- [x] 线程安全的修补:带有 IP 和栈返回地址重新定位的停止世界的冻结
- [x] 原子批量应用:`queue_enable` / `queue_disable` (+ `_all`) 和 `apply_queued`
- [x] 通过 `dlsym` 按符号名称钩子
- [x] 拒绝钩子比跳转短的函数(`PrologueTooShort`)
- [x] 紧急情况安全的 C / C++ ABI 桥接(`staticlib` + `include/smilehook.h`)
- [x] 在真实库语料库和差异模糊测试上证明重新定位
## 路线图
- [ ] 通过 `/proc/self/maps` 模块 + 偏移量查找模块钩子
- [ ] 32 位 x86 支持
- [ ] AArch64 支持(引擎是 ISA 特定的)
## 平台说明
- 目前仅支持 x86-64;在另一个架构上构建会失败并显示清晰的编译错误。
- 在 **SELinux 的 `Enforcing` 模式下**,可执行跳板 `mmap` 和 `.text` `mprotect` 需要 `execmem` / `execmod`,默认情况下 `unconfined_t` 进程具有这些权限。受限进程需要调整策略。
## 许可证
在 [MIT 许可证](LICENSE) 下发布。
标签:Absolute Jump, API, Atomic Operations, Binary Code Manipulation, C/C++, Code Analysis, Code Injection, Code Patching, Computer Science, Debugging, Detours, Development Tools, Function Hooking, Hooks by Address, Hooks by Symbol, In-process Library, MIT License, Performance Optimization, Programming, Reverse Engineering, Rust, Safe-by-Default, Security, Software Deployment, Software Development, Software Engineering, Software Maintenance, Software Testing, Software Tools, System Administration, Thread Safety, Trampoline Relocation, Trampolines, x86-64, 事务性I/O, 凭据导出, 可视化界面, 网络流量审计, 通知系统