Patch diffing in 60 seconds
A security patch is the vendor writing down, in public, exactly what was wrong. They do not intend it as a disclosure. They intend it as a fix. But the two artifacts are the same bytes, and anyone who can read the difference between the patched and the unpatched binary has been handed the vulnerability.
Patch diffing is the practice of reading that difference backwards. The vendor ships the answer; the work is reconstructing the question.
Why the fix is the best starting point
Most vulnerability research starts from a black box: a product, a protocol, a file format, and no idea where the bug is. That is the expensive way to work, and it is why so much of it fails to find anything.
A patch inverts the problem. Somewhere inside it is a specific code path that changed for a reason, and that reason is a bug. You do not have to find the bug. You have to find the change, and the change is legible — a function that grew a check, a length that changed, a call that moved.
The vendor has done the hard half of the research and published it. Diffing is how you collect the other half.
The workflow is five steps, and the middle one is everything
One: get both builds. The patched and the unpatched binary, firmware image, or package. This is the step that actually requires effort — the vendor does not archive the vulnerable version, and older firmware has a habit of vanishing from the download page. Mirrors, package registries, and update caches usually have it.
Two: diff them. The mechanical pass. Function-level diffing matters more than byte-level, because a compiler change reorders everything and hides the one real change in a sea of noise:
$ radiff2 -C vuln.bin patched.bin
-C produces a code-level diff that survives address and register renames. For heavier work,
Diaphora (IDA) and Ghidra’s version tracking produce a function-level map between the two
builds, which is what you actually want — the answer to “which function changed, and by how
much”.
Three: isolate the changed function. This is the whole game. A typical security patch changes a handful of functions, and most of those changes are trivial. The one that matters is the one that grew a length check, or a bounds comparison, or a validation call that was not there before.
Four: read the change backwards. The fix tells you the bug. A new if (len < 0) means
the length was attacker-controlled. A new null check means a null was reachable. A moved
memcpy with a smaller size means an overflow was possible. Every fix is a sentence about
the vulnerability it closes, and reading it backwards is the actual skill.
Five: hunt the variant. Once you understand the root cause, you stop being interested in this one binary. The same class of bug is elsewhere — in the same function’s siblings, in the same vendor’s other products, in every codebase that solved the same problem the same way.
Reading the delta backwards
The specific tells are small and consistent across every vendor:
- A size that changed.
memcpy(dst, src, n)becomesmemcpy(dst, src, min(n, limit)). The pre-patch value ofnwas the vulnerability, and the question is only where it came from. - A check that appeared. A comparison before an indexing operation, a range check before
a write, a
strnlenwhere astrlenused to be. The check is the patch; the thing it now rejects is the bug. - A call that moved. An authentication or authorisation call that used to run after the sensitive operation now runs before it. That ordering change is a missing-authorisation finding, written as a fix.
None of this requires understanding the whole binary. It requires understanding the delta, and the delta is small. The vendor did the hard part; the diff is a map, and maps are short.
From one patch to many bugs
The variant hunt is where patch diffing stops being a 1-day activity and becomes real research. The root cause is a class, not a location.
If the fix was “the length field was never validated before the copy”, the same field, in the same format, parsed by the same library, is copied somewhere else in the same binary — and in the vendor’s other products, and in every third-party implementation of the same protocol. One patch, read once, yields a list of places to look that no fuzzer would have prioritised, because the researcher now knows exactly what shape the bug takes.
That is the difference between a CVE and a line of research. The patch gave you a single instance. The root cause gives you a class, and the class is where the follow-up findings are.
Evidence matrix
| Signal | What it proves | Negative control | Defender verification |
|---|---|---|---|
| Old and fixed artifacts have recorded versions and hashes | The comparison is reproducible and not a packaging mismatch | Diff two identical builds and expect no security-relevant delta | Preserve provenance, symbols, compiler metadata, and hashes with the analysis |
| A small changed function introduces a new validation or bounds check | The patch points to the condition the vendor considered unsafe | Exercise an input that never reaches the changed branch | Map the binary delta back to source-level behavior or an equivalent control-flow trace |
| Old build accepts the minimal trigger and fixed build rejects it | The delta changes security behavior, not only implementation shape | Run a nearby valid input successfully on both versions | Test both versions in the same harness and record response, crash, or state difference |
| Sibling call sites repeat the pre-patch pattern | The root cause may be a bug class rather than one CVE instance | Confirm patched/guarded siblings do not match the trigger | Search semantic variants and triage each with the same positive and negative controls |
The pattern I keep seeing
Patch diffing gets treated as a vendor-adjacent, slightly disreputable practice — as though reading a published change were somehow cheating at vulnerability research. It is the opposite. The vendor published the change precisely so that people would apply it, and any competent attacker is reading it anyway. The only party that loses when researchers do not diff is the defence, which never learns the root cause and therefore never fixes the class.
The other thing that keeps coming up is how much of this is just version discipline. Most organisations cannot tell you what binary version they are running, which means they cannot tell you which vulnerabilities they have already been handed a map to. The patch is public. The delta is readable. The gap is not a shortage of information — it is that nobody is reading.
What to hand the defenders
The root cause, not the CVE. A report that says “apply the patch for CVE-XXXX” closes one instance. A report that says “this length field is trusted at three call sites, only one of which the patch fixed” closes a class. The diff gives you the second report for free, and it is the one that changes the outcome.
The affected version list. Patch diffing depends on knowing which build you are looking at, and most teams cannot produce that list. Establishing it is the prerequisite for every other finding in this area.
The variant list. Where else does the same pattern live? Name the siblings, the other products, the third-party libraries. That is the part that turns a vendor’s fix into a durable improvement in your own posture, instead of a one-off ticket.
The patch was never just a fix. It was the vendor telling you, in the clearest terms it has, exactly what to go looking for.
How current is this note?
The latest source-review, content-update, or publication date is shown.
The author completed a technical review. This does not, by itself, claim lab reproduction.
