While news is spreading around today about an updated attack against the Linux kernel’s BPF JIT that works around the random offset it applies to the start of the JITted code, I wanted to share our perspective on this, why everyone should have already seen this coming (as we did), and why these JIT spraying attacks have been irrelevant in grsecurity for many years and especially now with the release of RAP.
For the curious, the updated attack has been published at https://github.com/01org/jit-spray-poc-for-ksp and is based off the initial pre-random-offset BPF JIT attack published here: http://mainisusuallyafunction.blogspot. … -with.html .
It was this latter post from 2012 that inspired me to develop GRKERNSEC_BPF_HARDEN, which I believe was published within a day of my reading of the post. My implementation involved using the well-known constant-blinding technique against immediate values used in the encoded instruction stream generated by the JIT. As JIT spray attacks rely on returning into the middle of JITted code where these attacker-controlled immediates are treated as short, yet legitimate instructions, randomizing the immediates themselves effectively kills off the attack (at some performance hit). I chose this defense because it’s effective even in the case of an arbitrary read. Upstream chose a different approach of applying a random offset of breakpoint instructions to the start of a JITted BPF filter, and the updated attack speaks for itself. As mentioned before though, everyone should have already seen this coming given the weakened threat model used for upstream’s mitigation.
Some time later, in an attempt to demonstrate defeating the defenses of a grsecurity kernel, Comex (aka Pinkie Pie) published a youtube video using an unpublished vulnerability and exploit. Though in his first attempt he forgot to enable important protections like UDEREF, he finally got it right on the second try and demonstrated a valid attack. Unfortunately for him, he left too many indicators in the output (the kernel warned about an invalid BPF instruction, but let execution continue) and I deduced the method he had used: by overwriting a BPF interpreter buffer, he had been able to achieve arbitrary read/write through the BPF interpreter — effectively a data-only attack. I believe he also made the mistake of mentioning he used a stack overflow as part of the exploit.
What happened next was the hardening of the BPF interpreter in grsecurity to prevent such future abuse: the previously-abused arbitrary read/write from the interpreter was now restricted only to the interpreter buffer itself, and the previous warn on invalid BPF instructions was turned into a BUG() to terminate execution of the exploit. I also then developed GRKERNSEC_KSTACKOVERFLOW which killed off the stack overflow class of vulns on x64.
A short time later, there was work being done upstream to extend the use of BPF in the kernel. This new version was called eBPF and it came with a vastly expanded JIT. I immediately saw problems with this new version and noticed that it would be much more difficult to protect — verification was being done against a writable buffer and then translated into another writable buffer in the extended BPF language. This new language allowed not just arbitrary read and write, but arbitrary function calling.
For posterity, here’s the email I had sent to the eBPF JIT developer and Kees Cook back in 2014:
Date: Sun, 31 Aug 2014 12:45:16 -0400
From email@example.com Sun Aug 31 12:45:16 2014
From: Brad Spengler <firstname.lastname@example.org>
Subject: BPF security
[– PGP output follows (current time: Tue May 3 08:33:12 2016) –]
gpg: Signature made Sun Aug 31 12:45:09 2014 EDT using RSA key ID 2525FE49
gpg: Good signature from "Bradley Spengler (spender) <email@example.com>"
[– End of PGP output –]
[– The following data is signed –]
The way you are improving BPF is not secure (not that the original was
either, but you’re making it much worse in a way that’s much more difficult
to correct). I’ve wasted several days on it already and have decided to
revert the BPF code in the kernel back to that in the 3.14 stable kernel
for our users until you get your act in order.
First off, your entire threat model needs to be rethought. Random padding
at the beginning of the JIT code does nothing against an attacker with
an arbitrary read vuln (the JIT buffer can be leaked). Much more importantly,
there is *zero* protection against the BPF interpreter buffer from being
corrupted at runtime. Since unprivileged users are allowed to create
filters, control the size of the filters, and these filters go into the generic
slab caches, they are an easy target for heap overflows or arbitrary writes.
This isn’t some ivory tower theory, real exploits are *already* taking
advantage of this. I discovered it by seeing the WARN() about unknown
opcodes in someone’s published exploit video. Since that WARN() should never
be hit, it really should be a BUG() that terminates the attacker’s process.
You’re only doing verification of the interpreter buffer at load time — if
it’s corrupted after that point, *everything* is possible, including arbitrary
read, arbitrary write, and now arbitrary function calling as well.
Please please please before 3.17 is released, do *proper* validation of the
interpreter buffer. This involves generating a final representation
and marking the underlying allocation read-only, *then* performing the
validation. I highly doubt you will implement any real JIT defense like
constant blinding as clearly performance is more important than any semblance
of security, but at bare minimum the interpreter buffer needs to be secured
against runtime corruption.
Though they somewhat took one of my recommendations and made the interpreter buffer read-only in a later kernel, I’m still not happy with the new BPF implementation and particularly with exposing it at this early a stage to unprivileged users (it is still restricted to privileged users in grsecurity). Opening it up to unprivileged users actually exposed an exploitable vulnerability just recently: http://git.kernel.org/cgit/linux/kernel/git/davem/net.git/commit/?id=8358b02bf67d3a5d8a825070e1aa73f25fb2e4c7 which grsecurity was not vulnerable to.
With the release of RAP, the fear of JIT spraying goes away completely. It may not be immediately obvious, but it is for the same reason that unaligned instructions are no longer a threat, as described in the FAQ I wrote up last weekend: https://grsecurity.net/rap_faq.php . Since RAP ensures call sites can only transition to valid functions, and functions can only return to valid call sites, it’s no longer possible to redirect execution to the middle of a JITted instruction stream, provided that the JIT doesn’t allow attacker-controlled 64-bit immediates.
Keep watching this space for more in-depth discussions of current events that relate to grsecurity. Hopefully you find it more useful than 140 characters.