Last month at SummerCon, Dan Rosenberg and I talked about our stackjacking technique for exploiting kernel vulnerabilities on grsecurity/PaX-hardened Linux kernels, in a presentation titled "Stackjacking and Other Kernel Nonsense."
While we covered a lot of material from our original stackjacking presentation, we also presented on a couple new items extending the original stackjacking technique:
- We discussed about how enhancements to the vanilla Linux kernel, such as SMEP, may make stackjacking an effective approach for vanilla kernel exploitation. While an arbitrary write is currently still sufficient for exploiting a vanilla Linux kernel, we may eventually (years?) get to the point where the built-in protection mechanisms begin to approach the mitigations offered by grsecurity/PaX. Again, not really important in the short-term, but hopefully we'll revive stackjacking for use against vanilla kernels a few years from now. :-)
- We also presented a revised stackjacking approach that is effective against the stackjacking mitigations that spender released not-so-quietly after our HES presentation and before our Infiltrate presentation. To top it off, the new technique allows exploitation with even less attacker assumptions!
The latter item is obviously of more interest, so let's discuss that.
As discussed in our previous post, the fixes released by spender mitigated the Rosengrope technique by moving thread_info off the kernel stack and mitigated the Obergrope technique by enabling RANDKSTACK on amd64 in addition to x86. RANDKSTACK is able to sufficiently frustrate the Obergrope technique since it randomizes the kernel stack pointer on every system call, making a write to a precise offset within the kernel stack frame difficult to perform reliably.
So, RANDKSTACK is good since it stops my method of stack groping. However, it also has some harmful side effects on kernel stack memory disclosures, the foundation of our stackjacking technique. Simply put, RANDKSTACK is bad since it effectively amplifies the "read capabilities" of a kernel stack leak.
For example, normally when I have a stack leak, we can leak off a few bytes at a particular offset on the kernel stack. If these bytes happen to overlap a sensitive pointer, say a pointer to our process's cred struct, we could leak its address and use our arbitrary write to escalate privileges. However, we dismissed this approach since it is too specific: it requires the kernel stack leak to be perfectly offset and therefore is not universal. After all, we originally developed the stack groping technique to make the stackjacking attack universal regardless of the size/offset of the kernel stack leak.
However, when RANDKSTACK is enabled, the kernel stack pointer is randomized on every system call. So instead of leaking bytes at a particular offset on the kernel stack, we can now leak off a significant portion of the kernel stack since every call will give us a different "chunk" of the stack contents. So we simply repeat our leak over and over to read off as much of the kernel stack as we need. Since we can read a large portion of the kernel stack, we just need to prime the kernel stack with a function that puts a sensitive pointer (eg. cred struct) on the kernel stack, leak off its address, then write into it to escalate privileges.
In summary, having RANDKSTACK enabled makes the stackjacking attack even easier. In addition, we no longer need a stack leak + arbitrary write, now we only need a stack leak + NULL write, a much more common primitive. A NULL write is sufficient in this case since we've cut out the whole groping stage and can simply write 0's into the uid/gid fields of a leaked cred struct.
We reported this weakness to spender and pipacs soon after their original blog post on our presentation.
To mitigate this weakness, pipacs introduced a feature called PAX_MEMORY_STACKLEAK, which clears the kernel stack between system calls, something we suggested in our original presentation back in April. While this doesn't completely eliminate the risk of kstack disclosures (a kstack leak could still leak memory from a previous frame within the same system call), it covers the vast majority of the leaks, at the obvious cost of performance.
While the PAX_MEMORY_STACKLEAK feature is available in the test grsecurity patches, the changelog indicates that it is still undergoing some refinement and optimization and I fear it will be enabled about as frequently as PAX_MEMORY_SANITIZE (aka not very often). As spender mentions here, a future blog post by pipacs will hopefully cover the new PAX_MEMORY_STACKLEAK feature in more detail. I'm excited to read about pipacs' crazy optimizations and gcc instrumentation for reducing the performance impact of PAX_MEMORY_STACKLEAK.