Skip to content

About

Single-header C++17 library for IDA-style byte signature scanning. Header-only, no dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

pattern-scan: IDA-style byte signatures at 11 GiB/s, from a single header

CI Latest release C++17 Header-only MIT license

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.

Animated terminal demo: patscan finds a signature in a file with matched bytes highlighted, then the benchmark runs

Highlights

  • Fast: candidates are found with memchr on 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.
  • patscan command-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_argument instead of silently matching something else.

Example

#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

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 0C
patscan [--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".

Install

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.

Pattern syntax

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.

API

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);
}

How it works

Animated walk-through of the search: memchr finds candidates, a second-byte check rejects one, a full compare rejects another, and the third matches

Performance

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 MSVC

Testing

ctest 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.

Scope

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.

License

MIT, see LICENSE.

About

Single-header C++17 library for IDA-style byte signature scanning. Header-only, no dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages