Execute without read

A couple of years ago, during an idle moment, I wondered what we could do if we had the hardware CPU primitive of pages with permissions execute-only (i.e. no read and write): https://twitter.com/scarybeasts/status/174901935340666881

It turns out that aarch64 has exactly such support. Here's support heading in to the Linux kernel:

https://git.kernel.org/cgit/linux/kernel/git/cmarinas/linux-aarch64.git/commit/?h=upstream&id=bc07c2c6e9ed125d362af0214b6313dca180cb08

The original idea was to defeat ROP by having all of the instructions randomized a bit on a per-install basis. You know, the usual tricks such as applying equivalence transforms on the opcode stream. Such an approach would have some obvious downsides such as diagnosability and let's face it, implementing this would also feel a bit hacky. Can we do better?

Maybe we can. The original idea focused on the attacker knowing where the binaries are in virtual address space, but not knowing or being able to read or otherwise predict the content. What if we instead keep the binary content stable but try and make sure the attacker cannot discern the location of the binaries? With enough ASLR entropy, this would be an interesting approach.

For the sake of the exercise, imagine the attacker has the most powerful of bugs: an arbitrary read/write primitive relative to an existing heap location. The attacker can follow heap pointers to the stack, the BSS, vtables, etc. At first, this sounds prohibitively hard to deal with. But for every way the attacker might try to leak the address of the binary, there currently seems to be a solution:

  • The heap is riddled with vtable pointers. If the attacker follows a vtable pointer, they get to read function pointers and the location of the binary is revealed. We fix this in one of two ways: either get sneaky and turn vtables into code (jmp 0xblah) instead of data, and reuse our exec-without-read primitive. Or we burn a register (aarch64 has lots) as a storage for a secret ASLR base for the binary.
  • The heap is riddled with raw function pointers. We can redo function pointers as something like single-slot vtables and use the above trick. We don't want to directly store function pointers in writable memory as a relative position to our secret register, because the attacker could then easily jump to an arbitrary point in the binary.
  • The BSS and data sections are typically stacked adjacent to the binary. We need to not do this, so that pointers into the BSS and data sections do not reveal the location of the binary.
  • The stack contains saved return addresses. These return addresses reveal the address of the binary. And for sure, the heap will contain pointers to the stack from time to time. Separating your stack into control flow and data will sort this out -- perhaps burning another register to keep the control flow stack separate and at a secret location.
  • JIT engines are a pain. And your heap is going to contain chains of pointers leading to the JIT pages. Depending on the type of JIT engine, there are various tricks that can be pulled. Enumerating them here is going to make the post too long. Some of the more amusing tricks including having the kernel ban syscalls from a writable page.
Perhaps at this point we decide that the hacks are piling up and add an indirection to all indirect jumps that uses a secret register for the binary location, and an offset into a table of valid jump locations. (I think this maybe where @comex was heading in a tweet in a discussion today: https://twitter.com/comex/status/474656633281196032)

Such a system isn't going to be invulnerable to memory corruption, but it _is_ going to be a significant pain to attack. The most obvious remaining attack is probably to read a couple of different vtable pointers and interchange them, calling an arbitrary attacker-chosen _existing function_ in the binary. If your binary has function pointers to system() in the heap, you're going to be in trouble. But generally, going after the kernel is going to be hard. Valid functions in your binary are unlikely to have the side effect of calling syscalls with bad parameters.

We also find ourselves wondering if we've sort-of re-implemented something like NaCl, although the performance characteristics and granularity of attacker-chosen code blocks will be different.

Crazy idea? Plausible direction?

[Thanks to Lee Campbell for helping with discussions and this blog post]