How to use WDAC to secure PowerShell

This article describes how to set up a Windows Defender Application Control (WDAC) policy. You can configure the policy to enforce or audit the policy's rule. In audit mode, PowerShell behavior doesn't change but it logs Event ID 16387 messages to the PowerShellCore/Analytic event log. In enforcement mode, PowerShell applies the policy's restrictions.

This article assumes you're using a test machine so that you can test PowerShell behavior under a machine wide WDAC policy before you deploy the policy in your environment.

Create a WDAC policy

A WDAC policy is described in an XML file, which contains information about policy options, files allowed, and signing certificates recognized by the policy. When the policy is applied, only approved files are allowed to load and run. PowerShell either blocks unapproved script files from running or runs them in ConstrainedLanguage mode, depending on policy options.

You create and manipulate WDAC policy using the ConfigCI module, which is available on all supported Windows versions. This Windows PowerShell module can be used in Windows PowerShell 5.1 or in PowerShell 7 through the Windows Compatibility layer. It's easier to use this module in Windows PowerShell. The policy you create can be applied to any version of PowerShell.

Steps to create a WDAC policy

For testing, you just need to create a default policy and a self signed code signing certificate.

  1. Create a default policy

    New-CIPolicy -Level PcaCertificate -FilePath .\SystemCIPolicy.xml -UserPEs
    

    This command creates a default policy file called SystemCIPolicy.xml that allows all Microsoft code-signed files to run.

    Note

    Running this command can take up to two hours because it must scan the entire test machine.

  2. Disable Audit Mode in default policy

    A new policy is always created in Audit mode. To test policy enforcement, you need to disable Audit mode when you apply the policy. Edit the SystemCIPolicy.xml file using a text editor like notepad.exe or Visual Studio Code (VS Code). Comment out the Audit mode option.

    <!--
    <Rule>
      <Option>Enabled:Audit Mode</Option>
    </Rule>
    -->
    
  3. Create a self-signed code signing certificate

    You need a code signing certificate to sign any test binaries or script files that you want to run on your test machine. The New-SelfSignedCertificate is provided by the PKI module. For best results, you should run this command in Windows PowerShell 5.1.

    $newSelfSignedCertificateSplat = @{
        DnsName = $env:COMPUTERNAME
        CertStoreLocation = "Cert:\CurrentUser\My\"
        Type = 'CodeSigningCert'
    }
    $cert = New-SelfSignedCertificate @newSelfSignedCertificateSplat
    Export-Certificate -Cert $cert -FilePath c:\certs\signing.cer
    Import-Certificate -FilePath C:\certs\signing.cer -CertStoreLocation "Cert:\CurrentUser\Root\"
    $cert = Get-ChildItem Cert:\CurrentUser\My\ -CodeSigningCert
    
    dir c:\bin\powershell\pwsh.exe | Set-AuthenticodeSignature -Certificate $cert
    
  4. Add the code signing certificate to the policy

    Use the following command to add the new code signing certificate to the policy.

    Add-SignerRule -FilePath .\SystemCIPolicy.xml -CertificatePath c:\certs\signing.cer -User
    
  5. Convert the XML policy file to a policy enforcement binary file

    Finally, you need to convert the XML file to a binary file used by WDAC to apply a policy.

    ConvertFrom-CIPolicy -XmlFilePath .\SystemCIPolicy.xml -BinaryFilePath .\SIPolicy.p7b
    
  6. Apply the WDAC policy

    To apply the policy to your test machine, copy the SIPolicy.p7b file to the required system location, C:\Windows\System32\CodeIntegrity.

    Note

    Some policies definition must be copied to a subfolder such as C:\Windows\System32\CodeIntegrity\CiPolicies. For more information, see WDAC Admin Tips & Known Issues.

  7. Disable the WDAC policy

    To disable the policy, rename the SIPolicy.p7b file. If you need to do more testing, you can change the name back to reenable the policy.

    Rename-Item -Path .\SIPolicy.p7b -NewName .\SIPolicy.p7b.off
    

Test using WDAC policy auditing

PowerShell 7.4 added a new feature to support WDAC policies in Audit mode. In audit mode, PowerShell runs the untrusted scripts in ConstrainedLanguage mode without errors, but logs messages to the event log instead. The log messages describe what restrictions would apply if the policy were in Enforce mode.

Viewing audit events

PowerShell logs audit events to the PowerShellCore/Analytic event log. The log isn't enabled by default. To enable the log, open the Windows Event Viewer, right-click on the PowerShellCore/Analytic log and select Enable Log.

Alternatively, you can run the following command from an elevated PowerShell session.

wevtutil.exe sl PowerShellCore/Analytic /enabled:true /quiet

You can view the events in the Windows Event Viewer or use the Get-WinEvent cmdlet to retrieve the events.

Get-WinEvent -LogName PowerShellCore/Analytic -Oldest |
    Where-Object Id -eq 16387 | Format-List
TimeCreated  : 4/19/2023 10:11:07 AM
ProviderName : PowerShellCore
Id           : 16387
Message      : WDAC Audit.

    Title: Method or Property Invocation
    Message: Method or Property 'WriteLine' on type 'System.Console' invocation will not
        be allowed in ConstrainedLanguage mode.
        At C:\scripts\Test1.ps1:3 char:1
        + [System.Console]::WriteLine("pwnd!")
        + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    FullyQualifiedId: MethodOrPropertyInvocationNotAllowed

The event message includes the script position where the restriction would be applied. This information helps you understand where you need to change your script so that it runs under the WDAC policy.

Important

Once you have reviewed the audit events, you should disable the Analytic log. Analytic logs grow quickly and consume large amounts of disk space.

Viewing audit events in the PowerShell debugger

If you set the $DebugPreference variable to Break for an interactive PowerShell session, PowerShell breaks into the command-line script debugger at the current location in the script where the audit event occurred. The breakpoint allows you to debug your code and inspect the current state of the script in real time.