← Back to Blog
Thumbnail for Fuzzing Native Program

Fuzzing Native Program

October 12, 2025 at 10:50 AM
3 min read
#Fuzzing#Security#CodeAuditing

Fuzzing native programs — a practical, friendly guide

Fuzzing is a simple idea with powerful results: throw lots of (often random) inputs at a program and watch for crashes, hangs, or other surprising behavior. For native programs (C/C++/Rust/Go binaries, system daemons, device firmware, etc.), fuzzing is one of the most effective ways to find memory corruption bugs, logic errors, and security issues — assuming you do it right.

This blog walks through the what, why, and how of fuzzing native programs, with practical examples, useful tools, and responsible guidance so you can get started safely.

Why fuzz native programs? Native code often deals with manual memory, pointers, and low-level APIs — a fertile ground for bugs. Fuzzing can automatically discover edge-case inputs humans miss. When paired with sanitizers and coverage-guided fuzzers, fuzzing produces high-value, reproducible defects quickly. Useful for hardening software, testing parsers, image decoders, network services, and file format handlers.

Types of fuzzing (short)

Black-box — no knowledge of internals. Feed inputs to the program and watch outputs/crashes. Simple but less efficient.

Grey-box — the fuzzer observes lightweight runtime feedback (e.g., code coverage) and uses it to guide input generation. Highly effective for native programs (AFL, libFuzzer, Honggfuzz).

White-box — heavy symbolic/concolic analysis to reason about paths (slow, complex). Tools like KLEE operate here.

For most native-code testing, grey-box fuzzers are the sweet spot.

Key components of a fuzzing setup

Harness / Target — code or wrapper the fuzzer calls to exercise specific logic (e.g., fuzz_one(void *data, size_t size)).

Fuzzer engine — generates inputs and mutates them guided by coverage (AFL, libFuzzer, honggfuzz).

Sanitizers & instrumentation — detect issues and provide diagnostics (ASAN, UBSAN, MSAN, AddressSanitizer, UndefinedBehaviorSanitizer).

Corpus — initial set of inputs (seed files) for mutation.

Triage & minimization tools — dedupe, minimize failing inputs (afl-tmin, afl-cmin, llvm-cov, afl-whatsup).

Reproducibility — steps to reproduce the crash locally and create a test case.

Safety & ethics (you must read this)

Only fuzz binaries you own, have permission to test, or that are explicitly allowed by a vendor’s bug bounty / testing policy.

Don’t fuzz production systems or networks you don’t control (it can cause denial-of-service or data corruption).

When you find security-relevant bugs, follow responsible disclosure processes.

Tool short-list (what people use)

AFL / AFL++ (coverage-guided, great for binaries & harnesses)

libFuzzer (LLVM-integrated, good for in-process fuzzing of libraries)

honggfuzz (flexible and fast)

OSS-Fuzz (continuous fuzzing at scale — for open-source projects)

Sanitizers: AddressSanitizer (ASAN), UndefinedBehaviorSanitizer (UBSAN), MemorySanitizer (MSAN)

Triage: afl-tmin, afl-cmin, llvm-symbolizer, gdb/valgrind

Quick practical example 1 — fuzzing a simple native program with AFL

Imagine a tiny program that parses a binary message and may crash on malformed input.

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

void handle(const unsigned char *buf, size_t len) {
    if (len > 4 && buf[0] == 'B' && buf[1] == 'O' && buf[2] == 'OM' && buf[3] == 0xFF) {
        // oops — unsafe access demo
        char *p = malloc(100);
        memcpy(p, buf + 4, len - 4); // potential overflow if len - 4 > 100
        if (p[0] == 'X' && p[1] == 'P') {
            // special case
            *((volatile int*)0) = 0; // intentionally crash (for demo only)
        }
        free(p);
    }
}

int main(int argc, char **argv) {
    FILE *f = fopen(argv[1], "rb");
    if (!f) return 1;
    fseek(f, 0, SEEK_END);
    long n = ftell(f);
    fseek(f, 0, SEEK_SET);
    unsigned char *buf = malloc(n);
    fread(buf, 1, n, f);
    handle(buf, n);
    free(buf);
    return 0;
}