Building a minimal exploit-dev environment
A no-fluff setup for binary exploitation: Ubuntu in a VM, gcc, gdb with pwndbg, pwntools, checksec, ropper. The shortest path to writing your first exploit.
Setting up an exploit-dev box has a low ceiling and a frustrating floor. The ceiling is low because you only need a handful of tools. The floor is frustrating because the tools were each written by different people with different opinions about how to instrument a process, and gluing them together has cost more beginners than the actual concepts ever did.
This post is the version I'd hand to someone starting fresh today. Ubuntu LTS, in a VM, with five pieces of software and one kernel-flag flip. Skip the rest until something forces you to add it.
Why a VM, why Ubuntu
Run binary exploitation work in a VM. Not a container, not WSL, a proper VM with its own kernel.
Two reasons. First, exploitation work involves running untrusted binaries (challenge files, third-party samples, your own buggy POCs) and you'd rather not have a stray heap groom corrupt anything that matters. Second, you'll want to disable kernel mitigations and tweak /proc settings that you absolutely should not be turning off on your daily-driver machine. ASLR off, ptrace_scope wide open, core dumps everywhere. None of that is sensible on a host you also use for browsing.
Use Ubuntu LTS specifically — currently 24.04. The reason is mundane: the overwhelming majority of CTF challenges and exploit-dev tutorials assume an Ubuntu glibc and an Ubuntu libc symbol layout. If a writeup says "the tcache offset is X" and you're on Arch with a different glibc patchset, you'll burn an hour figuring out why the offsets don't match. Match the substrate everyone else uses and you save that hour every time.
VMware, VirtualBox, UTM (on Apple Silicon), or QEMU directly all work. Give it 4 GB of RAM, 40 GB of disk, two cores. That's enough for everything short of large-scale fuzzing.
The toolchain
Five things, plus a debugger plugin. Install them in this order.
# Compiler and debuggersudo apt updatesudo apt install -y build-essential gdb python3 python3-pip git
# pwndbg (or gef — pick one)git clone https://github.com/pwndbg/pwndbg ~/pwndbgcd ~/pwndbg && ./setup.sh
# pwntoolspython3 -m pip install --user pwntools
# checksec and roppersudo apt install -y checksecpython3 -m pip install --user ropperThat's the whole environment. Each piece does one thing.
gcc and make build your sample targets. You'll compile a lot of small C programs while learning, and having an actual local compiler is faster than rebuilding container images.
gdb is the debugger. Bare gdb is functional but spartan. Two community plugins make it pleasant: pwndbg and gef. Both render registers and stack on every break, decode common heap structures, and add commands like vmmap, checksec, and heap chunks that you'll use constantly. Pick one. Don't install both — they conflict because both want to hook the same gdb events. I lean toward pwndbg today because of its heap analysis commands, but gef is fine.
pwntools is the Python library every exploit you'll ever write is built on top of. It handles the irritating parts: packing integers into the right endian as bytes, talking to a process or remote socket with the same API, building ROP chains symbolically, and generating shellcode for half a dozen architectures. Once you're past hello-world exploits, every line of code you write will be from pwn import * and a few dozen lines below it.
checksec reads an ELF and tells you which mitigations are on. NX, PIE, RELRO, stack canaries. It's a one-line command that should be the first thing you run on any binary you're about to attack:
$ checksec --file=./targetRELRO STACK CANARY NX PIE RPATH ...Full RELRO Canary found NX enabled No PIE No RPATH ...ropper (or ROPgadget — same idea, different author) extracts ROP gadgets from a binary. You'll feed it a libc and ask for the gadgets you need to chain a return-oriented exploit. Both tools are interchangeable; install whichever your tutorials assume.
Disabling ASLR — for learning, only
Address Space Layout Randomisation puts the stack, heap, and shared libraries at different addresses every run. That makes life harder for an attacker, which is the whole point. It also makes life harder for someone trying to learn what an exploit looks like on a fixed target.
For learning, switch ASLR off so addresses are stable and your objdump output matches what you see in gdb:
sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"The values are 0 (off), 1 (stack and libraries randomised, heap not), and 2 (everything randomised — the default). When you've worked through a few static exploits and want to learn ASLR-bypass techniques, flip it back:
sudo bash -c "echo 2 > /proc/sys/kernel/randomize_va_space"To make either change persistent across reboots, add kernel.randomize_va_space = 0 to /etc/sysctl.d/99-pwn.conf. Just remember to undo it before you take the VM anywhere near real software.
While you're in /proc, the other knob worth knowing is kernel.yama.ptrace_scope, which controls who can attach a debugger to whom. Set it to 0 to attach to anything you can read:
sudo bash -c "echo 0 > /proc/sys/kernel/yama/ptrace_scope"Without that, you'll occasionally hit "ptrace: Operation not permitted" when attaching gdb to a process you spawned in another terminal, and waste five minutes wondering why.
A sample target
The smallest target worth playing with looks like this:
// vuln.c#include <stdio.h>#include <string.h>#include <unistd.h>
void win(void) { execl("/bin/sh", "sh", NULL);}
void vulnerable(void) { char buf[64]; printf("Input: "); gets(buf); printf("You said: %s\n", buf);}
int main(void) { vulnerable(); return 0;}Compile it deliberately broken so you can see the bug:
gcc -fno-stack-protector -no-pie -z execstack -o vuln vuln.c-fno-stack-protector removes canaries. -no-pie keeps the binary at a fixed address. -z execstack makes the stack executable so a shellcode tutorial actually works. With ASLR off and these flags, your first stack-overflow exploit becomes a half-page Python script.
$ checksec --file=./vulnRELRO STACK CANARY NX PIE ...Partial RELRO No canary found NX disabled No PIE ...Notice every defence is off. That's deliberate, and it's the right state for the very first exploit you write. Subsequent targets re-enable mitigations one at a time so you learn each technique against the constraint it was invented for.
How the pieces fit
That diagram is the whole rhythm of binary exploitation work. Read the binary, observe the bug, gather the materials, build the script, send the bytes. Every exploit you write follows the same loop. The tools above are the minimum you need to walk it from end to end.
What I deliberately left out
Skip IDA, Binary Ninja, and Ghidra for the first month. Binaries this small yield to objdump -d -M intel and gdb's own disassembly. Decompilers earn their keep on bigger targets, and they teach you nothing until you've read enough assembly to know when the decompiler is lying.
Containers, asciinema, and your own bespoke distro are publishing tools, not learning tools. Save them for when you have something to show.
Fuzzing waits too. It's a discipline of its own; bolting it on before you can read a stack trace just leaves you with crashes you can't triage.
Where this fits in
The early modules of the VR Foundations track walk you through the underlying machinery: registers, stack frames, calling conventions, and the layout of an ELF. With the environment above and a couple of those modules under your belt, you'll be writing your first ret2win in an evening and your first ROP chain by the weekend.