Detection and response
Matt Graeber

Steering clear of bad drivers: How to apply Microsoft's recommended driver block rules

Defenders can use Windows Defender Application Control (WDAC) to reduce their organization’s kernel attack surface without the burden of maintaining a complex allowlist.

If an adversary can load a driver, there is nothing they can’t do to adversely impact a compromised system, including disabling endpoint security products. Previously, aside from requiring elevated privileges to load a driver, the bar was low to load a rootkit. Microsoft raised the bar slightly when it started to enforce stricter signing requirements alongside Driver Signature Enforcement. While it’s still possible to get away with signing malicious drivers, the threat landscape has shifted accordingly to the abuse of signed drivers. The barrier to installing a malicious or abusable driver remains relatively low because everything must be signed, but there at least exists an opportunity to enforce policy based on digital signatures—and this is exactly what Windows Defender Application Control (WDAC) is designed to do.

What is WDAC?

WDAC is a robust application control solution built into Windows 10 and Server 2016 and above. It can be configured with an allowlist of explicitly defined code that is permitted to execute, a blocklist consisting of code that is explicitly denied permission to execute, or a combination thereof.

The granularity of its preventive controls is impressive and includes the ability to define separate policies for user and kernel-mode code. However, like with any application control solution, deploying a robust allowlist at scale requires a significant amount of work and can initially seem daunting.

Microsoft offers a number of template policies that defenders can use to get started, one of which is their recommended driver block rules, a policy designed to explicitly deny execution of known abused and malicious drivers, like the vulnerable capcom.sys kernel driver (to name a more infamous example on the list). Deploying this policy supplies an organization with the benefit of a reduced kernel attack surface without the burden of maintaining a complex allowlist. Of note, the Australian Cyber Security Centre now requires that organizations deploy this driver policy in order to qualify for Maturity Level Three in their Essential Eight Maturity Model.

Drivers listed in the recommended driver block rules include (but are not limited to):

  • vulnerable drivers that are known to be exploited by both state-backed and criminal adversaries
  • dual-purpose drivers that expose legitimate but otherwise dangerous functionality that an adversary can abuse
  • malicious drivers that managed to get signed by Microsoft

So if you’re interested in deploying this simple policy in production or if you just want to experiment with WDAC, this is a great, safe policy to start with in audit or enforcement mode.

Tweaking the recommended driver block rules for your environment

The current recommended driver block ruleset was not designed for enterprise use (i.e., one that populates the CodeIntegrity event log must have a PolicyTypeID value of {A244370E-44C9-4C06-B551-F6016E563076}), but with a few simple modifications, it can be made operationally useful without needing to merge it into another policy.

To get up and running, copy the XML policy from Microsoft, save it, update the path accordingly in the second line below, and run the code that follows from an elevated PowerShell session on a Windows 10 machine.

 

# Replace this with the path to your driver block policy
$DriverBlockRulesPolicy = 'C:\Users\TestUser\Desktop\DriverBlockRules.xml'
 
# Remove the following line if you want the policy to remain in audit mode
Set-RuleOption -FilePath $DriverBlockRulesPolicy -Option 3 -Delete # Enabled:Audit Mode
 
# Make it so that reboots are not required to refresh the policy
Set-RuleOption -FilePath $DriverBlockRulesPolicy -Option 16 # Enabled:Update Policy No Reboot
 
# Convert the driver policy type to an enterprise policy type so that Audit/Enforcement events will be populated in the CodeIntegrity log when a blocked driver is loaded.
# Policy type ID reference: https://gist.github.com/mattifestation/d2b211005dede84626c0c80b98e8dd4f
$PolicyContents = Get-Content -Path $DriverBlockRulesPolicy -Raw
 
# Note: there is no ConfigCI cmdlet that can change this for you so you have to do it yourself.
$PolicyContents.Replace('{D2BDA982-CCF6-4344-AC5B-0B44427B6816}', '{A244370E-44C9-4C06-B551-F6016E563076}') | Out-File -FilePath $DriverBlockRulesPolicy -Encoding ascii
 
# Borrow a driver allow-all rule in order to permit execution of all other drivers
$AllowAllRule = (Get-CIPolicy -FilePath "$Env:windir\schemas\CodeIntegrity\ExamplePolicies\AllowAll.xml").Where({-not $_.UserMode})
 
# Merge the allow-all rule
$null = Merge-CIPolicy -OutputFilePath AllowAllRuleAdded.xml -PolicyPaths $DriverBlockRulesPolicy -Rules $AllowAllRule
 
# Convert the policy to binary form and deploy it
ConvertFrom-CIPolicy -XmlFilePath AllowAllRuleAdded.xml -BinaryFilePath "$Env:windir\System32\CodeIntegrity\SiPolicy.p7b"
 
# Update the policy without having to reboot
Invoke-CimMethod -Namespace root\Microsoft\Windows\CI -ClassName PS_UpdateAndCompareCIPolicy -MethodName Update -Arguments @{ FilePath = "$Env:windir\System32\CodeIntegrity\SiPolicy.p7b" } 

 

The resulting, tweaked policy will look like this when the above code is executed. The most important tweak to Microsoft’s policy in the above code involves changing the policy type ID from {D2BDA982-CCF6-4344-AC5B-0B44427B6816} to {A244370E-44C9-4C06-B551-F6016E563076}, where the former represents a driver policy that is not designed to surface audit or enforcement events to the Microsoft-Windows-CodeIntegrity/Operational event log. The latter GUID value, on the other hand, represents a custom, enterprise code integrity policy intended for consumer consumption (i.e., non-Microsoft-specific use cases).

When a blocked driver attempts to load, two Microsoft-Windows-CodeIntegrity/Operational events will surface:

  • event ID 3077 or 3076, indicating an enforcement or audit event, respectfully
  • event ID 3089, consisting of corresponding signature information

Here are the contents of an example enforcement event (event ID 3077) when the RWEverything driver (which corresponds to the ID_SIGNER_RWEVERY driver rule) attempts to load:

 

FileNameLength 58 
FileName \Device\HarddiskVolume4\Windows\System32\drivers\RwDrv.sys 
ProcessNameLength 6 
Process Name System 
Requested Signing Level 4 
Validated Signing Level 1 
Status 0xc0e90002 
SHA1 Hash Size 20 
SHA1 Hash 39257FB86DF888207E4F3A7768561B4AB1557848 
SHA256 Hash Size 32 
SHA256 Hash D475C4FE917020D420B5D0CF1F074B1427F49BD1F4414873501BE51700F8832D 
SHA1 Flat Hash Size 20 
SHA1 Flat Hash 66E95DAEE3D1244A029D7F3D91915F1F233D1916 
SHA256 Flat Hash Size 32 
SHA256 Flat Hash D969845EF6ACC8E5D3421A7CE7E244F419989710871313B04148F9B322751E5D 
USN 3690490088 
SI Signing Scenario 0 
PolicyNameLength 31 
PolicyName Microsoft Windows Driver Policy 
PolicyIDLength 12 
PolicyID 10.0.19565.0 
PolicyHashSize 32 
PolicyHash 231FDDDB2FB732FE0957A846701CF52D5D90F093CB0DC4E7A70B0C2359A494BE 
OriginalFileNameLength 9 
OriginalFileName RwDrv.sys 
InternalNameLength 9 
InternalName RwDrv.sys 
FileDescriptionLength 12 
FileDescription RwDrv Driver 
ProductNameLength 12 
ProductName RwDrv Driver 
FileVersion 1.0.0.0 
PolicyGUID {a244370e-44c9-4c06-b551-f6016e563076} 
UserWriteable false 
PackageFamilyNameLength 0 
PackageFamilyName  

 

Here is the corresponding signature information event (event ID 3089):

 

TotalSignatureCount 1 
Signature 0 
CacheState 0 
Hash Size 20 
Hash 39257FB86DF888207E4F3A7768561B4AB1557848 
PageHash false 
SignatureType 1 
ValidatedSigningLevel 4 
VerificationError 26 
Flags 0 
PolicyBits 8 
NotValidBefore 2012-07-31T20:41:59.0000000Z 
NotValidAfter 2013-08-01T20:41:59.0000000Z 
PublisherNameLength 13 
PublisherName ChongKim Chan 
IssuerNameLength 30 
IssuerName GlobalSign CodeSigning CA - G2 
PublisherTBSHashSize 20 
PublisherTBSHash 519E011F6CAB88C812DA20225DD37CC1808D5180 
IssuerTBSHashSize 20 
IssuerTBSHash 589A7D4DF869395601BA7538A65AFAE8C4616385 

 

As you can see, these events offer a rich amount of data (far more than Sysmon driver load events), some of which is contrived, however, and warrants additional context. Microsoft explains many of the esoteric event fields in this article. Aside from file and signer information, a relevant event field is VerificationError, which, in this case, indicates that RwDrv.sys was explicitly denied by WDAC policy, as indicated by the value of 26. Considering that Microsoft’s recommended driver block ruleset consists solely of explicit deny rules, a VerificationError value of 26 serves to verify that the policy works as expected.

Investigating driver events in Microsoft Defender for Endpoint

One of the cool things about Microsoft Defender for Endpoint (MDE) is that it natively consumes WDAC code integrity events, and they are all represented in the DeviceEvents table under the following ActionType values:

  • AppControlCodeIntegrityPolicyBlocked (equivalent to CodeIntegrity event ID 3077)
  • AppControlCodeIntegrityPolicyAudited (equivalent to CodeIntegrity event ID 3076)
  • AppControlCodeIntegritySigningInformation (equivalent to CodeIntegrity event ID 3089)

Now, because Blocked/Audited events are distinct from SigningInformation events, in order to get the most value out of an MDE query, you would want to join fields from those two tables to get a more contextual picture of what was blocked. Here is a Kusto Query Language (KQL) query I used to demonstrate joining both events:

 

DeviceEvents
| where ActionType startswith "AppControlCodeIntegrityPolicy"
| extend Hash = SHA1
| join kind = inner (
  DeviceEvents
  | where ActionType == "AppControlCodeIntegritySigningInformation"
  | extend VerificationError = extractjson("$.VerificationError", AdditionalFields, typeof(string))
  | where VerificationError == "Explicitly denied by WDAC policy"
  | extend PublisherName = extractjson("$.PublisherName", AdditionalFields, typeof(string))
  | extend PublisherTBSHash = extractjson("$.PublisherTBSHash", AdditionalFields, typeof(string))
  | extend NotValidBefore = extractjson("$.NotValidBefore", AdditionalFields, typeof(string))
  | extend NotValidAfter = extractjson("$.NotValidAfter", AdditionalFields, typeof(string))
  | extend Hash = SHA256
  | project PublisherName, PublisherTBSHash, NotValidBefore, NotValidAfter, VerificationError, Hash
) on Hash
| extend PolicyName = extractjson("$.PolicyName", AdditionalFields, typeof(string))
| extend PolicyID = extractjson("$.PolicyID", AdditionalFields, typeof(string))
| project Timestamp, DeviceId, DeviceName, ActionType, FileName, FolderPath, SHA1, SHA256, PolicyName, PolicyID, PublisherName, PublisherTBSHash, NotValidBefore, NotValidAfter, VerificationError
 
DeviceEvents
| where ActionType startswith "AppControlCodeIntegrityPolicy"
| join kind = inner (
  DeviceEvents
  | where ActionType == "AppControlCodeIntegritySigningInformation"
  | extend VerificationError = extractjson("$.VerificationError", AdditionalFields, typeof(string))
  | where VerificationError == "Explicitly denied by WDAC policy"
  | extend PublisherName = extractjson("$.PublisherName", AdditionalFields, typeof(string))
  | extend PublisherTBSHash = extractjson("$.PublisherTBSHash", AdditionalFields, typeof(string))
  | extend NotValidBefore = extractjson("$.NotValidBefore", AdditionalFields, typeof(string))
  | extend NotValidAfter = extractjson("$.NotValidAfter", AdditionalFields, typeof(string))
  | project PublisherName, PublisherTBSHash, NotValidBefore, NotValidAfter, VerificationError, SHA256
) on SHA256
| extend PolicyName = extractjson("$.PolicyName", AdditionalFields, typeof(string))
| extend PolicyID = extractjson("$.PolicyID", AdditionalFields, typeof(string))
| project Timestamp, DeviceId, DeviceName, ActionType, FileName, FolderPath, SHA1, SHA256, PolicyName, PolicyID, PublisherName, PublisherTBSHash, NotValidBefore, NotValidAfter, VerificationError

 

The reason there are nearly identical, duplicative queries is because AppControlCodeIntegritySigningInformation events only have their SHA256 column populated, and it could be populated with either a SHA1 or SHA256 hash, depending on the hashing algorithm specified in the signing certificate. This was the best, albeit inefficient, solution I could come up with considering this issue and considering my inability to think of a more clever query. The above query would produce the following example output:

 

Timestamp: 2021-07-16T18:00:00.4930136Z
DeviceId: REDACTED
DeviceName: mattitestation
ActionType: AppControlCodeIntegrityPolicyBlocked
FileName: RwDrv.sys
FolderPath: \Device\HarddiskVolume3\Users\TestUser\Desktop
SHA1: e15698840eaa0d72abce8207b4e57966e8c064b2
SHA256: b1d0fdfddddfe520afc18b79b18b5eef730f7586639bd05857a41c0d09a9b9e6
PolicyName: Microsoft Windows Driver Policy
PolicyID: 10.0.19565.0
PublisherName: ChongKim Chan
PublisherTBSHash: 519e011f6cab88c812da20225dd37cc1808d5180
NotValidBefore: 2012-07-31T20:41:59Z
NotValidAfter: 2013-08-01T20:41:59Z
VerificationError: Explicitly denied by WDAC policy

Don’t forget to validate

In Microsoft’s policy XML, they don’t supply an end-user with sufficient context about what each rule represents and what binaries it might apply to. Fortunately, we’ve done much of the legwork in supplying that context. You can find detailed breakdown of each rule in Microsoft’s policy in this Gist.

 

Microsoft Identity: An intro to Windows Active Directory

 

Remote access tool or trojan? How to detect misbehaving RATs

 

When Dridex and Cobalt Strike give you Grief

 

The adversary’s gift: When one technique opens a Pandora’s box

Subscribe to our blog