In this article we are going to talk about the following WDEG features: Code Integrity Guard, Arbitrary Code Guard and "Do not allow child processes" policy.

Generall, there are two results that can be achieved with exploitation: arbitrary code execution and data-only corruption.

The arbitrary code execution can happen through a number of venues:

  • Code Reuse (with control flow integrity violation like ROP/COP/JOP)
  • Code Generation or Modification in Memory (W^X violation, JIT engines)
  • Code Loading from local or remote storage (DLL planting, path redirection, and symlink attacks)

To mitigate against these attacks, we can either completely block them (CIG) or analyze code for certain malicious attributes (AIG).

Code Integrity Guard

Code Integrity Guard (CIG) mitigates the Code Loading issue by requiring code to be digitally signed. CIG was part of Device Guard, and Microsoft used this mechanism to prevent tampering of OS drivers loaded in Windows 10. Following its launch, Microsoft allowed software developers to deploy CIG with their own applications.

To enable signature testing, the image must be compiled with /INTEGRITYCHECK option. When the option is set, the PE header of the DLL file or executable will contain a flag that tells the memory manager to check for a digital signature in order to load the image in Windows. The flag is called IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY and will only allow the to load images/DLLs signed by Microsoft, Microsoft Store, or WHQL signed DLLs.

If DLL/EXE is not signed, the kernel will fail attempts to load it by a process during process creation time rather than during process initialization to eliminate a process launch time gap where local injection of improperly signed DLLs. This was achieved by taking advantage of the UpdateProcThreadAttribute API to specify the code signing policy for the process being launched.

BOOL WINAPI UpdateProcThreadAttribute(
  _Inout_   LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
  _In_      DWORD                        	dwFlags,
  _In_      DWORD_PTR                    	Attribute,
  _In_      PVOID                        		lpValue,
  _In_      SIZE_T                       		cbSize,
  _Out_opt_ PVOID                        	lpPreviousValue,
  _In_opt_  PSIZE_T                     	lpReturnSize
);

The binary signature policy requires EXEs/DLLs to be properly signed. The following mitigation options are available for the binary signature policy:
PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_

  • BINARIES_MASK
  • BINARIES_DEFER
  • ALWAYS_ON
  • ALWAYS_OFF
  • ALLOW_STORE

As this policy is applied per-process, it is also important to prevent an attacker from spawning a new process with a weaker or non-existent CIG policy by setting the "Do not allow child processes" policy in WDEG. This policy is currently enforced as a property of the token for a content process which ensures both direct (e.g. calling WinExec) and indirect (e.g. out-of-process COM server) process launches are blocked.

Bypass

Usually CIG is bypassed with reflective memory based injection - where a malicious DLLs injects and mapsitself inside of the target process without using standard API like LoadLibrary to do that.

CIGslip is a new method (March, 2018) which can be exploited by attackers to bypass Microsoft’s Code Integrity Guard (CIG) and load malicious libraries into protected processes such as Microsoft Edge without using reflective injection techniques.

CIGSlip depends on our ability to execute a non-CIG image on the target system, which is usually possible. Then, we hook into ZwCreateSection method within the target process so that it will not go down to Kernel and will return the duplicated section handle.

NTSYSAPI NTSTATUS ZwCreateSection(
  PHANDLE            SectionHandle,
  ACCESS_MASK        DesiredAccess,
  POBJECT_ATTRIBUTES ObjectAttributes,
  PLARGE_INTEGER     MaximumSize,
  ULONG              SectionPageProtection,
  ULONG              AllocationAttributes,
  HANDLE             FileHandle
);

Since section handles are global objects managed by Kernel, handles could be duplicated between processes. Therefore, a section that correlated to a non-signed DLL could be created within the context of the malicious process and then duplicated into the target process.

Following that, the targeted process will map the dll code page into its memory as part of a regular dll loading process, assuming that the section went through the appropriate verification process. Which it did, since createsection now returns an already existing verified section handle!

Arbitrary Code Guard aka Memory Protection Check

ACG then complements this by ensuring that signed code pages are immutable and that new unsigned code pages cannot be created.

While CIG provides strong guarantees that only properly signed DLLs can be loaded from disk, it does not provide any guarantees about the state of image code pages after they are mapped into memory or dynamically generated code pages. This means an attacker can load malicious code by creating new code pages or modifying existing ones even when CIG is enabled. In practice, most modern web browser exploits eventually rely on invoking APIs like VirtualAlloc or VirtualProtect to do just this. Once an attacker has created new code pages, they then copy their native code payload into memory and execute it.

With ACG enabled, the Windows kernel prevents a content process from creating and modifying code pages in memory by enforcing the following policy:

Code pages are immutable.
Existing code pages cannot be made writable and therefore always have their intended content. This is enforced with additional checks in the memory manager that prevent code pages from becoming writable or otherwise be modified by the process itself. For example, it is no longer possible to use VirtualProtect to make an image code page become PAGE_EXECUTE_READWRITE.

New, unsigned code pages cannot be created. For example, it is no longer possible to use VirtualAlloc to create a new PAGE_EXECUTE_READWRITE code page.

Supporting Just-in-Time (JIT) Compilers.
Modern web browsers achieve great performance by transforming JavaScript and other higher-level languages into native code. As a result, they inherently rely on the ability to generate some amount of unsigned native code in a content process. Enabling JIT compilers to work with ACG enabled is a non-trivial engineering task. To support this, the JIT functionality is usually moved out of the main application into a separate process that runs in its own isolated sandbox. The JIT process is responsible for compiling JavaScript to native code and mapping it into the requesting content process. In this way, the content process itself is never allowed to directly map or modify its own JIT code pages.

In order to be able to write JITted (executable) data into the Content Process, JIT Process does the following:

  1. It creates a shared memory object using CreateFileMapping
  2. It maps it into Content Process as PAGE_EXECUTE_READ and in the JIT proces as PAGE_READWRITE using MapViewOfFile2. At this point the memory is reserved, but not yet committed.
  3. When individual pages need to be written to they are first allocated from the region in step 2 using VirtualAllocEx. This also marks the memory as committed

Limitations and bypasses

One of the limitations of CIG and ACG is that they don’t prevent an attacker from leveraging valid signed code pages in an unintended way. For Example, this means attackers could still use well-known techniques like return-oriented programming (ROP) to construct a full payload that doesn’t rely on loading malicious code into memory.

For example, in 2017, Ivan Fratric, a security engineer with Google's Project Zero team, has discovered a way to bypass ACG and allow an attacker to load unsigned code in memory, allowing attackers a way into Windows boxes via malicious websites loaded via Edge.

The issue lies in a predictability of the address that will be allocated with VirtualAllocEx and the fact that the memory is originally allocated with MEM_RESERVE and not MEM_COMMIT flag.

If a content process is compromised and the content process can predict on which address JIT process is going to call VirtualAllocEx next (note: it is fairly predictable), content process can:

  1. Unmap the shared memory mapped above above using UnmapViewOfFile
BOOL WINAPI UnmapViewOfFile(
  _In_ LPCVOID lpBaseAddress
);
  1. Allocate a writable memory region on the same address JIT server is going to write wit VirtualAlloc with PAGE_READWRITE and MEM_RESERVE | MEM_COMMIT flags and write an soon-to-be-executable payload there.
LPVOID WINAPI VirtualAlloc(
  _In_opt_ LPVOID lpAddress,
  _In_     SIZE_T dwSize,
  _In_     DWORD  flAllocationType,
  _In_     DWORD  flProtect
);
  1. When JIT process calls VirtualAllocEx, even though the memory is already allocated, the call is going to succeed and the memory protection is going to be set to PAGE_EXECUTE_READ.

This gives Content Process a block of memory that could be first written in and then executed. Which means the ACG is bypassed.

References


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