Overwatch was a 909 point exploitation challenge in BCTF 2017 that was released a little after the halfway point in the competition. It was one of the more creative heap pwning challenges I’ve seen in a CTF, and I really enjoyed solving it. It required the use of multiple heap exploitation techniques and was solved by only 3 teams. Unfortunately, we did not look at this challenge until late in the game and were unable to finish our exploit in time.
It was only after the CTF ended that I managed to solve this with the help of brieflyx who gave me some additional guidance on how to get the leak and re-affirmed some of the ideas I had come up with for my exploit plan.
Although I didn’t get the flag until after the CTF finished, I thought it’d still be worth it to do a write-up for educational purposes, because the way to get the leak is actually pretty neat and demonstrates several heap exploitation techniques that I’ve been meaning to write about for a while. So without further ado, let’s get started!
The binary is a menu-driven program that allows users to perform multiple actions for 15 rounds.
Each time a user begins a new round, 4 bytes from /dev/urandom are generated and placed in a global variable in the .BSS named randBytes. These 4 pseudorandom bytes are subsequently used to determine which map and hero gets printed out whenever the user calls option 1 or option 2 from the main menu.
randBytes is also used to determine the levels for teammate I and II, that are printed out when the user selects option 5.
Option 1 uses the high 2 bytes of randBytes to determine the map and hero selected for teammate I, and Option 2 uses the lower 2 bytes of randBytes to determine the map and hero for teammate II.
Remember this, because we will use these two options to help us get some infoleaks later.
Additionally, we can select options 3 and 4 to call updatePtr() and change our name and email, respectively.
The pseudo-code for updatePtr() is as follows:
Basically, our name and email are stored in heap chunks that are allocated via realloc(), and a pointer to each heap chunk is stored in the .BSS:
We can only change our name and email once per round and this check is done by updating the changeEmail_status and changeName_status globals.
Going back to the updatePtr() function, though, can you spot the vuln?
There is a subtle single NULL byte off-by-one vulnerability that is introduced by the memcpy().
This means that if we size a heap chunk appropriately, a NULL byte can potentially overflow into the LSB of the size metadata field of an adjacent heap chunk.
Although a single NULL byte doesn’t seem like much, this error actually opens up many ptmalloc2 heap attacks, some of which we will use to exploit this program and gain code execution.
Our first order of business to exploit this program is to bypass ASLR by getting some sort of infoleak.
Getting a leak in this program is tricky though, because while we can allocate heap chunks for our name and email, we can’t actually print out the contents of these chunks.
To make matters worse, there aren’t any additional vulnerabilities besides the off-by-one that could be abused to get an easy heap or libc leak (that I could see, anyway).
So without the ability to print out anything useful, how can we get a leak?
This is where things get interesting.
Remember the randBytes global we mentioned earlier?
Although randBytes isn’t directly printed out, it is used to determine the levels, maps, and heroes of each teammate that ARE.
If we can overwrite the data in randBytes with part of a libc address, we can use dynamic constraint solving based on the levels, maps, and heroes of each teammate that are printed out to determine the bytes we’ve overwritten randBytes with.
One heap exploitation technique that would allow us to do this, is the unsorted bin attack technique.
In an unsorted bin attack, we overwrite the BK pointer of a chunk in the unsorted bin with a controlled value, so that when this chunk is removed from the unsorted bin, it will write &main_arena.top to BK+0x10.
For our exploit we can use this attack to overwrite the BK pointer with an address in the .BSS so that part of &main_arena.top overwrites randBytes.
In order to be able to overwrite the BK pointer of a chunk in the unsorted bin though, we first need to produce 2 overlapping chunks. This will allow us to corrupt the contents of a free chunk without needing an overflow or a use-after-free (UAF) vulnerability!
To achieve this overlap, we can perform another heap exploitation technique called House of Einherjar.
House of Einherjar attack
House of Einherjar is a really cool heap exploitation technique that my teammate uafio taught me.
House of Einherjar works by exploiting a single NULL byte overflow to corrupt the size field of an allocated chunk and flip its prev_inuse bit off to trick it into thinking its previous chunk is free. Then, when this chunk with the corrupted size is free()‘d, because its prev_inuse bit is now off, it will attempt to perform a backwards consolidation with a fake chunk, using its prev_size field to calculate the location of this fake free chunk.
The following diagram demonstrates this technique. (Blue = free, Green = allocated)
In our exploit, we don’t have a heap leak, so our merge target is a real chunk, which saves us the extra steps of having to craft a fake chunk.
In GDB, this is what the heap looks like right before we trigger the NULL byte overflow:
After inserting the poisoned NUL byte and crafting the fake prev_size, the heap looks like this:
And finally, after free-ing C:
Notice that C has been backwards consolidated into the old A chunk and that it is now the new top chunk, even though B is still allocated after it!
This means that we can now allocate another chunk that overlaps with B to overwrite any data in B’s chunk!
For a more detailed explanation on how House of Einherjar works, feel free to check out uafio’s stream about the technique here and here.
Unsorted bin attack
Now that we have overlapping chunks, we will want to free B and place it in the unsorted bin. However, there are a few problems in the heap now that we need to fix in order to successfully perform this step.
The prev_inuse bit for both B @ 0x603090 and the old C chunk are now both flipped off. This is problematic, because this means it will attempt to perform additional chunk merging, which we don’t want.
So, we will fix this before free-ing B.
Now that we’ve restored these bits, we can successfully free B into the unsorted bin.
Next we will want to use our overlapping chunks to overwrite the BK pointer of B so that it points to target_address-0x10.
Since we already know the MSB of libc will always be 0x7f, we can skip leaking it, and instead leak the next 4 bytes of the libc address by overwriting BK with &randBytes-0xd.
After this is done, we can simply realloc the unsorted bin chunk to trigger the overwrite.
Libc leak via constraint solving
Now that we’ve written a partial libc address into randBytes, we can leak it using some Z3 magic.
To do this, I added some Z3 constraints based on the outputted map, hero and level of each teammate in order to dynamically find which bytes would satisfy those constraints and produce the same output.
I then solved these constraints to discover the bytes written to randBytes, which I subsequently used to calculate a final libc base address.
Now that we have the leak, the rest of the exploit is pretty straight forward.
In order to hijack the control flow of this program, we will want to overwrite a function pointer that gets called.
We will overwrite __realloc_hook, since full RELRO is enabled, preventing us from overwriting a GOT entry.
To perform this overwrite, we can use a fastbin attack to overwrite the FD pointer of a free()‘d fast chunk in order to get malloc() to return an almost-arbitrary address of our choosing.
As I’ve mentioned in previous posts, we can abuse the fact that we can point FD to misaligned addresses to get around the size check that malloc() does.
This looks like a good target:
After overwriting the FD pointer, to get malloc() to return our desired address, we need to allocate two0x68 sized chunks.
However, to do this in the program, we will need to free another chunk after allocating the first one, since after allocating the first 0x68 sized chunk, both our namePtr and emailPtr globals will be populated and so we will be unable to allocate another chunk until one of these 2 pointers is free()‘d.
The state of our unsorted bin right now is so messed up though that if we free the namePtr chunk again in its current state, it will fail a check and trigger a “corrupted unsorted chunks” error.
If we look at the current state of our heap, we can see why we won’t pass this check.
fwd is currently 0x603090 and it will compare fwd+0x18, or 0x4949494949494949 to 0x00007ffff7dd1b78, and if they are not equal, it will trigger a corrupted unsorted chunk error.
Therefore, to pass this check, we will write the following payload to 0x6030a0.
Now that we can successfully free the namePtr chunk, we can allocate another fast chunk and control the data in it to overwrite __realloc_hook with our “magic” one gadget RCE addr.
Finally, we call realloc() again to get a shell.
Putting everything together, we can get the flag using the following exploit.
Thanks again to brieflyx for his help and to vakzz for lending a second pair of eyes to look at this task.
Also to uafio and grazfather for taking away my free time by making me do pwnable.tw challenges.