Fuzzing libming with AFL++: From 177 Crashes to a Root Cause
Standing up an AFL++ and AddressSanitizer harness against libming's SWF parser, running it until it found memory-corruption crashes, and triaging 177 crash files down to the bug classes that actually matter — including one heap overflow traced to a single unbounded strcat.
Exploiting a planted bug teaches you technique. Finding a bug in real, shipped software is the part a vulnerability-research role actually cares about, so I wanted a project that produced real crashes in real code — and then the discipline of turning a pile of crashes into understood bugs.
The target is libming 0.4.8, an old Flash/SWF toolkit with a long history of parser CVEs. It’s a soft target on purpose: the goal here is to practice the loop — build with instrumentation, fuzz, triage, root-cause — not to claim a novel discovery. The bugs I found map to libming’s existing CVEs. Rediscovering them end to end, and explaining one, is the exercise.
Everything ran in a container with AFL++ and AddressSanitizer, offline, so a crashing SWF couldn’t do anything but crash the parser.
Build: instrument twice
The target is the swftophp tool. It gets compiled two ways at once:
1
2
3
export CC=afl-clang-fast AFL_USE_ASAN=1
./autogen.sh && ./configure --disable-shared --disable-freetype --without-expat
make
afl-clang-fastadds AFL’s edge-coverage instrumentation, so the fuzzer can see which branches each input reaches and steer mutation toward new code.AFL_USE_ASAN=1builds with AddressSanitizer, which turns a silent out-of-bounds access into an immediate, detailed crash with the faulting function, the access size, and the allocation it came from.
Without ASan, a lot of memory corruption just quietly succeeds and the fuzzer never knows anything went wrong. ASan is what makes the crashes mean something.
Seeds and the campaign
AFL needs a starting corpus. A handful of minimal but structurally valid SWF files (FWS header, a rect, a frame rate, a couple of tags) is enough — the first mutations then reach the tag parsers quickly.
1
afl-fuzz -i seeds -o out -m none -t 2000 -- ./swftophp @@
Then you let it run. The AFL status screen is worth reading while it works: corpus count (inputs it’s keeping because they hit new coverage), saved crashes, execution speed, and cycles done. Mine ran on a single core for a few hours:
1
2
3
4
run_time : ~3h
execs_done : 1,559,293
corpus_count : 754
saved_crashes : 177
A million and a half executions, 754 inputs kept for new coverage, 177 crashing inputs. First crashes landed within a minute — but a raw crash count is not a bug count.
Triage: 177 files are not 177 bugs
Many different inputs trigger the same underlying bug, and some crashes are far more interesting than others. Triage replays every crash under ASan and groups by the bug it reports. I wrote a small script that runs each crash through the ASan build and keys on the SUMMARY line; grouping the 177 files gives:
| Count | Bug class | Worth it? |
|---|---|---|
| 90 | SEGV (stack exhaustion) |
Low value |
| 70 | heap-buffer-overflow |
Yes |
| 3 | heap-use-after-free |
Yes |
The 90 SEGVs are the trap I was warned about: libming’s parser recurses on malformed, deeply-nested input until the stack runs out. It’s a real robustness bug, but it’s a low-severity one, and if you stop at the first crash (which is almost always one of these) you miss the interesting classes. The 70 heap-buffer-overflows and 3 use-after-frees are the memory-corruption bugs worth understanding — ASan pins each to a specific allocation and access.
Root-causing one heap overflow
Picking one heap-buffer-overflow and minimizing it with afl-tmin gets to a 53-byte input. Running the ASan build on it:
1
2
3
4
5
6
7
==18==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 45
#0 ... in strcat
#1 ... in dcputs util/decompile.c:104
#2 ... in decompileIMPLEMENTS util/decompile.c:3094
#3 ... in decompileAction util/decompile.c:3375
allocated by thread T0 here:
#0 ... in realloc
The whole bug is readable from that trace. swftophp tries to decompile the ActionScript bytecode inside the SWF. decompileIMPLEMENTS handles one bytecode action and, through dcputs, builds up an output string with strcat (decompile.c:104). dcputs appends to a buffer that was sized with realloc — but the size it grew the buffer to doesn’t account for how much this strcat will write. A crafted action makes the appended text longer than the allocation, and strcat writes 45 bytes past the end of the heap buffer.
The root cause is the classic one: an unbounded strcat into a buffer whose size was computed from the wrong thing. The decompiler assumes the string it’s building fits; attacker-controlled bytecode makes it not fit. This is exactly the shape of libming’s known decompile.c overflow CVEs, which is a good sanity check that the harness is finding the real bugs and not artifacts.
What I took away
- ASan is the multiplier. Coverage-guided fuzzing finds inputs that reach new code; ASan is what converts a reach-and-corrupt into a crash you can read. Fuzzing without a sanitizer finds a fraction of the memory bugs.
- Triage is the actual skill. Going from 177 files to “90 stack-exhaustion, 70 heap overflow, 3 UAF” is the work. De-duplicating by the ASan summary, and knowing which classes matter, is what separates a crash dump from a bug report.
- Don’t stop at the first crash. The quickest crash was the least interesting. The bugs worth writing up showed up only after the corpus had grown and after I sorted by bug class.
- A 53-byte input and a 4-line stack trace was enough to explain a heap overflow completely. You don’t need the whole program in your head — you need the faulting function and the allocation.
The next step from here is an n-day writeup: take one of these overflows, match it to its assigned libming CVE, and read the upstream patch that fixed it. That’s the bridge from “I can find bugs” to “I can analyze them.”