sigdrift

April 30, 2026 · View on GitHub

Track function signatures and struct member offsets across Windows builds.

Downloads Windows binaries from Winbindex, drives IDA Pro to generate signatures via ida-sigmaker, and produces minimal, cross-binary-verified signature sets.

Capabilities

  • Function signatures (analyze + report): for a function name, find byte patterns that uniquely identify it across builds. Supports direct and xref modes, emits C headers.
  • Offset signatures (offsets + offsets-report): for a struct member, find byte patterns whose displacement slot encodes the member's runtime offset. PDB-grounded; supports cross-binary verification and four selection strategies.

Requirements

  • Python 3.10+
  • IDA Pro with idat.exe
  • ida-sigmaker plugin installed in IDA

Installation

pip install -e .

Configuration

Create .sigdrift.toml in cwd or $HOME:

ida_path = "C:/Program Files/IDA Pro/idat64.exe"
data_dir = "./data"

Or set environment:

  • IDA_PATH
  • SIGDRIFT_DATA

Data layout

data/
  binaries/<filename>/<version>/<hash>_<filename>
  pdbs/<filename>/<version>/<hash>_<basename>.pdb
  signatures/<filename>/<version>/<hash>_<func>.json           # function-mode
  signatures/<filename>/<version>/<hash>_<struct>_<member>_offset.json   # offset-mode

<version> is the primary Windows feature update name (e.g. 11-24H2). <hash> is the first 16 hex chars of the file's PE timestamp+size hash, matching Winbindex.


Workflow A — Function signatures

Fetch

sigdrift fetch ntoskrnl.exe
sigdrift fetch ntoskrnl.exe --os win11 --min-ver 22H2
sigdrift fetch ntoskrnl.exe --min-ver 10:22H2,11:21H2 --pdb

Analyze

sigdrift analyze ntoskrnl.exe -f RtlAvlRemoveNode
sigdrift analyze ntoskrnl.exe -f RtlAvlRemoveNode -f RtlAvlInsertNode
sigdrift analyze ntoskrnl.exe -f SomeFn --mode xref
sigdrift analyze ntoskrnl.exe -f SomeFn --mode both

--mode:

  • direct — pattern of the function itself.
  • xref — patterns of functions that reference it.
  • both — try direct first, fall back to xref.

Report

sigdrift report ntoskrnl.exe -f RtlAvlRemoveNode --format table
sigdrift report ntoskrnl.exe -f RtlAvlRemoveNode --format header
sigdrift report ntoskrnl.exe -f RtlAvlRemoveNode --prefer auto

--format:

  • table — human-readable.
  • json — machine-readable.
  • header — C/C++ header with signature structs.

--prefer auto|direct|xref controls which mode's signatures are surfaced when both exist.


Workflow B — Offset signatures

Goal: given (binary, struct, member), find the minimal set of byte-patterns with a slot whose value, when extracted from a match, equals that build's actual member offset (read from the PDB).

Gather candidates

sigdrift offsets ntoskrnl.exe --target _EPROCESS.AddressCreationLock
sigdrift offsets ntoskrnl.exe --targets-file targets.toml
sigdrift offsets ntoskrnl.exe --target _EPROCESS.AddressCreationLock --cap 500

--cap bounds the per-build candidate count before sigmaker runs. Default 500.

--targets-file accepts:

targets = ["_EPROCESS.AddressCreationLock", "_EPROCESS.LargePrivateVadCount"]
# or
[[targets]]
struct = "_EPROCESS"
member = "AddressCreationLock"

The IDA-side script (offsetgen_batch.py) does PDB-grounded resolution, two-tier anchor discovery (stroff-typed = tier A, raw displacement = tier B), generates a unique-in-binary signature per anchor, validates each captured slot equals the PDB ground truth, and writes a per-build JSON with all surviving candidates.

Report

sigdrift offsets-report ntoskrnl.exe --target _EPROCESS.AddressCreationLock
sigdrift offsets-report ntoskrnl.exe --target _EPROCESS.AddressCreationLock \
  --strategy cover --format table --verify-parallel 16

Default --strategy greedy --format table produces the legacy aggregator output (set-cover over candidate-source-builds, no false-positive guarantees). For deploy-safe output, use --strategy cover (described below).

Strategies

StrategyWhat it picksFalse-positive guaranteeBest for
greedySet-cover over which builds produced each candidate. Picks shortest pattern on ties.NoneQuick exploration, parity with the function-mode report semantics.
universalSingle signature that is clean for every build (and never lucky/wrong anywhere). Falls back to top relaxed candidates if no strict universal exists.Strict (when found)"Is there one signature that just works?"
scopedPer Windows-version, the strict-clean candidates that are clean for every in-version build and never lucky/wrong anywhere.StrictPer-version pick lists when you want to choose by hand.
coverGreedy set-cover restricted to the strict-clean candidate pool. Each pick is clean where it matches and absent (or clean) elsewhere; picks never collide.StrictRecommended default for deploy-ready output.

Strategies other than greedy automatically gather all candidates from per-build JSONs and run cross-binary verification before picking.

Cross-binary verification

For each (signature, binary) pair, the verifier scans the binary's executable sections, counts matches, decodes the slot value at each match, and classifies into one of:

BucketMeaning
cleanExactly one match, captured value equals the PDB ground truth.
luckyMultiple matches, at least one captured the right value. Safe only with deploy-time disambiguation.
wrongMatch(es) but none captured the right value. Silent corruption if deployed.
absentNo match. Safe failure mode.
no_pdbMatch(es) but no ground-truth offset for this build.
no_binaryCached binary missing for this build.

A signature is strict-clean iff its bucket distribution is clean+absent only — no lucky, no wrong. This is the criterion universal/scoped/cover use.

You can also enable verification on the legacy greedy strategy with --verify:

sigdrift offsets-report ntoskrnl.exe --target _EPROCESS.AddressCreationLock --verify

This adds per-pick bucket counts to the report so you can see how trustworthy the greedy picks actually are.

--verify-parallel N (default 8) controls worker process count for cross-verification. Uses ProcessPoolExecutor with a one-shot pattern initializer; scales with cores.

Formats

--formatShape
tableHuman-readable, multi-line per pick, includes verification breakdown when present.
jsonFull structured output: per-pick bucket counts, flagged build-hash lists, version coverage.
compactComment-headed minimal text. Each pick is two lines: a metadata header, then the full untruncated pattern. Greppable, copyable.

--top N (default 5) caps how many picks are surfaced per scope in compact and table for universal/scoped. JSON returns full strict lists plus the top-N relaxed.

Examples

# Find the smallest set of strict-clean signatures and emit legacy-style table
sigdrift offsets-report ntoskrnl.exe --target _EPROCESS.AddressCreationLock \
  --strategy cover --format table --verify-parallel 16

# Per-version compact pick list
sigdrift offsets-report ntoskrnl.exe --target _EPROCESS.AddressCreationLock \
  --strategy scoped --format compact --top 3 --verify-parallel 16

# Search for a universal signature; if absent, surface best relaxed candidates
sigdrift offsets-report ntoskrnl.exe --target _EPROCESS.AddressCreationLock \
  --strategy universal --format compact --top 10 --verify-parallel 16

# Legacy greedy with verification annotations
sigdrift offsets-report ntoskrnl.exe --target _EPROCESS.AddressCreationLock \
  --verify --format compact

# JSON dump for downstream tooling (codegen, dashboards)
sigdrift offsets-report ntoskrnl.exe --target _EPROCESS.AddressCreationLock \
  --strategy cover --format json --verify-parallel 16 \
  -o reports/eprocess_address_creation_lock.json

Common flags

FlagCommandsPurpose
--osfetch, analyze, offsetsOS filter (win10, win11, comma-separated).
--min-ver, --max-verfetch, analyze, offsetsVersion filter; supports per-OS form 10:22H2,11:21H2.
--parallel, -pfetch, analyze, offsetsParallel downloads / IDA instances. Default 8.
--ida-pathanalyze, offsetsOverride IDA path.
--timeoutanalyze, offsetsIDA timeout per binary (seconds).
--pdbfetchAlso download PDBs.
--capoffsetsPer-build candidate cap before sigmaker. Default 500.
--target / --targets-fileoffsets, offsets-reportSpec(s) to process.
--strategyoffsets-reportgreedy / universal / scoped / cover. Default greedy.
--verifyoffsets-report (greedy only)Cross-verify the picked set.
--verify-paralleloffsets-reportWorker count for cross-verify. Default 8.
--topoffsets-reportPicks shown per scope in compact/table. Default 5.
--formatreport, offsets-reportOutput format.
--output, -oreport, offsets-reportWrite to file instead of stdout.

Design notes

  • Outside-in matching: the cross-verifier scans only PE sections with IMAGE_SCN_MEM_EXECUTE to mirror runtime kernel-mode signature scanners.
  • Strict-clean filter: picks that may produce lucky (multiple matches, ambiguous) or wrong (silent miscapture) anywhere in the corpus are filtered out before set-cover. The remaining picks cannot interfere when deployed together.
  • Greedy preference: on coverage ties, the aggregator now prefers longer patterns (more discriminative against unseen binaries). Earlier versions preferred shorter; that is fixed.
  • Coverage accounting: the per-pick count is the build set this pick uniquely contributed during greedy, not the union of all builds where it would match. Total coverage cannot exceed total builds.
  • Process pool: verification uses ProcessPoolExecutor with a one-shot pattern initializer (compiled regex per pattern) to bypass the GIL on re.finditer.

Verification model in one example

For _EPROCESS.AddressCreationLock across 197 builds (Win11 21H2 → 26H1):

  • No universal signature exists — the struct layout flips offset (0x4c8 → 0x258) at 24H2.
  • --strategy cover produces a small set of mutually non-interfering signatures, each tied to the version family where it's strict-clean. Two anchors typically suffice: one for 21H2/22H2/23H2 (capturing 0x4c8), one for 24H2/25H2/26H1 (capturing 0x258).