Skip to main content
Be Bitwise

When to stop reversing and start fuzzing

Reverse-engineering and fuzzing solve different problems on different attack surfaces. A practical guide to picking the right one (or both) for the target in front of you.

#fuzzing#reverse-engineering#methodology#vulnerability-research

There's a recurring argument in VR circles that's roughly "fuzzing has won; reversing is for hipsters". And a counter-argument that goes "fuzzing only finds dumb bugs; real research is reading code". Both takes are wrong in opposite ways and at the same volume.

The honest position is that fuzzing and reversing solve different problems. Fuzzing is good at finding memory-corruption bugs across large attack surfaces with cheap-to-generate inputs. Reversing is good at finding logic bugs, weird-state machines, and authentication slips in places a fuzzer cannot easily reach. Most real targets need both, in some order, and the order matters.

When reversing pays off

Reach for reversing when one or more of these is true.

The target is small. A network parser of 8,000 lines, a DRM blob of 200 KB, a bootloader, a single library function under audit. Below a certain size, you can hold the whole target in your head, and the time to map it out by hand is shorter than the time to set up a fuzzing harness.

The protocol is novel or proprietary. Fuzzers thrive on grammars they understand. Mature mutators exist for JSON, HTTP, and XML; a custom binary protocol has no such mutator. You'll either spend a week writing one (which is reverse-engineering work in disguise) or watch every input get rejected at the first parse step. Reading the protocol parser by hand is sometimes faster.

You're hunting logic bugs. Authentication bypasses, authorisation flaws, state-machine confusions, races in application logic rather than the runtime. These almost never crash. A fuzzer looking for SIGSEGV sails right past them. Reading the code makes them stand out — "wait, this state transition doesn't check whether the prior step actually completed" is a comment you write in IDA, not in AFL output.

The target is hardened against fuzzing. Some targets are deliberately resistant: heavy compression, mandatory crypto, integrity checksums over the whole message. Defeating those for a fuzzer is bookkeeping that consumes weeks. Reversing past them once unblocks the work.

You want a precise mental model. If the end goal is to write an exploit, not merely find a bug, the reverse engineering you'd do anyway as part of exploit dev frequently surfaces the bug along the way. The ratio of "bugs found while trying to understand the code well enough to exploit a different bug" is higher than people admit.

When fuzzing pays off

Reach for fuzzing when these are true.

The attack surface is large. A media format parser with thirty optional chunks, a font renderer, a kernel filesystem driver, a video codec. Branching factor too high to read; many independent sub-parsers each their own surface. Fuzzers handle that exhaustion well.

The code is parser-heavy. Anything that takes a complicated input format and walks it byte by byte. Image and document parsers, protocol decoders, deserialisation libraries. The bug class is overwhelmingly memory corruption — exactly what fuzzers find best.

You don't have source. Coverage-guided fuzzing works on binaries via emulation (qemu-mode in AFL++) or static instrumentation (TinyInst, Frida). Slower than source-instrumented fuzzing, more triage needed, still often productive. Reading a closed-source 4 MB DLL by hand is rough; pointing a fuzzer at it after a couple of hours of reversing the input parser is much more efficient.

You have time, hardware, patience. Fuzzing parallelises embarrassingly. With a 64-core box and three weeks, a well-tuned fuzzer will find things. The cost is up front (harness, custom mutator if needed, corpus) and the campaign runs while you do other things.

The bug class matches. Use-after-free, double-free, OOB read/write, integer overflow into a length, NULL deref, stack overflow. Targets full of those, fuzzing dominates. Targets full of TOCTOU races or logic flaws, fuzzing won't help much.

The decision tree

The diagram captures the rough flow. Reversing first is appropriate when the target is small enough or the surface is novel enough that a fuzzer would flounder. Fuzzing first works on large parser-heavy targets with standard input formats. The hybrid path — reverse to identify the input handlers, then point a fuzzer at them — is the workflow that matches most real vulnerability research, especially against closed-source binaries.

The hybrid approach in practice

The pattern I find most productive on a target I've never seen looks like this.

Spend a day or two on basic reverse engineering. Find the entry points: where input comes from, who reads it, what the first parsing function is. Find the trust boundaries: where data moves from "untrusted bytes" to "structured object". Sketch the call graph for the parser. The output is a mental map and a list of named functions that look interesting.

Now build a fuzzing harness specifically against one of those handlers. Not the whole binary; the handler. Skip the network stack, skip the auth, drop your input straight into the parser as a function call. This is "targeted" or "structure-aware" fuzzing, and it gets the most bugs per CPU-hour by a wide margin. AFL++ persistent mode, libFuzzer with a custom harness, honggfuzz with a manual entry point — all of them support this pattern.

The hybrid wins because it amortises the slow thing (understanding the target) once, then uses it to make the fast thing (fuzzing) much more efficient. A naive fuzzer pointed at the binary will burn 95% of its cycles on the message-authentication code; a harness pointed at the post-auth parser spends 100% of its cycles on the bug surface.

The time/value calculation

Roughly, ask yourself two questions before committing to one approach.

How long would it take a competent fuzzer to find a bug here, assuming I built a reasonable harness? For a parser-heavy open-source library with ASan support, the answer is often "hours". For a hardened proprietary binary with a complex protocol, the answer might be "weeks to months, with significant harness work".

How long would it take me to read the relevant code by hand? For a 5,000-line parser, maybe a week. For a 500,000-line browser, you'll never finish; you'll only ever cover the parts that look interesting.

Whichever estimate is shorter wins for the first pass. When neither answer is satisfactory, you're looking at a hybrid campaign. Absolutes are rare in this work.

A slightly opinionated closing

Junior researchers reach for fuzzing too quickly because the tool stack is mature and the payoff feels deterministic — set up the harness, walk away, come back to crashes. That's true sometimes. It's also true that a researcher who can't read code well will keep finding the same shallow bugs everyone else's fuzzer found six months ago, and won't develop the intuition that makes them good at picking targets in the first place.

The investment in being able to read assembly and decompiler output fluently pays back over the entire arc of a career. Reading skills compound; fuzzer-tooling skills age in step with the tools, which churn faster than you'd think. If you're choosing what to learn first, learn to read; pick up fuzzing once reading bores you in a particular target.

Where this fits in

Module 84: Advanced Fuzzing Techniques covers the harness-writing, structure-aware mutation, and corpus-management craft that takes fuzzing from "ran AFL on a binary" to "found multiple bugs in a hardened target". Pair it with the reverse-engineering modules to build out the hybrid workflow above; the modules are designed to interlock rather than to be read in isolation.