Toolkit: pe-walker · pe-diff · rtti-dump · vtable-dump · pattern-scan · unwind-map
pattern-scan is a single-header C++17 library for IDA-style byte signature scanning. You write a pattern like 48 8B ?? E8 ?? ?? ?? ?? 85 C0, with ?? for the bytes that change between builds (addresses, offsets, immediates), parse it once, and search any byte range for the first match or every match. It has no dependencies and no OS-specific code.
- Fast: candidates are found with
memchron the first concrete byte, filtered by a second byte, and only then fully compared. That is about 11× faster than a byte-by-byte scan on typical data, and faster even when the first byte occurs everywhere (numbers below). - Correct by construction: a randomized test compares every result with the obvious reference scanner over 3,000 generated inputs, including overlapping matches, leading wildcards and all-wildcard patterns.
patscancommand-line tool: search files for a signature from the shell, with matched bytes highlighted. Prebuilt for Windows, Linux and macOS.- Drop-in: one header, plain C++17, with a CMake target or copy-paste.
- Strict parser: malformed patterns throw
std::invalid_argumentinstead of silently matching something else.
#include "pattern_scan.hpp"
const auto sig = patscan::parse("48 8B ?? E8 ?? ?? ?? ?? 85 C0");
if (const auto* hit = patscan::find(data, size, sig)) {
std::printf("match at offset 0x%zx\n", static_cast<std::size_t>(hit - data));
}
for (std::size_t offset : patscan::find_all(data, size, sig)) {
// every match, in address order
}patscan puts the library on the command line. It prints the file, the offset and the matched bytes; on a terminal, concrete pattern bytes are green, bytes under a wildcard yellow and context bytes dim.
$ patscan --all -C 4 "48 8B ?? E8 ?? ?? ?? ?? 85 C0" sample.bin
sample.bin 0x00000010 C7 EC 11 36 48 8B C4 E8 12 34 56 78 85 C0 CD F2 17 3C
sample.bin 0x000000a0 97 BC E1 06 48 8B 05 E8 AA BB CC DD 85 C0 9D C2 E7 0Cpatscan [--all|-a] [--context|-C N] [--color auto|always|never] <pattern> <file>...
| Flag | Effect |
|---|---|
--all, -a |
Report every match instead of the first one in each file. |
--context N, -C N |
Also show N bytes before and after each match. |
--color WHEN |
auto (default) colors output only on a terminal; always and never force it. Setting NO_COLOR turns colors off. |
Exit status is 0 if anything matched, 1 if nothing did, and 2 for an invalid pattern or an unreadable file, so it works in scripts: patscan "$SIG" game.exe || echo "signature broke after the update".
It is one header. Copy include/pattern_scan.hpp into your project, download it, or a prebuilt patscan, from the latest release, or use CMake:
include(FetchContent)
FetchContent_Declare(pattern_scan
GIT_REPOSITORY https://github.com/sheratonc/pattern-scan.git
GIT_TAG v1.0.0)
FetchContent_MakeAvailable(pattern_scan)
target_link_libraries(my_target PRIVATE pattern_scan)add_subdirectory(pattern-scan) works too. The demo, tests and benchmark are only built when this is the top-level project.
| Token | Meaning |
|---|---|
AB, ab |
A concrete byte, 0xAB. A single nibble such as A means 0x0A. |
?, ??, * |
A wildcard that matches any byte. |
| whitespace | A separator; spaces, tabs and newlines are all accepted. |
An empty pattern, or any other character, throws std::invalid_argument from parse.
namespace patscan {
struct Signature {
std::vector<std::uint8_t> bytes; // byte values (0 where wildcard)
std::vector<bool> mask; // true = concrete byte
};
Signature parse(std::string_view pattern); // throws std::invalid_argument
// First match, or nullptr. The Signature overloads never allocate.
const std::uint8_t* find(const std::uint8_t* data, std::size_t size, const Signature& sig) noexcept;
const std::byte* find(const std::byte* data, std::size_t size, const Signature& sig) noexcept;
const std::uint8_t* find(const std::uint8_t* data, std::size_t size, std::string_view pattern);
// Offsets of every match, in order.
std::vector<std::size_t> find_all(const std::uint8_t* data, std::size_t size, const Signature& sig);
}bench/bench.cpp scans a 256 MiB buffer whose only match is planted at the very end, and compares against the byte-by-byte scanner this library used before (best of five runs, MSVC /O2, AMD Ryzen 9 7900X):
| Case | Byte-by-byte | pattern-scan | Speedup |
|---|---|---|---|
| Random data | 1.03 GiB/s | 11.7 GiB/s | 11.4× |
Worst case: the first byte (0x48) at every position |
0.65 GiB/s | 0.9–1.4 GiB/s | 1.4–2.2× |
In the worst case memchr cannot skip anything, so a short inline probe takes over and a second-byte check rejects candidates before the full comparison. Run it yourself:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --target bench
./build/bench # build\Release\bench.exe with MSVCctest runs the unit tests in tests/test_pattern_scan.cpp, which cover parsing, edge cases (empty input, a pattern longer than the data, a null pointer, a match at the very end) and the randomized comparison with the reference scanner. It also runs the demo and seven golden-file tests of patscan, covering output, colors and exit codes. CI runs everything on Windows (MSVC), Linux (GCC) and macOS (Clang), and cross-builds with MinGW-w64.
It scans one contiguous byte range for one pattern: a mapped file, a dumped module, or memory you have already read. It does not do multi-pattern search or SIMD. Bytes whose meaning changes between builds should be wildcards; for code, that usually means relative call and jump targets and RIP-relative displacements.
MIT, see LICENSE.