Kimaya Kelbaikar • Black-Box Fuzzing & Vulnerability Discovery • Python + Linux
| Crashing Inputs Found | Total Execution Time | Search Space | Return Code |
|---|---|---|---|
| 512 | 16 min | 2^64 (8 bytes) | −11 (SIGSEGV) |
Our professor handed us something deliberately mysterious: a compiled binary called parse_check — no source code, no documentation, no internals. Just an executable and a single challenge: find the inputs that make it crash.
The binary was anonymous by design. We had no idea what it parsed, how it worked, or what its author intended. All we had was a black box that accepted byte input — and the task of systematically breaking it. This is exactly the scenario a real security researcher faces when auditing closed-source software.
Fuzzing is the technique purpose-built for this situation. Feed the program large volumes of unexpected, malformed, or boundary-pushing input. Watch for crashes. Map the failure surface. The specific crash condition we were hunting: return code -11 — SIGSEGV, a segmentation fault — meaning the binary had accessed memory it shouldn't. That's the fingerprint of an exploitable parsing vulnerability.
🔍 The ChallengeOur professor dropped an anonymous compiled binary on our desks — no source code, no function names, no hints about what it does. Just one question: how many inputs can you find that make it crash? No reverse engineering the internals. Pure black-box fuzzing, exactly how real-world vulnerability researchers operate against closed-source software.
With no source code to inspect, the fuzzer became our only window into the binary's behaviour. We launched it with a single command — python3 fuzz.py — and let it interrogate the mystery binary systematically, one byte array at a time. What came back was a cascade of crashes.
parse_check CRASHED!!!!!!! followed by the exact bytearray that triggered the segfault, numbered sequentially. The fixed prefix AAAAAAAAAAAA is visible in every payload.📊 What the Output Reveals About the Black BoxWithout seeing a single line of the binary's source code, the crash output tells us something concrete: the vulnerability is deterministic, not random. Specific byte patterns reliably cause parse_check to access invalid memory — pointing to a structural flaw in how it handles the 8-byte input segment, not a race condition or environmental issue.
Since we had no source code to reason about, the fuzzer design had to be strategic. The starting point was a provided base fuzzer that iterated only the final 4 bytes of an 8-byte input segment — looping up to 232 iterations. That approach works but is painfully slow when you're probing a binary you know nothing about. The optimised version makes three targeted changes that turn a slow blind search into a precision strike.
datademo initialised from the known crash hint bytes, the for loop starting from datademo rather than 0, the count crash counter, and the try/except block catching CalledProcessError with returncode -11.The base code looped over the last 4 bytes of the 8-byte segment (232 iterations). The updated code iterates all 8 bytes — looping up to 264 — covering the full search space. This sounds like more work, but combined with the smart starting point below, it finds crashes far faster.
The brief hinted that one crashing byte sequence is ÿÿÿàÿ. Rather than starting the loop at 0 and searching blindly, int.from_bytes() converts this hint into an integer, which becomes the loop's starting value. The fuzzer jumps directly to the crash neighbourhood.
datademo is set using int.from_bytes() on the hint bytes with byteorder='big'. The for loop then starts at datademo rather than 0 — skipping the entire non-crashing region of the search space.
The base code ran the executable to completion on every input, only then checking the return code. The updated version wraps the subprocess call in a try/except chain that catches CalledProcessError immediately when a crash occurs — avoiding unnecessary process overhead and significantly reducing time complexity.
./bitstream1 and checks p.returncode == -11 after full process completion.check=True causes subprocess.run() to raise immediately on non-zero exit — caught by the except block, which increments count and prints the crash.| Property | Base Code | Optimised Code |
|---|---|---|
| Bytes iterated | Last 4 of 8 | All 8 bytes |
| Loop range | 0 → 2^32 | datademo → 2^64 |
| Starting point | Zero (blind) | Hint-guided integer |
| Crash detection | Post-run returncode check | try/except CalledProcessError |
| Time to 512 crashes | Very slow | 16 minutes |
| Crash counter | None | count variable, printed inline |
🧠 Fuzzing as Reverse EngineeringHere's what's remarkable: we deduced structural flaws in a binary we never read. Each SIGSEGV (return code -11) is parse_check trying to access memory it has no right to touch — and the fact that 512 specific byte patterns reliably trigger this tells us the parser has a consistent, exploitable boundary condition. Pure black-box analysis. No source required.
Having mapped the crash surface, the natural next question is: how do you actually defend against it? The answer is Control Flow Integrity (CFI) — a countermeasure designed specifically for the kind of memory corruption vulnerabilities the fuzzer just exposed.
CFI enforces a predefined set of rules that restrict a program's execution flow — specifying which memory locations may be modified, which variables are accessible, and which function call chains are permitted. Even if an attacker successfully corrupts memory, CFI prevents them from redirecting execution to arbitrary code.
🔗 The Fuzzing → CFI PipelineFuzzing identifies where a program breaks. CFI defines rules that prevent exploitation of those breakpoints. Together they form a complete vulnerability discovery and mitigation workflow.