Since we were playing hitcon this weekend, I missed this ctf, but was told, that they had an unsolved kernel challenge and decided to take a look at it afterwards.
Linux (none) 5.14.16 #2 SMP Mon Nov 22 19:24:06 UTC 2021 x86_64 GNU/Linux
Pretty new kernel with heap mitigations on, so we have to handle randomized free lists and freelist pointer hardening.
The challenge itself introduced a new syscall IPS (548)
So, we can allocate, free, edit and copy chunks (though the copy will only copy a reference).
By just taking a quick glance, I first jumped on the assumption, that you could just copy a chunk, free the original one and would directly have an UAF, but they also cleared the copied references in the remove_storage function.
So, it was a bit more subtle, how to get to an UAF chunk. Though copying a chunk wasn’t helpful in this direction, it had another bug, which would help us out here.
copy_storage will first check, if we passed it an index in the valid range (check_idx will return -1 otherwise and bail out). get_idx will then try to find the next free storage and return its index. But, if it doesn’t find a free chunk, it will also return -1 and that one isn’t checked in copy_storage. So, if the chunk table is full, it will copy the chunk reference to chunks[-1], which isn’t handled in remove_storage.
Still, having a chunk outside of our table wouldn’t be all that helpful, if there wasn’t another bug in edit_storage
edit_storage will also call check_idx to see, if we passed a valid index, but it doesn’t return, if check_idx returns -1. So by passing an invalid index, idx will be set to -1, and if we have an address at chunks[-1], it will copy our input into it.
Something to work with… But since we have to deal with randomized free list, we won’t know, in which order the chunks will be allocated in the storage. We also need to get around hardened freelist pointers, which will be xor’ed with a secret value, so even knowing some random addresses won’t help us much.
We need to exactly know, at which address, which chunk is allocated, so we need some proper leaks first.
For this, I completely filled up the storage with some marker values to be able to recognize them later in leak output.
Then, I used the bug in copy_storage to copy a chunk address to chunk[-1] and free it.
By this, we now have effectively a freed chunk in chunk[-1]. Despite randomization, we will get the last freed chunk on the next allocation, so I created a msg_msg object, which will be stored in the freed chunk, which chunk[-1] points to.
With the bug from edit_storage, we can now edit chunk[-1] and overwrite the data from msg_msg.
We just have to take care that chunk->data starts at offset 14, so we will overwrite the upper 2 bytes from msg_msg->m_list, but they will most likely just be \xff\xff.
The msg_msg struct now has a type and size of 0x4141414141414141, so we can now leak the data behind it (which hopefully contains some of our storage chunks).
We can retrieve quite some info from this leak, but the order of chunks will always be randomized (and it can also happen, that we don’t find enough information in the leak, since the chunks were allocated in a “bad” order.).
Though, even with knowing the offsets in the payload, we won’t know where the chunks are exactly located, but combining that knowledge with the next pointer of every chunk, we can recalculate, where some of the chunks are stored.
This could be optimized a lot by previously freeing some chunks via msgsend/msgrecv and also retrieve more information from every stage, but for simplicitys sake, I separated each step.
First, we just try to find the markers of every chunk in the leak and store the offset in the leak and the next pointer from that chunk.
With this we can now check, if we found two adjacent chunks, by which we’ll know the exact address of the next chunk.
I also calculated the address of our msg_msg struct itself (which will be at the top of our leak, since we’re just reading from there), which we’ll need later on.
If everything goes well and we find out the addresses of two chunks with this, we can continue
Looks good. If we now free some chunks, and check how they look like, we’ll see that for hardening, they moved the next pointers inside the chunk and also obfuscated them, so they cannot be overwritten that easily.
Checking freelist_ptr for kernel v5.14.16 shows, how it’s calculated.
This means, the address of the next free chunk (ptr) is xored with a secret value s->random and then xored with the address, where the freelist pointer is stored (ptr_addr). What gave me some headaches while writing the exploit was, that there’s a new addition in freelist pointer hardening, which was added some time ago.
The address of the obfuscated pointer is swabed (which means effectively all bytes are reversed), which resulted in all my calculated addresses being “a bit off”.
But when checking the exact code for 5.14.16 it became more clear.
So, to be able to create our own obfuscated pointer, which will point to an arbitrary address, we need to know, where the pointer will be stored (ptr_addr), which we can calculate from our leaks. We also need to know which will be the next free chunk, which we can control by freeing two chunks (so the next allocated chunks will be those freed chunks in reverse order) and s->random.
Since we can get all the information except s->random from our leaks, we can calculate s->random, which will be the same for every slab/slub region.
To do this, we’ll first need to prepare another leak and free 2 chunks, for which we know the address.
We can now again corrupt the msg_msg and retrieve the obfuscated pointers from the just freed chunks. Since we know the order, in which they were freed and the addresses from both chunks, we know next_free.
By knowing the secret, we can now finally overwrite the freelist ptr and allocate a chunk anywhere.
Since modprobe_path was available in that challenge, it’s the easiest to just overwrite modprobe_path and trigger a flag copy.
Since the remote server was already shut down, I could only execute it in my local environment and retrieve the “fake flag”…