Skip to content
racecar

racecar

May 2, 2022

racecar cover: leaking the flag off the stack with printf()

Hello hackers, time for another easy CTF. The HackTheBox racecar challenge is a small 32-bit binary with a classic format string vulnerability. No shellcode, no ROP, just a chatty printf() that tells us more than it should.

⚡
TL;DR: The winner’s message goes straight into printf() as the format string. The flag was already read onto the stack, so %08x dumps it as hex, and a little-endian decode gives it back as text.

How the flag leaks: fopen flag.txt, read user input, printf(fmt), %08x leaks stack words, decode hex to ascii

Recon

First, what kind of file are we dealing with?

% 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

32-bit, PIE and not stripped, so the function names are still there. That makes static analysis a lot friendlier.

Analysis in Ghidra

I loaded it into Ghidra and went through the function list.

Racecar functions
Racecar functions

There are a couple of interesting names. After a bit of poking around, car_menu turned out to be the one that matters.

Racecar car_menu
Racecar car_menu

Here is the relevant part. When you win the race, the program opens flag.txt, reads its contents into memory, then asks you for a victory message and prints it:

    __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);
🎯
Two things line up perfectly: fgets(local_3c, ...) puts the flag into a local stack variable, and printf(__format) uses our input as the format string. Every %x we send makes printf() read the next 4-byte word off the stack, flag included.

Exploit

Straight to the point: win the race, then send a message full of %08x to print the stack.

Racecar final
Racecar final

The output is a wall of hex words. Somewhere in the middle sits the flag, encoded as hex.

🔁
x86 is little endian. Each 4-byte word comes out with its bytes reversed: for example, the word 0x7b425448 is the string HTB{. Flip the bytes in every word before converting hex to ASCII.

Convert the hex values to ASCII, fix the byte order, and the flag is ours. Time to grab a 🍺 and enjoy it. Cheers!

Wrap-up

  • Never pass user input as the first argument to printf(). printf("%s", buf) would have fixed this whole challenge.
  • Secrets loaded onto the stack are only one format string away from being leaked.
  • When leaking 32-bit words, always remember little-endian byte order before decoding.
Last updated on