courses

HW5: Network Protocol Fuzzing with boofuzz

Due Date: 2026-06-07 23:59:59

Introduction

In this assignment you will use boofuzz — the maintained successor to Sulley and the de facto standard Python network fuzzing framework — to find a stack buffer overflow in a deliberately vulnerable FTP-like server. You will then analyze the crash in pwndbg to understand exactly what happened.

Unlike AFL-net, boofuzz requires no source instrumentation and no coverage-guided feedback loop. It fuzzes by:

  1. Defining the protocol message structure in Python
  2. Systematically mutating each fuzzable field with known-bad values (very long strings, format specifiers, null bytes, boundary integers, etc.)
  3. Detecting crashes by monitoring whether the target stops responding
  4. Logging each crashing test case for reproduction

See the Network-Based Fuzzing lecture page for full boofuzz API reference.


Part 1: Setup

Prerequisites

C memory layout. The vulnerable server has a 64-byte username buffer on the stack. When strcpy() copies more than 64 bytes into it, it overwrites adjacent memory — including the saved return address that tells the CPU where to jump when the function returns. There are no bounds checks. When the return address is overwritten with garbage, the CPU jumps to an invalid address and the OS sends SIGSEGV. You don’t need to write an exploit, but you need to understand why the crash happens and where in the stack frame the overflow lands.

Key registers: $rip is the instruction pointer (what the CPU executes next); $rsp is the stack pointer (points to the top of the current stack frame). At crash time, $rip will contain the garbled return address.

Python socket API. The reproduction script uses raw TCP sockets:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 2121))   # establish TCP connection
banner = s.recv(256)              # read server banner (bytes)
s.sendall(b"USER " + b"A"*200 + b"\r\n")  # send bytes — note b"..." prefix
s.close()

Network data is always bytes, not str. Use b"..." literals or .encode(). The server expects \r\n (CRLF) line endings, not just \n.

pwndbg. pwndbg is installed by the course setup.sh. If it’s missing, follow the official install instructions. Key commands you’ll use in Part 3:

Command What it shows
context registers, disassembly, stack, backtrace in one view
info registers all register values
x/20gx $rsp 20 quad-words of stack memory in hex
cyclic 200 generate a 200-byte De Bruijn pattern (all substrings unique)
cyclic --find 0x6161616b find offset of a 4-byte pattern in the sequence
disassemble handle_client disassemble the vulnerable function

Install boofuzz

boofuzz works on any platform and requires only Python 3.

pip install boofuzz

Verify:

python3 -c "import boofuzz; print(boofuzz.__version__)"

Build the target server

Save the following as hw5/vuln_server.c. It implements a minimal FTP-like protocol with an intentional stack buffer overflow in the USER command handler.

/* vuln_server.c — deliberately vulnerable FTP-like server */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT    2121
#define BACKLOG 10

char buf[256];

static void handle_client(int sock)
{
    char username[64];   /* intentionally small */
    ssize_t n;

    dprintf(sock, "220 VulnFTP 1.0 ready\r\n");

    while ((n = recv(sock, buf, sizeof(buf) - 1, 0)) > 0) {
        buf[n] = '\0';
        char *end = strpbrk(buf, "\r\n");
        if (end) *end = '\0';

        if (strncasecmp(buf, "USER ", 5) == 0) {
            strcpy(username, buf + 5);        /* ← stack buffer overflow */
            dprintf(sock, "331 Password required for %s\r\n", username);
        } else if (strncasecmp(buf, "PASS ", 5) == 0) {
            if (strcmp(username, "admin") == 0 &&
                strcmp(buf + 5, "secret") == 0) {
                dprintf(sock, "230 Login successful\r\n");
            } else {
                dprintf(sock, "530 Login incorrect\r\n");
            }
        } else if (strncasecmp(buf, "HELP", 4) == 0) {
            dprintf(sock, "214-Commands supported:\r\n"
                          "214 USER PASS HELP QUIT\r\n");
        } else if (strncasecmp(buf, "QUIT", 4) == 0) {
            dprintf(sock, "221 Goodbye\r\n");
            break;
        } else {
            dprintf(sock, "502 Unknown command\r\n");
        }
    }
    close(sock);
}

int main(void)
{
    int srv, cli, opt = 1;
    struct sockaddr_in addr = {0};

    srv = socket(AF_INET, SOCK_STREAM, 0);
    setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port        = htons(PORT);
    bind(srv, (struct sockaddr *)&addr, sizeof(addr));
    listen(srv, BACKLOG);

    printf("VulnFTP listening on port %d\n", PORT);
    for (;;) {
        cli = accept(srv, NULL, NULL);
        if (cli >= 0) handle_client(cli);
    }
}

Compile with stack protection disabled so the overflow produces a real crash:

gcc -o vuln_server vuln_server.c -fno-stack-protector -no-pie -g

Verify the server manually

In one terminal, start the server:

./vuln_server

In another, connect with netcat and exercise each command:

nc 127.0.0.1 2121

Expected session:

220 VulnFTP 1.0 ready
USER admin
331 Password required for admin
PASS secret
230 Login successful
HELP
214-Commands supported:
214 USER PASS HELP QUIT
QUIT
221 Goodbye

Document the banner format, each command/response pair, and the exact line terminator (\r\n). You will need this to define the protocol in boofuzz.


Part 2: Write the Fuzzer

Save the following as hw5/fuzzer.py. Read through it and understand what each section does before running it.

#!/usr/bin/env python3
"""fuzzer.py — boofuzz script for VulnFTP"""

import subprocess
import time
from boofuzz import *

TARGET_HOST = "127.0.0.1"
TARGET_PORT = 2121
SERVER_BIN  = "./vuln_server"


def restart_server(target=None, fuzz_data_logger=None, session=None, *args, **kwargs):
    """Kill any existing instance and restart the target after a crash.

    boofuzz calls restart callbacks with keyword arguments
    (target, fuzz_data_logger, session, sock); we accept and ignore them.
    """
    subprocess.run(["pkill", "-f", SERVER_BIN], capture_output=True)
    time.sleep(0.2)
    subprocess.Popen([SERVER_BIN])
    time.sleep(0.5)


def main():
    session = Session(
        target=Target(
            connection=TCPSocketConnection(TARGET_HOST, TARGET_PORT),
        ),
        restart_callbacks=[restart_server],
        crash_threshold_request=3,
        crash_threshold_element=5,
        sleep_time=0.05,
    )

    # --- USER command ---
    # boofuzz will mutate the username field with hundreds of test cases.
    s_initialize("USER")
    s_static("USER ")
    s_string("anonymous", name="username", max_len=512)
    s_static("\r\n")

    # --- PASS command ---
    s_initialize("PASS")
    s_static("PASS ")
    s_string("password", name="password", max_len=512)
    s_static("\r\n")

    # Graph: fuzz USER alone, then fuzz PASS after a valid USER
    session.connect(s_get("USER"))
    session.connect(s_get("USER"), s_get("PASS"))

    session.fuzz()


if __name__ == "__main__":
    main()

Start the server in one terminal, then run the fuzzer:

# Terminal 1
./vuln_server

# Terminal 2
python3 fuzzer.py

The boofuzz web UI is available at http://localhost:26000 while the fuzzer runs. It shows each test case, the mutation applied, and whether the target responded.

boofuzz logs crashing test cases to a SQLite database (boofuzz-results/ by default). When the fuzzer detects the server stopped responding, it records the exact payload that caused it.


Part 3: Crash Analysis

Find the crashing input

After boofuzz detects a crash, it prints the test case index and logs the payload. You can also replay the last crashing input from the boofuzz results database:

# List recent sessions
ls boofuzz-results/

# The crashing payload is stored per-test-case in the database.
# boofuzz also prints the raw bytes on crash detection — copy that output.

Reproduce the crash manually

Use Python to send the exact crashing payload and confirm the server crashes:

import socket

payload = b"USER " + b"A" * 200 + b"\r\n"   # adjust length as needed

s = socket.socket()
s.connect(("127.0.0.1", 2121))
print(s.recv(256))
s.send(payload)
try:
    print(s.recv(256))
except:
    print("Server stopped responding — crash confirmed")
s.close()

Analyze the crash in pwndbg

Start the server under pwndbg:

pwndbg ./vuln_server
run

In a second terminal, send the crashing payload. pwndbg will catch the SIGSEGV:

Program received signal SIGSEGV, Segmentation fault.
...
pwndbg> context
pwndbg> info registers
pwndbg> x/20gx $rsp

Answer the following questions in your writeup:

  1. What is the value of $rip (or $eip on 32-bit) at the point of the crash?
  2. What value in $rsp or the stack frame does the overflow overwrite?
  3. How many bytes of padding are needed before you control the return address? (Use cyclic and cyclic --find to determine the exact offset.)
  4. Which function does the crash occur in? Show the disassembly around the fault using disassemble or nearpc.
  5. What line in vuln_server.c is the root cause? Explain why strcpy is dangerous here.

Part 4: Extend the Fuzzer

Complete one of the following extensions and include it in your writeup.

Option A: Fuzz a second command

Add fuzzing for the HELP command to your script. The HELP command takes no argument in the current implementation — but what happens if you send one anyway? Define a new s_initialize("HELP") block, connect it after the PASS node in the session graph, and run the fuzzer. Document what you observe.

Option B: Fuzz a real service

Choose one of the following real services, install it, and write a boofuzz script for it. You do not need to find a crash — document your methodology, what commands you defined, and what the fuzzer produced.

For any real service, start by capturing a valid session with tcpdump and manually tracing the protocol commands and responses before writing the fuzzer.


What to Turn In

All files in a hw5/ directory in your GitLab repo:

File Contents
hw5/vuln_server.c The provided vulnerable server (unmodified)
hw5/fuzzer.py Your boofuzz script
hw5/hw5.md Written answers to all questions below

Written questions

Your hw5/hw5.md must answer:

  1. Protocol documentation — describe each VulnFTP command, its expected argument, and the server’s response. Include the raw bytes of the banner.
  2. Fuzzer design — explain why you defined the protocol the way you did. What did s_string generate that a human-written test would not?
  3. Crash confirmation — show the exact bytes that crashed the server (copy from boofuzz output or your reproduction script).
  4. Crash analysis — answer the five pwndbg questions in Part 3 with output and explanation.
  5. Extension — describe your Option A or B work and what you found.
  6. Comparison — in 2–3 sentences, compare the boofuzz approach (grammar-based, black-box) to coverage-guided fuzzing (e.g., AFL). When would you choose one over the other?

Everything should be committed and pushed to your private GitLab repo with dmcgrath and gtn added as Developer or higher.