26 Jan 2021

WDAC Notes

The place to learn the most about Windows Defender Application Control (WDAC) the fastest is youtube. Matt Graeber put together an amazing set of tutorials, and if you’re trying to learn how to do it the way a pro does, i’d recommend making them your first stop.

This post is just me saving the pointers that i always go back to the videos for. I don’t recommend using the pointers without getting the additional context from the videos, but hopefully they’ll help someone else who might have tuned in a while ago and forgot the commands.

Windows Defender Application Control (WDAC) is the latest iteration on Microsoft’s application whitelisting strategy for Windows. You’ll sometimes hear it called “Code Integrity” or “Windows Code Integrity Policies” but it is not synonymous with “AppLocker” (which still exists for slightly different use cases).

If you’re looking to learn more, the starting point for official application control documentation is here.

Baseline Policy

There are two schools of thought here:

  1. Create the policy based on what is already on the machine. You could do this if you completely trust the build and want all the signature based policies to use the same rules. This is the most straightforward approach but it’s lazy and not what Matt demonstrates in his series.
  2. Create the policy by inspecting the signers in the Microsoft templates and reducing down to a set you trust, then add additional 3rd parties manually.

For option 1, you can commence the scan with:

New-CIPolicy -Level PcaCertificate -FilePath C:\CI\basepolicy.xml -UserPEs

The output is the basepolicy.xml. If you inspect the output, you’ll see that it has scanned all files and attempted to include the signers of those files in the .xml. (Note that the scan takes a while. More than 30 minutes is not uncommon).

Edit the file if you wish. The most common edit would be to remove a signer that either has no business being in the policy OR that you wish to implement a more strict rule for. For example, you might see the signer for JetBrains in the file, but rather than allow all JetBrains signed updates, you might choose to implement a hash rule for this vendor, meaning that you would need a new CI policy for any updates they provide.

Next, convert the .xml to a .p7b binary and deploy with:

ConvertFrom-CIPolicy C:\CI\basepolicy.xml C:\CI\basepolicy.bin
cp C:\CI\basepolicy.bin C:\Windows\System32\CodeIntegrity\SIPolicy.p7b

Reboot the machine.

You’re now logging events for anything not covered in that file.

The events can be found in the \WindowsEvents\CodeIntegrity section of Event Viewer.

Deployment Policy

After you’ve configured the base policy using either the Microsoft templates, or a system scan you’re ready to start capturing the outliers being logged with your audit mode policy.

Allow the machine to operate for a while as “normal”. Doing so means that you’ll capture events about binaries that are not included in the baseline ruleset and be able to make decisions about them.

Install Matt Graeber’s WDAC toolset from: https://github.com/mattifestation/WDACTools

Import the module:

Import-Module C:\tools\WDACTools\WDACTools.psd1

Because it is less likely you’ll miss anything kernel related in the base policy, start by confirming. You’re aiming for zero events:

Get-WDACCodeIntegrityEvent -Kernel

Then, move to user mode.

Collect all the events in a variable that you can use for further inspection. For example:

$all = Get-WDACCodeIntegrityEvent -User
$all | select ResolvedFilePath | Sort ResolvedFilePath

A file being shown in this output means that it is not hitting any of the allow rules in the current policy and will need a rule of some sort to be able to run when WDAC is moved to enforcement mode.

You are able to address the files on an individual level, or (suggested), bucket them into applications. As an example, on an EC2 instance you might use:

$files = Get-SystemDriver -ScanPath "C:\Program Files (x86)\AWS Tools" -UserPEs
New-CIPolicy -DriverFiles $files -FilePath C:\Users\chad\Desktop\AWS.xml -Level FilePublisher -Fallback FileName

The first command obtains a list of binaries that we’ll ask the second command to look at and generate rules for. The second command generates an .xml file full of rules for the files picked up in the first command. We can merge this new ruleset into the base ruleset once we’ve repeated the process for all the packages we care about. The second command is saying: Try to capture them in a policy that looks at the publisher certificate on the file, but if one is not available, use the filename. Hash is an alternate fallback, but it’s very strict, any upgrade will cause the CI policy to block the file. There’s more detail here.

To merge the policies we create in the previous step, we do:

Merge-CIPolicy -OutputFileName merged.xml -PolicyPaths .\Baseline.xml, \AWS.xml

You should make sure your baseline policy is specified first in the merge command. This is because the merge takes the common sections of the policy from the first file.

To deploy in audit mode, with the changes:

ConvertFrom-CIPolicy C:\CI\merged.xml C:\CI\merged.bin
cp C:\CI\merged.bin C:\Windows\System32\CodeIntegrity\SIPolicy.p7b


Then, use the -SinceLastPolicyRefresh switch to gather any remaining events related to binaries you might have missed:

Get-WDACCodeIntegrityEvent -User -SinceLastPolicyRefresh

Once you’re satisfied that the policy contains a rule for everything that you ever want to run on the machine, you edit the latest merged .xml, setting the mode to enforced, and redeploy the policy as described previously.

Editing the Kernel Policy (adding drivers)

This is slightly more complicated, but honestly not too bad thanks to Matt G advice. The problem is that all your drivers are likely to be in the same place, so the previous approach of using the application directory does not work well. The variation looks like this:

$allkernel = Get-WDACCodeIntegrityEvent -Kernel -SinceLastPolicyRefresh

Take a look at the results to identify new drivers. I like to use the resolved file path name for a better view:


You could bundle all the changes into one xml file to be merged. A better recommendation is to group them into the developer, in this example, OESIS. I first create a new folder for OESIS then use:

$allkernel.ResolvedFilePath | ls | cp -Destination .\OESIS\

To make a copy of the drivers i want to work with. (This is the step that is making it possible to use the approach we use for usermode. Now we have a path that makes sense for scans).

We obtain the signing information the same way we do user mode

$SignerInfo = Get-SystemDriver -ScanPath .\OESIS\ -NoScript -NoShadowCopy

Then create an xml using:

New-CIPolicy -FilePath OESISDrivers.xml -DriverFiles $SignerInfo -Level WHQLFilePublisher

Then, we save the OESISDrivers.xml file to our code repo containing individual catalogs, and we merge the new stuff into the main merged file (same way as the usermode changes)

Merge-CIPolicy -OutputFileName merged.xml -PolicyPaths .\merged.xml, .\OESISDrivers.xml

If you’re ready to deploy the changes:

ConvertFrom-CIPolicy C:\CI\merged.xml C:\CI\merged.bin
cp C:\CI\merged.bin C:\Windows\System32\CodeIntegrity\SIPolicy.p7b


Adding a new custom thing to a user Policy

$DriverFiles = Get-SystemDriver -ScanPath C:\customthingcustom\files\ -UserPEs
New-CIPolicy -Level PcaCertificate -Fallback FilePath -FilePath newstuff.xml -UserPEs -UserWriteablePaths -DriverFiles $DriverFiles

i thought i’d changed my tune on the above. the approach below seems good because it combines the steps:

New-CIPolicy -FilePath Laps.xml -Level Publisher -Fallback Hash -ScanPath "C:\Program Files\LAPS" -UserPEs

From what i remember, the reason you might not want to do that is because the get-systemdriver step will limit the inputs to binary code. The alternate, all in one will pick up .pyc and other such things which might be undesirable in your policy. Although, in testing today i am confused because the .pyc and friends are still being included. Will need to do more research.


Merge-CIPolicy -OutputFilePath merged.xml -PolicyPaths .\base.xml, .\newstuff.xml

Adding a new file or folder wildcard

$NewCIPolicyfile = "C:\temp\path_rules.xml"
$rules = New-CIPolicyRule -FilePathRule "C:\windows\temp\*"
$rules += New-CIPolicyRule -FilePathRule "C:\Program files (x86)\*"
$rules += New-CIPolicyRule -FilePathRule "\\svr1\packages\*"
New-CIPolicy -FilePath NewCIPolicyfile -Rules $rules -UserPEs

Add a rule option to the base file

Set-RuleOption -FilePath <Path to policy XML> -Option 0

remove with:

Set-RuleOption -FilePath <Path to policy XML> -Option 0 -Delete

Multiple Policy rules

I still have a lot of work to do before i understand this well, but a starting point is here: https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-application-control/deploy-multiple-windows-defender-application-control-policies

To make an existing policy a multiple policy base file:

Set-CIPolicyIdInfo -FilePath .\kernelbase.xml -ResetPolicyID

Doing that gets us some new information at the bottom of the file.


This one is recognizable as a base policy because the BasePolicyID is the same as the PolicyID.

For the supplementatal policy we’d use:

Set-CIPolicyIdInfo -FilePath .\merged.xml -SupplementsBasePolicyID 7D0228D1-3C74-4854-AA75-43CD955314AA -BasePolicyToSupplementPath .\kernelbase.xml

And then in the supplemental policy we’d see:


Demonstrating the relationship between the two.

To deploy we’d use:

PS C:\Users\chad\Desktop> ConvertFrom-CIPolicy -XmlFilePath .\kernelbase.xml -BinaryFilePath 'C:\Windows\System32\CodeIntegrity\CiPolicies\Active\{7D0228D1-3C74-4854-AA75-43CD955314AA}.cip'

PS C:\Users\chad\Desktop> ConvertFrom-CIPolicy -XmlFilePath .\merged.xml -BinaryFilePath 'C:\Windows\System32\CodeIntegrity\CiPolicies\Active\{9826670F-D84C-41AB-9BEA-E0E3375A92E3}.cip'

# remove the old school version
PS C:\Users\chad\Desktop> rm C:\Windows\System32\CodeIntegrity\SIPolicy.p7b

Block Rule:

New-CIPolicy -DriverFiles "C:\windows\System32\drivers\NDIS.SYS" -FilePath .\Desktop\Block.xml -Level FileName -Deny

Then, we could merge this rule into another policy.

Test Driver:

Matt creates a service with the RWEverything driver in his video:

C:\Windows\system32>sc.exe create RWEverything binpath= C:\Users\chad\Desktop\RwDrv.sys type= kernel
[SC] CreateService SUCCESS

We can also use the startup mode like:

C:\Windows\system32>sc.exe create RWEverythingBoot binpath= C:\Users\chad\Desktop\RwDrv.sys type= kernel start= system
[SC] CreateService SUCCESS
Chad Duffey

Security Engineer