DirtyFetch had a simple race condition in its kernel module, which could be used to overflow the stack to rop.
The module was accessable via ioctl and provided functionality to
(0x10) set a max length for requests to be read/write
(0x20) allocate a storage buffer, which can be filled with user data
(0x30) save the storage buffer to stack
(0x40) load data from stack into user buffer
Conveniently, the stack buffer isn’t initialized, so we can directly read some leaks from it.
The leaked stack will look like this:
With a leak to kernel base and knowing the stack guard, we could now safely overflow the stack without breaking it.
But we’ll need to find a way to get around the validations in the module, which will try to hinder us to do that.
When we try to create a storage, edit_storage will first call validate_buf.
validate_buf will allocate a storage object, copy our input object into it and then check if request->length < MAX_SIZE and free the allocated storage object again.
If the length check succeeded, edit_storage would then allocate the storage again and copy our input object into it again.
But since there are no mutexes in place and our input object gets copied twice (once in validate_buf to check if it has a valid length and then again in edit_storage to create the final storage object) it’s a simple TOCTOU problem.
If we set length to a valid value before validate_buf, wait until it finished and then switch to a bigger value (> MAXSIZE) before edit_storage calls get_storage_contents again, this would result in a request object with length > MAX_SIZE.
We can do this by using two threads. One will constantly set length of our data object to a valid value, wait briefly and then set it to a bigger value. The second thread will then try to allocate a storage buffer over and over again until the size of the allocated buffer matches our target size.
As soon as we win the race and the request storage is allocated with a size of 0x200, the threads will stop.
Now, we can save the storage object into the stack variable content, which will then overflow it and executes our ropchain on returning from ioctl.
With this, we can now prepare a ropchain to do commit_creds(prepare_kernel_cred(0)) and hopefully get back into our code.
First, we use save_state to store the current registers from userland process cs, ss, rsp and rflags, which are needed later to switch back from kernelmode to usermode.
The offsets for prepare_kernel_cred, commit_creds, etc. can be retrieved from /proc/kallsyms (patching initramfs before to execute as root user).
When ioctl returns, it will run into our ropchain, and first do a pop rdi to set rdi to 0 and call prepare_kernel_cred to create a creds object (with user id 0). Normally we’d now have to move the result from rax to rdi, but after prepare_kernel_cred both rax and rdi already pointed to the correct object, so we can directly call commit_creds to make them the current creds.
Now we’re already root, but we still need to get back into usermode.
For this, we can use swapgs_restore_regs_and_return_to_usermode from the kernel itself (which can also be found via /proc/kallsyms).
We can skip all the pops at the beginning and just start at swapgs_restore_regs_and_return_to_usermod+22. Since this will pop 2 values (rax and rdi) from the stack, we just add two dummy values in our ropchain for this.
For swapgs we then add a pointer to safe_exit as new rip and add the cs, rflags, sp and ss from userland, which we retrieved in the begining. This should get us back into usermode and execute safe_exit.
Looking good. Since we’re now root in safe_exit, we can just read the flag and print it.