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
%sdereferencing 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:
- 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. - 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%nwill write to the target memory address. - 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()orputs()with the address of a shellcode payload or a standard library function likesystem()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_hookor__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
-Wformatand-Wformat-securityto detect and reject non-literal format strings at compile time. - Runtime Hardening: Tools like
-D_FORTIFY_SOURCE=2inspect format strings at runtime, disabling dangerous behaviors like using%non writable memory locations or referencing unpassed positional arguments.