47. Format String Vulnerabilities: A Recurring CTF Bug
Format string vulnerabilities are old bugs, but they remain a staple of CTFs. Once you understand the mechanism, they can enable everything from memory disclosure to arbitrary writes.
What Is It?
In C, the printf family of functions takes a format string as its first argument.
// Safe code
printf("%s", user_input);
// Vulnerable code
printf(user_input);
The second call is the problem. If user_input contains format specifiers such as %x, %s, or %n, it can read from or write to the stack directly.
Reading the Stack
./vuln
Input: %x.%x.%x.%x
Output: ff9a2040.0.f7e2c000.1
Stack values spill out one after another. Using %p produces cleaner pointer-formatted output.
To select an argument directly, use the %N$x syntax:
%1$x → 1st stack value
%6$p → 6th stack pointer
This can leak a canary or a libc address.
Reading Memory at an Arbitrary Address
To read a value at a specific address:
- First find the position, or offset, of the format-string buffer on the stack
- Place the address you want to read at the front of the buffer and reference it with
%N$s
# Find the offset (pwntools)
from pwn import *
p = process('./vuln')
for i in range(1, 20):
p.sendline(f'AAAA%{i}$x'.encode())
out = p.recvline()
if b'41414141' in out:
print(f'offset: {i}')
break
p = process('./vuln')
The offset is the position where 41414141 appears.
Arbitrary Writes with %n
%n writes the number of bytes printed so far to the address referenced by its pointer. This is the key weapon.
%100c%N$n → Write 100 to the address referenced by the Nth argument
Writing all four bytes at once can require too much output. In practice, use %hn to write two bytes at a time or %hhn to write one byte at a time.
pwntools automates this with fmtstr_payload:
from pwn import *
# Write the address of win into got['puts']
payload = fmtstr_payload(offset, {elf.got['puts']: win_addr})
p.sendline(payload)
Overwrite a function pointer in the GOT with the address of win() to obtain a shell.
CTF Approach
- Confirm the vulnerability — Find a
printf(buf)pattern and inspect protections with checksec - Find the offset — Repeat with
AAAA%1$x,%2$x, and so on - Leak data — Extract the canary, libc base, stack address, or anything else required
- Write data — Overwrite the GOT or a return address
- Exploit — Obtain a shell or the flag
Defense
- Never use
printf(buf); useprintf("%s", buf)instead - Enable compiler warnings with
-Wformat -Wformat-security - Enable FORTIFY_SOURCE with
-D_FORTIFY_SOURCE=2
The combination of ASLR, PIE, and RELRO has made exploitation more complex in modern binaries, but a single leak can still break through. In a medium-difficulty CTF Pwn challenge, this combination appears more often than not.
Recommended Practice
- pwnable.kr — The
fdandpasscodechallenges - pwn.college — The Format String module
- picoCTF — Numerous format string challenges in the archive
Although this is an old vulnerability, it still appears in embedded devices and legacy code. It is well worth mastering as a fundamental technique.