Skip to main content
Be Bitwise

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.

#exploit-development#tooling#setup#pwntools

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.

bash
# Compiler and debugger
sudo apt update
sudo apt install -y build-essential gdb python3 python3-pip git
# pwndbg (or gef — pick one)
git clone https://github.com/pwndbg/pwndbg ~/pwndbg
cd ~/pwndbg && ./setup.sh
# pwntools
python3 -m pip install --user pwntools
# checksec and ropper
sudo apt install -y checksec
python3 -m pip install --user ropper

That'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:

bash
$ checksec --file=./target
RELRO 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:

bash
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:

bash
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:

bash
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:

c
// 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:

bash
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.

bash
$ checksec --file=./vuln
RELRO 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.