Introduction to Control-Flow Integrity (CFI)
Control-Flow Integrity (CFI) is a crucial security mechanism designed to prevent arbitrary code execution by ensuring that software execution follows a pre-determined, valid path. In the context of exploit development, attackers often seek to redirect the program’s control flow to malicious code, typically by corrupting function pointers, return addresses, or virtual table (vtable) pointers. CFI aims to thwart these attempts by imposing strict runtime checks on all indirect transfers of control, such as indirect function calls, indirect jumps, and function returns. This article will focus on Android’s implementation of LLVM CFI, delving into its mechanics and exploring advanced techniques for analysis and bypass.
LLVM CFI in Android: An Overview
Android, as a leading mobile operating system, incorporates robust security features to protect its users. A significant component of this security arsenal is LLVM Control-Flow Integrity. Integrated into the Android Open Source Project (AOSP) build system, LLVM CFI is a compiler-based instrumentation technique. This means that security checks are injected into the compiled binaries during the compilation phase, specifically targeting indirect control-flow transfers. It operates at a fine-grained level, utilizing type-based checking to determine the legitimacy of an indirect call or jump target.
How LLVM CFI Works
At its core, LLVM CFI works by associating a unique type identifier with each function type. When an indirect call is made, the compiler inserts a runtime check. This check verifies that the type identifier of the target function matches the expected type identifier at the call site. If there’s a mismatch, indicating a potential control-flow hijack, the program is terminated. This mechanism effectively restricts indirect calls to only those functions whose type signatures are compatible with the call site’s expected type.
Consider a simple C++ virtual function call:
class Base {public: virtual void foo(int x) = 0;};class Derived : public Base {public: void foo(int x) override { /* implementation */ }};void call_foo(Base* obj, int val) { obj->foo(val); // Indirect call through vtable}
During compilation, the LLVM CFI pass would instrument the call to obj->foo(val). It would determine the expected type signature for foo (e.g., void(Base*, int) if considering the ‘this’ pointer implicitly, or just void(int) for the method signature itself). It then generates code to ensure that the function pointer resolved from the vtable for obj->foo actually points to a function with that specific type ID. If an attacker corrupts the vtable to point to an arbitrary address, the CFI check will likely fail unless the target function happens to have the exact same type signature.
Common CFI Bypass Strategies
Bypassing CFI is not about disabling the checks entirely (which is often impossible without code execution), but rather about finding ways to satisfy the checks while still achieving attacker-controlled execution. The fundamental premise is that CFI restricts *type-mismatched* calls; it does not prevent calling a *valid* function that happens to perform an attacker-desired action, provided its type matches.
Information Leaks and Valid Targets
A prerequisite for almost any modern exploit, an information leak is crucial. To bypass CFI, an attacker needs to know the addresses of legitimate functions and their type signatures within the target process’s memory space. This often involves leaking addresses from libc, the heap, or other loaded modules. Once addresses are known, an attacker can search for existing functions (gadgets) that are both useful and conform to the CFI type check at the point of the indirect call.
Abusing Dynamic Linking and Function Pointers
Functions like dlopen and dlsym, which are used for dynamic library loading and symbol resolution, often operate with generic function pointer types (e.g., void* (*)(...) or void*). If an attacker can manipulate the arguments to dlsym or similar functions (e.g., by controlling a string that specifies the library or symbol name), they might be able to resolve and obtain a pointer to an arbitrary function. While the CFI check on the dlsym call itself would likely pass (as dlsym has a legitimate type), the resulting function pointer could then be used in a subsequent indirect call. If that subsequent call site is also CFI-protected, the same type-matching problem arises.
Finding CFI-Compatible Gadgets
The most common and effective CFI bypass strategy involves finding a legitimate function within the program or its loaded libraries that has the *correct type signature* to pass the CFI check at a vulnerable indirect call site, but whose execution path leads to attacker-desired behavior. This is often referred to as finding a
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →