H0llyW00dzZ/pe-sigscan
GitHub: H0llyW00dzZ/pe-sigscan
一个快速、零依赖且兼容 no_std 的 Rust 签名扫描库,支持 IDA 风格通配符模式,用于在已加载 PE 模块的可执行节中定位字节序列。
Stars: 0 | Forks: 0
# pe-sigscan
[](#license)
[](https://codecov.io/gh/H0llyW00dzZ/pe-sigscan)
[](https://crates.io/crates/pe-sigscan)
[](https://docs.rs/pe-sigscan)
]`。
- **默认零依赖。** 启用可选的 `memchr` feature 仅会引入一个 SIMD 加速的依赖项。
## 快速开始
将此 crate 添加到你的 `Cargo.toml` 中:
```
[dependencies]
pe-sigscan = "0.1"
```
或者,用于 SIMD 加速扫描(推荐用于作弊器 / mod 加载器):
```
[dependencies]
pe-sigscan = { version = "0.1", features = ["memchr"] }
```
### 扫描已加载的进程
```
use pe_sigscan::{find_in_text, Pattern};
// Get a module base via your preferred means (GetModuleHandleW, PEB walk, etc.).
let module_base: usize = /* ... */ 0;
// Build a pattern from an IDA-style hex string. `?` and `??` are wildcards.
let pat = Pattern::from_ida("48 8B 05 ?? ?? ?? ?? 48 89 41 08").unwrap();
if let Some(addr) = find_in_text(module_base, pat.as_slice()) {
println!("matched at {addr:#x}");
}
```
### 编译时模式
```
use pe_sigscan::pattern;
// `_` is the wildcard token; bytes use 0xNN literals.
const SIG: &[Option] = pattern![0x48, 0x8B, _, _, 0x48, 0x89];
```
### 迭代每个匹配项
当单个模式故意匹配多个调用点时(例如,修补每个 `call HeapAlloc` 或记录对特定全局变量的每次引用),请使用迭代器变体:
```
use pe_sigscan::{iter_in_text, pattern};
# let module_base: usize = 0;
const HOOK_TARGETS: &[Option] = pattern![0xE8, _, _, _, _]; // call rel32
for addr in iter_in_text(module_base, HOOK_TARGETS) {
println!("call site at {addr:#x}");
// … install hook, log, or rewrite at `addr`
}
```
迭代器会生成非重叠的匹配(在偏移量 `i` 处命中后,下一次探测从 `i + pattern.len()` 开始),因此 `iter_in_text(..).count()` 始终等于 `count_in_text(..)`。
### 解析 rel32 偏移量
在匹配目标为 32 位 RIP 相对偏移的指令后,下一步几乎总是“跟随偏移量找到其绝对目标”。`resolve_rel32_at` 封装了该计算:
```
use pe_sigscan::{find_in_text, pattern, resolve_rel32_at};
# let module_base: usize = 0;
// mov rax, [rip+disp32]: 48 8B 05 ?? ?? ?? ?? — disp at +3, instr len 7.
const SIG: &[Option] = pattern![0x48, 0x8B, 0x05, _, _, _, _];
if let Some(addr) = find_in_text(module_base, SIG) {
let target = unsafe { resolve_rel32_at(addr, 3, 7) };
println!("global at {target:#x}");
}
```
| Instruction | Bytes (anchor + disp) | `rel32_offset` | `instr_len` |
| -------------------- | ---------------------------- | -------------- | ----------- |
| `mov rax, [rip+d32]` | `48 8B 05 ?? ?? ?? ??` | 3 | 7 |
| `lea rax, [rip+d32]` | `48 8D 05 ?? ?? ?? ??` | 3 | 7 |
| `call rel32` | `E8 ?? ?? ?? ??` | 1 | 5 |
| `jmp rel32` | `E9 ?? ?? ?? ??` | 1 | 5 |
| `jcc rel32` | `0F 8x ?? ?? ?? ??` | 2 | 6 |
对于离线分析(无已加载的 PE),`read_rel32(&bytes, offset)` 是返回原始 `i32` 偏移量的安全切片等效方法。
### 在安装 hook 前验证唯一性
```
use pe_sigscan::{count_in_text, find_in_text, pattern};
# let module_base: usize = 0;
const TARGET_SIG: &[Option] = pattern![
0x48, 0x89, 0x5C, 0x24, _, 0x48, 0x89, 0x74, 0x24, _,
0x48, 0x89, 0x7C, 0x24, _, 0x55, 0x41, 0x56, 0x41, 0x57,
];
let count = count_in_text(module_base, TARGET_SIG);
match count {
1 => {
let addr = find_in_text(module_base, TARGET_SIG).unwrap();
// … install hook at `addr`
}
0 => panic!("pattern not found — game may have been updated"),
n => panic!("pattern matched {n} sites — refusing to install (ambiguous)"),
}
```
### 遍历每个可执行节
一些编译器和链接器会将代码拆分到多个节(`.text$mn`、`.textbss`、优化布局的内存区域)。当你扫描的函数可能不在名为 `.text` 的节中时,请使用 `*_in_exec_sections` 变体:
```
use pe_sigscan::{find_in_exec_sections, pattern};
# let module_base: usize = 0;
const SIG: &[Option] = pattern![0x48, 0x8B, _, _, _, _, 0xFF, 0xE0];
let addr = find_in_exec_sections(module_base, SIG);
```
### 离线分析(无需加载 PE)
```
use pe_sigscan::{find_in_slice, pattern};
let bytes = [0x00, 0x11, 0x48, 0x8B, 0x05, 0x99];
let pat = pattern![0x48, 0x8B, 0x05];
let hit = find_in_slice(&bytes, pat).unwrap();
assert_eq!(hit, bytes.as_ptr() as usize + 2);
```
## 模式语法
`Pattern::from_ida` 接受以空格分隔的 token:
| Token | Meaning |
| ------ | -------------------------------------- |
| `XX` | 两位十六进制数字 — 匹配字面字节 `0xXX`。不区分大小写。 |
| `?` | 通配符 — 匹配任意字节。 |
| `??` | 通配符(长格式,与 `?` 相同)。 |
token 之间的 ASCII 空白字符(空格、制表符、换行符、回车符)将被忽略。其他任何内容都将返回一个包含错误 token 索引的 [`ParsePatternError`]。
```
use pe_sigscan::Pattern;
assert!(Pattern::from_ida("48 8B ?? 89").is_ok());
assert!(Pattern::from_ida("AB CD EF").is_ok()); // upper-case hex
assert!(Pattern::from_ida("ab cd ef").is_ok()); // lower-case hex
assert!(Pattern::from_ida(" 48\t??\n89 ").is_ok()); // extra whitespace
assert!(Pattern::from_ida("48 ZZ 89").is_err()); // invalid hex
assert!(Pattern::from_ida("48 8 89").is_err()); // single hex digit
assert!(Pattern::from_ida("").is_err()); // empty
```
## 性能
签名扫描主要由内部循环主导,该循环在每个候选偏移量处探测一个锚点字节(模式的第一个非通配符字节)。此 crate 提供了该热路径的两种实现:
- **SWAR(默认)** — 使用标准的“查找零字节”位操作技巧进行便携的 8 字节字搜索。纯 `no_std` Rust,无依赖,适用于 rustc 支持的每个目标。
- **memchr(`memchr` feature)** — 将锚点扫描委托给 [`memchr`](https://crates.io/crates/memchr) crate,该 crate 执行运行时 CPU 特性检测,并在 x86_64 上使用 AVX2 / SSE2,在 aarch64 上使用 NEON。
### 基准测试数据
基准测试(`benches/scan.rs`,criterion)在一个 1 MiB 的零值缓冲区(最坏情况,即锚点字节永远不匹配且内部循环必须遍历整个 haystack)中搜索带有一个通配符(`48 8B 05 ? ? ? ? 48`)的 8 字节模式。
| Backend | `find_in_slice` (1 MiB) | `count_in_slice` (1 MiB) | vs. naive |
| --- | --- | --- | --- |
| Naive byte-by-byte (pre-fastscan) | ~662 µs | ~331 µs | 1× |
| SWAR fallback (default features) | ~102 µs | ~99 µs | **6.5× / 3.3×** |
| memchr (`--features memchr`) | **~10 µs** | **~10 µs** | **63× / 32×** |
以上数据来自 Windows 11 / x86_64 主机;在 Linux 和 macOS 上相对差距相似。运行 `cargo bench`(默认后端)或 `cargo bench --features memchr` 以复现。
### 何时启用 `memchr`
在扫描吞吐量至关重要时启用它 —— 通常是每次扫描要处理数十到数百兆字节的进程内工具:
- 在注入时扫描 `client.dll`(约 30–60 MB)或 `GameAssembly.dll`(50–200 MB)的内部作弊器 / mod 加载器。
- 希望将 CPU 峰值保持在较短时间的反作弊感知代码。
- 在每次游戏更新后重新运行 100 多个签名的测试工具。
```
[dependencies]
pe-sigscan = { version = "0.1", features = ["memchr"] }
```
对于一次性离线工具(Ghidra/IDA 脚本、签名开发 REPL),默认的 SWAR 路径已经比朴素实现快 3-6 倍,并且你可以保持 crate 无依赖。
## 使用场景
`pe-sigscan` 可用于需要在 PE 模块内定位代码或数据的各种场景:
### 游戏 Mod 与内部工具
- 查找要 hook 的函数地址(在 `.text` 或其他可执行节中)
- 基于签名的偏移量扫描(取代硬编码地址)
- 在安装 hook 前使用 `count_*` 函数验证模式的唯一性
### 逆向工程
- 无需依赖调试符号即可快速定位函数和数据结构
- 构建用于重复二进制分析的自定义签名数据库
- 以编程方式支持 IDA/Ghidra 风格的工作流
### 恶意软件分析与安全研究
- 检测已知的恶意代码模式或脱壳存根
- 识别反调试、反虚拟机或规避技术
- 在沙箱、分析管道或安全工具中进行自动化扫描
### 开发与调试工具
- 自定义内存扫描器和运行时调试器
- 二进制修补和修改工具
- 运行时函数重定向或 hook 框架
### 离线分析
- 使用 `find_in_slice` 直接从磁盘扫描 PE 文件,而无需将其加载到内存中
- 适用于静态分析工具和自动化签名检查器
## 为什么采用直接内存读取?
已加载 DLL 的 `.text` 节是页对齐的、受 RX 保护的,并在模块的整个生命周期内保持提交状态。不存在 TOCTOU(检查时间与使用时间)问题;字节在读取之间不会发生变化。典型的扫描会遍历数十兆字节的数据 —— 如果通过 `ReadProcessMemory` 路由每一次探测,将耗费数千万次系统调用(数分钟的挂钟时间)。此 crate 通过原始指针解引用直接读取,并严格限制在 PE 声明的节范围内。
## 安全性
公共扫描函数接受一个从操作系统获取的 `module_base: usize`(例如通过 `GetModuleHandleW`)。实现在进行任何其他访问之前会解析该基址处的 PE 头,因此非 PE 指针会被干净地拒绝。在经过验证的节范围内,不安全的指针读取受节头中 `VirtualSize` 字段的限制 —— 除非加载器向我们传递了一个格式错误的 PE(加载器本身也会拒绝它),否则不会发生越界读取。
切片变体(`find_in_slice`、`count_in_slice`)由于 Rust 的切片不变量而安全,无需调用者提供额外的信任保证。
## 平台
仅限 Windows / PE。
该 crate 可以在每个平台上编译 —— 解析纯粹是计算 —— 但进程内函数签名假定 `module_base` 来自 Windows 加载器。在非 Windows 目标上,切片变体(`find_in_slice`、`count_in_slice`)仍然可用于分析你手动映射的 PE 字节。
## MSRV(最低支持的 Rust 版本)
Rust 1.70。
## 许可证
根据以下任一许可证授权:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) 或
)
- MIT license ([LICENSE-MIT](LICENSE-MIT) 或
)
由你选择。
## 贡献
除非你明确声明,否则你提交的任何旨在包含在本作品中的贡献,均按 Apache-2.0 许可证的定义,应按上述方式进行双重许可,不附加任何额外条款或条件。
标签:Crates.io, DAST, IDA特征码, no_std, PE文件解析, RIP相对寻址, Rust, .text节区, Windows开发, x64汇编, 二进制分析, 云安全运维, 云资产清单, 内存搜索, 内存补丁, 内联Hook, 可执行内存区, 可视化界面, 字节模式匹配, 恶意软件分析, 游戏模组, 特征码扫描, 网络流量审计, 逆向工程, 通知系统, 零依赖