Cogs and Levers A blog full of technical stuff

Fuzz testing C Binaries on Linux

Introduction

Fuzz testing is the art of breaking your software on purpose. By feeding random or malformed input into a program, we can uncover crashes, logic errors, or even security vulnerabilities — all without writing specific test cases.

In memory-unsafe languages like C, fuzzing is especially powerful. In just a few lines of shell script, we can hammer a binary until it falls over.

This guide shows how to fuzz a tiny C program using just cat /dev/urandom, and how to track down and analyze the crash with gdb.

The Target

First off we need our test candidate. By design this program is vulnerable through its use of strcpy.

#include <stdio.h>
#include <string.h>

void vulnerable(char *input) {
    char buffer[64];
    strcpy(buffer, input);  // Deliberately unsafe
}

int main() {
    char input[1024];
    fread(input, 1, sizeof(input), stdin);
    vulnerable(input);
    return 0;
}

In main, we’re reading up to 1kb of data from stdin. This pointer is then sent into the vulnerable function. A buffer is defined in there well under the 1kb that could come through the front door.

strcpy doesn’t care though. It’ll try and grab as much data until it encounters a null terminator.

This is our problem.

Let’s get this program built with some debugging information:

gcc -g -o vuln vuln.c

Basic “Dumb” Fuzzer

We have plenty of tools at our disposal, directly at the linux console. So we can put together a fuzz tester albeit simple, without any extra tools here.

Here’s fuzzer.sh:

# allow core dumps
ulimit -c unlimited

# send in some random data
cat /dev/urandom | head -c 100 | ./vuln

100 bytes should be enough to trigger some problems internally.

Running the fuzzer, we should see something similar to this:

*** stack smashing detected ***: terminated
[1]    4773 broken pipe                    cat /dev/urandom |
4774 done                                  head -c 100 |
4775 IOT instruction (core dumped)         ./vuln

We get some immediate feedback in stack smashing detected.

Where’s the Core Dump?

On modern Linux systems, core dumps don’t always appear in your working directory. Instead, they may be captured by systemd-coredump and stored elsewhere.

In order to get a list of core dumps, you can use coredumpctl:

coredumpctl list

You’ll get a big report of all the core dumps that your system has gone through. You can use the PID that crashed to reference the dump that is specifically yours.

TIME                            PID  UID  GID SIG     COREFILE EXE            SIZE
Sun 2025-04-20 11:02:14 AEST   4775 1000 1000 SIGABRT present  /path/to/vuln  19.4K

Debugging the dump

We can get our hands on these core dumps in a couple of ways.

We can launch gdb directly via coredumpctl, and This will load the crashing binary and the core file into GDB.

coredumpctl gdb 4775

I added the specific failing pid to my command, otherwise this will use the latest coredump.

Inside GDB:

bt              # backtrace
info registers  # cpu state at crash
list            # show source code around crash

Alternatively, if you want a phyical copy of the dump in your local directory you can get our hands on it with this:

coredumpctl dump --output=core.vuln

AFL

Once you’ve had your fun with cat /dev/urandom, it’s worth exploring more sophisticated fuzzers that generate inputs intelligently — like AFL (American Fuzzy Lop).

AFL instruments your binary to trace code coverage and then evolves inputs that explore new paths.

Install

First of all, we need to install afl on our system.

pacman -S afl

Running

Now we can re-compile our executable but this time with AFL’s instrumentation:

afl-cc -g -o vuln-afl vuln.c

Before we can run our test, we need to create an input corpus. We create a minimal set of valid (or near-valid) inputs. AFL will use this input to mutate in other inputs.

mkdir input
echo "AAAA" > input/seed

Before we run, there will be some performance settings that you need to push out to the kernel first.

We need to tell the CPU to run at maximum frequency with the following:

cd /sys/devices/system/cpu
echo performance | tee cpu*/cpufreq/scaling_governor

For more details about these settings, have a look at the CPU frequency scaling documentation.

Now, we run AFL!

mkdir output
afl-fuzz -i input -o output ./vuln-afl

You should now see a live updating dashboard like the following, detailing all of the events that are occuring through the many different runs of your application:

american fuzzy lop ++4.31c {default} (./vuln-afl) [explore]          
┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│        run time : 0 days, 0 hrs, 0 min, 47 sec      │  cycles done : 719   │
│   last new find : none yet (odd, check syntax!)     │ corpus count : 1     │
│last saved crash : none seen yet                     │saved crashes : 0     │
│ last saved hang : none seen yet                     │  saved hangs : 0     │
├─ cycle progress ─────────────────────┬─ map coverage┴──────────────────────┤
│  now processing : 0.2159 (0.0%)      │    map density : 12.50% / 12.50%    │
│  runs timed out : 0 (0.00%)          │ count coverage : 449.00 bits/tuple  │
├─ stage progress ─────────────────────┼─ findings in depth ─────────────────┤
│  now trying : havoc                  │ favored items : 1 (100.00%)         │
│ stage execs : 39/100 (39.00%)        │  new edges on : 1 (100.00%)         │
│ total execs : 215k                   │ total crashes : 0 (0 saved)         │
│  exec speed : 4452/sec               │  total tmouts : 0 (0 saved)         │
├─ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ───────┤
│   bit flips : 0/0, 0/0, 0/0                        │    levels : 1         │
│  byte flips : 0/0, 0/0, 0/0                        │   pending : 0         │
│ arithmetics : 0/0, 0/0, 0/0                        │  pend fav : 0         │
│  known ints : 0/0, 0/0, 0/0                        │ own finds : 0         │
│  dictionary : 0/0, 0/0, 0/0, 0/0                   │  imported : 0         │
│havoc/splice : 0/215k, 0/0                          │ stability : 100.00%   │
│py/custom/rq : unused, unused, unused, unused       ├───────────────────────┘
│    trim/eff : 20.00%/1, n/a                        │          [cpu000: 37%]
└─ strategy: explore ────────── state: started :-) ──

Unlike /dev/urandom, AFL:

  • Uses feedback to mutate inputs intelligently
  • Tracks code coverage
  • Detects crashes, hangs, and timeouts
  • Can auto-reduce inputs that cause crashes

It’s like the /dev/urandom method — but on steroids, with data-driven evolution.

The /output folder will hold all the telemetry from the many runs that AFL is currently performing. Any crashes and hangs are kept later for your inspection. These are just core dumps that you can use again with gdb.

Conclusion

Fuzzing is cheap, dumb, and shockingly effective. If you’re writing C code, run a fuzzer against your tools. You may find bugs that formal tests would never hit — and you’ll learn a lot about your program’s internals in the process.

If you’re interested in going deeper, check out more advanced fuzzers like:

  • AFL (American Fuzzy Lop): coverage-guided fuzzing via input mutation
  • LibFuzzer: fuzzing entry points directly in code
  • Honggfuzz: another smart fuzzer with sanitizer integration
  • AddressSanitizer (ASan): not a fuzzer, but an excellent runtime checker for memory issues

These tools can take you from basic input crashes to deeper vulnerabilities, all without modifying too much of your workflow.

Happy crashing.