C Format String Vulnerabilities and Memory Integrity

Format string vulnerabilities occur when untrusted user input is passed directly as the format argument to variadic C library functions such as printf(), sprintf(), or syslog(). This article examines why these flaws are catastrophic to memory integrity, detailing how the disconnect between expected and supplied arguments allows attackers to leak memory, overwrite arbitrary addresses using the %n token, bypass critical security mitigations, and gain full control over the execution flow of a C application.

The Mechanics of Variadic Functions in C

In the C programming language, functions that accept a variable number of arguments rely on the caller to push those arguments onto the stack or load them into specific registers, in accordance with the system's Application Binary Interface (ABI). The receiving function, such as printf(const char *format, ...), relies entirely on the format string to determine how many arguments were passed and what types they are.

When a developer writes printf(user_input) instead of printf("%s", user_input), the function interprets any format specifiers (such as %x, %s, or %n) within user_input as valid directives. Because the C runtime does not track the actual number of arguments passed at runtime, the function simply reads the next values off the stack or from parameter registers, whether those values were intentionally provided as arguments or not.

Arbitrary Memory Read and Information Leakage

The first breakdown in memory integrity comes from unauthorized reads. By providing sequences of %x or %p specifiers, an attacker forces the function to traverse the stack frame, outputting values stored in adjacent memory.

This behavior destroys the confidentiality of in-memory data. Attackers can expose:

  • Pointers: Leaking pointers to the stack, heap, and executable code segments allows an attacker to calculate base addresses and defeat Address Space Layout Randomization (ASLR).
  • Stack Canaries: Extracting stack guard values allows an attacker to later craft stack-based buffer overflows without triggering canary corruption defenses.
  • Sensitive Application Data: Cryptographic keys, passwords, and user data residing in nearby stack frames or accessible via leaked pointers (using the %s dereferencing specifier) can be read directly.

The Destructive Power of the %n Specifier

While memory disclosure is dangerous, the true catastrophe to memory integrity is arbitrary memory writing. The C standard defines the %n format specifier, which does not output text; instead, it writes the number of bytes printed so far into an integer pointer provided as an argument.

When an attacker controls the format string:

  1. Targeting the Destination Address: By using direct parameter access (e.g., %<index>$n) or padding the format string, the attacker can align the format string's parsing with an address they have placed in the input buffer itself.
  2. Controlling the Value: By manipulating field width specifiers (such as %1000u), the attacker controls the exact number of characters written, thereby dictating the precise integer value that %n will write to the target memory address.
  3. Writing Arbitrary Bytes: By chaining multiple writes using modifier lengths like %hn (write two bytes) or %hhn (write a single byte), an attacker can reliably write any 32-bit or 64-bit value to any writable memory location in the process address space.

System Takeover via Integrity Loss

Once an attacker can write arbitrary values to arbitrary addresses, memory integrity is fundamentally compromised. In a typical exploitation chain, the attacker targets pointers that control the flow of execution:

  • Global Offset Table (GOT): In dynamically linked binaries, functions resolve external library calls through the GOT. Overwriting a GOT entry for a common function like exit() or puts() with the address of a shellcode payload or a standard library function like system() redirects execution immediately upon the next call.
  • Saved Return Addresses: Overwriting the return address on the stack enables direct control of the instruction pointer, allowing the attacker to construct Return-Oriented Programming (ROP) chains.
  • Function Pointers and Hooks: Overwriting application-level function pointers, C++ virtual method tables (vtable), or glibc management hooks (like __free_hook or __malloc_hook) yields deterministic code execution with the permissions of the vulnerable process.

Prevention and Mitigation

Preventing format string vulnerabilities requires treating the format argument strictly as static code rather than dynamic data:

  • Strict Static Formatting: Never pass dynamic user buffers as the format argument. Always use explicit format specifiers: printf("%s", input).
  • Compiler Defenses: Modern compilers provide flags such as -Wformat and -Wformat-security to detect and reject non-literal format strings at compile time.
  • Runtime Hardening: Tools like -D_FORTIFY_SOURCE=2 inspect format strings at runtime, disabling dangerous behaviors like using %n on writable memory locations or referencing unpassed positional arguments.