Detection rules › Kusto
Security Service Registry ACL Modification
Identifies attempts to modify registry ACL to evade security solutions. In the Solorigate attack, the attackers were found modifying registry permissions so services.exe cannot access the relevant registry keys to start the service. The detection leverages Security Event as well as MDE data to identify when specific security services registry permissions are modified. Only some portions of this detection are related to Solorigate, it also includes coverage for some common tools that perform this activity. Reference on guidance for enabling registry auditing: - https://docs.microsoft.com/windows/security/threat-protection/auditing/advanced-security-auditing-faq - https://docs.microsoft.com/windows/security/threat-protection/auditing/appendix-a-security-monitoring-recommendations-for-many-audit-events - https://docs.microsoft.com/windows/security/threat-protection/auditing/audit-registry - https://docs.microsoft.com/windows/security/threat-protection/auditing/event-4670 - For the event 4670 to be created the audit policy for the registry must have auditing enabled for Write DAC and/or Write Owner - https://github.com/OTRF/Set-AuditRule - https://docs.microsoft.com/dotnet/api/system.security.accesscontrol.registryrights?view=dotnet-plat-ext-5.0
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
Rule body
id: 473d57e6-f787-435c-a16b-b38b51fa9a4b
name: Security Service Registry ACL Modification
description: |
'Identifies attempts to modify registry ACL to evade security solutions. In the Solorigate attack, the attackers were found modifying registry permissions so services.exe cannot access the relevant registry keys to start the service.
The detection leverages Security Event as well as MDE data to identify when specific security services registry permissions are modified.
Only some portions of this detection are related to Solorigate, it also includes coverage for some common tools that perform this activity.
Reference on guidance for enabling registry auditing:
- https://docs.microsoft.com/windows/security/threat-protection/auditing/advanced-security-auditing-faq
- https://docs.microsoft.com/windows/security/threat-protection/auditing/appendix-a-security-monitoring-recommendations-for-many-audit-events
- https://docs.microsoft.com/windows/security/threat-protection/auditing/audit-registry
- https://docs.microsoft.com/windows/security/threat-protection/auditing/event-4670
- For the event 4670 to be created the audit policy for the registry must have auditing enabled for Write DAC and/or Write Owner
- https://github.com/OTRF/Set-AuditRule
- https://docs.microsoft.com/dotnet/api/system.security.accesscontrol.registryrights?view=dotnet-plat-ext-5.0'
severity: High
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvents
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
tags:
- Solorigate
- NOBELIUM
query: |
let servicelist = dynamic(['Services\\HealthService', 'Services\\Sense', 'Services\\WinDefend', 'Services\\MsSecFlt', 'Services\\DiagTrack', 'Services\\SgrmBroker', 'Services\\SgrmAgent', 'Services\\AATPSensorUpdater' , 'Services\\AATPSensor', 'Services\\mpssvc']);
let filename = dynamic(["subinacl.exe",'SetACL.exe']);
let parameters = dynamic (['/deny=SYSTEM', '/deny=S-1-5-18', '/grant=SYSTEM=r', '/grant=S-1-5-18=r', 'n:SYSTEM;p:READ', 'n1:SYSTEM;ta:remtrst;w:dacl']);
let FullAccess = dynamic(['A;CI;KA;;;SY', 'A;ID;KA;;;SY', 'A;CIID;KA;;;SY']);
let ReadAccess = dynamic(['A;CI;KR;;;SY', 'A;ID;KR;;;SY', 'A;CIID;KR;;;SY']);
let DenyAccess = dynamic(['D;CI;KR;;;SY', 'D;ID;KR;;;SY', 'D;CIID;KR;;;SY']);
let timeframe = 1d;
(union isfuzzy=true
(
SecurityEvent
| where TimeGenerated >= ago(timeframe)
| where EventID == 4670
| where ObjectType == 'Key'
| where ObjectName has_any (servicelist)
| parse EventData with * 'OldSd">' OldSd "<" *
| parse EventData with * 'NewSd">' NewSd "<" *
| extend Reason = case( (OldSd has ';;;SY' and NewSd !has ';;;SY'), 'System Account is removed', (OldSd has_any (FullAccess) and NewSd has_any (ReadAccess)) , 'System permission has been changed to read from full access', (OldSd has_any (FullAccess) and NewSd has_any (DenyAccess)), 'System account has been given denied permission', 'None')
| project TimeGenerated, Computer, Account, ProcessName, ProcessId, ObjectName, EventData, Activity, HandleId, SubjectLogonId, OldSd, NewSd , Reason
),
(
SecurityEvent
| where TimeGenerated >= ago(timeframe)
| where EventID == 4688
| extend ProcessName = tostring(split(NewProcessName, '\\')[-1])
| where ProcessName in~ (filename)
| where CommandLine has_any (servicelist) and CommandLine has_any (parameters)
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type
),
(
WindowsEvent
| where TimeGenerated >= ago(timeframe)
| where EventID == 4670 and EventData has_any (servicelist) and EventData has 'Key'
| extend ObjectType = tostring(EventData.ObjectType)
| where ObjectType == 'Key'
| extend ObjectName = tostring(EventData.ObjectName)
| where ObjectName has_any (servicelist)
| extend OldSd = tostring(EventData.OldSd)
| extend NewSd = tostring(EventData.NewSd)
| extend Reason = case( (OldSd has ';;;SY' and NewSd !has ';;;SY'), 'System Account is removed', (OldSd has_any (FullAccess) and NewSd has_any (ReadAccess)) , 'System permission has been changed to read from full access', (OldSd has_any (FullAccess) and NewSd has_any (DenyAccess)), 'System account has been given denied permission', 'None')
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend ProcessName = tostring(EventData.ProcessName)
| extend ProcessId = tostring(EventData.ProcessId)
| extend Activity= "4670 - Permissions on an object were changed."
| extend HandleId = tostring(EventData.HandleId)
| extend SubjectLogonId = tostring(EventData.SubjectLogonId)
| project TimeGenerated, Computer, Account, ProcessName, ProcessId, ObjectName, EventData, Activity, HandleId, SubjectLogonId, OldSd, NewSd , Reason
),
(
WindowsEvent
| where TimeGenerated >= ago(timeframe)
| where EventID == 4688 and EventData has_any (filename) and EventData has_any (servicelist) and EventData has_any (parameters)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend ProcessName = tostring(split(NewProcessName, '\\')[-1])
| where ProcessName in~ (filename)
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has_any (servicelist) and CommandLine has_any (parameters)
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend AccountDomain = tostring(EventData.AccountDomain)
| extend Activity="4688 - A new process has been created."
| extend EventSourceName=Provider
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type
),
(
DeviceProcessEvents
| where TimeGenerated >= ago(timeframe)
| where InitiatingProcessFileName in~ (filename)
| where InitiatingProcessCommandLine has_any(servicelist) and InitiatingProcessCommandLine has_any (parameters)
| extend Account = iff(isnotempty(InitiatingProcessAccountUpn), InitiatingProcessAccountUpn, InitiatingProcessAccountName), Computer = DeviceName
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName = InitiatingProcessFileName, ProcessNameFullPath = FolderPath, Activity = ActionType, CommandLine = InitiatingProcessCommandLine, Type, InitiatingProcessParentFileName
)
)
| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
version: 1.1.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Stage 0: let
let servicelist = dynamic(['Services\\HealthService', 'Services\\Sense', 'Services\\WinDefend', 'Services\\MsSecFlt', 'Services\\DiagTrack', 'Services\\SgrmBroker', 'Services\\SgrmAgent', 'Services\\AATPSensorUpdater' , 'Services\\AATPSensor', 'Services\\mpssvc']);
let filename = dynamic(["subinacl.exe",'SetACL.exe']);
let parameters = dynamic (['/deny=SYSTEM', '/deny=S-1-5-18', '/grant=SYSTEM=r', '/grant=S-1-5-18=r', 'n:SYSTEM;p:READ', 'n1:SYSTEM;ta:remtrst;w:dacl']);
let FullAccess = dynamic(['A;CI;KA;;;SY', 'A;ID;KA;;;SY', 'A;CIID;KA;;;SY']);
let ReadAccess = dynamic(['A;CI;KR;;;SY', 'A;ID;KR;;;SY', 'A;CIID;KR;;;SY']);
let DenyAccess = dynamic(['D;CI;KR;;;SY', 'D;ID;KR;;;SY', 'D;CIID;KR;;;SY']);
let timeframe = 1d;
Stage 1: union
union of 5 branches
Stage 2: source
SecurityEvent
Stage 3: where
| where TimeGenerated >= ago(timeframe)
Stage 4: where
| where EventID == 4670
Stage 5: where
| where ObjectType == 'Key'
Stage 6: where
| where ObjectName has_any (servicelist)
Stage 7: parse
| parse EventData with * 'OldSd">' OldSd "<" *
Stage 8: parse
| parse EventData with * 'NewSd">' NewSd "<" *
Stage 9: extend
| extend Reason = case( (OldSd has ';;;SY' and NewSd !has ';;;SY'), 'System Account is removed', (OldSd has_any (FullAccess) and NewSd has_any (ReadAccess)) , 'System permission has been changed to read from full access', (OldSd has_any (FullAccess) and NewSd has_any (DenyAccess)), 'System account has been given denied permission', 'None')
Reason =OldSd has ";;;SY" and not (NewSd has ";;;SY")'System Account is removed'OldSd has "FullAccess" and NewSd has "ReadAccess"'System permission has been changed to read from full access'OldSd has "FullAccess" and NewSd has "DenyAccess"'System account has been given denied permission''None'Stage 10: project
| project TimeGenerated, Computer, Account, ProcessName, ProcessId, ObjectName, EventData, Activity, HandleId, SubjectLogonId, OldSd, NewSd , Reason
Stage 11: source
SecurityEvent
Stage 12: where
| where TimeGenerated >= ago(timeframe)
Stage 13: where
| where EventID == 4688
Stage 14: extend
| extend ProcessName = tostring(split(NewProcessName, '\\')[-1])
Stage 15: where
| where ProcessName in~ (filename)
Stage 16: where
| where CommandLine has_any (servicelist) and CommandLine has_any (parameters)
Stage 17: project
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type
Stage 18: source
WindowsEvent
Stage 19: where
| where TimeGenerated >= ago(timeframe)
Stage 20: where
| where EventID == 4670 and EventData has_any (servicelist) and EventData has 'Key'
Stage 21: extend
| extend ObjectType = tostring(EventData.ObjectType)
Stage 22: where
| where ObjectType == 'Key'
Stage 23: extend
| extend ObjectName = tostring(EventData.ObjectName)
Stage 24: where
| where ObjectName has_any (servicelist)
Stage 25: extend (9 consecutive steps)
| extend OldSd = tostring(EventData.OldSd)
| extend NewSd = tostring(EventData.NewSd)
| extend Reason = case( (OldSd has ';;;SY' and NewSd !has ';;;SY'), 'System Account is removed', (OldSd has_any (FullAccess) and NewSd has_any (ReadAccess)) , 'System permission has been changed to read from full access', (OldSd has_any (FullAccess) and NewSd has_any (DenyAccess)), 'System account has been given denied permission', 'None')
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend ProcessName = tostring(EventData.ProcessName)
| extend ProcessId = tostring(EventData.ProcessId)
| extend Activity= "4670 - Permissions on an object were changed."
| extend HandleId = tostring(EventData.HandleId)
| extend SubjectLogonId = tostring(EventData.SubjectLogonId)
Stage 26: project
| project TimeGenerated, Computer, Account, ProcessName, ProcessId, ObjectName, EventData, Activity, HandleId, SubjectLogonId, OldSd, NewSd , Reason
Stage 27: source
WindowsEvent
Stage 28: where
| where TimeGenerated >= ago(timeframe)
Stage 29: where
| where EventID == 4688 and EventData has_any (filename) and EventData has_any (servicelist) and EventData has_any (parameters)
Stage 30: extend
| extend NewProcessName = tostring(EventData.NewProcessName)
Stage 31: extend
| extend ProcessName = tostring(split(NewProcessName, '\\')[-1])
Stage 32: where
| where ProcessName in~ (filename)
Stage 33: extend
| extend CommandLine = tostring(EventData.CommandLine)
Stage 34: where
| where CommandLine has_any (servicelist) and CommandLine has_any (parameters)
Stage 35: extend (4 consecutive steps)
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend AccountDomain = tostring(EventData.AccountDomain)
| extend Activity="4688 - A new process has been created."
| extend EventSourceName=Provider
Stage 36: project
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type
Stage 37: source
DeviceProcessEvents
Stage 38: where
| where TimeGenerated >= ago(timeframe)
Stage 39: where
| where InitiatingProcessFileName in~ (filename)
Stage 40: where
| where InitiatingProcessCommandLine has_any(servicelist) and InitiatingProcessCommandLine has_any (parameters)
Stage 41: extend
| extend Account = iff(isnotempty(InitiatingProcessAccountUpn), InitiatingProcessAccountUpn, InitiatingProcessAccountName), Computer = DeviceName
Account =isnotempty(InitiatingProcessAccountUpn)InitiatingProcessAccountUpnInitiatingProcessAccountNameStage 42: project
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName = InitiatingProcessFileName, ProcessNameFullPath = FolderPath, Activity = ActionType, CommandLine = InitiatingProcessCommandLine, Type, InitiatingProcessParentFileName
Stage 43: extend (3 consecutive steps)
extend AccountNTDomain, AccountName, DomainIndex, HostName, HostNameDomain
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | match |
| field:"CommandLine" kind:match |
EventData | match |
| field:"EventData" kind:match |
EventID | eq |
| field:"EventID" kind:eq |
InitiatingProcessCommandLine | match |
| field:"CommandLine" kind:match |
InitiatingProcessFileName | in |
| field:"parent_process_name" kind:in |
ObjectName | match |
| field:"ObjectName" kind:match |
ObjectType | eq |
| field:"ObjectType" kind:eq value:"Key" |
ProcessName | in |
| field:"process_name" kind:in |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
AccountDomain | project |
Activity | project |
CommandLine | project |
Computer | project |
InitiatingProcessParentFileName | project |
ProcessName | project |
ProcessNameFullPath | project |
TimeGenerated | project |
Type | project |
AccountNTDomain | extend |
AccountName | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |