Supervisor Memory Execute Protection
If ret2usr is analogous to ret2shellcode, then SMEP is the new NX. SMEP is a primitive protection that ensures any code executed in kernel mode is located in kernel space. This means a simple ROP back to our own shellcode no longer works. To bypass SMEP, we have to use gadgets located in the kernel to achieve what we want to (without switching to userland code).
In older kernel versions we could use ROP to disable SMEP entirely, but this has been patched out. This was possible because SMEP is determined by the 20th bit of the CR4 register, meaning that if we can control CR4 we can disable SMEP from messing with our exploit.
We can enable SMEP in the kernel by controlling the respective QEMU flag (qemu64
is not notable):
Bypassing SMEP by ropping through the kernel
The previous approach failed, so let's try and escalate privileges using purely ROP.
First, we have to change the ropchain. Start off with finding some useful gadgets and calling prepare_kernel_cred(0)
:
Now comes the trickiest part, which involves moving the result of RAX to RSI before calling commit_creds()
.
This requires stringing together a collection of gadgets (which took me an age to find). See if you can find them!
I ended up combining these four gadgets:
Gadget 1 is used to set RDX to 0
, so we bypass the jne
in Gadget 2 and hit ret
Gadget 2 and Gadget 3 move the returned cred struct from RAX to RDX
Gadget 4 moves it from RAX to RDI, then compares RDI to RDX. We need these to be equal to bypass the jne
and hit the ret
Recall that we need swapgs
and then iretq
. Both can be found easily.
The pop rbp; ret
is not important as iretq
jumps away anyway.
To simulate the pushing of RIP, CS, SS, etc we just create the stack layout as it would expect - RIP|CS|RFLAGS|SP|SS
, the reverse of the order they are pushed in.
If we try this now, we successfully escalate privileges!
An old technique
Using the same setuo as ret2usr, we make one single modification in run.sh
:
Now if we load the VM and run our exploit from last time, we get a kernel panic.
It's worth noting what it looks like for the future - especially these 3 lines:
So, instead of just returning back to userspace, we will try to overwrite CR4. Luckily, the kernel contains a very useful function for this: native_write_cr4(val)
. This function quite literally overwrites CR4.
Assuming KASLR is still off, we can get the address of this function via /proc/kallsyms
(if we update init
to log us in as root
):
Ok, it's located at 0xffffffff8102b6d0
. What do we want to change CR4 to? If we look at the kernel panic above, we see this line:
CR4 is currently 0x00000000001006b0
. If we remove the 20th bit (from the smallest, zero-indexed) we get 0x6b0
.
The last thing we need to do is find some gadgets. To do this, we have to convert the bzImage
file into a vmlinux
ELF file so that we can run ropper
or ROPgadget
on it. To do this, we can run extract-vmlinux
, from the official Linux git repository.
All that changes in the exploit is the overflow:
We can then compile it and run.
This fails. Why?
If we look at the resulting kernel panic, we meet an old friend:
SMEP is enabled again. How? If we debug the exploit, we definitely hit both the gadget and the call to native_write_cr4()
. What gives?
Well, if we look at the source, there's another feature:
Essentially, it will check if the val
that we input disables any of the bits defined in cr4_pinned_bits
. This value is set on boot, and effectively stops "sensitive CR bits" from being modified. If they are, they are unset. Effectively, modifying CR4 doesn't work any longer - and hasn't since version 5.3-rc1.