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.
Directly after this, a chunk for the parts of our car will be placed
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.
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:
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
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.
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
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.
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.
Next, we just have to overwrite the ExitHook pointer in our custom heap metadata pointing to this chunk.
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")