Control Flow Integrity in the Android kernel
Posted by means of Sami Tolvanen, Staff Software Engineer, Android Security
Android’s safety fashion is enforced by means of the Linux kernel, which makes it a tempting goal for attackers. We have put numerous effort into hardening the kernel in earlier Android releases and in Android nine, we persevered this paintings by means of specializing in compiler-based safety mitigations in opposition to code reuse assaults.
Google’s Pixel three might be the first Android tool to send with LLVM’s forward-edge Control Flow Integrity (CFI) enforcement in the kernel, and we have now made CFI reinforce to be had in Android kernel variations four.nine and four.14. This submit describes how kernel CFI works and gives answers to the maximum commonplace problems builders would possibly run into when enabling the function.
Protecting in opposition to code reuse assaults
A commonplace means of exploiting the kernel is the use of a trojan horse to overwrite a serve as pointer saved in reminiscence, comparable to a saved callback pointer or a go back cope with that have been driven to the stack. This permits an attacker to execute arbitrary portions of the kernel code to finish their exploit, even though they can not inject executable code of their very own. This means of gaining code execution is especially well-liked by the kernel as a result of the large choice of serve as tips it makes use of, and the present reminiscence protections that make code injection tougher.
CFI makes an attempt to mitigate those assaults by means of including further tests to substantiate that the kernel’s regulate glide remains inside of a precomputed graph. This does not save you an attacker from converting a serve as pointer if a trojan horse supplies write get admission to to 1, nevertheless it considerably restricts the legitimate name goals, which makes exploiting this type of trojan horse harder in follow.
Figure 1. In an Android tool kernel, LLVM’s CFI limits 55% of oblique calls to at maximum five conceivable goals and 80% to at maximum 20 goals.
Gaining complete program visibility with Link Time Optimization (LTO)
In order to decide all legitimate name goals for every oblique department, the compiler wishes to peer all of the kernel code immediately. Traditionally, compilers paintings on a unmarried compilation unit (supply record) at a time and depart merging the object recordsdata to the linker. LLVM’s strategy to CFI is to require the use of LTO, the place the compiler produces LLVM-specific bitcode for all C compilation gadgets, and an LTO-aware linker makes use of the LLVM back-end to mix the bitcode and bring together it into local code.
Figure 2. A simplified review of the way LTO works in the kernel. All LLVM bitcode is mixed, optimized, and generated into local code at hyperlink time.
Linux has used the GNU toolchain for assembling, compiling, and linking the kernel for many years. While we proceed to make use of the GNU assembler for stand-alone meeting code, LTO calls for us to modify to LLVM’s built-in assembler for inline meeting, and both GNU gold or LLVM’s personal lld as the linker. Switching to a rather untested toolchain on an enormous device venture will result in compatibility problems, which we have now addressed in our arm64 LTO patch units for kernel variations four.nine and four.14.
In addition to creating CFI conceivable, LTO additionally produces quicker code because of international optimizations. However, further optimizations steadily outcome in a bigger binary measurement, that may be unwanted on gadgets with very restricted assets. Disabling LTO-specific optimizations, comparable to international inlining and loop unrolling, can cut back binary measurement by means of sacrificing a few of the efficiency features. When the use of GNU gold, the aforementioned optimizations may also be disabled with the following additions to LDFLAGS:
LDFLAGS += -plugin-opt=-inline-threshold=zero -plugin-opt=-unroll-threshold=zero
Note that flags to disable person optimizations aren’t a part of the strong LLVM interface and would possibly trade in long term compiler variations.
Implementing CFI in the Linux kernel
LLVM’s CFI implementation provides a take a look at earlier than every oblique department to substantiate that the goal cope with issues to a legitimate serve as with a proper signature. This prevents an oblique department from leaping to an arbitrary code location or even limits the purposes that may be referred to as. As C compilers don’t put into effect equivalent restrictions on oblique branches, there have been a number of CFI violations because of serve as sort declaration mismatches even in the core kernel that we’ve got addressed in our CFI patch units for kernels four.nine and four.14.
Kernel modules upload any other complication to CFI, as they’re loaded at runtime and may also be compiled independently from the remainder of the kernel. In order to reinforce loadable modules, we have now carried out LLVM’s cross-DSO CFI reinforce in the kernel, together with a CFI shadow that hurries up cross-module look-ups. When compiled with cross-DSO reinforce, every kernel module incorporates details about legitimate native department goals, and the kernel seems up data from the proper module in accordance with the goal cope with and the modules’ reminiscence format.
Figure three. An instance of a cross-DSO CFI take a look at injected into an arm64 kernel. Type data is handed in X0 and the goal cope with to validate in X1.
CFI tests naturally upload some overhead to oblique branches, however because of extra competitive optimizations, our assessments display that the affect is minimum, and general device efficiency even stepped forward 1-2% in many instances.
Enabling kernel CFI for an Android tool
CFI for arm64 calls for clang model >= five.zero and binutils >= 2.27. The kernel construct device additionally assumes that the LLVMgold.so plug-in is to be had in LD_LIBRARY_PATH. Pre-built toolchain binaries for clang and binutils are to be had in AOSP, however upstream binaries can be used.
The following kernel configuration choices are had to allow kernel CFI:
Using CONFIG_CFI_PERMISSIVE=y might also turn out useful when debugging a CFI violation or right through tool bring-up. This choice turns a contravention right into a caution as a substitute of a kernel panic.
As discussed in the earlier segment, the maximum commonplace factor we bumped into when enabling CFI on Pixel three have been benign violations brought about by means of serve as pointer sort mismatches. When the kernel runs into this type of violation, it prints out a runtime caution that incorporates the name stack at the time of the failure, and the name goal that failed the CFI take a look at. Changing the code to make use of a proper serve as pointer sort fixes the factor. While we have now mounted all recognized oblique department sort mismatches in the Android kernel, equivalent issues could also be nonetheless discovered in tool particular drivers, for instance.
CFI failure (goal: [<fffffff3e83d4d80>] my_target_function+0x0/0xd80): ------------[ cut here ]------------ kernel BUG at kernel/cfi.c:32! Internal error: Oops - BUG: zero [#1] PREEMPT SMP … Call hint: … [<ffffff8752d00084>] handle_cfi_failure+0x20/0x28 [<ffffff8752d00268>] my_buggy_function+0x0/0x10 …
Figure four. An instance of a kernel panic brought about by means of a CFI failure.
Another attainable pitfall are cope with area conflicts, however this will have to be much less commonplace in driving force code. LLVM’s CFI tests simplest perceive kernel digital addresses and any code that runs at any other exception stage or makes an oblique name to a bodily cope with will outcome in a CFI violation. These sorts of screw ups may also be addressed by means of disabling CFI for a unmarried serve as the use of the __nocfi characteristic, and even disabling CFI for whole code recordsdata the use of the $(DISABLE_CFI) compiler flag in the Makefile.
static int __nocfi address_space_conflict()
Figure five. An instance of adjusting a CFI failure brought about by means of an cope with area war.
Finally, like many hardening options, CFI can be tripped by means of reminiscence corruption mistakes that would possibly another way outcome in random kernel crashes at a later time. These could also be harder to debug, however reminiscence debugging gear comparable to KASAN can assist right here.
We have carried out reinforce for LLVM’s CFI in Android kernels four.nine and four.14. Google’s Pixel three might be the first Android tool to send with those protections, and we have now made the function to be had to all tool distributors via the Android commonplace kernel. If you might be transport a brand new arm64 tool working Android nine, we strongly suggest enabling kernel CFI to assist offer protection to in opposition to kernel vulnerabilities.
LLVM’s CFI protects oblique branches in opposition to attackers who organize to achieve get admission to to a serve as pointer saved in kernel reminiscence. This makes a commonplace means of exploiting the kernel harder. Our long term paintings comes to additionally protective serve as go back addresses from equivalent assaults the use of LLVM’s Shadow Call Stack, which might be to be had in an upcoming compiler unencumber.