AFL++ and libFuzzer Don't Work on Firmware Without Source Code - Here's What Does

If you've tried to apply AFL++ or libFuzzer to an IoT device or embedded firmware target, you've probably hit the same wall: both tools are built around compile-time instrumentation. They need source code. They need to rebuild the binary with coverage hooks baked in. For a closed-source medical device, a third-party IoT module, or automotive firmware you received as a flat binary, that path is closed before you even start.

This article looks at why source-based fuzzers fall short for firmware targets, what the real alternatives are, and what to expect from dynamic fuzzing tools built to work without source.


Why AFL++ and libFuzzer Require Source Code

AFL++ and libFuzzer both rely on compile-time instrumentation - compiler passes that inject branch-coverage feedback into every binary they produce. That feedback is what makes coverage-guided fuzzing work: the fuzzer mutates inputs, watches which new code paths open up, and keeps the mutations that expand coverage.

Without source code, you cannot run those compiler passes. You can use QEMU-mode AFL++ for some targets, but QEMU-mode has significant constraints: it works best on Linux userspace binaries for supported architectures, it adds substantial overhead, and it still cannot talk to a physical device over its actual protocol interface. For firmware that runs on a real chip, communicates over CAN bus, exposes a proprietary RF protocol, or expects a physical hardware environment, QEMU-mode is not a substitute for real dynamic testing.

libFuzzer has no equivalent workaround at all. It is strictly a library you link into a harness you compile yourself.


The Real Problem Is Protocol-Level Access, Not Just Binary Access

Even if you solve the binary instrumentation problem, you still have not solved the harder problem: embedded firmware does not consume files. It consumes messages.

A medical device receives frames over a serial interface, a CAN bus segment, or a custom radio protocol. An IoT gateway processes MQTT topics or proprietary TLV packets. A CAN ECU expects specific arbitration IDs and byte layouts. Feeding malformed JPEGs into stdin - the canonical AFL++ workflow - is not the right attack surface.

What you actually need is a fuzzer that can:

This is a different class of tool from AFL++ or libFuzzer.


What Dynamic Firmware Fuzzing Without Source Code Looks Like

Tools in this space work at the protocol and transport layer rather than the source layer. Instead of instrumenting compilation, they model the protocol the firmware speaks and generate structurally valid (but edge-case) messages to send to the device.

Penzzer is built specifically for this pattern - dynamic fuzzing for medical devices, IoT firmware, and CAN bus / embedded targets. It attacks behavior: the running protocol, the firmware on the device, the message handling at runtime. It does not require source code because it does not instrument compilation. It talks to the device the way the device expects to be talked to, then systematically breaks the assumptions baked into that protocol.

From the Penzzer API reference, the tool operates across several transport subsystems - including Wi-Fi protocol fuzzing (with specific parameters for wifi_module, wifi_interface, ssid, wifi_target_protocol, and channel configuration), web interface fuzzing, and embedded protocol interfaces. It also integrates Metasploit exploit validation, so when fuzzing surfaces a crash or anomalous behavior, you can move directly to exploit characterization without switching toolchains.

That integration matters for the firmware case: finding a crash in a black-box binary is only the first step. Determining whether that crash is exploitable - without source, without symbols, against a physical target - requires a different workflow than AFL++ provides even when it does work.


Comparing the Approaches

| | AFL++ / libFuzzer | Dynamic Protocol Fuzzer (e.g., Penzzer) | |---|---|---| | Source code required | Yes (QEMU-mode is partial, limited) | No | | Works on physical hardware | No | Yes | | Targets file formats | Yes | No | | Targets network / serial / CAN protocols | No | Yes | | Coverage-guided mutation | Yes | Protocol-model-guided | | Exploit validation built in | No | Yes (Metasploit integration) | | Medical device / IoT firmware scope | Rarely practical | Core use case |


When AFL++ Is Still the Right Tool

AFL++ is excellent when you control the source, you are targeting a file-parsing library (image decoders, PDF parsers, compression libraries), and you can compile a clean harness. For open-source firmware projects where you want to fuzz a specific parsing function in isolation, AFL++ with source instrumentation is fast and well-supported. libFuzzer is similarly strong for unit-level harness fuzzing in a build system you own.

If that describes your target, use AFL++. This article is not an argument against it - it is an argument that it is the wrong tool for a significant class of embedded targets.


What to Ask Before Picking a Fuzzing Approach

Before committing to a fuzzing tool for firmware, answer these questions:

  1. Do you have source code and build toolchain access, or only a binary / physical device?
  2. Does the target communicate over a protocol you need to speak, not a file you can drop into stdin?
  3. Is the target a physical device that needs to run in its real hardware environment?
  4. Do you need to move from "found a crash" to "characterized the vulnerability" without source-level debug access?

If you answered yes to any of 2, 3, or 4, a dynamic protocol fuzzer is a better starting point than AFL++ or libFuzzer.


Penzzer is designed for exactly these targets. If you are evaluating fuzzing options for a medical device, IoT firmware, or embedded CAN bus system without source code, penzzer.com is the right place to start - you can describe what you want to fuzz and the team will take it from there.

This post is about Penzzer.