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_PATHSIGDRIFT_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
| Strategy | What it picks | False-positive guarantee | Best for |
|---|---|---|---|
greedy | Set-cover over which builds produced each candidate. Picks shortest pattern on ties. | None | Quick exploration, parity with the function-mode report semantics. |
universal | Single 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?" |
scoped | Per Windows-version, the strict-clean candidates that are clean for every in-version build and never lucky/wrong anywhere. | Strict | Per-version pick lists when you want to choose by hand. |
cover | Greedy set-cover restricted to the strict-clean candidate pool. Each pick is clean where it matches and absent (or clean) elsewhere; picks never collide. | Strict | Recommended 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:
| Bucket | Meaning |
|---|---|
clean | Exactly one match, captured value equals the PDB ground truth. |
lucky | Multiple matches, at least one captured the right value. Safe only with deploy-time disambiguation. |
wrong | Match(es) but none captured the right value. Silent corruption if deployed. |
absent | No match. Safe failure mode. |
no_pdb | Match(es) but no ground-truth offset for this build. |
no_binary | Cached 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
--format | Shape |
|---|---|
table | Human-readable, multi-line per pick, includes verification breakdown when present. |
json | Full structured output: per-pick bucket counts, flagged build-hash lists, version coverage. |
compact | Comment-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
| Flag | Commands | Purpose |
|---|---|---|
--os | fetch, analyze, offsets | OS filter (win10, win11, comma-separated). |
--min-ver, --max-ver | fetch, analyze, offsets | Version filter; supports per-OS form 10:22H2,11:21H2. |
--parallel, -p | fetch, analyze, offsets | Parallel downloads / IDA instances. Default 8. |
--ida-path | analyze, offsets | Override IDA path. |
--timeout | analyze, offsets | IDA timeout per binary (seconds). |
--pdb | fetch | Also download PDBs. |
--cap | offsets | Per-build candidate cap before sigmaker. Default 500. |
--target / --targets-file | offsets, offsets-report | Spec(s) to process. |
--strategy | offsets-report | greedy / universal / scoped / cover. Default greedy. |
--verify | offsets-report (greedy only) | Cross-verify the picked set. |
--verify-parallel | offsets-report | Worker count for cross-verify. Default 8. |
--top | offsets-report | Picks shown per scope in compact/table. Default 5. |
--format | report, offsets-report | Output format. |
--output, -o | report, offsets-report | Write to file instead of stdout. |
Design notes
- Outside-in matching: the cross-verifier scans only PE sections with
IMAGE_SCN_MEM_EXECUTEto mirror runtime kernel-mode signature scanners. - Strict-clean filter: picks that may produce
lucky(multiple matches, ambiguous) orwrong(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
countis 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
ProcessPoolExecutorwith a one-shot pattern initializer (compiled regex per pattern) to bypass the GIL onre.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 coverproduces 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).