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.