Detection rules › Kusto

Security Service Registry ACL Modification

Severity
high
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
StealthT1562 Impair Defenses

Event coverage

Rule body kusto

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 isfuzzy=true

Stage 2: source

SecurityEvent

Stage 3: where time_window=86400s

| 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')

Stage 10: project

| project TimeGenerated, Computer, Account,  ProcessName, ProcessId, ObjectName, EventData, Activity, HandleId, SubjectLogonId, OldSd, NewSd , Reason

Stage 11: source

SecurityEvent

Stage 12: where time_window=86400s

| 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 time_window=86400s

| 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

| extend OldSd = tostring(EventData.OldSd)

Stage 26: extend

| extend NewSd = tostring(EventData.NewSd)

Stage 27: 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')

Stage 28: extend

| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))

Stage 29: extend

| extend ProcessName = tostring(EventData.ProcessName)

Stage 30: extend

| extend ProcessId = tostring(EventData.ProcessId)

Stage 31: extend

| extend Activity= "4670 - Permissions on an object were changed."

Stage 32: extend

| extend HandleId = tostring(EventData.HandleId)

Stage 33: extend

| extend  SubjectLogonId = tostring(EventData.SubjectLogonId)

Stage 34: project

| project TimeGenerated, Computer, Account,  ProcessName, ProcessId, ObjectName, EventData, Activity, HandleId, SubjectLogonId, OldSd, NewSd , Reason

Stage 35: source

WindowsEvent

Stage 36: where time_window=86400s

| where TimeGenerated >= ago(timeframe)

Stage 37: where

| where EventID == 4688 and EventData has_any (filename) and EventData has_any (servicelist) and EventData has_any (parameters)

Stage 38: extend

| extend NewProcessName = tostring(EventData.NewProcessName)

Stage 39: extend

| extend ProcessName = tostring(split(NewProcessName, '\\')[-1])

Stage 40: where

| where ProcessName in~ (filename)

Stage 41: extend

| extend CommandLine = tostring(EventData.CommandLine)

Stage 42: where

| where CommandLine has_any (servicelist) and CommandLine has_any (parameters)

Stage 43: extend

| extend  Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))

Stage 44: extend

| extend  AccountDomain = tostring(EventData.AccountDomain)

Stage 45: extend

| extend  Activity="4688 - A new process has been created."

Stage 46: extend

| extend  EventSourceName=Provider

Stage 47: project

| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type

Stage 48: source

DeviceProcessEvents

Stage 49: where time_window=86400s

| where TimeGenerated >= ago(timeframe)

Stage 50: where

| where InitiatingProcessFileName in~ (filename)

Stage 51: where

| where InitiatingProcessCommandLine has_any(servicelist) and InitiatingProcessCommandLine has_any (parameters)

Stage 52: extend

| extend Account = iff(isnotempty(InitiatingProcessAccountUpn), InitiatingProcessAccountUpn, InitiatingProcessAccountName), Computer = DeviceName

Stage 53: project

| project TimeGenerated, Computer, Account, AccountDomain, ProcessName = InitiatingProcessFileName, ProcessNameFullPath = FolderPath, Activity = ActionType, CommandLine = InitiatingProcessCommandLine, Type, InitiatingProcessParentFileName

Stage 54: extend

| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])

Stage 55: extend

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))

Stage 56: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
CommandLinematch
  • /deny=S-1-5-18
  • /deny=SYSTEM
  • /grant=S-1-5-18=r
  • /grant=SYSTEM=r
  • Services\\AATPSensor
  • Services\\AATPSensorUpdater
  • Services\\DiagTrack
  • Services\\HealthService
  • Services\\MsSecFlt
  • Services\\Sense
  • Services\\SgrmAgent
  • Services\\SgrmBroker
  • Services\\WinDefend
  • Services\\mpssvc
  • n1:SYSTEM;ta:remtrst;w:dacl
  • n:SYSTEM;p:READ
EventDatamatch
  • /deny=S-1-5-18
  • /deny=SYSTEM
  • /grant=S-1-5-18=r
  • /grant=SYSTEM=r
  • Key
  • Services\\AATPSensor
  • Services\\AATPSensorUpdater
  • Services\\DiagTrack
  • Services\\HealthService
  • Services\\MsSecFlt
  • Services\\Sense
  • Services\\SgrmAgent
  • Services\\SgrmBroker
  • Services\\WinDefend
  • Services\\mpssvc
  • SetACL.exe
  • n1:SYSTEM;ta:remtrst;w:dacl
  • n:SYSTEM;p:READ
  • subinacl.exe
EventIDeq
  • 4670 transforms: cased
  • 4688 transforms: cased corpus 312 (splunk 283, kusto 29)
InitiatingProcessCommandLinematch
  • /deny=S-1-5-18
  • /deny=SYSTEM
  • /grant=S-1-5-18=r
  • /grant=SYSTEM=r
  • Services\\AATPSensor
  • Services\\AATPSensorUpdater
  • Services\\DiagTrack
  • Services\\HealthService
  • Services\\MsSecFlt
  • Services\\Sense
  • Services\\SgrmAgent
  • Services\\SgrmBroker
  • Services\\WinDefend
  • Services\\mpssvc
  • n1:SYSTEM;ta:remtrst;w:dacl
  • n:SYSTEM;p:READ
InitiatingProcessFileNamein
  • SetACL.exe
  • subinacl.exe
ObjectNamematch
  • Services\\AATPSensor
  • Services\\AATPSensorUpdater
  • Services\\DiagTrack
  • Services\\HealthService
  • Services\\MsSecFlt
  • Services\\Sense
  • Services\\SgrmAgent
  • Services\\SgrmBroker
  • Services\\WinDefend
  • Services\\mpssvc
ObjectTypeeq
  • Key transforms: cased corpus 8 (sigma 4, kusto 4)
ProcessNamein
  • SetACL.exe
  • subinacl.exe

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Accountproject
AccountDomainproject
Activityproject
CommandLineproject
Computerproject
InitiatingProcessParentFileNameproject
ProcessNameproject
ProcessNameFullPathproject
TimeGeneratedproject
Typeproject
AccountNTDomainextend
AccountNameextend
DomainIndexextend
HostNameextend
HostNameDomainextend