LibAFL is a modern, high-performance fuzzing framework written in Rust. Instead of being a single “one size fits all” tool like AFL++ or libFuzzer, it’s built as a set of reusable components (Rust crates) that you can mix and match.Because of that modular design, security researchers can assemble fuzzers that fit a specific target and workflow whether that means snapshot-based fuzzing, custom coverage or crash feedback, or even combining fuzzing with symbolic execution without needing to fork and heavily modify a huge C codebase.
https://github.com/AFLplusplus/LibAFL
Please follow below url for installing Dependencies for Libafl
https://github.com/AFLplusplus/LibAFL?tab=readme-ov-file#building-and-installing
Here is a high-level overview of how the architecture interacts:
LibAFL QEMU mode integrates the legendary QEMU emulation engine natively within LibAFL. Specifically, using QEMU User-Mode emulation, it allows you to run binaries compiled for a different architecture (e.g., AArch64 Android) directly on your host machine
The big win is speed and feedback: instead of launching a fresh QEMU process for every single test case (which is painfully slow), LibAFL keeps the emulator running and drives executions from within the fuzzing loop. Under the hood, QEMU’s dynamic translation engine (TCG) tracks which blocks/edges run, and LibAFL hooks that coverage directly into its observer maps so your fuzzer can make smart, coverage-guided decisions.
You’ll usually see three chunks in a LibAFL QEMU setup:
For this blogpost , we will be fuzzing a custom library called libfuzzkit.so. Written in C++, this library parses structured Run-Length Encoded (RLE) payloads using the exposed function:
A common issue in C++ data parsers are Heap Buffer Overflows. To demonstrate a successful fuzzer, our library contains an intentional classic vulnerability: when a specific sequence occurs, it allocates a fixed 4-byte buffer but copies up to 127 bytes into it without bounds checking. Because this is an Android target, we compile this library to a native AArch64 shared object (`.so`) using the Android NDK.
libfuzzkit.so

A "harness" is a small wrapper program that takes input from the fuzzer and feeds it to our target library function.
We will write a C harness (`harness.c`) that exposes the standard `LLVMFuzzerTestOneInput` function. LibAFL will dynamically locate this symbol and hook it.
The fuzzer doesn't actually use `main` to loop. Instead, LibAFL will use QEMU to execute up to `LLVMFuzzerTestOneInput`, place a hook there, and repeatedly inject mutated inputs directly into guest memory!
Build the harness
Here is structure of code
host_fuzzer/src/main.rs → small Linux-only entrypointhost_fuzzer/src/fuzzer_impl.rs → the real fuzzer logic
host_fuzzer/cargo.toml → crates and dependencies
main.rs is intentionally minimal:
This does two things:
This section follows the code structure in `fuzzer_impl.rs`. If you’re new to LibAFL, don’t worry about the names focus on the roles:
This enables #[derive(Parser)] on a struct like Opts, so your fuzzer can accept flags like:
Think of this as: corpus + mutation + coverage feedback + scheduling + fuzz loop.
Corpus (where inputs are stored)
These are “support tools” used everywhere
This is the part that makes “QEMU mode fuzzing” happen.
ELF symbol resolution
First, we initialize the QEMU emulator with the correct arguments. Because we are fuzzing an Android binary, we must tell QEMU where to find the Android `rootfs` (the interpreter `linker64` and `libc.so`) and our target library.
When debugging: print qemu_args and try running an equivalent command manually.
Next, we parse the loaded ELF to locate `LLVMFuzzerTestOneInput`. This is where we will inject our fuzzing data. We place a breakpoint at the function's return address so the emulator hands control back to LibAFL after every execution.
This line looks scary but conceptually it’s simple: “make an observer that watches the global edges map”.
This is the QEMU side: it tells libafl_qemu, “when you translate/execute blocks, update _this_ observer’s map".
Then we build the emulator:
At this point:
We do two important things:
Conceptually:
Instead of allocating a new buffer each time, we map a fixed chunk once:
This is basically your “checkpoint”. Before every run you restore PC/SP/LR (return address), then set function arguments.
This is the most important block in the whole fuzzer, because it’s where your mutated bytes become “guest execution”.
Feedback decides what goes into the evolving corpus. Objective decides what’s “special”.
This setup is a pragmatic middle ground:
The executor:
Havoc mutations are a good baseline:
If everything above is correct, this should:
In Cargo.toml, the Crates and Dependencies section defines the core building blocks used by the fuzzer. clap (with the derive feature) is used to create a clean command-line interface for options like target path, corpus directories, and timeouts. log and env_logger provide lightweight logging so you can enable useful runtime output with RUST_LOG=info without hardcoding print statements. The main fuzzing logic comes from libafl, which provides the framework pieces like corpus management, schedulers, mutators, observers, feedback, and the fuzz loop itself. libafl_bolts adds shared utilities (randomness, tuple helpers, safe-ish map wrappers, and better backtraces via errors_backtrace) that LibAFL uses across components. libafl_qemu (enabled with usermode) is the key dependency that embeds QEMU user-mode emulation into LibAFL so the fuzzer can execute foreign-architecture binaries while collecting coverage. Finally, libafl_targets provides the standard shared coverage map layouts and target-side helpers that integrate neatly with LibAFL’s observers and feedback system.

After the fuzzer builds successfully, you’ll find the compiled binary in your project’s target/release/ directory
Before running the fuzzer, we need to copy the custom library that the harness depends on, because it isn’t included in the root filesystem. In my case, I created a target/build directory inside the project and copied the required files there.

After some time, our fuzzer discovered about 36 objectives.
To analyze the crash properly, we need to rebuild the harness with a couple of changes: make it read the input file path from a command-line argument, and compile it with AddressSanitizer enabled.
Now we’ll run the harness under QEMU user-mode to emulate the AArch64 binary and reproduce/check all the crashes.
After reviewing the crashes one by one, we found a few real, reproducible crashes that actually hit the target vulnerability in our custom library.



In this blog post, we built a simple and practical workflow for fuzzing AArch64 Android binaries using LibAFL in QEMU user-mode. We started by understanding how LibAFL works with its modular design and how QEMU helps us run binaries from a different architecture while still getting useful coverage feedback. Then we brought everything together—writing a basic harness, targeting a custom native library, and connecting the core LibAFL components that make the fuzzing loop run.
After getting the fuzzer running, we focused on what really matters: analyzing the results. By going through the objectives and replaying crashes using the harness, we were able to verify and better understand the issues we found. This gives us a complete, end-to-end setup—from taking an Android .so file to actually finding and confirming crashes—without needing to run a full Android system.
In the next blog post, we’ll go one step further by fuzzing a real-world Android library using LibAFL in Frida mode, where we can work directly with live processes and explore deeper, more realistic execution paths.
Check out our courses that cover full chain exploitation,
Android Userland Fuzzing and Exploitation
https://www.mobilehackinglab.com/afe-promo
Android Kernel Fuzzing and Exploitation
https://www.mobilehackinglab.com/courses/kernel-fuzzing.html
Get both together with a special bundle sale with 60% discount!
https://www.mobilehackinglab.com/bundles?bundle_id=android-kernel-userland
Want to learn to Chained AppSec bugs to get remote code execution?
Advanced Android Hacking - Road to Pwn2Own
https://www.mobilehackinglab.com/course/advanced-android-hacking
Copyright © 2024 (function() { if (!window.opener) return; if (window.location.search.indexOf('msg=not-logged-in') !== -1) return; if (window.location.pathname.indexOf('signin') !== -1) return; function send(email, lwToken) { if (!email || email.indexOf('@') === -1) return; window.opener.postMessage({ type: 'mhl-lw-login', email: email.toLowerCase().trim(), lwToken: lwToken || '' }, '*'); setTimeout(function() { try { window.close(); } catch(e) {} }, 500); } function findToken() { // Scan localStorage for any JWT that contains an email — that's the LW user token try { var keys = Object.keys(localStorage); for (var i = 0; i < keys.length; i++) { var v = localStorage.getItem(keys[i]); if (!v || v.split('.').length !== 3) continue; try { var p = JSON.parse(atob(v.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))); if (p && p.email) return { email: p.email, token: v }; } catch(e) {} } } catch(e) {} return null; } function tryGlobals() { var checks = [window.LW && window.LW.user, window.learner, window.__learner, window.lwUser, window.currentUser]; for (var i = 0; i < checks.length; i++) { if (checks[i] && checks[i].email) return checks[i].email; } return null; } function tryApis(done) { var endpoints = ['/api/v2/me', '/api/v2/learner', '/api/v2/learner/profile']; var i = 0; function next() { if (i >= endpoints.length) return done(null); fetch(endpoints[i++], { credentials: 'include' }) .then(function(r) { return r.ok ? r.json() : null; }) .then(function(d) { if (!d) return next(); var e = d.email || (d.data && d.data.email) || (d.user && d.user.email); e ? done(e) : next(); }).catch(next); } next(); } window.addEventListener('load', function() { // Best case: find token in localStorage (has email + is the LW JWT) var found = findToken(); if (found) return send(found.email, found.token); // Try JS globals var email = tryGlobals(); if (email) return send(email, ''); // Try LW's own same-origin API tryApis(function(email) { if (email) send(email, ''); }); }); })();
Socials