Nutty was a kernel pwn challenge of the UnionCTF 2021. I did not manage to solve the challenge during the CTF but solved it
afterwards with some help of other solutions.
Reconnaissance
We are given a kernel bzImage, an initramfs.cpio and some files related to Docker and QEMU.
Upon unpacking the initramfs.cpio we discover a vulnmod.c file.
Furthermore we discover a flag.txt file in the root directory.
Since this is a kernel pwn challenge our goal is clear: Exploit the kernel module to gain root and read the flag.
Additional info
Checking the start.sh file we can see that KASLR, SMEP and SMAP are enabled. Furthermore, the kernel uses the SLUB implementation of the Slab allocator.
Module code
Reversing
Basically the kernel module lets us create content which we can view, edit and delete.
We can communicate with the kernel module using the ioctl syscall and the req structure the module uses.
The module uses a data structure called nut which holds a pointer to our content and its size.
The content is allocated using kmalloc.
We are allowed to create a maximum of 10 nuts and the content buffer has to be <= 1023 bytes.
Vulnerability
There are two vulnerabilities that I noticed:
The kernel module does not use any locks. This enables us to create a race condition.
A heap overflow.
I tried to use the race condition at first but gave up and decided to go for the heap overflow because the race condition was quite unstable.
Heap overflow
The Heap overflow is in the append function.
The bug is that the return value of read_size is used to calculate new_size.
This works for all cases where size is in the range of 0 - 1023. If we provide a value > 1023 however,
-EOVERFLOW is returned (which is -112).
Consider following example: We have created a nut with size 1023 and are now calling append with
a size of 4096. read_size will return -112, so new_size = -112 + 1023 = 911.
Further below the module calls memcpy_safe and passes new_size - nuts[idx].size = -112 as size parameter to memcpy safe.
memcpy_safe then calculates size & 0x3ff. Since size is -112, memcpy_safe will use 912 (-112 & 0x3ff) as size.
Therefore the call to memcpy_safe looks like this: memcpy_safe(tmp + 1023, appended, 912). This is a heap overflow of 912 bytes.
Exploiting
With this Heap overflow our goal is to somehow poison the Kernel Heap and overwrite stuff in kernel space to gain root.
As I mentioned earlier the given kernel uses the SLUB implementation of the Slab allocator. For a good introduction to the Slab allocator in the Linux kernel refer to this article.
SLUB uses a so called freelist, which is a singly linked list of available slabs, to keep track of freed objects. Since we have a heap overflow we can corrupt the freelist in order to get a slab at a chosen address. This attack is similar to Tcache poisoning. The question remaining is: which address should we use ?
This is where I got stuck. Due to SMEP we can’t jump to userspace. Being fairly new to kernel pwn, the only privesc technique I have used before in this case is overwriting modprobe_path. I managed to successfully forge a freelist pointer and also overwrite modeprobe_path but the kernel kept crashing. I don’t really know why but probably because I messed up the heap too much.
Looking at other solutions I saw that r4j0x00 from SuperGuesser also overwrote modprobe_path but used a different way to do so based on the tty_struct.
The tty_struct is a kernel structure that gets allocated when we call open on /dev/ptmx which returns a file descriptor to a pseudoterminal master.
The tty_struct can be used to defeat KASLR as well as execute arbitrary code in kernel space. To do so we must overwrite the tty_operations pointer of the tty_struct. The tty_operations is a structure containing a bunch of function callbacks set by a tty driver. Examples are write or ioctl.
Looking for ways on how exactly to do this, I found this article mentioning that we can overwrite the ioctl
handler with a gadget like:
As can be seen above, the ioctl handler takes 3 arguments. cmd and arg are user controlled. Therefore when overwriting the ioctl handler with this gadget we have a 4 byte arbitrary write primitive to where rdx is pointing to. Using ROPGadget I found the gadget at offset 0xdc749 from kernel base.
Lets put everything together.
Defeating KASLR
The kernel module uses kmalloc to allocate slabs that hold our data. Due to kmalloc the contents of a slab are not zeroed out. Therefore to leak the contents of a tty_struct we simply open /dev/ptmx and immediately close it again so that the freelist of the kmalloc-512 cache contains the now freed tty_struct. Allocating a nut with size > 512 will return this chunk to us. We just have to set the size of the req struct to 0 in order to not overwrite the chunk.
With the leaked tty_struct we have defeated KASLR. Furthermore, the tty_struct contains heap pointers which we will use to forge a freelist pointer.
Overwriting modeprobe_path
To overwrite modeprobe_path based on corrupting tty_operations we call open on /dev/ptmx once again and overflow the heap with the heap address of the tty_struct of this pseudoterminal master. We calculate the heap address based on the heap leak we got from the tty_struct.
After corrupting the freelist we allocate a bunch more nuts until we get our forged slab which overlaps with the tty_struct of the pseudoterminal. Having an overlapping slab, we overwrite the tty_operations pointer to point to a forged struct.
Having done this we can simply use the icotl syscall and overwrite modeprobe_path based on the gadget we have overwritten the ioctl handler with.
Overwriting modeprobe_path this way does not make the kernel crash. Why ? Idk but probably because we corrupted the freelist with a pointer that is actually on the heap and not somewhere else like the address of modeprobe_path.
Exploit
If you want to try yourself refer to my repo where I uploaded the challenges files, my exploit and some helper scripts.