Format string bugs

Format string bugs

TL;DR: When printf-family functions consume an attacker-controlled format string, %s/%p give arbitrary read of stack memory and %n gives arbitrary write — a clean primitive for ASLR leak + GOT overwrite chains.

What it is

printf(user_input) instead of printf("%s", user_input) lets the format parser walk the variadic argument area (registers, then stack) as if the attacker supplied the arguments. %x/%p/%s leak whatever sits in those slots; %n writes the number of characters printed so far to a pointer pulled from the same argument slot. Combine them and you get arbitrary read and arbitrary 1/2/4-byte write at chosen addresses.

Preconditions / where it applies

  • A *printf, syslog, err, warn, vfprintf, or similar function called with attacker-controlled format.
  • Position-relative or absolute target address reachable from the stack as a pulled argument (with %N$ direct-index syntax this is most stack slots).
  • Binary lacks -Wformat=2 -Wformat-security build hardening or runtime FORTIFY checking; classic Linux ELF on glibc.

Technique

  1. Find the offset. Send AAAA|%p|%p|%p|... and count which slot prints 0x41414141 — that is your %N$ index.
  2. Leak. Pull saved instruction pointers, libc pointers, canary bytes:
    1
    2
    
    %7$p          # leak slot 7
    %6$s          # deref slot 6 as C string (arbitrary read)
    
  3. Arbitrary write with %n. Place target address on the stack as part of the format string, then use width specifiers to control the printed count:
    1
    
    <addr><addr+2>%<low>x%K$hn%<high>x%L$hn
    

    Two %hn writes are easier than one %n because the width counter would otherwise need to reach a billion-plus.

  4. Common targets: GOT entry of printf itself → next call jumps to controlled address; __free_hook / __malloc_hook on older glibc; return address on stack; exit_funcs pointers.
  5. pwntools helper: fmtstr_payload(offset, {addr: value}) builds the payload, including endian-correct addresses and width math.

Detection and defence

  • Compile with -Wformat -Wformat-security -Werror=format-security; modern glibc + _FORTIFY_SOURCE>=2 rejects %n to writable memory at runtime.
  • Static analysis (Coverity, CodeQL cpp/tainted-format-string) flags non-literal format arguments cheaply.
  • Logging libraries should mandate parameterised format strings; ban printf(buf) patterns in code review.
  • Crashes inside vfprintf with weird counts in the format-state struct are a tell at triage time.

References

See also: ret2libc, srop, stack-buffer-overflow.