omath::PePatternScanner — Scan PE images for byte patterns
Header: your project’s
pe_pattern_scanner.hppNamespace:omathPlatform: Windows / PE (Portable Executable) images Depends on:<filesystem>,<optional>,<cstdint>Companion: works well withomath::PatternScanner(same pattern grammar)
PePatternScanner searches Portable Executable (PE) binaries for a hex pattern (with wildcards). You can scan:
- a loaded module in the current process, or
- a PE file on disk (by section name; defaults to
.text).
Pattern string grammar (same as PatternScanner)
- Hex byte: two hex digits → one byte (
90,4F,00,ff). - Wildcard byte:
?or??matches any byte. - Whitespace: ignored (use to group tokens).
✔️ "48 8B ?? ?? 89", "55 8B EC", "??"
❌ odd digit counts, non-hex characters (besides ? and whitespace)
API
namespace omath {
struct PeSectionScanResult {
std::uint64_t virtual_base_addr; // RVA base of the scanned section (ImageBase + SectionRVA)
std::uint64_t raw_base_addr; // file offset (start of section in the file)
std::ptrdiff_t target_offset; // offset from section base to the first matched byte
};
class PePatternScanner final {
public:
// Scan a module already loaded in *this* process.
// module_base_address: HMODULE / ImageBase (e.g., from GetModuleHandle)
// Returns absolute address (process VA) of the first match, or nullopt.
static std::optional<std::uintptr_t>
scan_for_pattern_in_loaded_module(const void* module_base_address,
const std::string_view& pattern);
// Scan a PE file on disk, by section name (default ".text").
// Returns section bases (virtual + raw) and match offset within the section, or nullopt.
static std::optional<PeSectionScanResult>
scan_for_pattern_in_file(const std::filesystem::path& path_to_file,
const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
};
} // namespace omath
Return values
-
Loaded module:
std::optional<std::uintptr_t>value()= process virtual address (ImageBase + SectionRVA + match offset).nullopt= no match or parse/PE error.
-
File scan:
std::optional<PeSectionScanResult>virtual_base_addr= ImageBase + SectionRVA of the scanned section (as if mapped).raw_base_addr= file offset of section start.target_offset= offset from the section base to the first matched byte.-
To get addresses:
- Virtual (RVA) of hit =
virtual_base_addr + target_offset - Raw file offset of hit =
raw_base_addr + target_offset
- Virtual (RVA) of hit =
Usage examples
Scan a loaded module (current process)
#include <windows.h>
#include "pe_pattern_scanner.hpp"
using omath::PePatternScanner;
auto hMod = ::GetModuleHandleW(L"kernel32.dll");
if (hMod) {
auto addr = PePatternScanner::scan_for_pattern_in_loaded_module(
hMod, "48 8B ?? ?? 89" // hex + wildcards
);
if (addr) {
// Use the absolute process VA:
std::uintptr_t hit_va = *addr;
// ...
}
}
Scan a PE file on disk (default section “.text”)
#include "pe_pattern_scanner.hpp"
using omath::PePatternScanner;
auto res = PePatternScanner::scan_for_pattern_in_file(
R"(C:\Windows\System32\kernel32.dll)", "55 8B EC"
);
if (res) {
auto rva_hit = res->virtual_base_addr + res->target_offset;
auto raw_hit = res->raw_base_addr + res->target_offset;
// rva_hit: where it would be in memory; raw_hit: file offset
}
Scan another section (e.g., “.rdata”)
auto res = PePatternScanner::scan_for_pattern_in_file(
"foo.dll", "48 8D 0D ?? ?? ?? ??", ".rdata"
);
Notes & edge cases
- PE only: these functions assume a valid PE/COFF layout. Non-PE files or corrupted headers yield
nullopt. - Section name:
scan_for_pattern_in_filedefaults to.text; pass a different name to target other sections. - Alignment & RVAs:
virtual_base_addris computed from section headers (RVA aligned per section alignment). The returned “virtual” base is suitable for RVA math; the process VA returned by the “loaded module” API already includes the image base. - Architecture: works for 32-bit and 64-bit PEs;
std::uintptr_tsize matches the build architecture. - Performance: Pattern matching is O(N × M) (sliding window with wildcards). For large images, prefer scanning only necessary sections.
- Wildcards: Each
?matches one byte (no nibble masks). If you needA?-style nibble wildcards, extend the parser (seePatternScanner). - Safety: For loaded modules, you must have access to the memory; scanning read-only sections is fine, but never write. For file scans, ensure the file path is accessible.
Troubleshooting
nullopt: Verify the pattern (valid tokens), correct section, and that you’re scanning the intended module/file (check bitness and version).- “Loaded module” address math: If you need an offset from the module base, compute
offset = hit_va - reinterpret_cast<std::uintptr_t>(module_base_address). - Multiple matches: Only the first match is returned. If you need all matches, extend the implementation to continue scanning from
target_offset + 1.
See also
omath::PatternScanner— raw buffer/iterator scanning with the same pattern grammar.omath::Triangle,omath::Vector3— math types used elsewhere in the library.
Last updated: 31 Oct 2025