Functions/ConvertFrom-SysmonBinaryConfiguration.ps1
<#
.SYNOPSIS Parses a binary Sysmon configuration. .DESCRIPTION ConvertFrom-SysmonBinaryConfiguration parses a binary Sysmon configuration. The configuration is typically stored in the registry at the following path: HKLM\SYSTEM\CurrentControlSet\Services\SysmonDrv\Parameters\Rules ConvertFrom-SysmonBinaryConfiguration currently only supports the following schema versions: 3.30, 3.40 and 4.0 Author: Matthew Graeber (@mattifestation) License: BSD 3-Clause .PARAMETER RuleBytes Specifies the raw bytes of a Sysmon configuration from the registry. .EXAMPLE [Byte[]] $RuleBytes = Get-ItemPropertyValue -Path HKLM:\SYSTEM\CurrentControlSet\Services\SysmonDrv\Parameters -Name Rules ConvertFrom-SysmonBinaryConfiguration -RuleBytes $RuleBytes .OUTPUTS Sysmon.EventCollection Output a fully-parsed rule object including the hash of the rules blob. .NOTES ConvertFrom-SysmonBinaryConfiguration is designed to serve as a helper function for Get-SysmonConfiguration. #> function ConvertFrom-SysmonBinaryConfiguration { [OutputType('Sysmon.EventCollection')] [CmdletBinding()] param ( [Parameter(Mandatory = $True)] [Byte[]] [ValidateNotNullOrEmpty()] $RuleBytes ) #region Define byte to string mappings. This may change across verions. $SupportedSchemaVersions = @( [Version] '3.30.0.0', [Version] '3.40.0.0', [Version] '4.00.0.0' ) $EventConditionMapping = @{ 0 = 'Is' 1 = 'IsNot' 2 = 'Contains' 3 = 'Excludes' 4 = 'BeginWith' 5 = 'EndWith' 6 = 'LessThan' 7 = 'MoreThan' 8 = 'Image' } # The following value to string mappings were all pulled from # IDA and will require manual validation with with each new # Sysmon and schema version. Here's hoping they don't change often! $ProcessCreateMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' 4 = 'CommandLine' 5 = 'CurrentDirectory' 6 = 'User' 7 = 'LogonGuid' 8 = 'LogonId' 9 = 'TerminalSessionId' 10 = 'IntegrityLevel' 11 = 'Hashes' 12 = 'ParentProcessGuid' 13 = 'ParentProcessId' 14 = 'ParentImage' 15 = 'ParentCommandLine' } $ProcessCreateMapping_4_00 = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' 4 = 'FileVersion' 5 = 'Description' 6 = 'Product' 7 = 'Company' 8 = 'CommandLine' 9 = 'CurrentDirectory' 10 = 'User' 11 = 'LogonGuid' 12 = 'LogonId' 13 = 'TerminalSessionId' 14 = 'IntegrityLevel' 15 = 'Hashes' 16 = 'ParentProcessGuid' 17 = 'ParentProcessId' 18 = 'ParentImage' 19 = 'ParentCommandLine' } $FileCreateTimeMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' 4 = 'TargetFilename' 5 = 'CreationUtcTime' 6 = 'PreviousCreationUtcTime' } $NetworkConnectMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' 4 = 'User' 5 = 'Protocol' 6 = 'Initiated' 7 = 'SourceIsIpv6' 8 = 'SourceIp' 9 = 'SourceHostname' 10 = 'SourcePort' 11 = 'SourcePortName' 12 = 'DestinationIsIpv6' 13 = 'DestinationIp' 14 = 'DestinationHostname' 15 = 'DestinationPort' 16 = 'DestinationPortName' } $SysmonServiceStateChangeMapping = @{ 0 = 'UtcTime' 1 = 'State' 2 = 'Version' 3 = 'SchemaVersion' } $ProcessTerminateMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' } $DriverLoadMapping = @{ 0 = 'UtcTime' 1 = 'ImageLoaded' 2 = 'Hashes' 3 = 'Signed' 4 = 'Signature' 5 = 'SignatureStatus' } $ImageLoadMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' 4 = 'ImageLoaded' 5 = 'Hashes' 6 = 'Signed' 7 = 'Signature' 8 = 'SignatureStatus' } $ImageLoadMapping_4_00 = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' 4 = 'ImageLoaded' 5 = 'FileVersion' 6 = 'Description' 7 = 'Product' 8 = 'Company' 9 = 'Hashes' 10 = 'Signed' 11 = 'Signature' 12 = 'SignatureStatus' } $CreateRemoteThreadMapping = @{ 0 = 'UtcTime' 1 = 'SourceProcessGuid' 2 = 'SourceProcessId' 3 = 'SourceImage' 4 = 'TargetProcessGuid' 5 = 'TargetProcessId' 6 = 'TargetImage' 7 = 'NewThreadId' 8 = 'StartAddress' 9 = 'StartModule' 10 = 'StartFunction' } $RawAccessReadMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' 4 = 'Device' } $ProcessAccessMapping = @{ 0 = 'UtcTime' 1 = 'SourceProcessGUID' 2 = 'SourceProcessId' 3 = 'SourceThreadId' 4 = 'SourceImage' 5 = 'TargetProcessGUID' 6 = 'TargetProcessId' 7 = 'TargetImage' 8 = 'GrantedAccess' 9 = 'CallTrace' } $FileCreateMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' 4 = 'TargetFilename' 5 = 'CreationUtcTime' } $RegistryEventCreateKeyMapping = @{ 0 = 'EventType' 1 = 'UtcTime' 2 = 'ProcessGuid' 3 = 'ProcessId' 4 = 'Image' 5 = 'TargetObject' } $RegistryEventSetValueMapping = @{ 0 = 'EventType' 1 = 'UtcTime' 2 = 'ProcessGuid' 3 = 'ProcessId' 4 = 'Image' 5 = 'TargetObject' 6 = 'Details' } $RegistryEventDeleteKeyMapping = @{ 0 = 'EventType' 1 = 'UtcTime' 2 = 'ProcessGuid' 3 = 'ProcessId' 4 = 'Image' 5 = 'TargetObject' 6 = 'NewName' } $FileCreateStreamHashMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'Image' 4 = 'TargetFilename' 5 = 'CreationUtcTime' 6 = 'Hash' } $SysmonConfigurationChangeMapping = @{ 0 = 'UtcTime' 1 = 'Configuration' 2 = 'ConfigurationFileHash' } $PipeEventCreatedMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'PipeName' 4 = 'Image' } $PipeEventConnectedMapping = @{ 0 = 'UtcTime' 1 = 'ProcessGuid' 2 = 'ProcessId' 3 = 'PipeName' 4 = 'Image' } $WmiEventFilterMapping = @{ 0 = 'EventType' 1 = 'UtcTime' 2 = 'Operation' 3 = 'User' 4 = 'EventNamespace' 5 = 'Name' 6 = 'Query' } $WmiEventConsumerMapping = @{ 0 = 'EventType' 1 = 'UtcTime' 2 = 'Operation' 3 = 'User' 4 = 'Name' 5 = 'Type' 6 = 'Destination' } $WmiEventConsumerToFilterMapping = @{ 0 = 'EventType' 1 = 'UtcTime' 2 = 'Operation' 3 = 'User' 4 = 'Consumer' 5 = 'Filter' } $EventTypeMapping = @{ 1 = @('ProcessCreate', $ProcessCreateMapping) 2 = @('FileCreateTime', $FileCreateTimeMapping) 3 = @('NetworkConnect', $NetworkConnectMapping) # SysmonServiceStateChange is not actually present in the schema. It is here for the sake of completeness. 4 = @('SysmonServiceStateChange', $SysmonServiceStateChangeMapping) 5 = @('ProcessTerminate', $ProcessTerminateMapping) 6 = @('DriverLoad', $DriverLoadMapping) 7 = @('ImageLoad', $ImageLoadMapping) 8 = @('CreateRemoteThread', $CreateRemoteThreadMapping) 9 = @('RawAccessRead', $RawAccessReadMapping) 10 = @('ProcessAccess', $ProcessAccessMapping) 11 = @('FileCreate', $FileCreateMapping) 12 = @('RegistryEventCreateKey', $RegistryEventCreateKeyMapping) 13 = @('RegistryEventSetValue', $RegistryEventSetValueMapping) 14 = @('RegistryEventDeleteKey', $RegistryEventDeleteKeyMapping) 15 = @('FileCreateStreamHash', $FileCreateStreamHashMapping) # SysmonConfigurationChange is not actually present in the schema. It is here for the sake of completeness. 16 = @('SysmonConfigurationChange', $SysmonConfigurationChangeMapping) 17 = @('PipeEventCreated', $PipeEventCreatedMapping) 18 = @('PipeEventConnected', $PipeEventConnectedMapping) 19 = @('WmiEventFilter', $WmiEventFilterMapping) 20 = @('WmiEventConsumer', $WmiEventConsumerMapping) 21 = @('WmiEventConsumerToFilter', $WmiEventConsumerToFilterMapping) } #endregion $RuleMemoryStream = New-Object -TypeName System.IO.MemoryStream -ArgumentList @(,$RuleBytes) $RuleReader = New-Object -TypeName System.IO.BinaryReader -ArgumentList $RuleMemoryStream # I'm noting here for the record that parsing could be slightly more robust to account for malformed # rule blobs. I'm writing this in my spare time so I likely won't put too much work into increased # parsing robustness. if ($RuleBytes.Count -lt 16) { $RuleReader.Dispose() $RuleMemoryStream.Dispose() throw 'Insufficient length to contain a Sysmon rule header.' } # This value should be either 0 or 1. 1 should be expected for a current Sysmon config. # A value of 1 indicates that offset 8 will contain the file offset to the first rule grouping. # A value of 0 should indicate that offset 8 will be the start of the first rule grouping. # Currently, I am just going to check that the value is 1 and throw an exception if it's not. $HeaderValue0 = $RuleReader.ReadUInt16() if ($HeaderValue0 -ne 1) { $RuleReader.Dispose() $RuleMemoryStream.Dispose() throw "Incorrect header value at offset 0x00. Expected: 1. Actual: $HeaderValue0" } # This value is expected to be 1. Any other value will indicate the presence of a "registry rule version" # that is incompatible with the current Sysmon schema version. A value other than 1 likely indicates the # presence of an old version of Sysmon. Any value besides 1 will not be supported in this script. $HeaderValue1 = $RuleReader.ReadUInt16() if ($HeaderValue1 -ne 1) { $RuleReader.Dispose() $RuleMemoryStream.Dispose() throw "Incorrect header value at offset 0x02. Expected: 1. Actual: $HeaderValue1" } $RuleGroupCount = $RuleReader.ReadUInt32() $RuleGroupBeginOffset = $RuleReader.ReadUInt32() $SchemaVersionMinor = $RuleReader.ReadUInt16() $SchemaVersionMajor = $RuleReader.ReadUInt16() $SchemaVersion = New-Object -TypeName System.Version -ArgumentList $SchemaVersionMajor, $SchemaVersionMinor, 0, 0 Write-Verbose "Obtained the following schema version: $($SchemaVersion.ToString(2))" if (-not ($SupportedSchemaVersions -contains $SchemaVersion)) { $RuleReader.Dispose() $RuleMemoryStream.Dispose() throw "Unsupported schema version: $($SchemaVersion.ToString(2)). Schema version must be at least $($MinimumSupportedSchemaVersion.ToString(2))" } #region Perform offset updates depending upon the schema version here # This logic should be the first candidate for refactoring should the schema change drastically in the future. switch ($SchemaVersion.ToString(2)) { '4.0' { Write-Verbose 'Using schema version 4.00 updated offsets.' # ProcessCreate and ImageLoad values changed $EventTypeMapping[1][1] = $ProcessCreateMapping_4_00 $EventTypeMapping[7][1] = $ImageLoadMapping_4_00 } } #endregion $null = $RuleReader.BaseStream.Seek($RuleGroupBeginOffset, 'Begin') $EventCollection = for ($i = 0; $i -lt $RuleGroupCount; $i++) { $EventTypeValue = $RuleReader.ReadInt32() $EventType = $EventTypeMapping[$EventTypeValue][0] $EventTypeRuleTypes = $EventTypeMapping[$EventTypeValue][1] $OnMatchValue = $RuleReader.ReadInt32() $OnMatch = $null switch ($OnMatchValue) { 0 { $OnMatch = 'Exclude' } 1 { $OnMatch = 'Include' } default { $OnMatch = '?' } } $NextEventTypeOffset = $RuleReader.ReadInt32() $RuleCount = $RuleReader.ReadInt32() [PSObject[]] $Rules = New-Object -TypeName PSObject[]($RuleCount) # Parse individual rules here for ($j = 0; $j -lt $RuleCount; $j++) { $RuleType = $EventTypeRuleTypes[$RuleReader.ReadInt32()] $Filter = $EventConditionMapping[$RuleReader.ReadInt32()] $NextRuleOffset = $RuleReader.ReadInt32() $RuleTextLength = $RuleReader.ReadInt32() $RuleTextBytes = $RuleReader.ReadBytes($RuleTextLength) $RuleText = [Text.Encoding]::Unicode.GetString($RuleTextBytes).TrimEnd("`0") $Rules[$j] = [PSCustomObject] @{ PSTypeName = 'Sysmon.Rule' RuleType = $RuleType Filter = $Filter RuleText = $RuleText } $null = $RuleReader.BaseStream.Seek($NextRuleOffset, 'Begin') } [PSCustomObject] @{ PSTypeName = 'Sysmon.EventGroup' EventType = $EventType OnMatch = $OnMatch Rules = $Rules } $null = $RuleReader.BaseStream.Seek($NextEventTypeOffset, 'Begin') } $RuleReader.Dispose() $RuleMemoryStream.Dispose() # Calculate the hash of the binary rule blob $SHA256Hasher = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider $ConfigBlobSHA256Hash = ($SHA256Hasher.ComputeHash($RuleBytes) | ForEach-Object { $_.ToString('X2') }) -join '' [PSCustomObject] @{ PSTypeName = 'Sysmon.EventCollection' SchemaVersion = $SchemaVersion ConfigBlobSHA256Hash = $ConfigBlobSHA256Hash Events = $EventCollection } } |