pwntools features most tutorials skip
If your pwntools knowledge starts and ends at recvuntil and sendline, you're using maybe ten percent of the library. The other ninety is where the time savings live.
Open any CTF writeup that uses pwntools and you'll see the same five functions: process, recvuntil, sendline, recvline, interactive. That's a competent enough subset to solve most challenges, but the library is much larger than that subset suggests. The features below aren't obscure (they're documented and stable), and skipping them means writing code that pwntools would have written for you.
pwn template scaffolds the boilerplate
Every exploit script begins with the same fifteen lines: imports, context setup, an ELF object, a process or remote, a wrapper around gdb.attach. You can stop typing them.
pwn template ./vuln > exploit.pyThe output is a complete script with the binary already loaded, the libc autodetected, a start() helper that switches between local process and remote depending on args.REMOTE, and a gdb.attach block gated on args.GDB. Edit it, run it.
Run it locally:
python exploit.pyAttach GDB on launch:
python exploit.py GDBAim at a remote:
python exploit.py REMOTE HOST=ctf.example.com PORT=31337Those args flags are pwntools magic — anything in capitals on the command line shows up in pwn.args. You'll find yourself adding more of them.
context controls what the library guesses
context is the global configuration object that everything else consults. Setting it once at the top of your script saves repetitive arguments downstream.
context.binary = './vuln' # sets arch, bits, endianness, log_levelcontext.terminal = ['tmux', 'splitw', '-h'] # where gdb.attach openscontext.log_level = 'debug' # show every byte sent and receiveddebug is the one most people miss. With it on, every send and recv is hex-dumped to your terminal in colour. When your exploit fails because the server returned b' \n' instead of b'\n ', you find out immediately. When you've got it working, dial back to 'info' or 'warning'.
context.binary is a quiet workhorse. Setting it autodetects architecture (so you don't have to pass arch='amd64' to every shellcode generator), it autodetects endianness (so p64 knows what to do), and it makes ELF and ROP objects implicitly aware of which binary they're operating on.
cyclic finds your offset in one command
The classic offset-discovery dance (generate a de Bruijn sequence, send it, look at the crashed RIP, search for the four bytes) is one command in pwntools.
pwn cyclic 200Send those bytes to a vulnerable program, observe the crash. Whatever value ended up in the instruction pointer (0x6161616c, 0x61616173, whatever), feed it back:
pwn cyclic -l 0x6161616c84Done. Your offset is 84. Inside a Python script:
from pwn import *payload = cyclic(200)# crash, recover the patternoffset = cyclic_find(0x6161616c)Works for 32-bit too with -n 4, and works for arbitrary alphabets if you need them.
ELF gives you symbols, GOT, and PLT for free
You don't have to copy addresses out of objdump.
elf = ELF('./vuln')
elf.symbols['main'] # 0x401200elf.got['puts'] # 0x404018elf.plt['system'] # 0x401050elf.search(b'/bin/sh') # iterator over occurrencesAfter a leak, you can rebase the whole thing:
libc = ELF('./libc.so.6')libc.address = leaked_puts - libc.symbols['puts']
system = libc.symbols['system']binsh = next(libc.search(b'/bin/sh'))Now system and binsh are real runtime addresses. No more "PIE base offset 0x1234 + libc symbol offset 0xABCDE = ?" arithmetic in a comment. The ELF object also gives you elf.libc if pwntools can find a matching libc on disk, which it usually can.
ROP builds chains without manual packing
Hand-packing p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system) is tedious and easy to get wrong (forgetting the ret for stack alignment, in particular). ROP does it for you.
rop = ROP(elf)rop.call('puts', [elf.got['puts']])rop.call('main')payload = b'A' * offset + rop.chain()rop.chain() returns the packed bytes. rop.dump() prints a human-readable view of the chain that's invaluable for debugging. The constructor finds gadgets automatically using the binary's symbols and a built-in gadget search; you don't run ROPgadget separately unless you want something exotic.
For a libc-based chain after a leak:
rop = ROP(libc)rop.call('system', [next(libc.search(b'/bin/sh'))])Stack alignment for movaps (the perennial Ubuntu glibc gotcha) gets handled automatically when you call functions through ROP.call.
gdb.attach with a script makes triage tractable
Reaching for GDB during exploit development normally means alt-tabbing, attaching by PID, setting breakpoints. Skip all of it.
io = process('./vuln')gdb.attach(io, gdbscript=''' b *main+42 b *win continue''')When you run with GDB on the command line (assuming you used pwn template), this fires automatically. Without it, the script runs without attaching. Combine with context.terminal to control which terminal pane the GDB window opens in.
gdb.debug is the cousin: it spawns the process under GDB rather than attaching, so breakpoints in early initialisation actually fire.
io = gdb.debug('./vuln', gdbscript='b *_start\ncontinue')checksec from the command line, not the website
You don't need to upload your binary anywhere.
pwn checksec ./vulnOutputs canary, NX, PIE, RELRO, and a few related properties. Same information as the website you were about to use, faster, and works on binaries you can't send anywhere.
Inside a script: print(elf.checksec()) once you've made the ELF, but the CLI version is what you'll use most.
A handful of others that are worth knowing exist
asm() and disasm() for inline assembly that respects context.arch. shellcraft.sh() for "give me a shell shellcode that works on this architecture, please". flat() for laying out structured payloads with named fields instead of byte arithmetic. tube.recvregex() for when the server's framing is irregular. args.LOCAL_LIBC for swapping the loaded libc per-run. And pwn libcdb for matching a leaked libc against a local database of known versions.
There's a lot of pwntools. Five hours spent reading the docs return more than five days spent grinding through challenges with recvline and sendline.
Where to take this
The Vulnerability Research Foundations track on the modules page sequences exploit development from "what is the stack" through ROP, heap, format string bugs, and the modern bypass landscape. pwntools is the toolkit you'll use throughout. Worth investing the time to use it well.