Detection rules › Kusto

Registry Run Keys - Suspicious Registry Run Keys

Group by
NormalizedRegistryValueData
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Below query looks for suspicious additions to Run, RunOnce and several other registry keys. The query analyzes all values in the specified registry keys and finds anomalous ones based on commonality in the environment and excludes possible legitimate activities like software installations. The query might require tuning according to the environment.

MITRE ATT&CK coverage

Event coverage

Rule body kusto

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post: https://mergene.medium.com/threat-hunting-with-data-science-registry-run-keys-9ae329d1ad85
// Description: This query looks for suspicious additions to Run, RunOnce and several other registry keys. 
//              The query analyzes all values in the specified registry keys and finds anomalous ones based on
//              commonality in the environment and excludes possible legitimate activities like software installations.
//              The query might requiure tuning according to the environment.
let dataset= materialize (
DeviceRegistryEvents 
| where ActionType == "RegistryValueSet" 
// registry keys to be monitored
| where RegistryKey has @"Microsoft\Windows\CurrentVersion\RunOnce"
    or RegistryKey has @"Microsoft\Windows\CurrentVersion\RunOnceEx"
    or RegistryKey has @"Microsoft\Windows\CurrentVersion\Run"
    or RegistryKey has @"\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
    or (RegistryKey has @"\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")
    or (RegistryKey has @"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer" and RegistryValueName == "Run")
    or (RegistryKey has @"\Session Manager" and RegistryValueName == "BootExecute")
    or (RegistryKey has @"Microsoft\Windows NT\CurrentVersion\Winlogon" and RegistryValueName == "Userinit")
    or (RegistryKey has @"Microsoft\Windows NT\CurrentVersion\Winlogon" and RegistryValueName == "Shell")
    or (RegistryKey has @"Microsoft\Windows NT\CurrentVersion\Windows" and RegistryValueName == "load")
    or RegistryKey has @"Microsoft\Windows NT\CurrentVersion\Winlogon\Notify" 
    or RegistryKey has @"\Software\Microsoft\Windows\CurrentVersion\RunServices"
    or RegistryKey has @"\Software\Microsoft\Windows\CurrentVersion\RunServicesOnce"
    or RegistryKey has @"\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run"
        // below is related to the persistence using CLSID (junction folders, etc.)
    or (RegistryKey has @"SOFTWARE\Classes\CLSID" and RegistryKey endswith "InprocServer32" and isempty(RegistryValueName))
// exclude Config.Msi folder items
| where RegistryValueData !endswith @".rbf /restore"
// create NormalizedRegistryValueData field
| extend NormalizedRegistryValueData = replace(@'(C|D):\\Users\\.*?\\', @'C:\\Users\\userxx\\',RegistryValueData )
| extend NormalizedRegistryValueData = replace(@'\{.*\}', @'\{xxxxxxxxxx\}',NormalizedRegistryValueData ) //{fe07d7-d438-4dd9-bb0f-5721658f4f}
| extend NormalizedRegistryValueData = replace(@'\\[A-Za-z0-9-]+-[A-Za-z0-9]+\\', @'\\xxxxxxxxxx\\',NormalizedRegistryValueData ) //\fe07d7-d438-4dd9-bb0f-5721658f4f\
| extend NormalizedRegistryValueData = replace(@'\d+\.\d+\.\d+\.\d+', @'X.Y.Z.T',NormalizedRegistryValueData )
| extend NormalizedRegistryValueData = replace(@'-\d+\.\d+\.\d+', @'-X.Y.Z',NormalizedRegistryValueData )
| extend NormalizedRegistryValueData = replace(@'_\d+\.log', @'_XYZT.log',NormalizedRegistryValueData )
| extend NormalizedRegistryValueData = replace(@'--quiet|--passive', @'',NormalizedRegistryValueData )
| extend NormalizedRegistryValueData = replace(@'installSessionId\s[A-Za-z0-9-]+', @'installSessionId xxxxxx',NormalizedRegistryValueData )
| extend NormalizedRegistryValueData = replace(@'C:\\ProgramData\\.*?\\Microsoft\\Teams\\',@'C:\\ProgramData\\userxxx\\Microsoft\\Teams\\',NormalizedRegistryValueData)
);
dataset
| summarize dcount_device = dcount(DeviceId), total_count = count() by NormalizedRegistryValueData
| where dcount_device <=5 and total_count <20
| join kind=inner (dataset| where Timestamp > ago(1d)) on NormalizedRegistryValueData
| invoke FileProfile(InitiatingProcessSHA1,1000)
| where GlobalPrevalence <100
         or isempty(GlobalPrevalence)
         // inlcude processes that are involved in malicious attacks(e.g. office macro creating the registry key)
         or InitiatingProcessFileName in~ ("powershell.exe","reg.exe", "regedit.exe", "cmd.exe","winword.exe","excel.exe","powerpnt.exe")

Stages and Predicates

Stage 0: let

let dataset = materialize(<inlined as stages below>);

Stage 1: source

DeviceRegistryEvents

Stage 2: where

| where ActionType == "RegistryValueSet"

Stage 3: where

| where RegistryKey has @"Microsoft\Windows\CurrentVersion\RunOnce"
    or RegistryKey has @"Microsoft\Windows\CurrentVersion\RunOnceEx"
    or RegistryKey has @"Microsoft\Windows\CurrentVersion\Run"
    or RegistryKey has @"\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
    or (RegistryKey has @"\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")
    or (RegistryKey has @"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer" and RegistryValueName == "Run")
    or (RegistryKey has @"\Session Manager" and RegistryValueName == "BootExecute")
    or (RegistryKey has @"Microsoft\Windows NT\CurrentVersion\Winlogon" and RegistryValueName == "Userinit")
    or (RegistryKey has @"Microsoft\Windows NT\CurrentVersion\Winlogon" and RegistryValueName == "Shell")
    or (RegistryKey has @"Microsoft\Windows NT\CurrentVersion\Windows" and RegistryValueName == "load")
    or RegistryKey has @"Microsoft\Windows NT\CurrentVersion\Winlogon\Notify" 
    or RegistryKey has @"\Software\Microsoft\Windows\CurrentVersion\RunServices"
    or RegistryKey has @"\Software\Microsoft\Windows\CurrentVersion\RunServicesOnce"
    or RegistryKey has @"\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run"
    or (RegistryKey has @"SOFTWARE\Classes\CLSID" and RegistryKey endswith "InprocServer32" and isempty(RegistryValueName))

Stage 4: where

| where RegistryValueData !endswith @".rbf /restore"

Stage 5: extend

| extend NormalizedRegistryValueData = replace(@'(C|D):\\Users\\.*?\\', @'C:\\Users\\userxx\\',RegistryValueData )

Stage 6: extend

| extend NormalizedRegistryValueData = replace(@'\{.*\}', @'\{xxxxxxxxxx\}',NormalizedRegistryValueData )

Stage 7: extend

| extend NormalizedRegistryValueData = replace(@'\\[A-Za-z0-9-]+-[A-Za-z0-9]+\\', @'\\xxxxxxxxxx\\',NormalizedRegistryValueData )

Stage 8: extend

| extend NormalizedRegistryValueData = replace(@'\d+\.\d+\.\d+\.\d+', @'X.Y.Z.T',NormalizedRegistryValueData )

Stage 9: extend

| extend NormalizedRegistryValueData = replace(@'-\d+\.\d+\.\d+', @'-X.Y.Z',NormalizedRegistryValueData )

Stage 10: extend

| extend NormalizedRegistryValueData = replace(@'_\d+\.log', @'_XYZT.log',NormalizedRegistryValueData )

Stage 11: extend

| extend NormalizedRegistryValueData = replace(@'--quiet|--passive', @'',NormalizedRegistryValueData )

Stage 12: extend

| extend NormalizedRegistryValueData = replace(@'installSessionId\s[A-Za-z0-9-]+', @'installSessionId xxxxxx',NormalizedRegistryValueData )

Stage 13: extend

| extend NormalizedRegistryValueData = replace(@'C:\\ProgramData\\.*?\\Microsoft\\Teams\\',@'C:\\ProgramData\\userxxx\\Microsoft\\Teams\\',NormalizedRegistryValueData)

Stage 14: summarize

| summarize dcount_device = dcount(DeviceId), total_count = count() by NormalizedRegistryValueData

Stage 15: where

| where dcount_device <=5 and total_count <20

Stage 16: join

| join kind=inner (dataset| where Timestamp > ago(1d)) on NormalizedRegistryValueData

Stage 17: invoke

| invoke FileProfile(InitiatingProcessSHA1,1000)

Stage 18: where

| where GlobalPrevalence <100
         or isempty(GlobalPrevalence)
         or InitiatingProcessFileName in~ ("powershell.exe","reg.exe", "regedit.exe", "cmd.exe","winword.exe","excel.exe","powerpnt.exe")

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

StageFieldKindExcluded values
4RegistryValueDataends_with.rbf /restore
16RegistryValueDataends_with.rbf /restore

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
ActionTypeeq
  • RegistryValueSet transforms: cased corpus 4 (kusto 4)
GlobalPrevalenceis_null
  • (no value, null check)
GlobalPrevalencelt
  • 100 transforms: cased corpus 4 (kusto 4)
InitiatingProcessFileNamein
  • cmd.exe corpus 15 (elastic 10, splunk 4, kusto 1)
  • excel.exe corpus 8 (elastic 8)
  • powerpnt.exe corpus 7 (elastic 7)
  • powershell.exe corpus 12 (elastic 9, kusto 2, splunk 1)
  • reg.exe
  • regedit.exe
  • winword.exe corpus 8 (elastic 8)
RegistryKeyends_with
  • InprocServer32
RegistryKeymatch
  • Microsoft\Windows NT\CurrentVersion\Windows
  • Microsoft\Windows NT\CurrentVersion\Winlogon
  • Microsoft\Windows NT\CurrentVersion\Winlogon\Notify
  • Microsoft\Windows\CurrentVersion\Run
  • Microsoft\Windows\CurrentVersion\RunOnce
  • Microsoft\Windows\CurrentVersion\RunOnceEx
  • SOFTWARE\Classes\CLSID
  • Software\Microsoft\Windows\CurrentVersion\Policies\Explorer
  • \Microsoft\Windows\CurrentVersion\Explorer\Shell Folders
  • \Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders
  • \Session Manager
  • \Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run corpus 4 (sigma 3, kusto 1)
  • \Software\Microsoft\Windows\CurrentVersion\RunServices
  • \Software\Microsoft\Windows\CurrentVersion\RunServicesOnce
RegistryValueNameeq
  • BootExecute transforms: cased
  • Run transforms: cased
  • Shell transforms: cased
  • Userinit transforms: cased
  • load transforms: cased
RegistryValueNameis_null
  • (no value, null check)
dcount_devicele
  • 5 transforms: cased
total_countlt
  • 20 transforms: cased

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
NormalizedRegistryValueDatasummarize
dcount_devicesummarize
total_countsummarize