Last active
December 19, 2023 10:23
-
-
Save mattifestation/0669a250e64b2578819908e2b84acabb to your computer and use it in GitHub Desktop.
Script I use to make sense of code integrity audit/enforcement events for primarily baselining purposes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Get-CodeIntegrityEvent { | |
<# | |
.SYNOPSIS | |
Returns code integrity event log audit/enforcement events in a more human-readable fashion. | |
.DESCRIPTION | |
Get-CodeIntegrityEvent retrieves and parses Microsoft-Windows-CodeIntegrity/Operational PE audit and enforcement events into a format that is more human-readable. This function is designed to facilitate regular code integrity policy baselining. | |
Author: Matthew Graeber | |
License: BSD 3-Clause | |
.PARAMETER User | |
Specifies that only user-mode events should be returned. If neither -User nor -Kernel is specified, user and kernel events are returned. | |
.PARAMETER Kernel | |
Specifies that only kernel-mode events should be returned. If neither -User nor -Kernel is specified, user and kernel events are returned. | |
.PARAMETER Audit | |
Specifies that only audit events (event ID 3076) should be returned. If neither -Audit nor -Enforce is specified, audit and enforcement events are returned. | |
.PARAMETER Enforce | |
Specifies that only enforcement events (event ID 3077) should be returned. If neither -Audit nor -Enforce is specified, audit and enforcement events are returned. | |
.PARAMETER SinceLastPolicyRefresh | |
Specifies that events should only be returned since the last time the code integrity policy was refreshed. This option is useful for baselining purposes. | |
.PARAMETER MaxEvents | |
Specifies the maximum number of events that Get-CodeIntegrityEvent returns. The default is to return all the events. | |
.EXAMPLE | |
Get-CodeIntegrityEvent -SinceLastPolicyRefresh | |
Return all code integrity events (user/kernel/audit/enforcement) since the last code intgrity policy refresh. | |
.EXAMPLE | |
Get-CodeIntegrityEvent -User -SinceLastPolicyRefresh | |
Return all user-mode code integrity events (audit/enforcement) since the last code intgrity policy refresh. | |
.EXAMPLE | |
Get-CodeIntegrityEvent -Kernel -MaxEvents 5 | |
Return the most recent 5 kernel mode code integrity events. | |
.EXAMPLE | |
Get-CodeIntegrityEvent -Kernel -Enforce | |
Return all kernel mode enforcement events. | |
#> | |
[CmdletBinding()] | |
param ( | |
[Switch] | |
$User, | |
[Switch] | |
$Kernel, | |
[Switch] | |
$Audit, | |
[Switch] | |
$Enforce, | |
[Switch] | |
$SinceLastPolicyRefresh, | |
[Switch] | |
$SkipSignerAndWhqlChecks, | |
[Int64] | |
$MaxEvents | |
) | |
# If neither -User nor -Kernel are supplied, do not filter based on signing scenario | |
# If -User and -Kernel are supplied, do not filter based on signing scenario | |
# Only filter in a mutually exclusive scenario. | |
$ScenarioFilter = '' | |
if ($User -and !$Kernel) { | |
# 1 == A user-mode rule triggered | |
$ScenarioFilter = " and EventData[Data[@Name='SI Signing Scenario'] = 1]" | |
} elseif ($Kernel -and !$User) { | |
# 2 == A kernel-mode rule triggered | |
$ScenarioFilter = " and EventData[Data[@Name='SI Signing Scenario'] = 0]" | |
} | |
# If neither -Audit nor -Enforce are supplied, do not filter based on event ID | |
# If -Audit and -Enforce are supplied, do not filter based on event ID | |
# Only filter in a mutually exclusive scenario. | |
$ModeFilter = '(EventID = 3076 or EventID = 3077)' | |
if ($Audit -and !$Enforce) { | |
# Event ID 3076 == an audit event | |
$ModeFilter = "EventID = 3076" | |
} elseif ($Enforce -and !$Audit) { | |
# Event ID 3077 == an enforcement event | |
$ModeFilter = "EventID = 3077" | |
} | |
$PolicyRefreshFilter = '' | |
if ($SinceLastPolicyRefresh) { | |
# Only consider failed audit events that occured after the last CI policy update (event ID 3099) | |
$LastPolicyUpdateEvent = Get-WinEvent -FilterHashtable @{ LogName = 'Microsoft-Windows-CodeIntegrity/Operational'; Id = 3099 } -MaxEvents 1 -ErrorAction Ignore | |
# Sometimes this event will not be present - e.g. if the log rolled since the last update. | |
if ($LastPolicyUpdateEvent) { | |
$DateTimeAfter = [Xml.XmlConvert]::ToString($LastPolicyUpdateEvent.TimeCreated.ToUniversalTime()) | |
$PolicyRefreshFilter = " and TimeCreated[@SystemTime >= '$DateTimeAfter']" | |
} else { | |
Write-Verbose "No policy update event was present in the event log. Ignoring the -SinceLastPolicyRefresh switch." | |
} | |
} | |
$Filter = "*[System[$($ModeFilter)$($PolicyRefreshFilter)]$ScenarioFilter]" | |
Write-Verbose "XPath Filter: $Filter" | |
# File paths are often in the format of device path (e.g. \Device\HarddiskVolumeN\). | |
# Get-Partition is used to map the volume number to a partition so that file paths can be normalized. | |
$Partitions = Get-Partition | |
$PartitionMapping = @{} | |
foreach ($Partition in $Partitions) { | |
if ($Partition.DriveLetter) { | |
$PartitionMapping[$Partition.PartitionNumber.ToString()] = $Partition.DriveLetter | |
} | |
} | |
# This hashtable is used to resolve RequestedSigningLevel and ValidatedSigningLevel fields into human-readable properties | |
# For more context around signing levels, Alex Ionescu (@aionescu) has a great resource on them: | |
# http://www.alex-ionescu.com/?p=146 | |
$SigningLevelMapping = @{ | |
[Byte] 0x0 = 'Unchecked' | |
[Byte] 0x1 = 'Unsigned' | |
[Byte] 0x2 = 'Enterprise' | |
[Byte] 0x3 = 'Custom1' | |
[Byte] 0x4 = 'Authenticode' | |
[Byte] 0x5 = 'Custom2' | |
[Byte] 0x6 = 'Store' | |
[Byte] 0x7 = 'Antimalware' | |
[Byte] 0x8 = 'Microsoft' | |
[Byte] 0x9 = 'Custom4' | |
[Byte] 0xA = 'Custom5' | |
[Byte] 0xB = 'DynamicCodegen' | |
[Byte] 0xC = 'Windows' | |
[Byte] 0xD = 'WindowsProtectedProcessLight' | |
[Byte] 0xE = 'WindowsTcb' | |
[Byte] 0xF = 'Custom6' | |
} | |
$MaxEventArg = @{} | |
# Pass -MaxEvents through to Get-WinEvent | |
if ($MaxEvents) { $MaxEventArg = @{ MaxEvents = $MaxEvents } } | |
Get-WinEvent -LogName 'Microsoft-Windows-CodeIntegrity/Operational' -FilterXPath $Filter @MaxEventArg -ErrorAction Ignore | ForEach-Object { | |
if ($_.Version -lt 5) { | |
Write-Warning "Get-CodeIntegrityEvent does not support event version $($_.Version). Version 5+ is supported. Ensure you are running Windows 1903+." | |
} | |
switch ($_.Id) { | |
3076 { $EventType = 'Audit' } | |
3077 { $EventType = 'Enforce' } | |
default { | |
$EventType = $null | |
Write-Warning "Unsupported event type: $($_.Id)" | |
} | |
} | |
$WHQLFailed = $False | |
if (-not $SkipSignerAndWhqlChecks) { | |
# A correlated 3082 event indicates that WHQL verification failed | |
$WHQLEvent = Get-WinEvent -LogName 'Microsoft-Windows-CodeIntegrity/Operational' -FilterXPath "*[System[EventID = 3082 and Correlation[@ActivityID = '$($_.ActivityId.Guid)']]]" -ErrorAction Ignore | |
if ($WHQLEvent) { $WHQLFailed = $True } | |
} | |
$ResolvedSigners = $null | |
if (-not $SkipSignerAndWhqlChecks) { | |
# Retrieve correlated signer info (event ID 3089) | |
# Note: there may be more than one correlated signer event in the case of the file having multiple signers. | |
$SignerInfo = Get-WinEvent -LogName 'Microsoft-Windows-CodeIntegrity/Operational' -FilterXPath "*[System[EventID = 3089 and Correlation[@ActivityID = '$($_.ActivityId.Guid)']]]" -ErrorAction Ignore | |
$ResolvedSigners = $SignerInfo | ForEach-Object { | |
$SignatureTypeVal = $_.Properties[6].Value | |
# Note: these signature type mappings were determined based on inference. | |
switch ($SignatureTypeVal) { | |
0 { $SignatureType = 'Hash' } | |
1 { $SignatureType = 'Authenticode' } | |
4 { $SignatureType = 'Catalog' } | |
default { | |
$SignatureType = 'Unknown' | |
Write-Warning "Unknown signature type value: $SignatureTypeVal. Investigate what this might correspond to and update this function accordingly." | |
} | |
} | |
[PSCustomObject] @{ | |
SignatureIndex = $_.Properties[1].Value | |
PageHash = $_.Properties[5].Value | |
SignatureType = $SignatureType | |
ValidatedSigningLevel = $SigningLevelMapping[$_.Properties[7].Value] | |
NotValidBefore = $_.Properties[11].Value | |
NotValidAfter = $_.Properties[12].Value | |
PublisherName = $_.Properties[14].Value | |
IssuerName = $_.Properties[16].Value | |
PublisherTBSHash = (($_.Properties[18].Value | ForEach-Object { '{0:X2}' -f $_ }) -join '') | |
IssuerTBSHash = (($_.Properties[20].Value | ForEach-Object { '{0:X2}' -f $_ }) -join '') | |
} | |
} | |
} | |
$SigningScenarioVal = $_.Properties[16].Value | |
switch ($SigningScenarioVal) { | |
0 { $Scenario = 'Driver' } | |
1 { $Scenario = 'UserMode' } | |
default { | |
$Scenario = 'Unknown' | |
Write-Warning "Unknown signing scenario value: $SigningScenarioVal. Investigate what this might correspond to and update this function accordingly." | |
} | |
} | |
$FilePath = $_.Properties[1].Value | |
$ResolvedFilePath = $null | |
# Make a best effort to resolve the device path to a normal path. | |
if ($FilePath -match '(?<Prefix>^\\Device\\HarddiskVolume(?<VolumeNumber>\d)\\)') { | |
$ResolvedFilePath = $FilePath.Replace($Matches['Prefix'], "$($PartitionMapping[$Matches['VolumeNumber']]):\") | |
} elseif ($FilePath.ToLower().StartsWith('system32')) { | |
$ResolvedFilePath = "$($Env:windir)$($FilePath.Substring(8))" | |
} | |
# If all else fails regarding path resolution, show a warning. | |
if ($ResolvedFilePath -and !(Test-Path -Path $ResolvedFilePath)) { | |
Write-Warning "The following file path was either not resolved properly or was not present on disk: $ResolvedFilePath" | |
} | |
$SHA1FileHash = $null | |
if ($_.Properties[11].Value -eq 20) { | |
$SHA1FileHash = ($_.Properties[12].Value | ForEach-Object { '{0:X2}' -f $_ }) -join '' | |
} | |
[PSCustomObject] @{ | |
TimeCreated = $_.TimeCreated | |
EventType = $EventType | |
SigningScenario = $Scenario | |
FilePath = $FilePath | |
ResolvedFilePath = $ResolvedFilePath | |
SHA1FileHash = $SHA1FileHash | |
ProcessID = $_.ProcessId | |
ProcessName = $_.Properties[3].Value | |
RequestedSigningLevel = $SigningLevelMapping[$_.Properties[4].Value] | |
ValidatedSigningLevel = $SigningLevelMapping[$_.Properties[5].Value] | |
PolicyName = $_.Properties[18].Value | |
PolicyID = $_.Properties[20].Value | |
PolicyGUID = $_.Properties[32].Value.Guid.ToUpper() | |
OriginalFileName = $_.Properties[24].Value | |
InternalName = $_.Properties[26].Value | |
FileDescription = $_.Properties[28].Value | |
ProductName = $_.Properties[30].Value | |
FileVersion = $_.Properties[31].Value | |
FailedWHQL = $WHQLFailed | |
SignerInfo = $ResolvedSigners | |
} | |
} | |
} |
how do i get this to run ? i tried running from powershell didnt work
Hey, @gd2009. Can you supply the steps you went through to execute it? I suspect you may have just dot-sourced it without calling the Get-CodeIntegrityEvent function.
. .\WDACBaselining.ps1
Get-CodeIntegrityEvent
I recommend the version in the WDACTools module. This gist isn't maintained.
It also won't return anything if you don't have any 3076 or 3077 events in the Microsoft-Windows-CodeIntegrity/Operational event log.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
how do i get this to run ? i tried running from powershell didnt work