This weekend, I played with DiceGang in pbctf 2021 and we ended up placing 1st. There were so many cool (and absolutely insane) challenges, but school was really busy so I was unable to play as much as I liked. However, I still worked on some challenges and solved the kernel pwnable NightClub by IKEA with noopnoop, and thought it would be nice to make a writeup about it. Feel free to let me know if anything is unclear or incorrect.
In some of our initial analysis, we notice that SMAP, SMEP, and KASLR are turned on, and we are on version 5.14.1. In terms of important kernel hardening options, the SLUB allocator is used without randomization or hardening, and usercopy is not hardened; userfaultfd was disabled as well (but didn't really matter as unprivileged userfaultfd has been disabled by default since 5.11). The nightclub driver itself was a generic character device, and can be interfaced with via ioctls.
In this driver, there are two main structs: a user request struct and the note struct itself. The note struct in kernel land itself was 0x80 in size, placing it under the kmalloc-128 slab, and a lot of useless padding was placed throughout just to make it more difficult for players to exploit.
Four functions are also provided. If you make an ioctl request with 0xcafeb001, an allocation is made. You specify the size of data to copy in (which is capped up to 0x20), and an offset and 0x10 bytes of “special” data to copy in (both of which is never used again for some reason). The kernel also generates 4 random bytes for the key to store in the note, and returns it to the user. After setting up the struct, it is linked into a doubly linked list with the head located in the module data section with the symbol “master_list." When copying data over, alloc sets a null byte at note->data[size], leading to a trivial null byte poisoning bug.
An ioctl request with 0xcafeb002 triggers a deletion. The user specifies a key to delete a specified note from the list. The driver unlinks it and sets the pointers of the note itself to poison constants before freeing. Edit allows you to specify a key, data, size, and a offset (which is capped at 0x10). This leads to a trivial heap overflow and also has a poison null byte bug afterwards.
Note that in all the functions above, the note pointers during linked list traversal or kmalloc return are checked to be in range of a few constants. Noopnoop and I didn't dig too deeply and aren't completely sure, but just assumed those acted as a pseudo "address sanitizer."
Lastly, 0xcafeb004 is a “leak” function. It just leaks the difference between the addresses of edit_ioctl in the module and __kmalloc from the kernel text itself. While information from this paper and page about Linux memory mappings show that kernel text and modules have known regions (and the first only has 9 bits of entropy with KASLR and the latter only has 10 bits), the difference still doesn't provide us with enough information to avoid bruteforcing, which isn't feasible with remote POW.
Without SLAB randomization or hardening, this challenge should be pretty trivial with the overflow right?
Sadly, the answer was no. Not only was there no easy way to achieve leaks in this driver, but in recent Linux kernel versions, the freelist pointer in SLUB chunks have been moved into the middle of chunks (so a 0x10 byte overflow cannot reach it in kmalloc-128 chunks). In our case, it is at offset 0x40 upon freeing. We could also attempt to attack via msg_msg structure, but arb write would be quite difficult without userfaultfd, and we cannot hit the size and next fields (located at offset 0x18 and 0x20). There is also lack of well documented structs for exploitation in kmalloc-128. subprocess_info used to be in this slab, but has since moved to kmalloc-96; I went through both of these papers (paper 1, paper 2) and was not able to find a useful struct given our overflow limits.
Our first breakthrough was noopnoop's heap massage. He massaged the heap in the following manner to achieve a UAF chunk in the linked list.
First, we want to create a linked list chain of consecutive chunks in the following order (only the arrow in the direction of traversal is showed for simplicity's purposes, but keep in mind that it is a doubly linked list).
Then we poison null byte the next pointer of the second chunk, causing the next pointer to now point to chunk III.
Our second breakthrough came once I realized that kmalloc-96 existed (we somehow both thought it didn't exist); I guess the moral of the story here is to always check /proc/slabinfo. With this knowledge, we managed to solve pretty quickly via the following methodology.
First ,we performed the same heap massage mechanism to achieve a freed chunk in a linked list. Then, we replaced that chunk with a msg_msg object in the kmalloc-128 slab, and added two more msg_msg objects in the kmalloc-96 slab. This way, you get msg_msg objects (along with the msg_queue in kmalloc-64) linked into your list.
Take note that the offset write is really important. If you just do a generic overflow, you will end up corrupting msg_msg pointers. This kernel did not have the checkpoint_restore option compiled in, so MSG_COPY will not work, and any attempt to unlink without known kernel addresses will lead to a panic. Now, if we message receive from our queue, we can achieve kernel leaks!
We now freed another chunk in kmalloc-128 (I saved a msg_msg object in another msg_queue for this), then freed the UAF'd note in our linked list again. With the LIFO behavior of the kernel allocator, I allocated a new msg_msg object, overwrote the freelist pointer with modprobe_path, allocated 2 more messages, and finally allocated a new msg_msg object to then overwrite modprobe. Any attempt to execute an invalid executable will trigger the kernel execution of the program specified in modprobe_path, effectively giving us RCE with root privileges.
How did you figure out where the structures were in the beginning. I feel like I am missing a piece of analysis or something will connect the dots for me.
ReplyDelete