pwning your kernelz
This is the writeup for the challenge pwning your kernelz, created by Linus Henze(@LinusHenze),
I came across this challenge when Linus tweeted a status update for the CTF.
Of course, I didn’t solve this challenge during the time of the CTF. In fact, no one does.
So I decided to pick it up and exploit it with the support of Linus after the CTF ends.
You may want to checkout the exploit code
Update: the bug has been assigned as CVE-2019-8781 by Apple and fixed in the macOS Catalina 10.15 release.
The challenge required us to perform a Local Privilege Escalation to r00t user to get the flag.
SMEP is on,
SMAP is off,
kASLR slide is provided,
and the kernel is the latest development macOS kernel from Apple’s KDK.
Actually, at the time of the CTF,
Apple messed up and unpatched the 121 day (CVE-2019-8605) and you can just port the iOS exploit code to macOS and here we go.
But I didn’t think of that during the time of the CTF, also I was pretty busy at that time so I didn’t think about it.
But later, I decided to pick up the intended bug.
So the author provided us the following POC:
You will need to compile it to a 32bit program
-m32 to make it works.
The POC is such a simple one that it immediately hang the whole machine.
I wasted a lot of time to setup the VM and trying to debug with Apple’s kdp, but with this bug, triage it with kdp is hard.
The machine just hang because it’s constantly doublefault.
Later, when I knew about the VMWare’s gdb debugging stub, it actually makes my life much easier
Triaging the bug
The challenge mentioned about the need of 32bit apps, so we knows that the bug is somewhere in the
The cross-arch code starts to differ from the
So I found 2 snippets of code that share the same purpose but have different logic.
Inside the switch
flavor case of the x86 version of
machine_thread_set_state, you can find 2 different version
If the flavor is
x86_SAVED_STATE32, the machine just hang.
If the flavor is
x86_THREAD_STATE32, it does not cause any problem.
But they are supposed to have the similar behavior, so lets
diff them out.
Although going through the same amount of checks, the second version forced the segment registers to be a value that’s constantly defined
In the buggy one, we can see that it allows a wider range of segment registers' value, which could be malicious.
So, the bug is that we can set the segment registers' values to any malicious values.
This is similar to the BadIRET bug
Attaching the debugger, tracing down through the
iretq instruction, it failed to return due to invalid segment registers and jump to the fault handler,
Following the execution, we can observe that the fault handler does MISS a
So why does this is troublesome?
swapgs instruction changes the current GS base of the running code from kernelspace to userspace and vice versa.
Some data are accessed relatively through the GS register so we control some of the kernel’s data.
In case you don’t know what the segment registers are, they are just the index of some entries in the GDT (Global Descriptor Table).
Each entries contains meaningful data, one of them are the base address. And we can access data relatively from that base address through the registers
Following the buggy path, we can see that it repeatedly jump to the fault handler and always fault at the same instruction
It fault because it accesses data through the GS base, which is currently pointing to the user’s GS base and access to the unmapped memory.
The reason is that it’s using the userspace’s GS base has the base address at 0x0.
To get over, we need to remove the
_PAGEZERO segment by a linker switch, and allocate memory there with
Continuing with our zero-filled memory we just allocated, the kernel panic in
kernel_trap with the error type is 13 (general protection (#GP))
According to the Intel’s SDM,
iretq with invalid segment registers can cause a #GP fault
So how can we get over that that?
A piece of code that we did not consider yet is the specific handler for #GP fault:
This might seems rather unintersting,
but if you disassemble it and resolve the macro,
we can see that we controlled the
thread->recover value because it’s accessed that through the GS base, which we controlled.
set_recovery_ip set the location of the handler code in the next time the fault is occured, then we are dismissed from the fault handler.
So, by the next time we
iretq fails, we have control over the kernel’s RIP.
Next, I observed that we have some registers that we control over the
thread_set_state call. One of them is the $RBP register.
So, I find a
leave; ret; gadget (which should be plenty in the kernel code base) to pivot the kernel’s stack to a userspace address.
There, we set up our ROP chain to escalate ourselves.
You can find the typical privilege escalation ROP chain here
But there’s still something to note.
First, we need to bear in mind that the GS base is still in the userspace upon the start of the ROP chain,
which causes some fault in the
current_proc function as it used the GS base, so we need to fix that.
thread_exception_return will NOT work as the
saved_state is invalid and messed up.
Because there aren’t any
swapgs gadgets, we need to make ourselves at the userspace and return there.
Before we can do that, we need to ROP to turn off SMEP by unset the 20th(0-indexed) bit of the $cr4 register.
To return to the userspace, we need to set up ourselves the
iretq stack, which looks like this
|--------------------------| | Low mem addr | ^ |--------------------------| | | RIP | | <-- current RSP |--------------------------| | | CS | | |--------------------------| | | EFLAGS | | |--------------------------| | | RSP | | |--------------------------| | | SS | | |--------------------------| | | High mem addr | | |--------------------------| |
iretq should do the trick.
Upon coming back, I encountered the
misaligned_stack_error_ when going through the dyld stub.
I workaround this by catching that SIGSEGV error:
During exploitation of this bug, I stuck lots of time and need to be pointed out.
I found lots of flaws in my reverse engineering and code reading skills and missed some of important points.
But at least, I make time for myself to reading through the kernel code.
Also, this’s the first time I exploit a kernel 0 day that hasn’t been disclosed and exploited publicly yet.
I enjoy this challenge.
Apple for the 0day.
Linus Henze(@LinusHenze). He created a challenge in which I learnt a lot.
Also, he helped me a lot in exploitation by pointing out the points that I have missed. Thank you very much.