Introduction and Motivations

As is typically the case for me, whenever a new Windows build is released, I diff the Windows Defender Application Control (WDAC, formerly Device Guard) code integrity policy schema (located in %windir%\schemas\CodeIntegrity\cipolicy.xsd ) to see if there are any new, interesting features. I resort to doing this because new WDAC features are seldom documented and I’m always intrigued by new security features that are related to my areas of interest. When Windows 10 1803 was released, I noticed a new policy rule option called “Enabled:Dynamic Code Security.” Googling this feature name, as expected, I found nothing. The name of the feature intrigued me though because it felt like it might be related to my blog post (a recommended read for background) where I described a race condition vulnerability in dynamic .NET code compilation methods. The vulnerability resulted in a generic WDAC bypass that Microsoft chose to not fix at the time.

My goal for this blog post is to not only describe the mechanics of this new feature, but more importantly, I wanted to use this opportunity to paint a picture of the methodology I applied to understand and attempt to bypass the feature. So, if you’re already interested in WDAC features, great. If you’re not, that’s also cool but I hope you’ll follow along with the specific strategies I took to understand an undocumented Windows feature. My motivation to offer you this post is an acknowledgment that while there is an incredible wealth of security research out there, too seldom does the researcher offer their unique insight into how they arrived at their conclusions. I personally always care more about the “how” than the “what”.

Setting the Stage: Enabling “Enabled:Dynamic Code Security” and Conducting Observations

To test the “Enabled:Dynamic Code Security” option, I applied the new configuration option to the AllowAll.xml policy present in %windir%\schemas\CodeIntegrity\ExamplePolicies yielding the following simple policy:

<?xml version="1.0" encoding="utf-8"?>

<SiPolicy xmlns="urn:schemas-microsoft-com:sipolicy">

<VersionEx>1.0.0.0</VersionEx>

<PolicyTypeID>{A244370E-44C9-4C06-B551-F6016E563076}</PolicyTypeID>

<PlatformID>{2E07F7E4-194C-4D20-B7C9-6F44A6C5A234}</PlatformID>

<Rules>

<Rule>

<Option>Enabled:Unsigned System Integrity Policy</Option>

</Rule>

<Rule>

<Option>Enabled:Advanced Boot Options Menu</Option>

</Rule>

<Rule>

<Option>Enabled:UMCI</Option>

</Rule>

<Rule>

<Option>Enabled:Update Policy No Reboot</Option>

</Rule>

<Rule>

<Option>Enabled:Dynamic Code Security</Option>

</Rule>

</Rules>

<!--EKUS-->

<EKUs />

<!--File Rules-->

<FileRules>

<Allow ID="ID_ALLOW_A_1" FileName="*" />

<Allow ID="ID_ALLOW_A_2" FileName="*" />

</FileRules>

<!--Signers-->

<Signers />

<!--Driver Signing Scenarios-->

<SigningScenarios>

<SigningScenario Value="131" ID="ID_SIGNINGSCENARIO_DRIVERS_1" FriendlyName="Auto generated policy on 08-17-2015">

<ProductSigners>

<FileRulesRef>

<FileRuleRef RuleID="ID_ALLOW_A_1" />

</FileRulesRef>

</ProductSigners>

</SigningScenario>

<SigningScenario Value="12" ID="ID_SIGNINGSCENARIO_WINDOWS" FriendlyName="Auto generated policy on 08-17-2015">

<ProductSigners>

<FileRulesRef>

<FileRuleRef RuleID="ID_ALLOW_A_2" />

</FileRulesRef>

</ProductSigners>

</SigningScenario>

</SigningScenarios>

<UpdatePolicySigners />

This policy allows all user mode and kernel mode code to execute. At this point, I wasn’t sure if this lax policy would yield any noticeable enforcement differences, but it was worth a try. I enabled the policy with the following PowerShell command and then rebooted:

ConvertFrom-CIPolicy -XmlFilePath .\AllowAll_Modified.xml -BinaryFilePath C:\Windows\System

32\CodeIntegrity\SIPolicy.p7b

My first enforcement test was to try calling Add-Type (the trigger I used for my original race condition bypass). My assumption was that with this WDAC feature enabled, C# compilation and subsequent loading from trusted code (i.e. approved per policy) should just work. In my blog post, I imported the “PSDiagnostics” module to trigger the race condition as it is a signed module that calls Add-Type (a cmdlet that is otherwise not permitted to execute from untrusted code). Trying to import the module failed, however, with “Dynamic Code Security” enabled.

“Dynamic Code Security” feature causing PowerShell to throw an exception

Well that was certainly not expected. To confirm that the error was related to “Dynamic Code Security” enforcement, I validated that the PSDiagnostics module loaded without issue when Device Guard was not enforced as well as with Device Guard enforced using the vanilla, unmodified AllowAll.xml policy. An important skill in security research is the application root cause analysis which includes isolating issues. So, at this point, all I’ve confirmed is the following:

Enabling “Dynamic Code Security” in a code integrity policy for a currently unknown reason breaks calls to Add-Type in trusted code which is not expected behavior. This is counter-intuitive to me because trusted code calling Add-Type should work. From a research perspective, however, the exception thrown is good as it gives me a concrete place to start identifying where within code “Dynamic Code Security” is enforced.

Isolating the Issue

To identify the code that threw the exception in the screenshot above, the first thing I often do is obtain a stack trace for the exceptions. There are two exceptions here, so I will dump the stack traces of both:

An absence of a stack trace following the two exceptions thrown

Well that’s not incredibly helpful. Now, before I crack open my .NET decompiler and debugger to find the root cause of the exceptions, I thought it would be helpful to validate that C# artifact compilation was actually taking place. To do that, I’ll use procmon to validate that compilation artifacts (i.e. temporary .cs and .dll files) are dropped. If compilation does or does not occur, that can help narrow my investigation.

After running procmon and filtering “Process Create” and “WriteFile” operations for the powershell.exe process and any of its child processes, I confirmed that compilation artifacts were not being created which I know from previous experience observing DLLs being dropped and cvtres.exe launching as a child process of csc.exe:

C# Compilation artifacts viewed in procmon.exe

To validate that command-line arguments to csc.exe were supplied properly, I wanted to inspect the contents of the temporary .cmdline file (the file that supplies most of the arguments to csc.exe) present in the procmon trace. Because those files are very quickly deleted, I used a dumb brute-force approach to obtain it using the following one-liner which I ran in another PowerShell process while I attempted to import PSDiagnostics in the other process:

while ($true) { ls $Env:TEMP\*.cmdline | cp -Destination C:\Test\ }

That little trick was successful. Here are the contents of the .cmdline file:

/t:library /utf8output /EnforceCodeIntegrity /R:"System.dll" /R:"C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll" /R:"System.Core.dll" /out:"C:\Users\UnprivilegedUser\AppData\Local\Temp\jz24g5tn.dll" /debug- /optimize+ /warnaserror /optimize+ "C:\Users\UnprivilegedUser\AppData\Local\Temp\jz24g5tn.0.cs"

Immediately, the /EnforceCodeIntegrity switch stood out to me as I had never seen it and as the name clearly describes, it’s related to the enforcement of code integrity. This is a great clue that I can search for in code now.

I have several investigation paths ahead of me now: I could begin to reverse csc.exe to identify how /EnforceCodeIntegrity works or I could identify the .NET code that supplies the /EnforceCodeIntegrity switch to csc.exe… or both. I determined that the easiest route initially would be to identify the .NET code that supplies the switch. Before doing that, however, let’s check if there’s any documentation for the command-line switch. The built-in help for csc.exe provided me with a little bit of context:

Enforce code intergrity checks on all inputs to the compiler and enable loading compiled assemblies by other programs that enforce code integrity if the operating system is configured to do so.

No, that’s not my spelling error. As a reminder at this point, it is important to remember that when performing security research/reverse engineering, there is no one right way to arrive to the answers you seek. You simply follow the breadcrumbs (which veer off in multiple directions) until you reach a clearing in the forest (yes, just follow along with the metaphor) that presents you with the clarity needed to proceed to the final destination. Reverse engineering is the equivalent of collecting puzzle pieces for a puzzle where you don’t have a clear picture of its final, completed state.

Anyway, back to our current puzzle… I used my favorite .NET decompiler, dnSpy to hunt down the /EnforceCodeIntegrity string. Success. It found and decompiled the following snippet present in the Microsoft.CSharp.CSharpCodeGenerator.CmdArgsFromParameters method in System.dll:

if (FileIntegrity.IsEnabled)

{

stringBuilder.Append("/EnforceCodeIntegrity ");

}

Cool. Now to identify the conditions that result in FileIntegrity.IsEnabled returning true, which to be clear, can be inferred that that was the case since /EnforceCodeIntegrity was supplied in the .cmdline file.

Clicking on “IsEnabled” and observing at its references (right-click, select “Analyze”), you will see that it is set with the following code:

private static readonly Lazy<bool> s_lazyIsEnabled = new Lazy<bool>(delegate()

{

Version version = Environment.OSVersion.Version;

if (version.Major < 6 || (version.Major == 6 && version.Minor < 2))

{

return false;

}

bool result;

using (SafeLibraryHandle safeLibraryHandle = SafeLibraryHandle.LoadLibraryEx("wldp.dll", IntPtr.Zero, 2048))

{

if (safeLibraryHandle.IsInvalid)

{

result = false;

}

else

{

IntPtr moduleHandle = UnsafeNativeMethods.GetModuleHandle("wldp.dll");

if (!(moduleHandle != IntPtr.Zero) || !(IntPtr.Zero != UnsafeNativeMethods.GetProcAddress(moduleHandle, "WldpIsDynamicCodePolicyEnabled")) || !(IntPtr.Zero != UnsafeNativeMethods.GetProcAddress(moduleHandle, "WldpSetDynamicCodeTrust")) || !(IntPtr.Zero != UnsafeNativeMethods.GetProcAddress(moduleHandle, "WldpQueryDynamicCodeTrust")))

{

result = false;

}

else

{

int num = 0;

int errorCode = UnsafeNativeMethods.WldpIsDynamicCodePolicyEnabled(out num);

Marshal.ThrowExceptionForHR(errorCode, new IntPtr(-1));

result = (num != 0);

}

}

}

return result;

});

This was definitely the first explosion of clues/paths to investigate based on all the references to wldp.dll functions. At this point, I was happy because I’ve reversed wldp.dll (Windows Lockdown Policy) previously as it is the DLL that user mode code uses to obtain the WDAC enforcement status/policy. The wldp.dll functions in the snippet above are all new for 1803 so I’ll certainly have some reversing to do.

This is a good point in which to pause though. There is a lot of code potentially that should be reversed. Where to start? To answer that question, you need to frequently remind yourself what the goal of your reversing efforts are. Thus far, the more I learn about the implementation, my goals have shifted and/or expanded. My first goal was to confirm that enabling “Dynamic Code Security” would mitigate the Device Guard bypass I reported and blogged about. When I discovered that the mitigation seems to be broken, my goal then became to identify the root cause of why it was breaking. A new, tangential goal that entices me as an attacker though is to identify conditions in which I could perhaps trick System.dll (or any other application that performs such enforcement validation) to think that “FileIntegrity” is not enabled. We’ll come back to later. My priority remains to identify why Add-Type fails with trusted code though. After all, what good is a bypass to a broken mitigation?

So, I will make a note to myself to come back to the wldp.dll functions and understand how they work after I’ve identified the root cause of the Add-Type issue.

Root Cause Analysis

Now, I still don’t have a clear picture of why Add-Type failed when it was called from legitimate code. The original exception supplied some context though:

‘c:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll’ could not

be opened — ‘Common Language Runtime Internal error: 0xd0000428’

I sensed that this was possibly related to the inclusion of System.Mangement.Automation.dll as a reference in the .cmdline file:

/R:"C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll"

The 0xd0000428 error code doesn’t tell us much though and Googling it didn’t yield anything immediately obvious. It might serve as a frame of reference later on though so it’s important to take note of it. In reverse engineering, the few clues you may have will almost always be of value at some later point. Bonus points by the way if you recognize 0xd0000428 as an NTSTATUS value converted to an HRESULT based on the 0xd prefix.

Looking at the decompiled code for the Add-Type command, I saw that there was no way to avoid not supplying csc.exe with the System.Management.Automation.dll reference. Personally, I’d rather add the reference myself to Add-Type using the -ReferencedAssemblies parameter if I was actually compiling PowerShell-related code (e.g. a cmdlet) but that’s just me.

Since the stack trace from earlier didn’t give me any hints about where the exception originated from, I decided to crack open WinDbg and I traced calls to kernel32!CreateFileW (the function most often used for file operations) in csc.exe that referenced System.Management.Automation.dll. It didn’t take long to see that wldp!WldpQueryDynamicCodeTrust was called using the handle returned by CreateFileW in clr.dll. Since we determined earlier on that the wldp.dll functions were of interest, I noted the return value of WldpQueryDynamicCodeTrust. Sure enough, it was 0xd0000428 which translates to the following human-readable error:

Windows cannot verify the digital signature for this file. A recent hardware or software change might have installed a file that is signed incorrectly or damaged, or that might be malicious software from an unknown source.)

By the way, I performed the error code translation using the “!error” command in WinDbg, an extremely useful command when working with unknown error codes. It is helpful to get accustomed to recognizing error code types. In this case, I recognized that 0xd0000428 was an HRESULT value converted from the NTSTATUS value of 0xc0000428. Knowing that, I could have searched for that value in the SDK or WDK. The value is defined in ntstatus.h and is named STATUS_INVALID_IMAGE_HASH.

So, the 0xd0000428 is the error code is exactly what was reported in the exception message. At this point, without knowledge of how WldpQueryDynamicCodeTrust works, my gut is telling me that perhaps there was code that forgot to establish trust or the validate image integrity of System.Management.Automation.dll. Now that I’ve seen several references to the wldp.dll functions, I’ve justified to myself that I need to spend time understanding them now. I also don’t really feel like trying to identify precisely why that specific error code was returned upon attempting to validate System.Management.Automation.dll. I would like to see though if I can circumvent the error by just removing the command line reference to it.

My strategy to remove the System.Management.Automation.dll assembly reference line from the dropped .cmdline file was to overwrite the file with the same content as before minus the System.Management.Automation.dll reference. This is basically what I did before in my original blog post only instead of hijacking the .cs file dropped, I’ll hijack the .cmdline file dropped. While we’re at it, is there anything else that I could consider hijacking in the .cmdline file? How about removing /EnforceCodeIntegrity ? Hehe. We’ll come back to that later.

After performing the .cmdline hijack excluding the assembly reference for System.Management.Automation.dll the Add-Type call in the PSDiagnostics module worked great! I’ll leave it to the .NET team to fix that bug. It’s worth noting now too that other applications that use the C# compilation methods (e.g. msbuild.exe) don’t suffer from this bug if no additional assembly references are supplied. Considering PowerShell is a frequent vector for code execution though, it’s nice to know that I can now evade the bug (which sounds really funny when I say that to myself — “evade the bug”) and carry on with my research of the “Dynamic Code Security” feature. Also, I wanted to exploit the opportunity to elaborate on root cause analysis methodology. I hope this section was useful to you.

Our next goal will now be to understand the implementation of the following dynamic code-related export functions in wldp.dll:

WldpIsDynamicCodePolicyEnabled

WldpQueryDynamicCodeTrust

WldpSetDynamicCodeTrust

Reversing the New WLDP Functions

Let’s start with WldpIsDynamicCodePolicyEnabled as that sounds like it would be a function that would be called first prior to setting or retrieving dynamic code policy trust. Throwing it into IDA with symbols loaded yields a relatively straightforward function (intentionally left unannotated for now):

Unannotated WldpIsDynamicCodePolicyEnabled function in IDA

So, this function consists of just a simple call to NtQuerySystemInformation and some sort of comparison. It’s unclear though what type of information is retrieved from NtQuerySystemInformation. To determine what information is retrieved, you should look at the enum value passed via the first argument (RCX — the first argument to a function in the x64 ABI) — 0xA4 (164 in decimal). That enum value is not documented, however. Fortunately, symbols can supply us with context by dumping the SYSTEM_INFORMATION_CLASS enum. This is performed in WinDbg with the following command:

dt ole32!SYSTEM_INFORMATION_CLASS

After dumping the enum in WinDbg, the 0xA4 resolves to “SystemCodeIntegrityPolicyInformation”. This enum value specifies the type of structure returned by NtQuerySystemInformation. What structure is returned? Well, that is also not documented. To determine what type of structure might be returned, my idea was to search loaded symbols for structures containing the string “CODEINTEGRITY” and “INFORMATION”.

dt ole32!*CODEINTEGRITY*INFORMATION*

I was lucky in that it returned a good candidate structure definition — ole32! SYSTEM_CODEINTEGRITYPOLICY_INFORMATION. Now, how did I know that that was the right structure? I compared the size of the structure reported in WinDbg to that of the size passed to NtQuerySystemInformation — 0x20:

dt -v ole32!SYSTEM_CODEINTEGRITYPOLICY_INFORMATION

struct _SYSTEM_CODEINTEGRITYPOLICY_INFORMATION, 4 elements, 0x20 bytes

+0x000 Options : Uint4B

+0x004 HVCIOptions : Uint4B

+0x008 Version : Uint8B

+0x010 PolicyGuid : struct _GUID, 4 elements, 0x10 bytes

At this point, I was confident that that was the right structure because the sizes matched, and the name of the structure matched that of the enum value. Now I had enough information to apply the structure to the function in IDA which allowed me to focus on the portion of the function that performed the comparison of the data returned from NtQuerySystemInformation:

Annotated basic block in WldpIsDynamicCodePolicyEnabled that validates configured code integrity options

So, what does the “Options” field refer to and why is it compared to 0x110? Again, unfortunately, this field is not documented but sometimes you can get away with cheating by finding enum and structure definitions in .NET code. Fortunately, I was lucky and some of the values are present in System.Management.Automation.dll code:

internal enum CodeIntegrityPolicyOptions : uint

{

CODEINTEGRITYPOLICY_OPTION_ENABLED = 1u,

CODEINTEGRITYPOLICY_OPTION_AUDIT_ENABLED,

CODEINTEGRITYPOLICY_OPTION_WHQL_SIGNED_ENABLED = 4u,

CODEINTEGRITYPOLICY_OPTION_EV_WHQL_SIGNED_ENABLED = 8u,

CODEINTEGRITYPOLICY_OPTION_UMCI_ENABLED = 16u,

CODEINTEGRITYPOLICY_OPTION_SCRIPT_ENFORCEMENT_DISABLED = 32u,

CODEINTEGRITYPOLICY_OPTION_HOST_POLICY_ENFORCEMENT_ENABLED = 64u,

CODEINTEGRITYPOLICY_OPTION_POLICY_ALLOW_UNSIGNED = 128u

}

I can see that the 0x10 (16) in the 0x110 reference translates to “CODEINTEGRITYPOLICY_OPTION_UMCI_ENABLED” but 0x100 isn’t present in that enum. I can only assume that 0x100 (256) was added recently and the .NET enum wasn’t updated accordingly because there was no immediate need for that value. So, my assumption currently is that “DynamicCodePolicy” is enabled if both UMCI and the “Dynamic Code Security” options are enabled — i.e. binary ORing 0x10 and 0x100 together, resulting in 0x110. This should seem intuitive since the mitigation is related to the feature we’re talking about in this post and dynamic code enforcement is only relevant in user mode code enforcement (i.e. UMCI) scenarios.

Moving forward, I could investigate how the kernel supplies the SYSTEM_CODEINTEGRITYPOLICY_INFORMATION structure to WldpIsDynamicCodePolicyEnabled to see if there’s any attack surface (i.e. identifying ways to have the OS report that “DynamicCodePolicy” is not enforced) but I’d rather spend my time first understanding the implementation of WldpSetDynamicCodeTrust. That’s definitely a function I’d ideally like to influence as an attacker since it, as the name implies, can “set trust” on a file from user-mode. What is “trust” in the context of that function? No clue. Let’s find out.

Reversing WldpSetDynamicCodeTrust

Like WldpIsDynamicCodePolicyEnabled, WldpSetDynamicCodeTrust is also a pretty straightforward function. Rather than calling NtQuerySystemInformation though, it calls NtSetSystemInformation, Like before though, we will need to determine the enum value and structure type supplied to NtSetSystemInformation if we want to gain an understanding of the function.

Unannotated WldpSetDynamicCodeTrust implementation in IDA

So, the enum value needing to be resolved for the first argument (again, passed via RCX) is 0xC7. Using the same discovery process discussed above, 0xC7 resolves to “SystemCodeIntegrityVerificationInformation” which corresponds to another undocumented structure: SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION.

dt -v ole32!SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION

struct _SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION, 3 elements, 0x18 bytes

+0x000 FileHandle : Ptr64 to Void

+0x008 ImageSize : Uint4B

+0x010 Image : Ptr64 to Void

So all we can see is that WldpSetDynamicCodeTrust receives a file handle as an argument and passes it through to NtSetSystemInformation. If you were to look at the implementation of NtSetSystemInformation, you would see that it is just a syscall. So in order to identify what SystemCodeIntegrityVerificationInformation does after it transitions into the kernel, we’ll need to reverse some kernel code. One strategy could be to trace the syscall into the kernel with a kernel debugger or to try to identify code that might be related to “dynamic code trust” functionality, set a breakpoint on that function, and see if we arrive at that function. We’ll attempt the latter since it might yield a quicker result, if we’re lucky.

The first obvious place to look for relevant functionality would be in ntoskrnl.exe since that is the module that implements NtSetSystemInformation on the kernel side. From experience, I also know that code integrity/image verification functionality is implemented in ci.dll (ci — code integrity). That was the first place I wanted to look so I loaded it into IDA, applied symbols, and searched for a function that had “DynamicCode” in its name.

The following functions showed up as a result of my search:

SIPolicyDynamicCodeSecurityEnabled

CiValidateDynamicCodePages

CipQueryDynamicCodeTrustClaim

CiSetDynamicCodeTrustClaim

CiHvciValidateDynamicCodePages

See a good kernel candidate related to the WldpSetDynamicCodeTrust function? CiSetDynamicCodeTrustClaim looks like a good one to me. Why? Well, it has a very similar naming scheme as WldpSetDynamicCodeTrust.

CiSetDynamicCodeTrustClaim is not a terribly complex function and it essentially does one thing — it sets an NTFS extended attribute on the file handle (technically, a FILE_OBJECT) it receives.

CiSetDynamicCodeTrustClaim setting an NTFS extended attribute

The “$Kernel.Purge” string stood out to me because I had seen it described/abused previously by James Forshaw in the Device Guard bug he described here. The “$Kernel.Purge.TrustClaim” extended attribute name is new though and I’m curious to see what data is associated with that extended attribute.

The second argument to FsRtlSetKernelEaFile takes a FILE_FULL_EA_INFORMATION structure. How it’s populated can be seen in IDA but dumping it in WinDbg can be useful as well:



+0x000 NextEntryOffset : 0

+0x004 Flags : 0 ''

+0x005 EaNameLength : 0x18 ''

+0x006 EaValueLength : 0xc

+0x008 EaName : [1] "$" kd> dt OLE32!FILE_FULL_EA_INFORMATION @rdx +0x000 NextEntryOffset : 0+0x004 Flags : 0 ''+0x005 EaNameLength : 0x18 ''+0x006 EaValueLength : 0xc+0x008 EaName : [1] "$"

ffffb48f`f390bed1 00080001 00000000 00000000 kd> dd @rdx +21 L3ffffb48f`f390bed1 00080001 00000000 00000000

The “dd” (dump dword) command in the example dumps the value of the extended attribute. It is unclear at the moment what the 0x80001 value refers to. To be clear, “L3” tells WinDbg to dump 3 DWORD values which equals 0xC bytes — the value reported in the EaValueLength field.

So, my understanding thus far is that when WldpSetDynamicCodeTrust is called in user mode, the kernel applies the “$Kernel.Purge.TrustClaim” extended attribute to the file specified as some sort of marker to be referenced later. Reversing WldpQueryDynamicCodeTrust ought to confirm that theory as well.

Also, it is beneficial to confirm that we actually hit CiSetDynamicCodeTrustClaim in a debugger and to view the stack frame. As can be seen below, we do indeed arrive to this function from the “MarkAsTrusted” .NET method (described earlier):

kd> k

# Child-SP RetAddr Call Site

00 ffff818a`78d97578 fffff806`2349a087 CI!CiSetDynamicCodeTrustClaim

01 ffff818a`78d97580 fffff800`e5cb1e06 CI!CiSetInformation+0x27

02 ffff818a`78d975b0 fffff800`e57c0223 nt!NtSetSystemInformation+0x17c2fe

03 ffff818a`78d97a80 00007ffe`ef53d3c4 nt!KiSystemServiceCopyEnd+0x13

04 000000ec`ec88e6d8 00007ffe`eaec4259 ntdll!NtSetSystemInformation+0x14

05 000000ec`ec88e6e0 00007ffe`c33d39fa wldp!WldpSetDynamicCodeTrust+0x29

06 000000ec`ec88e730 000002a6`acbf0bf8 System_ni!DomainBoundILStubClass.IL_STUB_PInvoke(Int32 ByRef)$##6000000+0x14a

07 000000ec`ec88e738 000002a6`acc03880 0x000002a6`acbf0bf8

08 000000ec`ec88e740 000002a6`acbf0bf8 0x000002a6`acc03880

09 000000ec`ec88e748 00007ffe`c4de46d5 0x000002a6`acbf0bf8

0a 000000ec`ec88e750 00007ffe`c33c96bf clr!ThePreStub+0x55

0b 000000ec`ec88e800 000002a6`acb4bf50 System_ni!System.CodeDom.Compiler.FileIntegrity.MarkAsTrusted(Microsoft.Win32.SafeHandles.SafeFileHandle)$##6003CEB+0xf

Reversing WldpQueryDynamicCodeTrust

What I’m expecting to see from the implementation of WldpQueryDynamicCodeTrust is effectively the inverse of WldpSetDynamicCodeTrust. Looking at the implementation in IDA confirms that theory:

An annotated portion of the WldpQueryDynamicCodeTrust disassembly

This screenshot shows the main portion of the WldpQueryDynamicCodeTrust function where it calls NtQuerySystemInformation using the same enum value that was passed to NtSetSystemInformation in WldpSetDynamicCodeTrust. No actual validation is performed on the “$Kernel.Purge.TrustClaim” extended attribute that was set by the kernel. Instead, it trusts that the kernel validated it properly and it only considers whether or not NtQuerySystemInformation returned an error/warning — an error/warning being any return value that has its high bit set (i.e. greater than or equal to 0x80000000). For reference, I knew that based on the use of the “jns” instruction.

So, you might be curious to know why user mode should be trusting of the kernel validating the extended attribute. I was curious too so I decided to look at the implementation of CipQueryDynamicCodeTrustClaim. I’m going to skip over some of the function implementation and just show the relevant portion that performs validation of the extended attribute data:

CipQueryDynamicCodeTrustClaim extended attribute data validation

CipQueryDynamicCodeTrustClaim retrieves the data portion of the “$Kernel.Purge.TrustClaim” extended attribute. Then, it compares the first two bytes to 1 (the second to last instruction in the screenshot above). If it is set to 1, then CipQueryDynamicCodeTrustClaim considers the file to be trusted. So this supplies a little context as to why at least a portion of the extended attribute data was set earlier:



ffffb48f`f390bed1 00080001 00000000 00000000 kd> dd @rdx +21 L3ffffb48f`f390bed1 00080001 00000000 00000000

It is still unclear what the static 0x0008 value is used for but I’m not really too concerned about that since all I’m seeing validated is the 0x0001 value.

So, at this point, I’m confident that I have a fairly clear understanding of how WldpIsDynamicCodePolicyEnabled, WldpQueryDynamicCodeTrust, and WldpSetDynamicCodeTrust are implemented from both user and kernel mode. To summarize, they do the following:

WldpIsDynamicCodePolicyEnabled — validates that both user mode code integrity (UMCI) and dynamic code security (i.e. the “Enabled:Dynamic Code Security” option) are enforced.

WldpSetDynamicCodeTrust — sets the “$Kernel.Purge.TrustClaim” NTFS extended attribute on a file.

WldpQueryDynamicCodeTrust — validates that the “$Kernel.Purge.TrustClaim” NTFS extended attribute was set on the file.

So what ultimately is the purpose of setting and reading the extended attribute on a file for the purpose of mitigating the .cs race condition hijack attack? Well, it’s used so that trusted user mode code can mark dropped .cs files as trusted which can then be validated as having originated from a trusted process at a later point. The benefit of using the “$Kernel.Purge” prefix for extended attributes is that if the file is overwritten, the kernel will automatically remove the extended attribute. That means that the act of hijacking the .cs file as described in the blog post would force the removal of the extended attribute, rendering the file “untrusted.” On the surface, this seems like it should be a good mitigation… assuming the application of the mitigation is applied properly— i.e. ensuring that the proper files are marked as trusted and that files that shouldn’t be marked as trusted are not considered trusted.

Now that I have a good understanding of how things are implemented, I am now armed with the ability to ask informed questions about potential attack surface.

Attack Surface Analysis

Wanting to bypass the “Dynamic Code Security” mitigation, with which I am now intimately familiar, I asked myself the following questions:

Would it be possible to influence calling WldpSetDynamicCodeTrust on arbitrary files? For example, if I performed the .cs file hijack, could I somehow influence code (like the MarkAsTrusted method) to mark my attacker-supplied file as trusted? Are there any places where file trust is not validated or not validated properly? If so, could I influence code flow to reach a path where that validation doesn’t occur for an attacker-supplied file. Could I somehow get the host process to either not load wldp.dll into the current process or to supply a version of wldp.dll that doesn’t export the dynamic code trust functions? Could I influence WldpIsDynamicCodePolicyEnabled to report that dynamic code security is not being enforced? Reporting that it’s not being enforced would likely be the path of least resistance since file validation should not be performed if the feature is not enabled. This question will be the first focus of my attack research.

I consider these questions to be comprehensive given my current understanding of the mitigation implementation as well as my understanding of the implementation of extended attributes. As a researcher though, one should never claim to have considered all possible attack surface. Your efficacy as an offensive researcher is inherently bound by the extent of your knowledge and creativity, both of which have the potential to expand and allow you to consider attack surface you would have never known existed previously.

Attempting to Circumvent WldpIsDynamicCodePolicyEnabled

Considering there are a lot of moving parts related to compiling C# code on the fly and ultimately enforcing trust on the compilation artifacts, it is important to invest your time wisely. So to determine how I should invest my time trying to evade WldpIsDynamicCodePolicyEnabled, I needed to consider the ultimate operational goal of bypassing the mitigation — compel the C# compilation process to load my untrusted DLL. Considering this goal, the path of least resistance to a bypass would be to start looking at the very end of the compilation process — where System.Reflection.Assembly.Load(byte[]) is called on the compiled DLL. That brought me to the System.CodeDom.Compiler.FromFileBatch method:

if (!FileIntegrity.IsEnabled)

{

compilerResults.CompiledAssembly = Assembly.Load(array, null, options.Evidence);

return compilerResults;

}

if (!FileIntegrity.IsTrusted(fileStream2.SafeFileHandle))

{

throw new IOException(SR.GetString("FileIntegrityCheckFailed", new object[]

{

options.OutputAssembly

}));

}

compilerResults.CompiledAssembly = CodeCompiler.LoadImageSkipIntegrityCheck(array, null, options.Evidence);

return compilerResults;

So, if “FileIntegrity” is not enabled, the DLL will load via the normal Assembly.Load method. I would love to influence that code path if possible. How? I have to see how the “IsEnabled” property is populated. It’s populated with this code (which was also shown previously):

private static readonly Lazy<bool> s_lazyIsEnabled = new Lazy<bool>(delegate()

{

Version version = Environment.OSVersion.Version;

if (version.Major < 6 || (version.Major == 6 && version.Minor < 2))

{

return false;

}

bool result;

using (SafeLibraryHandle safeLibraryHandle = SafeLibraryHandle.LoadLibraryEx("wldp.dll", IntPtr.Zero, 2048))

{

if (safeLibraryHandle.IsInvalid)

{

result = false;

}

else

{

IntPtr moduleHandle = UnsafeNativeMethods.GetModuleHandle("wldp.dll");

if (!(moduleHandle != IntPtr.Zero) || !(IntPtr.Zero != UnsafeNativeMethods.GetProcAddress(moduleHandle, "WldpIsDynamicCodePolicyEnabled")) || !(IntPtr.Zero != UnsafeNativeMethods.GetProcAddress(moduleHandle, "WldpSetDynamicCodeTrust")) || !(IntPtr.Zero != UnsafeNativeMethods.GetProcAddress(moduleHandle, "WldpQueryDynamicCodeTrust")))

{

result = false;

}

else

{

int num = 0;

int errorCode = UnsafeNativeMethods.WldpIsDynamicCodePolicyEnabled(out num);

Marshal.ThrowExceptionForHR(errorCode, new IntPtr(-1));

result = (num != 0);

}

}

}

return result;

});

The workflow of this code snippet is as follows:

Ensure that wldp.dll is loaded into the current process by calling LoadLibraryEx. To be clear, wldp.dll must be loaded into the process because it’s the DLL that implements the “DynamicCode” functions we reversed earlier. If wldp.dll isn’t loaded into the process, then dynamic code validation cannot occur. Could an attacker somehow compel wldp.dll to not load into the current process? Yup! It’s actually really easy under normal circumstances with Windows Defender Application control being enforced by copying wldp.dll to the same directory as the executing program, flipping an insignificant bit in the PE file (e.g. a bit in the Rich header, for example), rendering the signature of wldp.dll invalid, causing it to not be loaded. To mitigate that attack scenario, this is exactly why LoadLibraryEx is called with the 2048 flag which refers to the LOAD_LIBRARY_SEARCH_SYSTEM32 option. That option, as should be obvious, overrides the default DLL load order and loads wldp.dll from %windir%\System32 first, thus mitigating the attack I just described. Well, more specifically, the attack is mitigated as a non-admin. An admin could perform this attack by modifying wldp.dll within the System32 directory. Ensure that wldp.dll exports the functions necessary to to perform dynamic code validation: WldpIsDynamicCodePolicyEnabled, WldpSetDynamicCodeTrust, and WldpQueryDynamicCodeTrust. Considering these are new functions in wldp.dll, not all versions of Windows will have these functions. There’s another attack scenario here, by the way. An attacker could supply an older version of wldp.dll in the current directory that doesn’t implement those functions. That would bypass the check, right? Nope! LoadLibraryEx to the rescue again. Call WldpIsDynamicCodePolicyEnabled and return True if it indicates that the dynamic code policy is enabled. Now, I never did reverse how the kernel determines if dynamic code security is enabled in the code integrity policy. There may be attack surface worthy of exploration. I’ll leave that as an exercise to those who are curious.

At this point, I’m not confident that I can circumvent the “FileIntegrity.IsEnabled” check. What’s next then? Follow along in the next section to find out.

Attempting to Circumvent WldpQueryDynamicCodeTrust

So assuming I can’t bypass the “FileIntegrity.IsEnabled” check, I simply proceed to the next line in the FromFileBatch method:

if (!FileIntegrity.IsTrusted(fileStream2.SafeFileHandle))

{

throw new IOException(SR.GetString("FileIntegrityCheckFailed", new object[]

{

options.OutputAssembly

}));

}

This snippet of code will throw an exception and not load the compiled DLL if it is not marked as trusted. I realize this may sound redundant but I prefer to make the following statement of inverse logic: an exception will not be thrown if the file is marked as trusted. Wording the logic that way makes it clearer to me as an attacker that I should try to find a way to influence code to mark the compiled DLL as trusted. The first step in identifying if that is possible is to identify the code that would mark the compiled DLL as trusted — i.e. calls WldpSetDynamicCodeTrust on the file. That code is not present in .NET so I looked in csc.exe. It wasn’t there either so I assumed that that code was likely present in a DLL loaded by csc.exe. To make that determination, I attached WinDbg to csc.exe when it was launched by powershell.exe, set a breakpoint for when wldp.dll was loaded ( sxe ld wldp ), and then set a breakpoint on WldpSetDynamicCodeTrust. I hit my breakpoint once and only once. I landed on the PEWriter::write function in mscorpehost.dll.

Fortunately, the PEWriter::write function is open-source. Unfortunately, the latest version that calls WldpSetDynamicCodeTrust is not on GitHub yet. That’s okay. Having outdating source code makes following along in IDA disassembly much easier. Here’s the portion of code that calls WldpSetDynamicCodeTrust:

PEWriter::write function calling WldpSetDynamicCodeTrust

So the attack scenario here would entail overwriting the compiled DLL before WldpSetDynamicCodeTrust is called. This is not possible, however because PEWriter::write holds a handle to the DLL and any other process will be denied access from trying to overwrite it while the handle is held. From the point where a handle is obtained to begin writing the DLL to disk to when WldpSetDynamicCodeTrust is called, the handle is not released. Also, mscorpehost.dll calls wldp.dll functions using the same LoadLibraryEx mitigation as System.dll does, preventing the attack described in the previous section.

I mentioned previously that it is also possible to hijack the .cmdline file dropped and remove the /EnforceCodeIntegrity switch. I was able to successfully hijack the file and remove the switch but ultimately, that didn’t buy me anything. The side effect of removing the switch is that the compiled DLL was never marked as trusted but the FromFileBatch method expects the file to be marked as trusted. So when FromFileBatch validates the trust of the DLL, it won’t be marked as trusted and an exception will be thrown.

Wrapping Up

There is still likely to be unexplored attack surface in assessing the “Dynamic Code Security” mitigation. What I focused on in particular was the point where an unsigned DLL would be loaded using the unsafe Assembly.Load(byte[]) method. There may very well be a bypass vector somewhere but in scratching the surface, a bypass is not currently obvious. Aside from the assembly reference bug mentioned earlier, I’d like to commend Microsoft on investing the effort in mitigation the race condition bypass.

Conclusion

So, after all that effort, did I find any bypasses? Nope. Have I failed as a reverser and researcher? Hell no! Throughout this process, I learned how what I view as an effective mitigation (based on my current knowledge/creativity) was implemented. I’ve also likely matured my offensive research methodology in the process. Also, how does one even find bugs if you don’t look in the first place? For me, bug hunting is less about finding bugs than it is just satisfying intellectual curiosity.

I spent a lot of time documenting this process and my methodology so I hope you found it to be interesting, educational, and motivating. I also hope that this post will motivate you to talk about and document your research methodology as applied to specific problems. What better way to help people who would otherwise be intimidated by the process?!

Also, hopefully, Microsoft will fix that silly assembly reference bug. If not, an effective mitigation that looks like it was a significant effort to implement will all be for nothing.

Thanks for reading!