A quick and easy guide to exploiting format strings. Assumed knowledge:
So if you have C code that looks like this:
C will print addr as an address. You probably know this. But what if the vulnerable code we’re exploiting is written like this,
We could try to supply maliclous code, such as the string “pwned %x”, which would cause the vulnerable program to execute the equivalent of:
In this example, the printf isn’t actually given any address to be printed. So instead of throwing an error like any reasonable language should do, it instead does its best to give you any address it can find in the memory it has access to. If we throw in our classic A’s with enough %x’s, such as this:
We’ll get C helpfully dumping some memory for us, e.g.:
See the 41414141 (and its lonely, un-aligned counterpart)? Whenever the print function is called, it stores what it’s meant to print (in this case, the hex encoding of “AAAAA”) as a temp value to be accessed later. What we’re seeing here is C showing us where the printf input we’re giving it is stored.
Moving on, lets say after doing some other recon, we know that there’s some critical data we need to change at the address 0xffffd24b. By running the following:
(the A at the start may or may not be necessary as an offset, to make sure the critical address is word aligned in memory - this will depend on the binary) We’ll get something like this output:
And we can see our target address there, on the stack. “But Kat, you dumbshit, how does that help us?” Well - if dumping memory was C’s only screw-up that’d be fine, but wait, there’s more.
%n is a special format string that writes the number of bytes you’ve given printf so far into memory. So if you supplied:
C would write the integer “3” into memory. Because there’s 3 A’s.
Where in memory? You can specify this with %D$n, where integer “D” is the argument C should be writing in to. In reality, developers somewhere would have found this feature useful if they were doing something like this:
printf("Hi, my name is %s %2$n", name, num_chars_written_so_far);
num_chars_written_so_far would come in as 0, and come out as 15 + name.length. Convenient! But also stupid, and here’s why.
The same way C tries to compensate when you give it %x without an argument, it’ll compensate when you give it %n without an argument. Let’s go back to where we dropped off:
Will give us:
Notice that our address of interest is the 7th “argument” C looking for.
Will write the integer “5” into the address stored in the 7th argument - our critical address.
Hooray! We might already have a segfault. From here, you can get creative with padding your input to write the value you need into your critical address.