Kernel support for control-flow enforcement
The patches adding CET support were broken up into four separate groups: CPUID support and documentation, some memory-management work, shadow stacks, and indirect-branch tracking (IBT). The current patches support 64-bit systems only, and they only support CET for user-space code. Future versions are supposed to lift both of those restrictions.
ROP attacks generally work by loading a set of fabricated call frames onto the stack, each of which "returns" into a carefully chosen fragment of code. By stringing these "ROP gadgets" together, the attacker is able to execute enough useful code to take control of the system. Gadgets are plentiful in any large program; the ability to "return" into the middle of a multi-byte instruction to get an entirely different sequence of operations makes them even more available on x86 systems. The stack is, of course, writable by the running program; it contains a mixture of control-flow information (return addresses, for example) and other data. It is that mixing that has made ROP attacks possible.
One way to thwart such attacks is to move the return addresses to another context where they are not so easy to mess with; that is the core idea behind the shadow-stack functionality. Briefly, when shadow stacks are enabled, a function call will push the return address onto both the regular stack and a special shadow stack. When a return instruction is encountered, the return address is popped from both stacks and compared; if they do not match, a fault results. Both the push and pop operations are handled by the processor. As long as the attacker is unable to tamper with the shadow stack, it should prevent the use of a return instruction to divert the flow of control.
Preventing that tampering requires some special treatment for the shadow stack. It is allocated from a virtual-memory range, and the base address is stored into a model-specific register (MSR). The pages within the shadow stack must have a strange combination of bits set: read-only but dirty. Until now, the dirty state has been used almost exclusively by the kernel to track pages that must be written to backing store, but shadow stacks won't work without it. As a result, a new "software dirty" bit must be allocated in the page tables to fill the role that the hardware dirty bit handled previously.
The read-only protection on the shadow stack should prevent attackers from adding their own special entries — if that protection cannot be changed. To that end, shadow stacks are allocated in a special type of virtual-memory area (VMA) marked with the new VM_SHSTK flag. System calls like madvise(), mprotect(), mremap(), and munmap() will refuse to operate on a shadow-stack VMA. There is a new set of arch_prctl() operations that will operate on shadow stacks; they are described in this documentation patch. These calls, which are unprivileged, are meant to be used at program startup to set up the stack; one of them (ARCH_CET_LOCK) can be used to prevent disabling of shadow stacks (and IBT).
One interesting issue with shadow stacks is how they will interact with retpolines, which are used to thwart Spectre variant-2 attacks. Retpolines replace indirect function calls (those where the address of the function is determined at run time) with an instruction sequence that looks a lot like a ROP attack; they will not work when a shadow stack is in use. Intel claims (in section 4.3 of this document [PDF]) that retpolines will be unneeded on processors that support CET. Hopefully there will be no surprises that will force a choice between these two protective technologies.
Jump-oriented programming is a ROP-like technique that exploits indirect jumps and function calls. One way to severely restrict such exploits is to prevent jumps to any location that was not actually intended to be jumped to. IBT does this by adding a new pair of instructions (endbr32 and endbr64) that function as no-ops but which indicate a possible target for an indirect jump. These instructions will be treated as no-ops by older processors that lack CET support. When IBT is enabled, the processor will require that an endbr instruction is the first one encountered after an indirect jump; if something else is encountered, a fault will result.
Shadow stacks should be largely transparent to any program that is not, itself, doing strange things with return addresses on the stack. IBT is different, though; if it is enabled, the entire program must have been compiled with the necessary options to insert the endbr instructions in the right places. If a program has been so compiled, but it requires a library that has not, then IBT cannot be enabled without breaking the program. One of the jobs of the ELF loader on a CET-enabled system will be to check the CET-readiness of each library and only enable CET if all components are ready for it.
That leaves one interesting case uncovered, though. A program may need only CET-ready libraries to get started, but it might at some later point call dlopen() to load a library that has not been built for CET. At that point, there are only two options: turn off CET for that process, or fail the operation. If the ARCH_CET_LOCK operation described above has been invoked, only the latter option will be available. So locking can only be done at the cost of introducing a real chance of breaking programs when IBT is enabled.
That led to a long discussion about whether ARCH_CET_LOCK makes sense at all. Kees Cook argued that, in its absence, attackers will focus all of their energies on finding a way to turn CET off before carrying out the real attack. Andy Lutomirski responded that, by the time an attacker can disable CET, they are already in control and there's not much CET can do anyway. How that will be resolved is unclear at this time.
Disagreements over details like that notwithstanding, there appear to be no
concerns (outside of grsecurity
land anyway) about the CET features overall. They should make the
system far more resistant to some common attack techniques with, seemingly,
little in the way of performance or convenience costs. Chances are,
though, that this technology won't be accepted until it is able to cover
kernel code as well, since that is where a lot of attacks are focused. So
CET support in Linux won't happen in the immediate future — but neither
will the availability of CET-enabled processors.
| Index entries for this article | |
|---|---|
| Kernel | Security/Control-flow integrity |
| Security | Linux kernel |