Skip to main content

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}