Coverage-guided mutation works great until your target parses something with strong structure. Then you plateau: the fuzzer spends its whole budget producing inputs that get rejected in the first 50 bytes.
Options, roughly in order of effort:
- Dictionaries. Cheapest possible win. Feed AFL++/libFuzzer the magic constants, keywords, and tag names from the format. Often worth several percent coverage for ten minutes of work.
- libprotobuf-mutator. Define the input as a protobuf message, mutate the message, serialize to the real format in the harness. You get structurally valid inputs for free.
- Custom mutators. AFL++ lets you plug in your own. More work, maximum control — appropriate when the format has cross-field dependencies (lengths, offsets, checksums) that generic mutation will never satisfy.
- Grammar-based generation. For text formats — languages, config files, query syntaxes. Nautilus and friends.
The tradeoff nobody mentions: structure-aware fuzzing constrains your inputs to valid ones, and plenty of real bugs live in how the parser handles invalid input. Best results I’ve had come from running both a structured campaign and a dumb one, with a shared corpus.
What are you fuzzing where plain mutation hit a wall?