How Control Flow Integrity is implemented in Windows 10
This blog post resume a month of work analyzing how Windows has implemented Control Flow Integrity in Win10 build 14393 and 14986.
A lot have been said about CFG, the Windows’s version of control flow integrity. It has been released first in Windows 8.1 Update 3 (
KB3000850) in 2014 and had been improved upon since. Coming recently with the Windows 10 Anniversary Update (i.e. build
14393) several new features like
The rationale behind
CFG is to ensure dynamically that indirect pointer calls jumps to a valid target. Microsoft teams takes advantage of the fact they control the
PE executable file format, the compiler and the NT
PE loader which acts together to provide runtime check of an
icall’s target address.
msvc, the compiler behind Visual Studio, has a link directive
/cfguard which, when enabled, will wrap all the indirect calls in a given binary by a call to
_guard_check_icall (and on build 14986 by
_guard_dispatch_icall). It will also generate a whitelist of
CFG approved targets within the given binary. This list is stored in the
Load Configuration data directory in the
The NT loader will, on a module load (see
ntdll!LdrSystemDllInitBlock ), parse the
Load Configuration entry to look for
CFG aware capabilities and, if enabled, will generate a
CFG bitmap storing all the valid targets address from the
CFG whitelist in the module.
The wrapper function
_guard_check_icall is actually a placeholder at compile-time and will be patched by the NT loader on module loading to point to
LdrpValidateUserCallTarget to do the actual check.
CFG entries in PE
Currently on build
14393, the load configuration data directory is the following structure :
There is a
GuardFlags members specifying which
CFG mitigations are met :
They are three optional tables for
Each table is an virtual address to an array of RVA entries concatenated with an optional flag “header” based on which
GuardFlags flag is set. In theory the flags header can be up to 8 bytes, but it probably won’t exceed a byte.
+============+-------++============+-------++============+-------++============+-------+ | RVA | flags || RVA | flags || RVA | flags || RVA | flags | +============+-------++============+-------++============+-------++============+-------+ Entry #1 Entry #2 Entry #3 Entry #4
GuardCFFunctionTable table is historically the first
CFG mitigations implemented. It protects all the indirect calls in userland as well as in kernel mode. You can dump it from a
PE via the following command :
dumpbin /HEADERS /LOADCONFIG "some_executable.exe".
Some executables have extended CFG entries (i.e. with a header) in their load configuration, like
kernel32.dll. The only flag used for the moment in user mode is the bit 0, marking a
CFG target as a
If you try to indirectly call a
CFG approved target containing a
SuppressedCall bit flag (like
kernelbase!GetProcAddress), the process will crash. If you look at some
SuppressedCall targets, you can notice they are not so innocent looking :
From this list we can infer most of them either have a direct impact on the way
CFG is implemented (
RtlSetProtectedPolicy) or are legit control flow mechanisms (
It is understable that these “dangerous” functions should be allowed to be called indirectly, but they should be deactivated by default. The function which explicitly activate the call target is
ntdll!RtlpGuardGrantSuppressedCallAccess and for obvious reasons is not
SuppressedCall header flag parsing has been added to Process Hacker’s
peview.exe executable (Github PR):
There is also an unknown header flag (bit 1) located in the
CFGuardFunctions table entries present only in kernel-side
PE's, but I haven’t found the reason behind it or where it’s used.
Only drivers PE’s contains entries in
CFGuardAddressInIatEntry, indicating a kernel-side protection. We know
MSR hooking are already watched and protected by
GuardAddressTakenIatEntryTable extend those protections by marking some imports as protected through the use of
ntosknrl!MiProcessKernelCfgAddressTakenImports. The protection looks like to be done in
Virtual Secure Mode (VSM). Since I don’t have a
VSM enabled system around, I can’t say much about this mitigation.
If you want to know more about
VSM, A.Ionescu bothered to investigate
Hyper-V hyperguard protection here :
The MS CRT (
libcmt, etc.) has a non-local
goto functionality using
longjmp coming from the Unix world which completely bypass CFG by design. Since it’s a legit control flow feature (although I never used it) MS cannot just blacklist it.
Instead, on a
longjmp call, the process will call
kernelbase!GuardCheckLongJumpTargetImpl which check that the target stack pointer (
*sp) is actually located in the thread stack space.
For more information on this security feature, read this extremely detailed blog post :
RFG has not been released yet in build 14986. Once it’s out there, this section will be improved.
In the meantime, you can read this white paper : http://xlab.tencent.com/en/2016/11/02/return-flow-guard
CFG, Microsoft and the Windows team has done a tremendous job of raising the bar for attackers to actually use their exploit primitives. Combined with
CFG has the potential of annihilating memory corruption exploits. (well the same thing was also said for
ASLR+DEP before popularization of
heap spraying techniques)
One thing to mention is
CFG is the second mitigation technique taking advantage of the fact that Windows is an integrated environment, the first being
KASLR. It is interesting from a “marketing” point of view :
CFG can not be easily ported in a
linux since there is a myriad of C CRT running in the same userland. We might see more mitigations techniques based on this uniqueness in the future.
CFG is not the perfect mitigiation technique (none really are). Firstly it only check the call target, not the call arguments. Secondly, the backward compatibility legacy code does not benefit from
CFG until they are re-compiled by a compiler >= VS2015. Lastly, there are already some
CFG bypass publicly present on the Internet : https://medium.com/@mxatone/mitigation-bounty-4-techniques-to-bypass-mitigations-2d0970147f83#.a5oytwbnq.
Maybe in the future a new type of generic bypass will be invented (like
DEP) but actually all the existing bypass relies on pretty strong hypothesis (
CLR module loaded, etc.). Here is the take of a
pwn2own winner on current NT kernel security state and post-CFG exploitation : http://www.slideshare.net/PeterHlavaty/you-didnt-see-its-coming-dawn-of-hardened-windows-kernel
- MS blog post from CFG dev : https://blogs.msdn.microsoft.com/vcblog/2014/12/08/visual-studio-2015-preview-work-in-progress-security-feature/
- Overview of CFG : https://blog.trailofbits.com/2016/12/27/lets-talk-about-cfi-microsoft-edition
- CFG From windows : https://msdn.microsoft.com/en-us/library/windows/desktop/mt637065(v=vs.85).aspx
- GrantSuppressedCallAccess : https://breakingmalware.com/documentation/documenting-undocumented-adding-control-flow-guard-exceptions/
- CFG limitations : https://blog.scrt.ch/2017/01/27/exploiting-a-misused-c-shared-pointer-on-windows-10
- setjmp : http://man7.org/linux/man-pages/man3/setjmp.3.html
- setjmp in MS : https://msdn.microsoft.com/en-us/library/yz2ez4as.aspx
- LongJump hardening : http://blog.trendmicro.com/trendlabs-security-intelligence/control-flow-guard-improvements-windows-10-anniversary-update
- RFG : http://xlab.tencent.com/en/2016/11/02/return-flow-guard