Let’s talk about CFI: Microsoft Edition

We’re back with our promised second installment discussing control flow integrity. This time, we will talk about Microsoft’s implementation of control flow integrity. As a reminder, control flow integrity, or CFI, is an exploit mitigation technique that prevents bugs from turning into exploits. For a more detailed explanation, please read the first post in this series.

Security researchers should study products that people use, and Microsoft has an overwhelming share of the desktop computing market. New anti-exploitation measures in Windows and Visual Studio are a big deal. These can and do directly impact a very large number of people.

For the impatient who want to know about control flow guard right now: add /guard:cf to both your compiler and linker flags, and take a look at our examples showing what CFG does and does not do.

Microsoft’s CFI

Microsoft’s implementation of CFI is called Control Flow Guard (CFG), and it requires both operating system and compiler support. The minimum supported operating system is Windows 8.1 Update 3 and the minimum compiler version is Visual Studio 2015 (VS2015 Update 3 is recommended). All the examples in this blog post use Visual Studio 2015 and Windows 10 on x86-64.

Control Flow Guard is very well documented — there is an official documentation page, the documentation for the compiler option, and even a blog post from when the feature was in development. CFG is a very straightforward implementation of CFI:

  • First, the compiler identifies all indirect branches in a program
  • Next, it determines which branches must be protected. For instance, indirect branches that have a statically identifiable target don’t need CFI checks.
  • Finally, the compiler inserts lightweight checks at potentially vulnerable branches to ensure the branch target is a valid destination.

As in the previous blog post, we will not explore the technical implementation of CFG. There is already plenty of excellent literature on the subject. Instead this blog post will focus on how to use CFG in your programs, and show what CFG does and does not protect. However, we will mention some important differences between CFG and Clang’s CFI implementation.

Comparing CFG with Clang’s CFI

This comparison is meant to show the differences between how each implementation translates theoretical ideas behind control flow integrity into shipping application protection mechanisms. Neither implementation is better or worse than the other; they target different software ecosystems. Each works within real-world constraints (e.g. source availability, performance, ease of use, API/ABI stability, backwards compatibility, etc.) to achieve meaningful software protection.

What’s protected?

Programs protected with Microsoft’s CFG or Clang’s CFI execute lightweight checks before indirect control flow transfers. The check validates that the target of the flow belongs to a pre-determined set of valid targets.

Windows programs have many indirect calls that cannot be hijacked. For instance, API calls are performed via an indirect call through the IAT, which is set to read-only after program load. The Visual Studio compiler safely omits CFG checks for these calls.

Clang’s CFI also includes checks that are not exactly CFI related, such as runtime validation of pointer casts. See the previous blog post for more details and examples.

What is a valid target?

Control Flow Guard has a single per-process mapping of all valid control flow targets. Anything in the mapping is considered a valid target (Figure 1b). CFG provides a way to adjust the valid target map at runtime, via the the aptly named SetProcessValidCallTargets API. This is especially helpful when dealing with JITted code or manually loading dynamic libraries.

CFG also provides three compiler directives that control CFG behavior in a specified method. These directives are defined in ntdef.h in the Windows SDK, but not well documented. We would like to thank Matt Miller from Microsoft for explaining what they do:

  • __declspec(guard(ignore)) will disable CFG checks for all indirect calls inside a method, and ignore any function pointers referenced in the method.
  • __declspec(guard(nocf)) will disable CFG checks for all indirect calls inside a method, but track any function pointers referenced in the method and add those functions to the valid destination map.
  • __declspec(guard(suppress)) will prevent an exported function from being a valid CFG destination. This is used to prevent security sensitive functions from being called indirectly (for instance, SetProcessValidCallTargets is protected in this way).

Clang’s CFI is more fine grained in its protection. The target of each indirect control flow transfer must match an expected type signature (Figure 1a). Depending on the options enabled, calls to class member functions are also verified to be within the proper class hierarchy. Effectively, there is a valid target mapping per type signature and per class hierarchy. The target sets are fixed at compile time and cannot be changed.

Figure 1: Differences in the valid call targets for the cfg_icall example. The true valid destination is in green, and everything else is in red.
clang_valid_dests vs_valid_dests
(a) Valid destinations at the indirect call for Clang’s CFI. Only functions matching the expected function signature are in the list. (b) Valid destinations at the indirect call for CFG using Visual Studio 2015. Every legal function entry point is in the list.

How is protection enforced?

Control Flow Guard splits enforcement duties between the compiler and the operating system. The compiler inserts the checks and provides an initial valid target set, and the operating system maintains the target set and verifies destinations.

Clang’s CFI does all enforcement at the compiler level; the operating system is not aware of CFI.

What about dynamic libraries, JITed code, and other edge cases?

Control Flow Guard supports cross-library calls, but enforcement only occurs if the library is also compiled with Control Flow Guard. Dynamically generated code pages can be added to or excluded from the valid target map. External functions retrieved via GetProcAddress are always valid call targets*.

Clang’s CFI supports cross-library calls via the -fsanitize-cfi-cross-dso flag. Both the library and the application must be compiled with this flag. As far as we can tell, dynamically generated code does not receive CFI protection. External functions retrieved via dlsym are automatically added as a valid target when -fsanitize-cfi-cross-dso is used, otherwise these calls trigger a CFI violation.

* The exception to this rule are functions protected with __declspec(guard(suppress)). These functions must be linked via the import table or they will not be callable.

Using CFI with Visual Studio 2015

Using control flow guard with Visual Studio is extremely simple. There is a fantastic documentation page on the MSDN website that describes how to enable CFG, both via the GUI and via the command line. The quick and summary: add /guard:cf to you compiler and linker flags. That’s it.

There are a few caveats, which are only applicable if you are going to dynamically adjust valid indirect call targets via SetProcessValidCallTargets. First, you will need a new-ish version of the Windows SDK. The version that came by default with our Visual Studio 2015 install didn’t have the proper definitions, we had to install the latest (as of this writing) version 10.0.14393.0. Second, you must set the SDK to target Windows 10 (#define _WIN32_WINNT 0x0A00). Third, you must link with mincore.lib, as it includes the necessary import definitions.

Control Flow Guard Examples

We have created samples with specially crafted bugs to show how to use CFG, and some errors CFG protects against. The bugs that these examples have are not statically identified by the compiler, but are detected at runtime by CFG. Where possible, we simulate potential malicious behavior that CFG would prevent, and which malicious behavior CFG would not prevent.

These CFG examples are modified from the Clang CFI examples to show the different meaning of a valid call destination between the two implementations. Each example builds two binaries, one with CFG (e.g. cfg_icall.exe) and one without CFG (e.g. no_cfg_icall.exe). These binaries are built from the same source, and used to illustrate CFG features and protections.

We have provided the following examples:

cfg_icall

This example is an analogue of the cfi_icall example from the Clang CFI blog post, but modified slightly to work with Visual Studio 2015 and Control Flow Guard. The example binary accepts a single command line argument, with the valid values being 0-3. Each value demonstrates different aspects of indirect call protection.

  • Option 0 is a normal, valid indirect call that should always work. This should run properly under any CFI scheme.
  • Option 1 is an invalid indirect call (the destination is read from outside array bounds), but the destination is a function with the same function signature as a valid call. This works under both Clang’s CFI and CFG, but it could fail under some future scheme.
  • Option 2 is an invalid indirect call, and the destination is a valid function entry but with a signature different than the caller expects. This call fails under Clang’s CFI but works under CFG.
  • Option 3 is an invalid indirect call to a destination that is an invalid function entry point. This should fail under any CFI scheme, and this call fails under Clang’s CFI and CFG.
  • All other options should point to uninitialized memory, and correctly fail for both tested CFI implementations.

cfg_vcall

The cfg_vcall example (derived from the cfi_icall example from the previous post) shows that virtual calls are protected by CFG, when the destination is not a valid entry point. The example shows two simulated bugs: the first bug is an invalid cast to simulate something like a type confusion vulnerability. This will fail under Clang’s CFI, but succeed under CFG. The second bug simulates a use-after-free or similar memory corruption, where the object pointer is replaced by an attacker-created object, with a function pointer that points to the middle of a function. The bad call is blocked by both Clang’s CFI and CFG.

Figure 2: A Control Flow Guard violation as seen in WinDbg.
cfg_violation

cfg_valid_targets

This example is cfg_icall but modified to show how to use SetProcessValidCallTargets. The CFG bitmap is manually updated to remove bad_int_arg and float_arg from the valid call target list. Only option 0 will work; every other option will return a CFG error.

cfg_guard_ignore

Control flow guard can be disabled for certain methods; this example shows how to use the __declspec(guard(ignore)) compiler directive to completely disable CFG inside the specified method.

cfg_guard_nocf

Control flow guard can be partially disabled for certain methods; this example shows how to use the __declspec(guard(nocf)) compiler directive to disable CFG for indirect calls in a specified method, but still enable CFG for any referenced function pointers. The example compares the effects of __declspec(guard(nocf)) to __declspec(guard(ignore)).

cfg_guard_suppress and cfg_suppressed_export

Sometimes a library has security sensitive methods that should never be called indirectly. The __declspec(guard(suppress)) directive will prevent exported functions from being called via function pointer. These two examples work together to show how suppressed exports work. Cfg_suppressed_export is a DLL with a suppressed export and a normal export. Cfg_guard_suppress tries to call both exports via a pointer retrieved via GetProcAddress.

All flows must end

Now that you know what Control Flow Guard is and how it can protect your applications, go turn it on for your software! Enabling CFG is very simple, just add /guard:cf to your compiler and linker flags. To see real examples of how CFG can protect your software, take look at our CFG examples showcase. We hope that Microsoft continues to improve CFG with future Visual Studio releases.

9 thoughts on “Let’s talk about CFI: Microsoft Edition

  1. Pingback: 2016 Year in Review | Trail of Bits Blog

  2. Pingback: Control Flow Guard – win10_64bit | Hardik05's Blog

  3. Pingback: The Challenges of Deploying Security Mitigations | Trail of Bits Blog

  4. Pingback: A walk down memory lane | Trail of Bits Blog

  5. Pingback: How to check if Control Flow Guard is enabled – Anything about IT

  6. Pingback: 2017 in review | Trail of Bits Blog

  7. Pingback: Protecting Software Against Exploitation with DARPA’s CFAR | Trail of Bits Blog

Leave a Reply