pe_sigscan/lib.rs
1//! # pe-sigscan
2//!
3//! Fast byte-pattern ("signature") scanning over the executable sections of a
4//! loaded PE (Portable Executable) module on Windows.
5//!
6//! This crate is a building block for game mods, hookers, debuggers, and any
7//! other tool that needs to locate non-exported, non-vtable-accessible code by
8//! its byte signature. It mirrors the workflow common across the
9//! reverse-engineering ecosystem — derive a pattern from a disassembler (IDA,
10//! Ghidra, Binary Ninja, Cutter), then scan either the live process's mapped
11//! image or bytes read through a custom backend.
12//!
13//! ## Quick start
14//!
15//! ```no_run
16//! use pe_sigscan::{find_in_text, Pattern};
17//!
18//! // Get a module base via your preferred means (GetModuleHandleW,
19//! // PEB walk, etc.). For demonstration we assume a known base.
20//! # let module_base = 0usize;
21//!
22//! // Build a pattern from an IDA-style hex string. `?` and `??` are
23//! // wildcards; whitespace between bytes is ignored.
24//! let pat = Pattern::from_ida("48 8B 05 ?? ?? ?? ?? 48 89 41 08").unwrap();
25//!
26//! if let Some(addr) = find_in_text(module_base, pat.as_slice()) {
27//! println!("matched at {addr:#x}");
28//! }
29//! ```
30//!
31//! Or with the `pattern!` macro (no allocation, fully `const`-eligible):
32//!
33//! ```
34//! use pe_sigscan::pattern;
35//!
36//! const SIG: &[Option<u8>] = pattern![0x48, 0x8B, _, _, 0x48, 0x89];
37//! assert_eq!(SIG.len(), 6);
38//! assert_eq!(SIG[0], Some(0x48));
39//! assert_eq!(SIG[2], None);
40//! ```
41//!
42//! ## Two scanning modes
43//!
44//! - [`find_in_text`] / [`count_in_text`] / [`iter_in_text`] — walk only
45//! the section literally named `.text`. The simplest case, suitable for
46//! MSVC-built DLLs that put everything in one code section.
47//! - [`find_in_exec_sections`] / [`count_in_exec_sections`] /
48//! [`iter_in_exec_sections`] — walk every section whose
49//! `IMAGE_SCN_MEM_EXECUTE` characteristic is set. Required when the
50//! function you're scanning for might live in a companion section like
51//! `.text$mn`, `.textbss`, a jump-table arena, or any of the
52//! optimized-layout code sections that some compilers and linkers emit.
53//!
54//! Both modes have [`find_in_slice`] / [`count_in_slice`] / [`iter_in_slice`]
55//! companions that work on a `&[u8]` instead of a loaded PE — useful for
56//! offline analysis, unit testing, and scanning extracted bytes.
57//!
58//! ## Reader-backed scanning
59//!
60//! For out-of-process backends such as `ReadProcessMemory`, kernel drivers,
61//! DMA, or hypervisor introspection, use the [`MemoryReader`] trait plus the
62//! `*_with` helpers such as [`find_in_text_with`] and
63//! [`find_in_exec_sections_with`].
64//!
65//! Those APIs read the target section bytes into local memory once, then reuse
66//! the same slice scanner as the in-process path. They return the remote match
67//! address, not the local scratch-buffer address.
68//!
69//! ```no_run
70//! use pe_sigscan::{find_in_text_with, pattern, MemoryReader};
71//!
72//! struct Remote;
73//!
74//! impl MemoryReader for Remote {
75//! fn read_bytes(&self, addr: usize, buf: &mut [u8]) -> Option<()> {
76//! # let _ = (addr, buf);
77//! // Fill `buf` from your remote backend here.
78//! None
79//! }
80//! }
81//!
82//! # let reader = Remote;
83//! # let module_base = 0usize;
84//! const SIG: &[Option<u8>] = pattern![0x48, 0x8B, 0x05, _, _, _, _];
85//! let _ = find_in_text_with(&reader, module_base, SIG);
86//! ```
87//!
88//! ## Resolving rel32 displacements
89//!
90//! Real signature workflows almost always end with "match the
91//! instruction, then follow its `rel32` displacement to the actual target
92//! address". The [`resolve_rel32`] / [`resolve_rel32_at`] helpers package
93//! that arithmetic so callers don't reinvent the off-by-one-prone
94//! `next_ip + disp32` calculation:
95//!
96//! ```no_run
97//! use pe_sigscan::{find_in_text, pattern, resolve_rel32_at};
98//! # let module_base = 0usize;
99//!
100//! // mov rax, [rip+disp32]: 48 8B 05 ?? ?? ?? ?? (7 bytes total).
101//! const SIG: &[Option<u8>] = pattern![0x48, 0x8B, 0x05, _, _, _, _];
102//! if let Some(addr) = find_in_text(module_base, SIG) {
103//! let target = unsafe { resolve_rel32_at(addr, 3, 7) };
104//! println!("global at {target:#x}");
105//! }
106//! ```
107//!
108//! ## Why direct memory reads?
109//!
110//! The `.text` section of a loaded DLL is page-aligned, RX-protected, and
111//! stays committed for the lifetime of the module. There is no TOCTOU
112//! concern; bytes don't change between reads. A typical scan walks tens of
113//! megabytes of bytes — routing every probe through `ReadProcessMemory`
114//! would cost tens of millions of syscalls (minutes of wall time). This
115//! crate's in-process path reads directly via raw pointer dereference, bounded
116//! to PE-declared section ranges.
117//!
118//! For out-of-process backends, use the reader-backed `*_with` APIs. Those
119//! copy the target section bytes once into local memory, then reuse the same
120//! slice scanner as [`find_in_slice`].
121//!
122//! ## Safety
123//!
124//! Public functions take a `module_base: usize` you must obtain from the OS
125//! (e.g. `GetModuleHandleW`). The implementation parses the PE headers at
126//! that base before any other access, so a non-PE pointer is rejected
127//! cleanly. Inside the validated section ranges, the unsafe pointer reads
128//! are bounded by the `VirtualSize` field from the section header — outside
129//! the loader handing us a malformed PE (which the loader itself would have
130//! rejected), there is no path to an out-of-bounds read.
131//!
132//! The slice variants are safe by Rust's slice invariants and need no
133//! further trust from the caller.
134//!
135//! ## Platform
136//!
137//! Windows / PE only.
138//!
139//! The crate compiles on every platform — the parsing is pure compute —
140//! but the in-process function signatures assume a `module_base` that came
141//! from the Windows loader. On non-Windows targets, the slice variants
142//! still work for analysing PE bytes you have mapped manually.
143//!
144//! ## License
145//!
146//! MIT OR Apache-2.0.
147
148#![cfg_attr(not(any(feature = "std", test)), no_std)]
149#![warn(missing_docs)]
150#![warn(rust_2018_idioms)]
151#![warn(unreachable_pub)]
152#![allow(unsafe_op_in_unsafe_fn)]
153
154extern crate alloc;
155
156/// Abstract byte reader for address spaces that cannot be dereferenced
157/// directly from the current process.
158///
159/// This is the intended integration point for out-of-process backends such as
160/// `ReadProcessMemory`, kernel drivers, DMA, and VM introspection.
161pub trait MemoryReader {
162 /// Fill `buf` with bytes starting at `addr`.
163 ///
164 /// Returns `None` when the read fails or the range is not available.
165 fn read_bytes(&self, addr: usize, buf: &mut [u8]) -> Option<()>;
166}
167
168mod error;
169mod fastscan;
170mod instr;
171mod pattern;
172mod pe;
173mod scan;
174
175pub use crate::error::{ParseErrorKind, ParsePatternError};
176pub use crate::instr::{read_rel32, resolve_rel32, resolve_rel32_at};
177pub use crate::pattern::{Pattern, WildcardPattern};
178pub use crate::pe::module_size_with;
179pub use crate::scan::{
180 count_in_exec_sections, count_in_exec_sections_with, count_in_slice, count_in_text,
181 count_in_text_with, find_in_exec_sections, find_in_exec_sections_with, find_in_slice,
182 find_in_text, find_in_text_with, iter_in_exec_sections, iter_in_slice, iter_in_text, Matches,
183 SliceMatches,
184};
185
186// Section-targeted scanners (feature `section-info`).
187//
188// `find_in_section` / `count_in_section` / `iter_in_section` live in
189// `scan.rs` alongside the always-available scanners; they're the
190// same shape as `find_in_text` etc. but take a section name as a
191// parameter, letting callers scan inside `.rdata`, `.pdata`,
192// `.text$mn`, etc. Internally they delegate to `crate::pe::find_section`.
193//
194// The feature also re-exports the section-lookup helpers from
195// `crate::pe` so that advanced users can implement their own
196// section-specific logic if needed.
197#[cfg(feature = "section-info")]
198pub use crate::scan::{
199 count_in_section, count_in_section_with, find_in_section, find_in_section_with, iter_in_section,
200};
201
202// `module_size` is a standalone reader for
203// `IMAGE_OPTIONAL_HEADER.SizeOfImage` — useful for cross-module
204// rel32 disambiguation (pairs naturally with the always-available
205// `resolve_rel32*` helpers). It is exported unconditionally.
206pub use crate::pe::module_size;
207
208// ---------------------------------------------------------------------------
209// pattern! macro
210// ---------------------------------------------------------------------------
211//
212// Macros must be defined at the crate root (or re-exported with
213// `#[macro_export]`) to be reachable as `pe_sigscan::pattern!`. We keep the
214// definition here rather than in a `macros` submodule so the public macro
215// path is the natural `crate::pattern!`.
216
217/// Build a `&'static [Option<u8>; N]` at compile time from a list of byte
218/// literals and `_` wildcards.
219///
220/// # Examples
221///
222/// ```
223/// use pe_sigscan::pattern;
224///
225/// // `_` is the wildcard token. Use byte literals (0xNN) for fixed bytes.
226/// const SIG: &[Option<u8>] = pattern![0x48, 0x8B, _, _, 0x48, 0x89];
227/// assert_eq!(SIG, &[Some(0x48), Some(0x8B), None, None, Some(0x48), Some(0x89)]);
228/// ```
229///
230/// This is the zero-cost / no-allocation alternative to
231/// [`Pattern::from_ida`]. Use it when the pattern is known at compile time
232/// (the common case for hard-coded signatures); use `Pattern::from_ida`
233/// when the pattern is loaded from config, a dump file, or user input at
234/// runtime.
235#[macro_export]
236macro_rules! pattern {
237 [ $( $tok:tt ),* $(,)? ] => {
238 &[ $( $crate::__pattern_token!($tok) ),* ]
239 };
240}
241
242/// Helper for [`pattern!`] — converts a single token into `Some(byte)` or
243/// `None`. Hidden from the public surface; users should not call this
244/// directly.
245#[doc(hidden)]
246#[macro_export]
247macro_rules! __pattern_token {
248 (_) => {
249 ::core::option::Option::<u8>::None
250 };
251 ($byte:literal) => {
252 ::core::option::Option::<u8>::Some($byte)
253 };
254}
255
256// ---------------------------------------------------------------------------
257// Crate-level integration tests
258// ---------------------------------------------------------------------------
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 // -- pattern! macro --------------------------------------------------
265
266 #[test]
267 fn pattern_macro_no_wildcards() {
268 const SIG: &[Option<u8>] = pattern![0x48, 0x8B, 0x05];
269 assert_eq!(SIG, &[Some(0x48), Some(0x8B), Some(0x05)]);
270 }
271
272 #[test]
273 fn pattern_macro_with_wildcards() {
274 const SIG: &[Option<u8>] = pattern![0x48, _, 0x05, _, _];
275 assert_eq!(SIG, &[Some(0x48), None, Some(0x05), None, None]);
276 }
277
278 #[test]
279 fn pattern_macro_trailing_comma() {
280 // `$(,)?` — trailing comma should compile.
281 const SIG: &[Option<u8>] = pattern![0x48, 0x8B,];
282 assert_eq!(SIG, &[Some(0x48), Some(0x8B)]);
283 }
284
285 #[test]
286 fn pattern_macro_empty() {
287 // Zero-token form should compile to an empty slice.
288 const SIG: &[Option<u8>] = pattern![];
289 assert!(SIG.is_empty());
290 }
291
292 #[test]
293 fn pattern_macro_single_byte() {
294 const SIG: &[Option<u8>] = pattern![0xCC];
295 assert_eq!(SIG, &[Some(0xCC)]);
296 }
297
298 #[test]
299 fn pattern_macro_single_wildcard() {
300 const SIG: &[Option<u8>] = pattern![_];
301 assert_eq!(SIG, &[None]);
302 }
303
304 // -- error display ---------------------------------------------------
305
306 #[test]
307 fn error_display_empty() {
308 let e = ParsePatternError {
309 token_index: 0,
310 kind: ParseErrorKind::Empty,
311 };
312 let s = alloc::format!("{e}");
313 assert!(s.contains("no tokens"), "got: {s}");
314 }
315
316 #[test]
317 fn error_display_invalid_length() {
318 let e = ParsePatternError {
319 token_index: 3,
320 kind: ParseErrorKind::InvalidLength,
321 };
322 let s = alloc::format!("{e}");
323 assert!(s.contains("token #3"), "got: {s}");
324 assert!(s.contains("two hex digits"), "got: {s}");
325 }
326
327 #[test]
328 fn error_display_invalid_hex_digit() {
329 let e = ParsePatternError {
330 token_index: 1,
331 kind: ParseErrorKind::InvalidHexDigit,
332 };
333 let s = alloc::format!("{e}");
334 assert!(s.contains("token #1"), "got: {s}");
335 assert!(s.contains("non-hex"), "got: {s}");
336 }
337
338 #[test]
339 fn error_is_copy_and_clone() {
340 let e = ParsePatternError {
341 token_index: 0,
342 kind: ParseErrorKind::Empty,
343 };
344 let copied = e;
345 let cloned = e;
346 assert_eq!(copied, e);
347 assert_eq!(cloned, e);
348 }
349
350 #[test]
351 fn error_kind_equality() {
352 // Touch the `PartialEq` derive on every variant so coverage tools
353 // see the discriminant comparisons exercised.
354 assert_eq!(ParseErrorKind::Empty, ParseErrorKind::Empty);
355 assert_eq!(ParseErrorKind::InvalidLength, ParseErrorKind::InvalidLength);
356 assert_eq!(
357 ParseErrorKind::InvalidHexDigit,
358 ParseErrorKind::InvalidHexDigit
359 );
360 assert_ne!(ParseErrorKind::Empty, ParseErrorKind::InvalidLength);
361 assert_ne!(ParseErrorKind::Empty, ParseErrorKind::InvalidHexDigit);
362 assert_ne!(
363 ParseErrorKind::InvalidLength,
364 ParseErrorKind::InvalidHexDigit
365 );
366 }
367
368 #[cfg(feature = "std")]
369 #[test]
370 fn error_implements_std_error() {
371 // Existence proof: this only compiles if the trait impl exists.
372 fn assert_error<E: std::error::Error>(_: &E) {}
373 let e = ParsePatternError {
374 token_index: 0,
375 kind: ParseErrorKind::Empty,
376 };
377 assert_error(&e);
378 }
379
380 // -- Pattern API smoke -----------------------------------------------
381
382 #[test]
383 fn pattern_clone_and_eq() {
384 let p1 = Pattern::from_ida("48 8B ?? 89").unwrap();
385 let p2 = p1.clone();
386 assert_eq!(p1, p2);
387 }
388
389 #[test]
390 fn pattern_debug() {
391 // Touch the `Debug` derive so coverage records it.
392 let p = Pattern::from_ida("48").unwrap();
393 let s = alloc::format!("{p:?}");
394 assert!(s.contains("Pattern"), "got: {s}");
395 }
396
397 #[test]
398 fn pattern_as_slice_round_trip() {
399 let p = Pattern::from_ida("48 ??").unwrap();
400 let s = p.as_slice();
401 assert_eq!(s.len(), 2);
402 assert_eq!(s[0], Some(0x48));
403 assert_eq!(s[1], None);
404 }
405}