Let’s talk about CFI: clang edition
Our previous blog posts often mentioned control flow integrity, or CFI, but we have never explained what CFI is, how to use it, or why you should care. It’s time to remedy the situation! In this blog post, we’ll explain, at a high level, what CFI is, what it does, what it doesn’t do, and how to use CFI in your projects. The examples in this blog post are clang-specific, and have been tested on clang 3.9, the latest release as of October 2016.
This post is going to be long, so if you already know what CFI is and simply want to use it in your clang-compiled project, here’s the summary:
- Ensure you are using a link-time optimization capable linker (like GNU gold or MacOS ld).
- Add
-flto
to your build and linker flags - Add
-fvisibility=hidden
and-fsanitize=cfi
to your build flags - Sleep happier knowing your binary is more protected against binary level exploitation.
For an example of using CFI in your project, please take a look at the Makefile that comes with our CFI samples.
What is CFI?
Control flow integrity (CFI) is an exploit mitigation, like stack cookies, DEP, and ASLR. Like other exploit mitigations, the goal of CFI is to prevent bugs from turning into exploits. Bugs in a program, like buffer overflows, type confusion, or integer overflows, may allow an attacker to change the code a program executes, or to execute parts of the program out of order. To convert these bugs to exploits, an attacker must force the target program to follow a code path the programmer never intended. CFI works by reducing the attacker’s ability to do that. The easiest way to understand CFI is that it aims to enforce at run-time what the programmer intended at compile time.
Another way to understand CFI is via graphs. A program’s control flow may be represented as a graph, called the control flow graph (CFG). The CFG is a directed graph where each node is a basic block of the program, and each directed edge is a possible control flow transfer. CFI ensures the CFG determined by the compiler at compile time is followed by the program at run time, even in the presence of vulnerabilities that would otherwise allow an attacker to alter control flow.
There are more technical details, such as forward-edge CFI, backwards-edge CFI, but these are best absorbed from the numerous academic papers published on control flow integrity.
History of CFI
The original paper on CFI from Microsoft Research was released in 2005, and since then there have been numerous improvements to the performance and functionality of various CFI schemes. Continued improvements mean that now CFI is mainstream: recent versions of both the clang compiler and Microsoft Visual Studio include some form of CFI.
Clang’s CFI
In this blog post, we will look at the various options provided by clang’s CFI implementation, what each does and does not protect, and how to use it in your projects. We will not cover technical implementation details or performance numbers; a thorough technical explanation is already available from the implementation team in their paper.
Control flow integrity support has been in mainline clang since version 3.7, invoked as a part of the supported sanitizers suite. To operate, CFI requires the full control flow graph of a program. Since programs are typically built from multiple compilation units, the full control flow is not available until link time. To enable CFI, clang requires a linker capable of link-time optimization. Our code examples assume a Linux environment, so we will be using the GNU gold linker. Both GNU gold and recent versions of clang are available as packages for common Linux distributions. GNU gold is already included in modern binutils packages; Clang 3.9 packages for various Linux distributions are available from the LLVM package repository.
Some of the CFI options in clang actually have nothing to do with control flow. Instead these options detect invalid casts or other similar violations before they turn into worse bugs. These options are spiritually similar to CFI, however, because they ensure “abstraction integrity” — that is, what the programmer intended to happen is what happens at runtime.
Using CFI in Clang
The clang CFI documentation leaves a lot to be desired. We are going to describe what each option does, what limitations it has, and example scenarios where using it would prevent exploitation. These directions assume clang 3.9 and an LTO capable linker are installed and working. Once installed, both the linker and clang 3.9 should “just work”; specific installation instructions are beyond the scope of this blog post.
Several new compilation and linking flags are needed for your project: -flto
to enable link-time optimization, -fsanitize=cfi
to enable all CFI checks, and -fvisibility=hidden
to set default LTO visibility. For debug builds, you will also want to add -fno-sanitize-trap=all
to see descriptive error messages when CFI violation is detected. For release builds, omit this flag.
To review, your debug command line should now look like:
clang-3.9 -fvisibility=hidden -flto -fno-sanitize-trap=all -fsanitize=cfi -o [output] [input]
And your release command line should look like:
clang-3.9 -fvisibility=hidden -flto -fsanitize=cfi -o [output] [input]
You most likely want to enable every CFI check, but if you want to only enable select checks (each is described in the next section), specify them via -fsanitize=[option]
in your flags.
CFI Examples
We have created samples with specially crafted bugs to test each CFI option. All of the samples are designed to compile cleanly with the absolute maximum warning levels* (-Weverything
). The bugs that these examples have are not statically identified by the compiler, but are detected at runtime via CFI. Where possible, we simulate potential malicious behavior that occurs without CFI protections.
Each example builds two binaries, one with CFI protection (e.g. cfi_icall) and one without CFI protections (e.g. no_cfi_icall). These binaries are built from the same source, and used to illustrate the difference CFI protection makes.
We have provided the following examples:
- cfi_icall demonstrates control flow integrity of indirect calls. The example binary accepts a single command line argument (valid values are 0-3, but try invalid values with both binaries!). The command line argument shows different aspects of indirect call CFI protection, or lack thereof.
- cfi_vcall shows an example of CFI applied to virtual function calls. This example demonstrates how CFI would protect against a type confusion or similar attack.
- cfi_nvcall shows clang’s protections for calling non-virtual member functions via something that is not an object that has those functions defined.
- cfi_unrelated_cast shows how clang can prevent casts between objects of unrelated types.
- cfi_derived_cast expands on cfi_unrelated_cast and shows how clang can prevent casts from an object of a base class to an object of a derived class, if the object is not actually of the derived class.
- cfi_cast_strict showcases the very specific instance where the default level of base-to-derived cast protection, like in cfi_derived_cast, would not catch an illegal cast.
* Ok, we lied, we had to disable two warnings, one about C++98 compatibility, and one about virtual functions being defined inline. The point is still valid since those warnings do not relate to potential bugs.
CFI Option: -fsanitize=cfi
This option enables all CFI checks. Use this option! The various CFI protections will only be inserted where needed; you aren’t saving anything by not using this option and picking specific protections. So if you want to enable CFI, use -fsanitize=cfi
.
The currently implemented CFI checks, as of clang 3.9, are described in more detail in the following sections.
CFI Option: -fsanitize=cfi-icall
The cfi-icall option is the most straightforward form of CFI. At each indirect call site, such as calls through a function pointer, an extra check verifies two conditions:
The address being called is a valid destination, like the start of a function
The destination’s static function signature matches the signature determined at compile time.
When would these conditions be violated? When exploiting memory corruption attacks! Attackers want to hijack the program’s control flow to perform their bidding. These days, anti-exploitation protections are good enough force attackers to reuse pieces of the existing program. The program re-use technique is called ROP, and the pieces are referred to as gadgets. Gadgets are almost never whole functions, but snippets of machine code close to a control flow transfer instruction. The important aspect is that these gadgets are not at the start of a function; an attacker attempting to start ROP execution will fail CFI checks.
Attackers may be clever enough to point the new function pointer to a valid function. For instance, think of what would happen if a call to write was changed to call to system. The second condition attempts to mitigate these errors, by ensuring that runtime type signatures of destinations have to fall within a list of pre-selected destinations. Both of these condition violations are illustrated in option 2 and 3 of the the cfi_icall example.
Example Output
$ ./no_cfi_icall 2 Calling a function: CFI should protect transfer to here In float_arg: (0.000000) $ ./cfi_icall 2 Calling a function: cfi_icall.c:83:12: runtime error: control flow integrity check for type 'int (int)' failed during indirect function call (cfi_icall+0x424610): note: (unknown) defined here $ ./no_cfi_icall 3 Calling a function: CFI ensures control flow only transfers to potentially valid destinations In not_entry_point: (2) $ ./cfi_icall 3 Calling a function: cfi_icall.c:83:12: runtime error: control flow integrity check for type 'int (int)' failed during indirect function call (cfi_icall+0x424730): note: (unknown) defined here
Limitations
- Indirect call protection doesn’t work across shared library boundaries; indirect calls into shared libraries are not protected.
- All translation units have to be compiled with
-fsanitize=cfi-icall
. - Only works on x86 and x86_64 architectures
- Indirect call protection does not detect calls to the same function signature. Think of changing a call from
delete_user(const char *username)
tomake_admin(const char *username)
. We show this limitation in cfi_icall option 1:$ ./cfi_icall 1 Calling a function: CFI will not protect transfer to here In bad_int_arg: (1)
CFI Option: -fsanitize=cfi-vcall
To explain cfi-vcall, we need a quick review of virtual functions. Recall that virtual functions are functions that can be specialized in derived classes. Virtual functions are dynamically bound — that, is the actual function called is determined at runtime, depending on the object’s type. Due to dynamic binding, all virtual calls will be indirect calls. But these indirect calls may legitimately call functions with different signatures, since the class name is a part of the function signature. The cfi-vcall protection addresses this gap, by verifying that a virtual function call destination is always a function in the class hierarchy of the source object.
So when would a bug like this ever occur? The classic example is type confusion bugs in complex C++-based software like PDF readers, script interpreters, and web browsers. In type confusion, an object is re-interpreted as an object of a different type. The attacker can then use this mismatch to redirect virtual function calls to attacker controlled locations. A simulated example of such a scenario is in the cfi_vcall example.
Example Output
$ ./no_cfi_vcall Derived::printMe CFI Prevents this control flow Evil::makeAdmin $ ./cfi_vcall Derived::printMe cfi_vcall.cpp:45:5: runtime error: control flow integrity check for type 'Derived' failed during virtual call (vtable address 0x00000042eb20) 0x00000042eb20: note: vtable is of type 'Evil' 00 00 00 00 c0 6f 42 00 00 00 00 00 d0 6f 42 00 00 00 00 00 00 70 42 00 00 00 00 00 00 00 00 00
Limitations
- Only applies to C++ code that uses virtual functions.
- All translation units have to be compiled with
-fsanitize=cfi-vcall
. - There can be a noticeable increase in the output binary size.
- Need to specify the
-fvisibility
flag when building (for most purposes use-fvisibility=hidden
)
CFI Option: -fsanitize=cfi-nvcall
The cfi-nvcall option is spiritually similar to the cfi-vcall option, except it works on non-virtual calls. The key difference is that non-virtual calls are direct calls known statically at compile time, so this protection is not strictly a control flow integrity issue. What the cfi-nvcall option does is identify non-virtual calls and ensure the calling object’s type at runtime can be derived from the type of the object known at compile time.
In simple terms, imagine a class hierarchy of Ball
s and a class hierarchy of Brick
s. With cfi-nvcall, a compile-time call to Ball::Throw
may execute Baseball::Throw
, but will never execute Brick::Throw
, even if an attacker substitutes a Brick
object for a Ball
object.
Situations fixed by cfi-nvcall may arise from memory corruption, type confusion, and deserialization. While these instances do not allow an attacker to redirect control flow on their own, these bugs may result in data-only attacks, or enable enough misdeeds to permit future bugs to work. This type of attack using data-only bugs is shown in the cfi-nvcall example: a low privilege user object is used in-place of a high privilege administrator object, leading to in-application privilege escalation.
Example Output
$ ./no_cfi_nvcall Admin check: Account name is: admin Would do admin work in context of: admin User check: Account name is: user Admin Work not permitted for a user account! Account name is: user CFI Should prevent the actions below: Would do admin work in context of: user $ ./cfi_nvcall Admin check: Account name is: admin Would do admin work in context of: admin User check: Account name is: user Admin Work not permitted for a user account! Account name is: user CFI Should prevent the actions below: cfi_nvcall.cpp:54:5: runtime error: control flow integrity check for type 'AdminAccount' failed during non-virtual call (vtable address 0x00000042f300) 0x00000042f300: note: vtable is of type 'UserAccount' 00 00 00 00 80 77 42 00 00 00 00 00 a0 77 42 00 00 00 00 00 90 d4 f0 00 00 00 00 00 41 f3 42 00
Limitations
- The cfi-nvcall checks only apply to polymorphic objects.
- All translation units have to be compiled with
-fsanitize=cfi-nvcall
. - Need to specify the
-fvisibility
flag when building (for most purposes use -fvisibility=hidden)
CFI Option: -fsanitize=cfi-unrelated-cast
This is the first of three cast related options that are grouped with control flow integrity protections, but have nothing to do with control flow. These cast options verify “abstraction integrity”. Using these cast checks guards against insidious C++ bugs that may eventually lead to control flow hijacking.
The cfi-unrelated-cast option performs two runtime checks. First, it verifies that casts between object types must be in the same class hierarchy. Think of this as permitting casts from a variable of type Ball*
to Baseball*
but not from a variable of type Ball*
to Brick*
. The second runtime check verifies that casts from void*
to an object type refer to objects of that type. Think of this as ensuring that a variable of type void*
that points to a Ball
object can only be converted back to Ball
, and not to a Brick
.
This property is most effectively verified at runtime, because the compiler is forced to treat all casts from void*
to another type as legal. The cfi-unrelated-cast option ensures that such casts make sense in the runtime context of the program.
When would this violation ever happen? A common use of void*
pointers is to pass references to objects between different parts of a program. The classic example is the arg
argument to pthread_create
. The target function would have no way to determine if the void* argument is of the correct type. Similar situations happen in complex application, especially in those that use IPC, queues, or other cross-component messaging. The cfi_unrelated_cast example shows a sample scenario that is protected by the cfi-unrelated-cast option.
Example Output
$ ./no_cfi_unrelated_cast I am in fooStuff And I would execute: system("/bin/sh") $ ./cfi_unrelated_cast cfi_unrelated_cast.cpp:55:19: runtime error: control flow integrity check for type 'Foo' failed during cast to unrelated type (vtable address 0x00000042ec40) 0x00000042ec40: note: vtable is of type 'Bar' 00 00 00 00 70 71 42 00 00 00 00 00 a0 71 42 00 00 00 00 00 00 00 00 00 00 00 00 00 88 ec 42 00
Limitations
- All translation units must to be compiled with cfi-unrelated-cast
Need to specify the-fvisibility
flag when building (for most purposes use-fvisibility=hidden
) - Some functions (e.g. allocators) legitimately allocate memory of one type and then cast it to a different, unrelated object. These functions can be blacklisted from protection.
CFI Option: -fsanitize=cfi-derived-cast
This is the second of three cast related “abstraction integrity” options. The cfi-derived-cast option ensures that an object of a base class cannot be cast to a an object of a derived class unless the object is actually a derived object. As an example, cfi-derived-cast will prevent an variable of type Ball*
being cast to Baseball*
. This is a stronger guarantee than cfi-unrelated-cast, which verifies that the destination type is in the same class hierarchy as the source.
The potential causes of this issue are the same as most other issues on this list, namely memory corruption, de-serialization issues, and type confusion. In the cfi_derived_cast example, we show how a hypothetical base-to-derived casting bug can be used to disclose memory contents.
Example Output
$ ./no_cfi_derived_cast I am: derived class, my member variable is: 12345678 I am: base class, my member variable is: 7fffb6ca1ec8 $ ./cfi_derived_cast I am: derived class, my member variable is: 12345678 cfi_derived_cast.cpp:32:21: runtime error: control flow integrity check for type 'Derived' failed during base-to-derived cast (vtable address 0x00000042ef80) 0x00000042ef80: note: vtable is of type 'Base' 00 00 00 00 00 73 42 00 00 00 00 00 30 73 42 00 00 00 00 00 00 00 00 00 00 00 00 00 b0 ef 42 00
Limitations
- All translation units must to be compiled with cfi-derived-cast
- Need to specify the
-fvisibility
flag when building (for most purposes use-fvisibility=hidden
)
CFI Option: -fsanitize=cfi-cast-strict
This is the third and most confusing of all the cast-related “abstraction integrity” options is a stricter version of cfi-derived-cast. The cfi-derived-cast option is not enabled when a derived class meets a very specific set of requirements:
- It has only a single base class.
- It does not introduce any virtual functions.
- It does not override any virtual functions, other than an implicit virtual destructor.
If all of the above conditions are met, the base class and the derived class have an identical in-memory layout, and casting from the base class to the derived class should not introduce any security vulnerabilities. Performing such a cast is undefined and should never be done, but apparently enough projects utilize this undefined behavior to warrant a separate CFI option. The cfi_cast_strict example shows this behavior in action.
Example Output
$ ./no_cfi_cast_strict Base: func $ ./cfi_cast_strict cfi_cast_strict.cpp:22:18: runtime error: control flow integrity check for type 'Derived' failed during base-to-derived cast (vtable address 0x00000042e790) 0x00000042e790: note: vtable is of type 'Base' 00 00 00 00 10 6d 42 00 00 00 00 00 20 6d 42 00 00 00 00 00 50 6d 42 00 00 00 00 00 90 c3 f0 00
Limitations
- All translation units must to be compiled with cfi-cast-strict
Need to specify the-fvisibility
flag when building (for most purposes use-fvisibility=hidden
) - May break projects that rely on this undefined behavior.
Conclusion
Control flow integrity is an important exploit mitigation, and should be used whenever possible. Modern compilers such as clang already have support for control flow integrity, and you can use it today. In this blog post we described how to use CFI with clang, example scenarios where CFI prevents exploitation and otherwise detects subtle bugs, and discussed some limitations of CFI protections.
Now that you’ve read about what clang’s CFI does, try out out the examples and see how CFI can benefit your software development process.
But clang isn’t the only compiler to implement CFI! Microsoft Research originated CFI, and CFI protections are available in Visual Studio 2015. In our next installment, we are going to discuss Visual Studio’s control flow integrity implementation: Control Flow Guard.