In this article, we are going to look at Control Flow Guards, how they work and what are the common weaknesses.

Executive Summary

Control Flow Guard (CFG) is a new layer of defense against memory corruption vulnerabilities such as buffer/heap overflows. It works by blocking execution flow redirection to unauthorized memory locations.

It was first introduced in Microsoft Visual Studio 2015 and runs only on "CFG-Aware" versions of Windows - the x86 and x64 releases for Desktop and Server of Windows 10 and Windows 8.1 with KB3000850 update. Which means CFG must be supported by both the compiler and the operating system in order for it to work.

The Windows Defender Exploit Guard only enables CFG for images compiled and linked with /guard:cf flag (which is off by default). It's important to note that, just like with ASLR, only the code compiled with the /guard:cf has CFG protection. If the software uses modules compiled wihout CFG, those modules are not protected.

When /guard:cf is set, the compiler and linker insert extra runtime security checks to detect attempts to compromise your code.

MSEdge_-Win10__installed_idbg___Running

During compiling and linking, all indirect calls in the code are analyzed to find all locations that the code can reach when it runs correctly. This information is stored in extra structures in the headers of the EXEs and DLLs. The compiler also injects runtime checks before every indirect call in the code that ensures the target function is allowed to be called. If the check fails at runtime on a CFG-aware operating system, the operating system closes the program.

IC840004

Pentester's Summary

Let's say we are trying to execute the following indirect call.

mov esi, [esi]
call esi

Here esi contains the address of our shellcode, so we load the pointer to the shellcode back into esi and call it.

The goal of Microsoft's CFGs is to prevent this construct from being exploited by calling a malicious target. Specifically, if [esi] contains some intended program functionality instead of a shellcode, it should be allowed to execute, but not otherwise.

If we look at the same code compiled with CFG, the compiler will append a couple of extra instructions.

mov esi, [esi]
mov esx, esi
push 1
call @_guard_check_icall@4 
call esi

Before the indirect call is executed, its address is passed to _guard_check_icall function. If the operating system doesn't support CFG, nothing will happen, however, if it does, NT loader will replce the _guard_check_icall placeholder to redirect the execution to ntdll!LdrpValidateUserCallTarget , which will perform CFG-related check.

CFG Internals

At the compile time, the compiler generated a whitelist of valid CFG functions within the given binary. This list is stored in the Load Configuration data directory of the PE header, specifically:

    ULONGLONG  GuardCFCheckFunctionPointer;  
    ULONGLONG  GuardCFDispatchFunctionPointer;
    ULONGLONG  GuardCFFunctionTable;        
    ULONGLONG  GuardCFFunctionCount;
    DWORD      GuardFlags;
    ULONGLONG  GuardAddressTakenIatEntryTable;
    ULONGLONG  GuardAddressTakenIatEntryCount;
    ULONGLONG  GuardLongJumpTargetTable; 
    ULONGLONG  GuardLongJumpTargetCount;

There are 3 tables, each containing an entry to the relative virtual address (RVA) concatinated with optional flags (see below).

GuardCFFunctionTable was the first mitigation implemented. It contains the pointer to list of functions’ RVAs which the application’s code contains. It protects all the indirect calls in userland as well as in kernel mode. If the flag is set to 0, then each entry in the table will be a SuppressedCall. These are legitimate functions that will cause the application to crash if called indirectly from a not allowed source. Here is the sample of this list:

  • RaiseException
  • GetProcAddress/GetProcAddressForCaller
  • SetProcessValidCallTargets
  • SwitchToFiber
  • SetProtectedPolicy
  • SetThreadContext
  • RtlRestoreContext
  • RtlUnwindExStub
  • GetProcAddressStub
  • RtlRestoreContextStub
  • ExecuteUmsThread
  • (Wow64)SetThreadContextStub
  • UmsThreadYield
  • LdrGetProcedureAddress(Ex)
  • LdrInitializeThunk
  • longjmp
  • _C_specific_handler
  • RtlGuardRestoreContext
  • RtlSetProtectedPolicy
  • RtlFindExportedRoutineByName
  • KiUserApcDispatch
  • KiUserException
  • RtlProtectHeap
  • NtContinue
  • NtSetInformationVirtualMemory

GuardCFFunctionCount the list count of function’s RVA.

GuardAddressTakenIatEntryTable is user by drivers for kernel-side protection.

CFGuardLongJumpTarget contains security checks for the MS CRT (msvcrt, libcmt, etc) non-local goto functionality using _setjmp/longjmp. 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.

GuardCFCheckFunctionPointer is a pointer to the address of _guard_check_icall.

GuardFlags are defined as follows:

  • IMAGE_GUARD_CF_INSTRUMENTED 0x00000100
    Module performs control flow integrity checks using system-supplied support
  • IMAGE_GUARD_CFW_INSTRUMENTED 0x00000200
    Module performs control flow and write integrity checks
  • IMAGE_GUARD_CF_FUNCTION_TABLE_PRESENT 0x00000400
    Module contains valid control flow target metadata
  • IMAGE_GUARD_SECURITY_COOKIE_UNUSED 0x00000800
    Module does not make use of the /GS security cookie
  • IMAGE_GUARD_PROTECT_DELAYLOAD_IAT 0x00001000
    Module supports read only delay load IAT
  • IMAGE_GUARD_DELAYLOAD_IAT_IN_ITS_OWN_SECTION 0x00002000
    Delayload import table in its own .didat section (with nothing else in it) that can be freely reprotected
  • IMAGE_GUARD_CF_EXPORT_SUPPRESSION_INFO_PRESENT 0x00004000
    Module contains suppressed export information
  • IMAGE_GUARD_CF_ENABLE_EXPORT_SUPPRESSION 0x00008000
    Module enables suppression of exports
  • IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT 0x00010000
    Module contains longjmp target information

When a function is being called, ntdll!LdrpValidateUserCallTarget looks into the tables (also called CFGBitmaps in MicroTrend article). This table contains the starting locations of all the functions in the process space with the 8byte to a bit mapping. For example, if ntdll!LdrpValidateUserCallTarget gets 0x00f0f030 as an argument, the corresponding bit mapping will be calculated as follows:
0x00f0f030 is 00000000 11110000 11110000 00110000 in binary. The highest 3 bytes 00000000 11110000 11110000 (0x7878) is the offset for CFGBitmap. Therefore, the pointer to a four byte unit in CFGBitmap is the base address of CFGBitmap plus 0x7878.

The offset within the table is calculated by looking at the high 5 bits of the last byte. In our case, they are 00110 , let's call this value X.

If target address & 0xf == 0, then the offset is X
If target address & 0xf != 0, then the offset is X | 0x1

Since in our example 0x00f0f030 & 0xf == 0, then the bit is 00110b = 0x6 = 6.

If our value in CFG looks like this: 00000000 00000000 00100000 01000000, hence the 6th lowest bit will be one and the function will be allowed to execute: 00000000 00000000 00100000 0*1*000000.

Bypasses of CFG

The CFGBitmap space’s base address is stored in a fixed address which can be retrieved from user mode code. This was described in the implementation of CFG. This is important, security data but however, it can be easily gotten.

Non-CFG modules are a problem

If the main executable is not enabled for CFG, the process is not protected by CFG even if it loaded a CFG-enabled module.

Disabled DEP

If a process’s main executable has disabled DEP, it will bypass the CFG violation handle, even if the indirect call target address is invalid.

Low Entropy

Every bit in the CFGBitmap represents eight bytes in the process space. So if an invalid target call address has less than eight bytes from the valid function address, the CFG will think the target call address is “valid.”

JIT

JIT generated code is not protected by CFG. This is because NtAllocVirtualMemory will set all “1” in CFGBitmap for allocated executable virtual memory space. It’s possible that customizing the CFGBitmap via MiCfgMarkValidEntries can address this issue.

You can get a pretty good idea of CFG's shortcomings by looking at the Microsoft's bug bounty schedule. The following CFG attacks are out of scope:

  • Hijacking control flow viare turn address corruption
  • Bypasses related to limitations of coarse-grained CFI (e.g. calling functions out of context)
  • Leveraging non-CFG images
  • Bypasses that rely on modifying or corrupting read-only memory
  • Bypasses that rely on CONTEXT record corruption
  • Bypasses that rely on race conditions or exception handling
  • Bypasses that rely on thread suspension
  • Instances of missing CFG instrumentation prior to an indirect call
  • Code replacement attacks

And only this one is in scope:
Techniques that make it possible to gain control of the instruction pointer through an indirect call in a process that has enabled CFG.

This means the general trend to bypassing CFG is not engage it at all. Quoting Sun Tzu: "The supreme art of war is to subdue the enemy without fighting."

Conclusion

Just like with ASLR+DEP, the CFG measures are making the life of exploit writers more complicated, and, at the same time, motivating to evolve. ASLR+DEP have us ROPs and Heap Spraying, CFG will bring something of its own. And since CFG relies on both the compiler and the OS to operate, it makes it very challenging to port CFG onto Unix environment, which gives Microsoft some competitive advantage (you can learn more on CFG in Unix from Hanno Böck's presentation


Part 1 - Windows Defender Exploit Guard for Pentesters - Validate Exception Chains (SEHOP)
Part 2 - Windows Defender Exploit Guard for Pentesters - ASLR
Part 3 - Windows Defender Exploit Guard for Pentesters - DEP
Part 4 - Windows Defender Exploit Guard for Pentesters - CFG
Part 5 - Windows Defender Exploit Guard for Pentesters - CIG & ACG