Every technique in vulnerability research eventually traces back to one of three disciplines: reading code (or its equivalent when there’s no source), understanding a system by taking it apart, and throwing carefully-designed chaos at it until it breaks. I’ve come to think of code review, reverse engineering, and fuzzing as three complementary pillars — each with its own strengths, blind spots, and ideal use cases, and the researchers who get the most out of their time are the ones who know which pillar to lean on for a given target, and how to combine them rather than relying on just one.
Why Three Pillars, Not One “Best” Method
A common beginner mistake is treating one of these as universally superior. In reality, each pillar answers a different question:
- Code review answers: given the logic as written, where could it fail?
- Reverse engineering answers: what does this system actually do, when I have no source to trust?
- Fuzzing answers: what inputs, that I wouldn’t have thought to craft by hand, cause this system to misbehave?
They’re not competing methodologies — they’re complementary lenses, and most serious vulnerability research combines all three at different stages.
flowchart TD
A[Target Software] --> B{Source Available?}
B -->|Yes| C[Code Review]
B -->|No/Partial| D[Reverse Engineering]
C --> E[Identify Candidate Weak Points]
D --> E
E --> F[Fuzzing to Confirm & Discover]
F --> G[Crash/Anomaly Triage]
G --> H[Root Cause Analysis]
H --> I{Exploitable?}
I -->|Yes| J[Proof of Concept & Reporting]
I -->|No| E
Pillar One: Code Review
Code review is the most direct and, when source is available, usually the highest-signal method — you’re reading the actual logic rather than inferring it. Effective security code review isn’t the same as general code review; it’s specifically oriented around trust boundaries, data flow, and dangerous operations (see the source-to-sink and sink-to-source disciplines discussed elsewhere).
Strengths:
- Highest precision — you see exactly what the code does, not an approximation
- Finds logic bugs (authorization flaws, business logic errors) that fuzzing and reverse engineering typically miss entirely, since these bugs don’t crash anything
- Fastest path to root-cause understanding once a bug is found
Limitations:
- Requires source access (or very good decompilation)
- Doesn’t scale well to enormous codebases without static analysis tooling assistance
- Easy to miss subtle interactions between distant parts of a codebase that don’t manifest as an obvious local issue
- Reviewer fatigue and cognitive load are real — human reviewers miss things, especially in repetitive or large-volume code
Typical workflow:
- Build a mental (or literal) map of trust boundaries and data flow.
- Catalog sources and sinks relevant to the language/framework.
- Trace data flow, manually and with static analysis tooling (Semgrep, CodeQL) as a force multiplier.
- Pay special attention to authentication/authorization logic, which fuzzing essentially never finds, since these bugs are about missing checks rather than crashes.
- Review recent diffs/commits with extra scrutiny — newly introduced code has a disproportionately higher bug density than stable, long-reviewed code.
Pillar Two: Reverse Engineering
When source isn’t available — proprietary software, firmware, compiled binaries, obfuscated mobile apps — reverse engineering becomes the primary way to understand what a system actually does. This spans a spectrum from relatively light techniques (dynamic analysis, API tracing) to deep static disassembly and decompilation.
Core techniques:
| Technique | Tools | What It Reveals |
|---|---|---|
| Static disassembly/decompilation | Ghidra, IDA Pro, Binary Ninja | Control flow, function boundaries, approximate source reconstruction |
| Dynamic analysis / debugging | x64dbg, GDB, WinDbg, Frida | Runtime behavior, actual values in memory, execution tracing |
| Binary diffing | BinDiff, Diaphora | Comparing patched vs. unpatched binaries to identify what a security patch actually fixed |
| Network/API tracing | Wireshark, Frida hooks, mitmproxy | Protocol behavior and data formats without needing full disassembly |
| String/symbol analysis | strings, symbol table inspection | Quick clues about functionality, embedded credentials, debug info leaks |
Strengths:
- Works with zero source access — applicable to any compiled target
- Binary diffing against a patch is one of the single most efficient ways to find a vulnerability, since the vendor has effectively told you where the bug was (n-day vulnerability research)
- Reveals ground truth about actual runtime behavior, including compiler-introduced quirks that wouldn’t be visible in source
Limitations:
- Time-intensive, especially for large or heavily obfuscated binaries
- Requires specialized skills (assembly, calling conventions, compiler internals) with a steep learning curve
- Anti-reverse-engineering measures (packing, obfuscation, anti-debugging) can significantly slow progress
- Reconstructed logic from decompilation is an approximation — always subject to some interpretation error, especially with heavy compiler optimization
Typical workflow (n-day patch analysis, a very common professional pattern):
- Obtain both the patched and unpatched binary versions.
- Run a binary diffing tool to identify exactly which functions changed.
- Disassemble/decompile the changed functions to understand what the patch actually does differently.
- Infer the original vulnerability from the nature of the fix (a new bounds check added, a validation function introduced, a use-after-free pattern corrected).
- Confirm by crafting an input that triggers the vulnerable path on the unpatched version.
This “patch diffing” workflow is how a huge fraction of real-world n-day exploitation research is actually conducted — vendors routinely under-disclose the security relevance of a fix in their changelog, so researchers (and, unfortunately, attackers targeting unpatched systems) diff the binaries directly to understand what was actually broken.
Pillar Three: Fuzzing
Fuzzing is automated testing that feeds a program large volumes of malformed, unexpected, or randomly-mutated input, monitoring for crashes, hangs, or other anomalous behavior. It’s the pillar most capable of finding bugs that neither a human code reviewer nor a reverse engineer would think to construct by hand, precisely because it doesn’t rely on human intuition about what input “should” break something.
Major fuzzing approaches:
| Approach | Description | Example Tools |
|---|---|---|
| Mutation-based | Starts from valid seed inputs, randomly mutates bytes | AFL++, honggfuzz |
| Generation-based | Constructs inputs from a grammar/model of the expected format | Peach Fuzzer, custom grammar-based fuzzers |
| Coverage-guided | Uses code coverage feedback to intelligently guide mutation toward unexplored paths | AFL++, libFuzzer, go-fuzz |
| Structure-aware | Understands the input format (e.g., a specific file format or protocol) to generate structurally valid-but-malicious input | Custom harnesses using libprotobuf-mutator, format-specific fuzzers |
| Differential fuzzing | Runs the same input against multiple implementations, flags discrepancies | Used heavily in cryptographic and parser correctness research |
Strengths:
- Finds edge cases no human would think to test
- Scales extremely well — can run continuously, generating millions of test cases
- Particularly effective against memory-unsafe languages (C/C++) for finding memory corruption bugs
- Requires no deep manual understanding of the target’s internals to start finding crashes (though understanding helps enormously with triage)
Limitations:
- Needs a working harness — isolating the specific function/parser to fuzz, which itself requires some code understanding
- Poor at finding logic/authorization bugs that don’t manifest as a crash
- Coverage plateaus are common; getting past complex validation checks (magic bytes, checksums) often requires either a smarter fuzzer or manual harness adjustments
- Triage overhead — a fuzzer can produce thousands of crashes that need deduplication and root-cause analysis to determine which are actually distinct, exploitable bugs versus noise
Typical workflow:
- Build a harness that isolates the target function/parser from the full application (critical for performance and precision).
- Gather a good seed corpus of valid inputs representative of real-world usage.
- Run coverage-guided fuzzing, monitoring for new coverage and crashes.
- Triage crashes: deduplicate by stack trace/crash signature, then determine root cause for each unique crash.
- Assess exploitability — not every crash is a security vulnerability; a null pointer dereference causing a clean crash is very different from a heap corruption bug with attacker-controlled write primitives.
# Example: a minimal AFL++ fuzzing session against a parsing binary
afl-fuzz -i seeds/ -o findings/ -- ./target_parser @@
How the Three Pillars Combine in Practice
The most effective real-world research workflows don’t pick one pillar — they move between all three:
sequenceDiagram
participant CR as Code Review
participant RE as Reverse Engineering
participant FZ as Fuzzing
CR->>CR: Identify complex parsing function as high-risk
CR->>FZ: Build harness for that specific function
FZ->>FZ: Discover crashing input via mutation
FZ->>RE: Hand off crash for root-cause analysis
RE->>RE: Disassemble crash site, understand memory layout
RE->>CR: Confirm root cause matches suspected code pattern
CR->>CR: Assess broader codebase for similar patterns elsewhere
A concrete real-world pattern: a researcher reviewing source code identifies a custom binary parser with a suspicious length-handling pattern (code review). Rather than manually tracing every possible malformed input, they build a fuzzing harness around just that parsing function and let a coverage-guided fuzzer generate thousands of malformed variants (fuzzing). When a crash surfaces, they use a debugger and disassembler to understand exactly what memory corruption occurred and confirm it’s a genuinely exploitable primitive rather than a benign crash (reverse engineering/dynamic analysis) — and having confirmed one instance, they go back to code review to check whether the same flawed pattern was copy-pasted elsewhere in the codebase.
Choosing Pillar Emphasis by Target Type
| Target Characteristic | Recommended Primary Pillar | Supporting Pillars |
|---|---|---|
| Open-source web application | Code review | Fuzzing for input handlers, minimal RE |
| Closed-source desktop binary | Reverse engineering | Fuzzing on isolated functions, code review if partial source/SDK exists |
| Network protocol parser (source available) | Code review + Fuzzing | RE only if binary-only components involved |
| Firmware/embedded device | Reverse engineering | Fuzzing via emulation (QEMU-based harnesses) |
| Cryptographic library | Code review + Differential fuzzing | RE for compiled dependencies |
| Mobile application | Reverse engineering (APK/IPA decompilation) | Code review of any exposed source, dynamic analysis via Frida |
Frequently Asked Questions
Which pillar should a beginner learn first? Code review, generally — it builds foundational understanding of common vulnerability patterns (injection, deserialization, path traversal) using readable source code, before tackling the steeper learning curves of assembly-level reverse engineering or fuzzing harness construction.
Is fuzzing becoming less necessary now that static analysis tools are more advanced? No — they find different, largely non-overlapping bug classes. Static analysis and code review are strong at logic bugs and known unsafe patterns; fuzzing remains uniquely effective at finding memory corruption in complex parsing logic that would be extremely tedious to identify through manual review alone, especially for bugs involving unusual, non-obvious input sequences.
Do professional vulnerability researchers specialize in just one pillar? Many do develop a primary specialization (a lot of browser/kernel researchers lean heavily reverse-engineering and fuzzing-focused; a lot of web application security researchers lean heavily code-review-focused), but the strongest researchers are generally competent across all three, because real targets rarely respect a clean division between “needs code review” and “needs reverse engineering.”
How does AI-assisted tooling fit into these three pillars? It’s increasingly used to augment all three — LLM-assisted code review for spotting suspicious patterns at scale, AI-assisted decompilation cleanup for reverse engineering, and machine-learning-guided fuzzers that use learned models to generate more effective test cases than pure random mutation — but each still requires human validation, since these tools currently produce meaningful false positive/negative rates on their own.
References
- OWASP Code Review Guide
- Ghidra documentation (NSA)
- AFL++ documentation
- Google Project Zero: publications and case studies
- MITRE CWE Top 25 Most Dangerous Software Weaknesses
- NIST SP 800-53: Vulnerability Monitoring and Scanning controls
Summary and Recommendations
Code review, reverse engineering, and fuzzing each answer a different question about a target, and treating them as interchangeable — or worse, only ever using one — leaves real findings on the table. Code review gives you precision and finds the logic bugs nothing else catches; reverse engineering gives you ground truth when there’s no source to read; fuzzing gives you scale and finds the inputs no human would think to craft. The researchers who consistently find high-value vulnerabilities are the ones who move fluidly between all three — using code review to identify where to focus fuzzing effort, using fuzzing to surface crashes that reverse engineering then explains at the root-cause level, and feeding that understanding back into a sharper, more targeted code review pass. Build competence in all three, and let the target — not habit — decide which one leads at any given moment.