Kernel ROP - ret2usr
ROPpety boppety, but now in the kernel
Last updated
ROPpety boppety, but now in the kernel
Last updated
By and large, the principle of userland ROP holds strong in the kernel. We still want to overwrite the return pointer, the only question is where.
The most basic of examples is the ret2usr technique, which is analogous to ret2shellcode - we write our own assembly that calls commit_creds(prepare_kernel_cred(0))
, and overwrite the return pointer to point there.
Note that the kernel version here is 6.1, due to some added protections we will come to later.
The relevant code is here:
As we can see, it's a size 0x100
memcpy
into an 0x20
buffer. Not the hardest thing in the world to spot. The second printk
call here is so that buffer
is used somewhere, otherwise it's just optimised out by make
and the entire function just becomes xor eax, eax; ret
!
Firstly, we want to find the location of prepare_kernel_cred()
and commit_creds()
. We can do this by reading /proc/kallsyms
, a file that contains all of the kernel symbols and their locations (including those of our kernel modules!). This will remain constant, as we have disabled KASLR.
For obvious reasons, you require root permissions to read this file!
Now we know the locations of the two important functions: After that, the assembly is pretty simple. First we call prepare_kernel_cred(0)
:
Then we call commit_creds()
on the result (which is stored in RAX):
We can throw this directly into the C code using inline assembly:
The next step is overflowing. The 7th qword
overwrites RIP:
Finally, we create a get_shell()
function we call at the end, once we've escalated privileges:
If we run what we have so far, we fail and the kernel panics. Why is this?
The reason is that once the kernel executes commit_creds()
, it doesn't return back to user space - instead it'll pop the next junk off the stack, which causes the kernel to crash and panic! You can see this happening while you debug (which we'll cover soon).
What we have to do is force the kernel to swap back to user mode. The way we do this is by saving the initial userland register state from the start of the program execution, then once we have escalate privileges in kernel mode, we restore the registers to swap to user mode. This reverts execution to the exact state it was before we ever entered kernel mode!
We can store them as follows:
The CS, SS, RSP and RFLAGS registers are stored in 64-bit values within the program. To restore them, we append extra assembly instructions in escalate()
for after the privileges are acquired:
Here the GS, CS, SS, RSP and RFLAGS registers are restored to bring us back to user mode (GS via the swapgs
instruction). The RIP register is updated to point to get_shell
and pop a shell.
If we compile it statically and load it into the initramfs.cpio
, notice that our privileges are elevated!
We have successfully exploited a ret2usr!
How exactly does the above assembly code restore registers, and why does it return us to user space? To understand this, we have to know what all of the registers do. The switch to kernel mode is best explained by a literal StackOverflow post, or another one.
GS - limited segmentation. The contents of the GS register are swapped one of the MSRs (model-specific registers); at the entry to a kernel-space routine, swapgs
enables the process to obtain a pointer to kernel data structures.
Has to swap back to user space
SS - Stack Segment
Defines where the stack is stored
Must be reverted back to the userland stack
RSP
Same as above, really
CS - Code Segment
Defines the memory location that instructions are stored in
Must point to our user space code
RFLAGS - various things
GS is changed back via the swapgs
instruction. All others are changed back via iretq
, the QWORD variant of the iret
family of intel instructions. The intent behind iretq
is to be the way to return from exceptions, and it is specifically designed for this purpose, as seen in Vol. 2A 3-541 of the Intel Software Developer’s Manual:
Returns program control from an exception or interrupt handler to a program or procedure that was interrupted by an exception, an external interrupt, or a software-generated interrupt. These instructions are also used to perform a return from a nested task. (A nested task is created when a CALL instruction is used to initiate a task switch or when an interrupt or exception causes a task switch to an interrupt or exception handler.)
[...]
During this operation, the processor pops the return instruction pointer, return code segment selector, and EFLAGS image from the stack to the EIP, CS, and EFLAGS registers, respectively, and then resumes execution of the interrupted program or procedure.
As we can see, it pops all the registers off the stack, which is why we push the saved values in that specific order. It may be possible to restore them sequentially without this instruction, but that increases the likelihood of things going wrong as one restoration may have an adverse effect on the following - much better to just use iretq
.
The final version