Binary Diffing
Overview
Binary diffing compares two versions of a compiled binary to isolate exactly what changed. This is one of the highest-value reverse engineering techniques because it converts a vendor's patch into a roadmap pointing straight at the vulnerability. The same technique applies to tracking malware evolution, understanding firmware updates, and auditing software changes between releases.
Why this matters: When Microsoft ships a Patch Tuesday update, the patch itself tells you what was vulnerable. A well-executed diff workflow can identify the exploitable condition in hours, not days.
Core Use Cases
| Use Case | Input | Output |
|---|---|---|
| Patch analysis (1-day development) | Pre-patch + post-patch binary | Vulnerable function, root cause, exploitable condition |
| CVE reproduction | Advisory + patched binary pair | Proof of concept targeting the patched flaw |
| Malware variant tracking | Sample A + Sample B | Behavioral delta, new capabilities, C2 changes |
| Firmware update analysis | Firmware v1 + v2 (extracted) | Changed drivers, new mitigations, removed features |
| Software audit | Release N + Release N+1 | Security-relevant changes, regression candidates |
Tool Routing
| Task | Tool | When to Use |
|---|---|---|
| Byte-level diff | radiff2 <old> <new> | Quick triage — see raw byte changes |
| Code block comparison | radiff2 -C <old> <new> | Identify changed basic blocks |
| Graph-based diffing | BinDiff (via Ghidra/IDA export) | Deep function matching with CFG comparison |
| Scriptable diffing | Diaphora (Ghidra/IDA plugin) | Customizable matching, SQL-backed results |
| Similarity scoring | ssdeep -l <old> <new> | Quick similarity percentage between files |
| Section comparison | r2 -qc 'iS' <old> vs <new> | Detect section size/permission changes |
| Import/export delta | r2 -qc 'ii' / r2 -qc 'iE' | New APIs, removed functions |
| String delta | strings or floss on both | New URLs, error messages, config values |
| Function listing | r2 -qc 'aaa; afl' on both | Function count, name, and size comparison |
Methodology
Phase 1 — Triage
Determine whether a meaningful diff is feasible and estimate scope.
- Hash both binaries — confirm they are actually different (
sha256sum,md5sum) - Check fuzzy similarity —
ssdeep -l old.dll new.dllgives a percentage; above 90% means a targeted patch, below 50% suggests a major rewrite - Compare file metadata — timestamps, version info (
r2 -qc 'iV'), digital signatures, file size delta - Compare sections —
r2 -qc 'iS'on both; a changed.textsection means code modifications, changed.rdatamay indicate string/constant changes - Compare imports/exports — new imports reveal new functionality or mitigations (e.g., adding
__security_check_cookiesuggests stack cookie enforcement)
If ssdeep similarity is high (>85%) and only .text changed, this is a surgical patch — ideal for 1-day analysis.
Phase 2 — Function-Level Diffing
This is where the real work happens. The goal is to identify which functions changed and how.
Using radiff2 (fast, CLI-native):
# List all function differences with sizes
radiff2 -C old.dll new.dll
# Graph diff for a specific function pair
radiff2 -g old.dll new.dll > diff.dot
Using BinDiff (gold standard for large binaries):
- Export
.BinExportfiles from Ghidra or IDA for both binaries - Run BinDiff on the export pair
- Sort results by similarity score — functions at 0.9-0.99 are partially matched (the interesting ones)
- Ignore 1.0 matches (identical) and 0.0 matches (unmatched/new)
Using Diaphora (scriptable, SQL-backed):
- Run Diaphora export on both binaries in Ghidra/IDA
- Open the diff database — results are stored in SQLite
- Focus on "partial matches" tab — these are modified functions
- Use the pseudo-code diff view to see decompiled changes side-by-side
Phase 3 — Root Cause Analysis
For each modified function, determine what the patch actually fixes.
- Decompile both versions — compare pseudo-code side by side
- Identify the delta — look for added bounds checks, new validation, changed buffer sizes, added error handling
- Classify the change:
- Added length/size check → buffer overflow fix
- Added NULL check → null pointer dereference fix
- Changed integer arithmetic → integer overflow fix
- Added input validation → injection or type confusion fix
- Changed memory allocator → use-after-free or double-free fix
- Added authentication check → privilege escalation fix
- Map to vulnerability class — CWE ID, attack vector, exploitability
- Assess exploitability — can you reach the vulnerable code path? What are the constraints?
Phase 4 — Reporting
Document findings in a structured format. See the report template below.
Patch Change Classification
| Change Pattern | Likely Vulnerability | CWE | Exploitability |
|---|---|---|---|
| Added bounds check on buffer write | Stack/heap buffer overflow | CWE-120/122 | High — classic memory corruption |
| Added integer overflow check before alloc | Integer overflow → heap overflow | CWE-190 | High — controllable allocation size |
| Added NULL pointer check | NULL dereference (DoS or code exec) | CWE-476 | Medium — depends on mapping NULL page |
| Added reference counting / free guard | Use-after-free | CWE-416 | High — heap spray + type confusion |
Changed memcpy size calculation | Buffer over-read or over-write | CWE-125/787 | High — information leak or RCE |
| Added type validation | Type confusion | CWE-843 | High — object layout mismatch |
| Added authentication/authorization check | Privilege escalation / auth bypass | CWE-862 | Critical — direct security boundary skip |
| Removed dangerous API call | Command/code injection | CWE-78/94 | Varies — depends on input control |
| Changed deserialization logic | Insecure deserialization | CWE-502 | High — RCE through crafted objects |
| Added ASLR/CFG/CET markers | Missing mitigation | N/A | Raises exploitation difficulty |
Function Matching Strategies
When automated tools produce poor matches (common with heavily optimized or obfuscated binaries), use these manual strategies:
- String anchoring — find unique strings in the patched function, locate the same string in the old binary
- Import cross-references — if the patched function calls a distinctive API, find all callers of that API in the old binary
- Constant matching — magic numbers, error codes, and hash constants survive compilation changes
- Call graph topology — match based on the shape of who-calls-whom, not just function content
- Basic block count heuristic — functions with similar block counts and edge counts are likely matches
- Symbol carryover — if either binary has partial symbols (PDB, DWARF), use them to anchor matches
Diff Report Template
# Binary Diff Report: <component-name>
**Date**: YYYY-MM-DD
**Analyst**: <name>
**CVE/Advisory**: <CVE-YYYY-XXXXX or advisory reference>
## Binaries Compared
| Property | Old (Pre-patch) | New (Post-patch) |
|----------|----------------|-----------------|
| File | <filename> | <filename> |
| SHA256 | <hash> | <hash> |
| Version | <version> | <version> |
| Size | <bytes> | <bytes> |
| ssdeep similarity | <percentage> | — |
## Executive Summary
<1-2 sentences: what the patch fixes and its security impact>
## Changed Functions
| Function | Old Address | New Address | Similarity | Change Type |
|----------|-----------|-----------|------------|-------------|
| <name> | 0x... | 0x... | 0.XX | Modified |
## Detailed Analysis
### <function-name>
**What changed**: <description of the code delta>
**Vulnerability**: <what was exploitable before the patch>
**Root cause**: <CWE, attack vector, constraints>
**Exploitability assessment**: <can t