You're working a CTF pwn challenge, or an interview take-home, whichever, and the naive move is instinct at this point: throw a long string at the input, watch for a crash, work out the offset to the return address. Nothing happens. No crash, no segfault, no "stack smashing detected." The binary just prints your string back, cheerfully, and waits for the next line of input. Which is confusing right up until you feed it %x %x %x %x instead, and get four hex values back that you never gave it. That's the moment the actual bug reveals itself: this was never a buffer overflow. It's a format string vulnerability, and it's an easier bug to introduce than almost anything else in C, which is exactly why it still shows up in real code and real CTF binaries years after everyone supposedly learned better.
The bug, in source, is usually one missing argument: printf(user_input); instead of printf("%s", user_input);. Both compile. Both run. The difference is invisible unless you know to look for it, and it's the difference between "print this string" and "interpret this string as a format specification I control."
Here's why that one missing "%s" is catastrophic. printf and its relatives don't know how many arguments you actually passed, they trust the format string to tell them. Every %x, %s, %d in the format string tells printf "consume one more argument off the stack (or the next register, depending on the calling convention) and print it as this type." If the attacker controls the format string and there are no real arguments backing it up, printf keeps consuming anyway, it has no way to know the arguments ran out. Each %x you feed it prints whatever value happens to be sitting in the next stack slot or register, which is a direct, unauthenticated read of the program's own memory: saved registers, stack canaries, pointers into the binary or its libraries, exactly what you'd want to defeat ASLR before attempting any further exploitation.
Reading memory is only half of what a format string bug can do. The %n specifier doesn't print anything, it writes the number of bytes printf has output so far to the pointer argument in that position, which means a format string bug with %n in reach isn't just an information leak, it's an arbitrary write primitive. Combined with the read primitive above, that's often enough to overwrite a return address or a GOT entry without ever touching a single byte past the end of a buffer. Modern toolchains have made this specific escalation harder than it used to be: -Wformat-security flags the naive printf(user_input) pattern at compile time, and glibc's FORTIFY_SOURCE disables %n from a non-read-only format string by default. But "harder to hit accidentally" is not "gone," and a deliberately vulnerable CTF binary, or a real codebase compiled without those flags, still has the full primitive available.
The instinct that actually generalizes here, more than memorizing this one bug class, is: when the obvious attack (overflow, crash, controlled return address) doesn't work, that's data, not a dead end. It means you're looking at the wrong bug, not that there isn't one. That habit, systematically ruling out what a binary isn't vulnerable to on the way to what it is, is most of what separates "I know what a buffer overflow is" from actually working exploit development.
Codelivly's Exploit Development Book PDF: Memory Corruption Guide covers this bug class alongside stack and heap corruption, building the same systematic instinct for recognizing what you're actually looking at before you try to exploit it.
Top comments (0)