Expand description
§pe-sigscan
Fast byte-pattern (“signature”) scanning over the executable sections of a loaded PE (Portable Executable) module on Windows.
This crate is a building block for game mods, hookers, debuggers, and any other tool that needs to locate non-exported, non-vtable-accessible code by its byte signature. It mirrors the workflow common across the reverse-engineering ecosystem — derive a pattern from a disassembler (IDA, Ghidra, Binary Ninja, Cutter), then scan either the live process’s mapped image or bytes read through a custom backend.
§Quick start
use pe_sigscan::{find_in_text, Pattern};
// Get a module base via your preferred means (GetModuleHandleW,
// PEB walk, etc.). For demonstration we assume a known base.
// Build a pattern from an IDA-style hex string. `?` and `??` are
// wildcards; whitespace between bytes is ignored.
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}");
}Or with the pattern! macro (no allocation, fully const-eligible):
use pe_sigscan::pattern;
const SIG: &[Option<u8>] = pattern![0x48, 0x8B, _, _, 0x48, 0x89];
assert_eq!(SIG.len(), 6);
assert_eq!(SIG[0], Some(0x48));
assert_eq!(SIG[2], None);§Two scanning modes
find_in_text/count_in_text/iter_in_text— walk only the section literally named.text. The simplest case, suitable for MSVC-built DLLs that put everything in one code section.find_in_exec_sections/count_in_exec_sections/iter_in_exec_sections— walk every section whoseIMAGE_SCN_MEM_EXECUTEcharacteristic is set. Required when the function you’re scanning for might live in a companion section like.text$mn,.textbss, a jump-table arena, or any of the optimized-layout code sections that some compilers and linkers emit.
Both modes have find_in_slice / count_in_slice / iter_in_slice
companions that work on a &[u8] instead of a loaded PE — useful for
offline analysis, unit testing, and scanning extracted bytes.
§Reader-backed scanning
For out-of-process backends such as ReadProcessMemory, kernel drivers,
DMA, or hypervisor introspection, use the MemoryReader trait plus the
*_with helpers such as find_in_text_with and
find_in_exec_sections_with.
Those APIs read the target section bytes into local memory once, then reuse the same slice scanner as the in-process path. They return the remote match address, not the local scratch-buffer address.
use pe_sigscan::{find_in_text_with, pattern, MemoryReader};
struct Remote;
impl MemoryReader for Remote {
fn read_bytes(&self, addr: usize, buf: &mut [u8]) -> Option<()> {
// Fill `buf` from your remote backend here.
None
}
}
const SIG: &[Option<u8>] = pattern![0x48, 0x8B, 0x05, _, _, _, _];
let _ = find_in_text_with(&reader, module_base, SIG);§Resolving rel32 displacements
Real signature workflows almost always end with “match the
instruction, then follow its rel32 displacement to the actual target
address”. The resolve_rel32 / resolve_rel32_at helpers package
that arithmetic so callers don’t reinvent the off-by-one-prone
next_ip + disp32 calculation:
use pe_sigscan::{find_in_text, pattern, resolve_rel32_at};
// mov rax, [rip+disp32]: 48 8B 05 ?? ?? ?? ?? (7 bytes total).
const SIG: &[Option<u8>] = 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}");
}§Why direct memory reads?
The .text section of a loaded DLL is page-aligned, RX-protected, and
stays committed for the lifetime of the module. There is no TOCTOU
concern; bytes don’t change between reads. A typical scan walks tens of
megabytes of bytes — routing every probe through ReadProcessMemory
would cost tens of millions of syscalls (minutes of wall time). This
crate’s in-process path reads directly via raw pointer dereference, bounded
to PE-declared section ranges.
For out-of-process backends, use the reader-backed *_with APIs. Those
copy the target section bytes once into local memory, then reuse the same
slice scanner as find_in_slice.
§Safety
Public functions take a module_base: usize you must obtain from the OS
(e.g. GetModuleHandleW). The implementation parses the PE headers at
that base before any other access, so a non-PE pointer is rejected
cleanly. Inside the validated section ranges, the unsafe pointer reads
are bounded by the VirtualSize field from the section header — outside
the loader handing us a malformed PE (which the loader itself would have
rejected), there is no path to an out-of-bounds read.
The slice variants are safe by Rust’s slice invariants and need no further trust from the caller.
§Platform
Windows / PE only.
The crate compiles on every platform — the parsing is pure compute —
but the in-process function signatures assume a module_base that came
from the Windows loader. On non-Windows targets, the slice variants
still work for analysing PE bytes you have mapped manually.
§License
MIT OR Apache-2.0.
Macros§
- pattern
- Build a
&'static [Option<u8>; N]at compile time from a list of byte literals and_wildcards.
Structs§
- Matches
- Iterator over non-overlapping match addresses within one or more raw
byte ranges of a loaded PE module. Returned by
iter_in_textanditer_in_exec_sections. - Parse
Pattern Error - Error returned by
crate::Pattern::from_idawhen the input string contains an invalid token. - Pattern
- A wildcard byte pattern with parsed-from-string ergonomics.
- Slice
Matches - Iterator over non-overlapping match addresses within a
&[u8]haystack. Returned byiter_in_slice.
Enums§
- Parse
Error Kind - Categories of
ParsePatternError.
Traits§
- Memory
Reader - Abstract byte reader for address spaces that cannot be dereferenced directly from the current process.
Functions§
- count_
in_ exec_ sections - Count occurrences of
patternacross ALL executable sections of the PE module loaded atmodule_base. Companion tofind_in_exec_sections; same hook-install uniqueness contract ascount_in_text. - count_
in_ exec_ sections_ with - Count occurrences of
patternacross all executable sections of a PE module read throughMemoryReader. - count_
in_ slice - Count occurrences of
patternwithin the slicehaystack. Non- overlapping: a pattern that matches at offsetiadvances the search pasti + pattern.len()rather thani + 1. - count_
in_ text - Count occurrences of
patternwithin the named.textsection of the PE module loaded atmodule_base. - count_
in_ text_ with - Count occurrences of
patternwithin the named.textsection of a PE module read throughMemoryReader. - find_
in_ exec_ sections - Find the first occurrence of
patternwithin ANY executable section of the PE module loaded atmodule_base. - find_
in_ exec_ sections_ with - Find the first occurrence of
patternacross all executable sections of a PE module read throughMemoryReader. - find_
in_ slice - Find the first occurrence of
patternwithin the slicehaystack. - find_
in_ text - Find the first occurrence of
patternwithin the named.textsection of the PE module loaded atmodule_base. - find_
in_ text_ with - Find the first occurrence of
patternwithin the named.textsection of a PE module read throughMemoryReader. - iter_
in_ exec_ sections - Iterate over every non-overlapping occurrence of
patternacross ALL executable sections of the PE module loaded atmodule_base. - iter_
in_ slice - Iterate over every non-overlapping occurrence of
patternwithin the slicehaystack. - iter_
in_ text - Iterate over every non-overlapping occurrence of
patternwithin the section literally named.textof the PE module loaded atmodule_base. - module_
size - Read
IMAGE_OPTIONAL_HEADER.SizeOfImage— the total mapped size of the module in bytes. The virtual address range[module_base, module_base + module_size)covers every section the loader mapped. - module_
size_ with - Read
IMAGE_OPTIONAL_HEADER.SizeOfImagethrough aMemoryReader. - read_
rel32 - Read a little-endian signed 32-bit displacement from a byte slice.
- resolve_
rel32 ⚠ - Read a signed 32-bit displacement at
rel32_addrand add it tonext_ipto produce an absolute target address. - resolve_
rel32_ ⚠at - Convenience wrapper over
resolve_rel32for the typical workflow: you have amatch_addrfromfind_in_text, and you know the byte offset of the displacement inside the matched instruction (rel32_offset) and the total length of the instruction (instr_len).
Type Aliases§
- Wildcard
Pattern - Wildcard-aware byte pattern as a slice of
Option<u8>.Some(b)matches the literal byteb;Nonematches any byte (the IDA-style?token).