reverse-eng-patch-diff
Turn a vendor security patch into a working N-day: diff the patched and unpatched binaries, read the newly added safety checks back to a bug class, then write a PoC that crashes the unpatched build. L
SKILL.mdskills/reverse-engineering/reverse-eng-patch-diff/SKILL.md
GitHubN-day patch diffing to exploit
When it applies
A vendor shipped a fix but no details: a CVE advisory says "out-of-bounds write in component
X" with no PoC, a Patch Tuesday drop needs triage, or a Linux distro backport may be
incomplete on some branch. The goal is to recover what the patch fixed and prove the bug
against the unpatched build. Weaponizing an N-day is only legitimate against in-scope,
authorized targets (a program that accepts it, an engagement, your own lab) — confirm with
tradecraft-scope-roe and record it in scope.txt.
Why it works
A security patch is a confession. The added bounds check, lock, zeroing, or refcount tells you exactly which invariant the old code violated — and the unpatched binary still violates it. Diff the two builds, filter to functions that changed moderately (identical = untouched, completely different = new feature), and the fix pattern maps almost one-to-one onto a bug class you can then trigger.
| The patch adds... | Likely bug class |
|---|---|
if (a + b < a) / __builtin_add_overflow |
integer overflow |
KeAcquireSpinLock / mutex_lock |
race condition (TOCTOU / double-free) |
if (idx >= MAX) / if (len > buf_size) |
OOB read / write |
RtlZeroMemory / memset(struct, 0, ...) |
uninitialized-memory info leak |
InterlockedDecrement + refcount check |
UAF / refcount error |
ProbeForRead / ProbeForWrite / access_ok |
unvalidated user-mode pointer |
SeAccessCheck / capability check |
missing authorization |
| removed / tightened IOCTL codes | attack-surface reduction — study the old interface |
Method
- Get the before/after binaries. Windows: download the MSU for build N (patched) and N-1
from the Microsoft Update Catalog, unpack with
expand.exe. Linux:apt downloadthe two kernel/package versions anddpkg-deb -x/rpm2cpiothem. Third-party software: grab the N-1 and N installers. Exact commands per platform incheatsheet.md. - Align symbols. Windows: pull PDBs from the Microsoft symbol server with
symchk. Linux: matching dbgsym / debuginfo, andextract-vmlinuxto turnvmlinuzback into an ELF. No symbols for one side → migrate them from the nearest version before diffing. - Diff. Feed both binaries to BinDiff, ghidriff, or Diaphora:
ghidriff ntoskrnl_old.exe ntoskrnl_new.exe -o diff_out/ bindiff --primary=old.BinExport --secondary=new.BinExport --output_dir=./bindiff_out/ - Locate the change. Filter to functions with similarity ≈ 0.5–0.95. Read what was
added: new
ifguards, new loop bounds, new locks — and what was deleted (removed code is a clue too). Map the pattern through the table above to a bug class; before/after pseudocode pairs are ideal LLM input for a root-cause hypothesis (patterns incheatsheet.md). - Write and verify the PoC against the unpatched build:
- integer overflow → boundary values (
0xFFFFFFF0 + 0x100) so the wrap yields a small allocation but a large copy; - race → threads hammering two syscalls on one object (close + IOCTL concurrently);
- UAF → spray → free → reclaim → use;
- OOB → drive length/index just past the boundary the new check now guards. Success criteria are symmetric: the PoC crashes the unpatched build reliably and runs clean on the patched one.
- integer overflow → boundary values (
Gotchas
- Mitigation ≠ fix. Added CFG/XFG instrumentation (
_guard_xfg_dispatch_icall_fptr) is hardening, not the bug fix — keep looking. - Compiler noise fakes changes. Inlining decisions, switch-table reordering, and PGO make unchanged source look different — diff N against N-1 (same toolchain), never across major versions, and read control/data flow rather than token-level diffs.
- Alignment failure. If overall matched ratio < ~90%, stop: wrong pairing, different compiler, or rebase mismatch — fix the inputs before reading results.
- A patch may shrink the blast radius, not fix the bug — the same root cause may still be reachable via another path (one bug, multiple harvests).
- A crash on the unpatched build alone proves nothing — environment faults look identical;
you need the patched build clean and a stated root cause before claiming the CVE
(
reporting-triage-validation). - Redact in write-ups — target hostnames, internal IPs, usernames become placeholders.
Verify success
You can name the function, the added check, and the violated invariant; your PoC crashes the unpatched build (BSOD/panic/KASAN naming the expected class) within a predictable window and exits cleanly on the patched build. That pairing — plus the root cause — is the reproducible N-day.
References
Microsoft Update Catalog & MSRC CVRF API; BinDiff / ghidriff / Diaphora docs;
reverse-eng-binary-triage, exploit-poc-development in this library.
Portions adapted from reverse-skill by zhaoxuya520, MIT License.