To understand what is going on, the important point is that in C, functions do not "know" how many arguments the caller gave them. For normal functions, the compiler detects invalid usages. For instance, if you write this:
/* ... */
then the compiler will complain loudly, and refuse to finish compilation: the included header
<string.h> declares the
memcpy() function as taking three parameters (two pointers, and an integer of type
size_t). If the compiler then sees a call to
memcpy() with only two parameters, the mismatch will make him scream.
However, some functions take a variable number of parameters, e.g.
printf(). For these functions, the compiler cannot do much checking.
printf() uses as first argument a "format string" whose contents tell
printf() how many other arguments it should find, and their expected type. When the format string is a literal constant, smart compilers can do some extra checks (with
gcc, this is done with the "format" attribute, a gcc-specific extension). But it cannot do anything when the format string is obtained at runtime. Imagine, for instance, the following code:
s is a string which the attacker can choose (e.g. some data sent over the network, or an invocation parameter). The programmer just assumed that the string was "plain" (only alphanumeric characters) but the attacker can put "
%" signs in it, which
printf() interprets by looking at extra parameters. There are no extra parameters, so
printf() will actually look at the memory slots where extra parameters would have been, had there been any. These slots are "lower in the stack" (technically, at higher addresses, since stacks "grow down" in most architectures); local variables, function return address, and other stack elements for the current function (and its caller, and its caller's caller, and so on) are found there.
%u specifiers, the attacker can read all these slots (
printf() will dutifully print out their contents), and that's already bad. With the
%n parameter, the powers of the attacker are even enhanced:
%n takes the next parameter, interprets it as a pointer value, follows it, and writes at the pointed-to address the current number of characters so far (the number of characters printed out by
printf() up to the
%n). If the faulty
printf() call occurs in a function where there is a local variable which is also under the control of the attacker (e.g. a simple integer value), then the attacker can make a
%n to use that local variable as a pointer, and write data where it points to. In shorter words, this gives the attacker the ability to write whatever bytes he wants, wherever he wants in RAM. At that point, you are pretty much doomed.
The correct code would have been:
which then forced
printf() to print the attacker-controlled string "as is", without doing anything special with the "
printf() vulnerability highlights the fact that C is a rather dangerous language: no bounds checks, no strong type checks... you can make the program look at memory slots as if they were parameters, while they are not, and also interpret byte patterns as if they were pointers, which they are not. Competent C programmers will write their code so as to give the compiler as most information as possible, so as to detect errors (security holes are just programming errors with an effect that can be twisted to the attacker's advantage). But competence is a scarce resource, and even the best programmers occasionally do mistakes.