HW5: Network Protocol Fuzzing with boofuzz
Due Date: 2026-06-07 23:59:59
- HW5: Network Protocol Fuzzing with boofuzz
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:
- Defining the protocol message structure in Python
- Systematically mutating each fuzzable field with known-bad values (very long strings, format specifiers, null bytes, boundary integers, etc.)
- Detecting crashes by monitoring whether the target stops responding
- 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:
- What is the value of
$rip(or$eipon 32-bit) at the point of the crash? - What value in
$rspor the stack frame does the overflow overwrite? - How many bytes of padding are needed before you control the return address? (Use
cyclicandcyclic --findto determine the exact offset.) - Which function does the crash occur in? Show the disassembly around the fault using
disassembleornearpc. - What line in
vuln_server.cis the root cause? Explain whystrcpyis 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.
- vsftpd (
sudo apt install vsftpd) — standard FTP - OpenSMTPd (
sudo apt install opensmtpd) — SMTP - nginx (
sudo apt install nginx) — HTTP
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:
- Protocol documentation — describe each VulnFTP command, its expected argument, and the server’s response. Include the raw bytes of the banner.
- Fuzzer design — explain why you defined the protocol the way you did. What did
s_stringgenerate that a human-written test would not? - Crash confirmation — show the exact bytes that crashed the server (copy from boofuzz output or your reproduction script).
- Crash analysis — answer the five pwndbg questions in Part 3 with output and explanation.
- Extension — describe your Option A or B work and what you found.
- 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.