Skip to main content

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:

  1. First find the position, or offset, of the format-string buffer on the stack
  2. 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

  1. Confirm the vulnerability — Find a printf(buf) pattern and inspect protections with checksec
  2. Find the offset — Repeat with AAAA%1$x, %2$x, and so on
  3. Leak data — Extract the canary, libc base, stack address, or anything else required
  4. Write data — Overwrite the GOT or a return address
  5. Exploit — Obtain a shell or the flag

Defense

  • Never use printf(buf); use printf("%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.

  • pwnable.kr — The fd and passcode challenges
  • 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.