H0llyW00dzZ/pe-sigscan

GitHub: H0llyW00dzZ/pe-sigscan

一个快速、零依赖且兼容 no_std 的 Rust 签名扫描库,支持 IDA 风格通配符模式,用于在已加载 PE 模块的可执行节中定位字节序列。

Stars: 0 | Forks: 0

# pe-sigscan [![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license) [![codecov](https://codecov.io/gh/H0llyW00dzZ/pe-sigscan/graph/badge.svg?token=16K4XYW1LB)](https://codecov.io/gh/H0llyW00dzZ/pe-sigscan) [![Crates.io](https://img.shields.io/crates/v/pe-sigscan.svg)](https://crates.io/crates/pe-sigscan) [![Documentation](https://docs.rs/pe-sigscan/badge.svg)](https://docs.rs/pe-sigscan)

pe-sigscan logo

针对 Windows 上已加载 PE 模块的可执行节进行快速的进程内字节模式("签名")扫描。 一个小巧、无依赖的基础组件,适用于游戏 mod、hook 程序、调试器以及任何其他需要通过字节签名定位非导出、非 vtable 可访问代码的进程内工具。 ## 功能特性 - **IDA 风格的通配符模式**,可在运行时从字符串解析(`Pattern::from_ida("48 8B 05 ?? ?? ?? ?? 48 89 41 08")`),或通过 `pattern!` 宏在编译时构建(无内存分配)。 - **两种扫描模式**:仅遍历名为 `.text` 的节,或者遍历每个设置了 `IMAGE_SCN_MEM_EXECUTE` 特性的节(某些会将代码拆分到如 `.text$mn` 等伴随节的编译器/链接器需要此功能)。 - **Hook 安装的唯一性**:配套的 `count_*` 函数允许你在修补之前验证模式是否仅匹配一次,从而绝不会错误地 hook 到其他函数。 - **流式迭代**:`iter_in_text`、`iter_in_exec_sections` 和 `iter_in_slice` 会惰性地生成每个非重叠匹配的地址,因此你可以在单次遍历中应用每个匹配的过滤器或修补多个调用点,而无需编写手动扫描循环。 - **rel32 辅助工具**:`resolve_rel32` / `resolve_rel32_at` 封装了容易产生差一错误的 `next_ip + disp32` 算术运算,该运算几乎跟随在 x64 代码(RIP 相对寻址的 `mov`、`call rel32`、`jmp rel32`)中的每一次签名匹配之后。 - **切片变体**(`find_in_slice`、`count_in_slice`、`iter_in_slice`)用于在没有加载 PE 的情况下的离线分析和单元测试。 - **直接内存读取**(无需为每个字节进行 `ReadProcessMemory` 往返)——适用于在不到一秒的时间内扫描数十兆字节的 `.text` 数据。 - **向量化首字节搜索**。热点锚点预过滤器提供两种风格:默认的便携式 SWAR(8 字节字)实现,以及可选的由 `memchr` 支持的路径(使用运行时检测的 AVX2 / SSE2 / NEON)。具体数据请参见 [性能](#performance)。 - **兼容 `#![no_std]`**,仅在从 IDA 风格字符串构建拥有的 `Pattern` 时进行分配。编译时的 `pattern!` 宏会生成一个零分配的 `&'static [Option]`。 - **默认零依赖。** 启用可选的 `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, 可执行内存区, 可视化界面, 字节模式匹配, 恶意软件分析, 游戏模组, 特征码扫描, 网络流量审计, 逆向工程, 通知系统, 零依赖