Crashing Inputs FoundTotal Execution TimeSearch SpaceReturn Code
51216 min2^64 (8 bytes)−11 (SIGSEGV)

A Binary With No Source Code and One Question

Our professor handed us something deliberately mysterious: a compiled binary called parse_checkno 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 Challenge

Our 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.

Watching the Black Box Fall Apart

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.

Terminal showing first 8 crashes from fuzz.py
First crashes detected (21:02): The fuzzer starts finding crash inputs immediately. Each line shows parse_check CRASHED!!!!!!! followed by the exact bytearray that triggered the segfault, numbered sequentially. The fixed prefix AAAAAAAAAAAA is visible in every payload.
Terminal showing final crashes 488-512 at 21:18
Final outputs (21:18, 16 minutes later): The fuzzer completes its run, logging crash #488 through #512. The clock shows 21:18 — exactly 16 minutes after the first screenshot at 21:02. 512 unique crashing inputs discovered.
📊 What the Output Reveals About the Black Box

Without 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.

How the Fuzzer Was Built

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.

Full updated fuzzer Python code
Updated fuzz.py: The complete optimised fuzzer. Key elements visible: 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.

Modification 1 — Iterate All 8 Bytes

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.

Modification 2 — Smart Starting Point (The Key Optimisation)

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.

Code snippet showing datademo initialisation and range loop
Strategy snippet: 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.

Modification 3 — Try/Except Instead of Subprocess Check

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.

Base code subprocess snippet
Base code: Runs ./bitstream1 and checks p.returncode == -11 after full process completion.
Updated code try/except snippet
Updated code: check=True causes subprocess.run() to raise immediately on non-zero exit — caught by the except block, which increments count and prints the crash.

What We Learned About a Binary We Never Saw

PropertyBase CodeOptimised Code
Bytes iteratedLast 4 of 8All 8 bytes
Loop range0 → 2^32datademo → 2^64
Starting pointZero (blind)Hint-guided integer
Crash detectionPost-run returncode checktry/except CalledProcessError
Time to 512 crashesVery slow16 minutes
Crash counterNonecount variable, printed inline
🧠 Fuzzing as Reverse Engineering

Here'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.

What Comes After Fuzzing: Control Flow Integrity

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 Pipeline

Fuzzing identifies where a program breaks. CFI defines rules that prevent exploitation of those breakpoints. Together they form a complete vulnerability discovery and mitigation workflow.

For educational purposes only  ·  All testing conducted in isolated VM environments