Windows Defender Application Control (WDAC) Updates in 20H2 and Building a Simple, Secure Windows-only Policy
Until recently, I had gotten away from configuring Windows Defender Application Control (WDAC) until the lead-up to Christmas when I wanted to repurpose an older Microsoft Surface Gen. 1 Laptop as my young daughter’s first Windows-based computer for play and experimentation.
As a security practitioner, obviously, I want to protect her from external threats as much a possible but as a dad who is acutely aware and in awe of a child’s willingness to experiment, I acknowledge that my daughter will do everything in her power to corrupt (intentionally or otherwise) her Windows installation. Along with traditional least-privilege principles in place, WDAC, with a proper policy in place should serve as an ideal, built-in solution to both help protect her from external threats but to also mitigate self-inflicted destruction. 😉
The goal of this post is to highlight some of the awesome WDAC improvements made since the release of Windows 10 20H2 in the context of building and applying a relatively simple but very secure policy to a single system. My hope is to help supply further evidence that WDAC is worthy of your consideration if mitigating and detecting large classes of attack techniques is a priority for you and/or if you’re a parent wanting to squeeze as much shelf life out of a Windows computer for your kids.
Latest Improvements to WDAC in 20H2
Before diving into the weeds, I wanted to highlight the improvements to WDAC in 20H2 that I observed.
- There are no longer SKU licensing restrictions when building WDAC policies. Previously, one needed Window 10 Enterprise or Education to configure and build WDAC policies. This restriction has since been lifted. The policy described below was built and deployed on a Windows 10 Professional host.
- The CodeIntegrity event log no longer appears to be polluted with NGEN events — i.e. system .NET assemblies otherwise permitted by policy that cease to retain their signature upon being converted into a native image. Previously, these events always served as a nuisance and needed to be filtered accordingly while also consuming unnecessary log space.
- Since Windows version 2004, driver signing practices now finally appear to be more stringently enforced, permitting “EV Signers” and “WHQL” enforcement on at least 1st-party Microsoft hardware now. Ever since owning Microsoft hardware, I was always under the assumption that 1st-party Microsoft drivers would be properly signed, permitting the far more secure driver enforcement WDAC configuration that enforces all drivers permitted to load per policy to be WHQL and EV-signed. For the longest time, there was always a handful of drivers that prevented this configuration but at least on a fully-updated Surface Gen. 1 Laptop, this is no longer the case. From this point forward, there is no excuse for a driver to not be signed in accordance with WHQL certification requirements.
My WDAC Policy Objective
Build a secure, Windows-only policy that just works.
This objective sounds nice but warrants additional explanation:
- “Secure”: When I refer to secure, I am referring to a policy that is not overly permissive and wouldn’t present any immediately obvious bypasses. I would prefer that my policy not have any rules that would implicitly permit execution of something outside the scope of the policy, in this case, “Windows-only.” For example, if at all possible, I always try to avoid FilePath rules as those can be prone to introducing unintentional bypasses if an attacker is able to drop an executable or script into a permitted directory.
- I want a policy that will only execute Windows-signed and Microsoft Store-signed code. This laptop I’m building is for my young daughter where at this point in her life, she currently has no need to execute anything outside of the Microsoft ecosystem. “Windows-signed” is distinguished from “Microsoft-signed” code in that I am referring to Microsoft-signed code that is signed with a special certificate used only for code included by default with the operating system (and in relevant update code as well). On a technical level, Windows-signed code is identified by a Microsoft signature (yes, there’s nuance in what defines a truly legitimate Microsoft signature) where the certificate has a special enhanced key usage (EKU) attribute of 22.214.171.124.4.1.3126.96.36.199. Fortunately, WDAC is unique in its ability to build allowlist logic from certificate EKUs. Additionally, knowledge of certificate minutiae is unnecessary as Microsoft supplies a default, Windows-only policy template out of the box. The point here is to be knowledgeable of the distinction between Windows-signed and Microsoft-signed code. For example, Sysinternals, because it is not an in-box utility is Microsoft-signed, not Windows-signed.
- “Just works”: In other words, I want the absolute minimum maintenance burden for this policy. The laptop must run Windows and Store-signed code, block known bypasses, and update without issue. In theory, the only occasional maintenance that should be required will be to occasionally update block rules covering new WDAC bypasses in Windows-signed code.
When working with WDAC, it is important to consider not only the software you intend to permit but to also be mindful of its hardware since that can affect both kernel and user-mode rules for 3rd-party drivers and relevant, accompanying software.
- Laptop: Microsoft Surface Gen. 1 Laptop
- Operating System: Windows 10 Professional 20H2
- Account: My daughter’s account will be a standard, non-admin user. I will maintain an admin account that will only be used to perform maintenance and build/deploy WDAC policy.
This specification affords a unique privilege in that in theory, with both the hardware and software all being Microsoft, I should be able to get away with a minimal WDAC policy that conforms to my scope of Windows and Microsoft-Store-signed only. I’ve had mixed results in the past but as you will see in this post, 20H2 and general improvements to signing practices at Microsoft permit extremely simple and secure WDAC policies for 1st-party Microsoft hardware.
Another challenge that would have been previously imposed was that building WDAC policies used to only be possible on Windows 10 Enterprise systems. That licensing restriction has since been lifted which will permit building and deploying a policy without issue on this Windows 10 Professional build.
Building and Deploying a Custom Windows-only Policy
For this simple policy, I modified and then merged two separate policies:
- The DefaultWindows_Audit.xml template policy located in C:\Windows\schemas\CodeIntegrity\ExamplePolicies. I copy and rename this policy as BasePolicyWindows.xml in this post.
- The Microsoft recommended block rules that will serve to prevent execution of Windows-signed executables that can be used to circumvent code integrity enforcement. I name this policy UserBlockRules.xml in this post. Microsoft also supplies a set of driver block rules but these are unnecessary for this use case since we will only permit Microsoft-signed drivers to load.
- I then merge the above two policies and name the merged policy Merged.xml prior to deployment.
I made the following modifications to DefaultWindows_Audit.xml:
- I added the “Disabled:Flight Signing” rule. I have no intention to run Windows Insider Preview (WIP) builds, therefore flight-signed code (i.e. code specially signed for WIP builds) should not be permitted to execute.
- I added the “Required:EV Signers” and “Required:WHQL” rules. Again, from Windows version 2004 moving forward, there should be no reason that drivers are not EV and WHQL signed.
- I removed all signer rules that would permit flight-signed code to execute. Even with the “Disabled:Flight Signing” rule enabled, there is no reason for the signer rules to remain in the policy. The philosophy I strive to apply is that rules should only be present in my policy that I explicitly want to allow or deny.
- I removed test root signer rules. Again, I have no need to permit the execution of test-signed code so the rules should not be present in my policy.
- Finally, after performing auditing, I will remove the “Enabled:Audit Mode” rule, placing my policy into enforcement mode.
I am a huge fan of the DefaultWindows_Audit.xml template policy supplied by Microsoft as it allows Windows (excluding 3rd-party software) in-box kernel and user-mode code to boot, execute, and update without issue. Regardless of the WDAC policy I’m building, I almost always use DefaultWindows_Audit.xml as my baseline policy upon which additional rules are added/merged.
Next, I made the following modifications to the Microsoft recommended block rules:
- I removed all the policy rules. Since I will merge these rules into a base policy, I like to minimize confusion by maintaining policy rules in a single base policy, BasePolicyWindows.xml in this case.
Finally, I merged and deployed my policy using the following PowerShell cmdlets:
I then rebooted and ran the Windows build through its courses in audit mode to find no surprises. I then placed WDAC into enforcement mode by removing the “Enabled:Audit Mode” rule and since my daughter has been using it, I’ve been monitoring CodeIntegrity event log events and thus far, I only get block events for Microsoft.Build.dll and Microsoft.Build.Framework.dll being loaded by mscorsvw.exe. Both of these rules are present in the Microsoft recommended block rules added as the result of Jimmy’s findings here.
There are additional steps that could be taken to further secure the integrity of the deployed WDAC policy against a rogue admin (e.g. policy signing and UEFI policy protection) but I don’t feel they are relevant to this post.
When all was said and done, this whole process of building a policy, auditing it, placing it into enforcement mode and performing follow-on monitoring took just over an hour.
My hope is that any security practitioner who is comfortable with Windows at least experiment with WDAC. As a defender who’s professional work involves helping to develop detection logic for attack technique after attack technique, I sometimes feel defeated at the notion and expectation that every attack technique should be detected, a reality that is far from realistic or achievable in many cases. Knowing what I know about the strength of WDAC enforcement, I have direct experience in working within a reality where the majority of attack techniques are almost completely mitigated by enforcing code integrity.
Again for reference, here are the modified/generated policy files I used on my daughter’s laptop: https://gist.github.com/mattifestation/425b2c8a3b5a9b74d6938394019cc177