Detection rules › Kusto

Microsoft Entra ID Local Device Join Information and Transport Key Registry Keys Access

Status
available
Severity
medium
Time window
1d
Source
github.com/Azure/Azure-Sentinel

This detection uses Windows security events to detect suspicious access attempts by the same process to registry keys that provide information about an Microsoft Entra ID joined or registered devices and Transport keys (tkpub / tkpriv). This information can be used to export the Device Certificate (dkpub / dkpriv) and Transport key (tkpub/tkpriv). These set of keys can be used to impersonate existing Microsoft Entra ID joined devices. This detection requires an access control entry (ACE) on the system access control list (SACL) of the following securable objects: HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin (Microsoft Entra ID joined devices) HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin (Microsoft Entra ID registered devices) HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey (Transport Key) Make sure you set the SACL to propagate to its sub-keys. You can find more information in here https://github.com/OTRF/Set-AuditRule/blob/master/rules/registry/aad_connect_health_service_agent.yml Reference: https://aadinternals.com/post/deviceidentity/

MITRE ATT&CK coverage

TacticTechniques
Discovery

Telemetry coverage

Rule body

id: a356c8bd-c81d-428b-aa36-83be706be034
name: Microsoft Entra ID Local Device Join Information and Transport Key Registry Keys Access 
description: |
  'This detection uses Windows security events to detect suspicious access attempts by the same process to registry keys that provide information about an Microsoft Entra ID joined or registered devices and Transport keys (tkpub / tkpriv).
   This information can be used to export the Device Certificate (dkpub / dkpriv) and Transport key (tkpub/tkpriv).
   These set of keys can be used to impersonate existing Microsoft Entra ID joined devices.
   This detection requires an access control entry (ACE) on the system access control list (SACL) of the following securable objects:
   HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin (Microsoft Entra ID joined devices)
   HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin (Microsoft Entra ID registered devices)
   HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey (Transport Key)
   Make sure you set the SACL to propagate to its sub-keys. You can find more information in here https://github.com/OTRF/Set-AuditRule/blob/master/rules/registry/aad_connect_health_service_agent.yml
   Reference: https://aadinternals.com/post/deviceidentity/'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Discovery
relevantTechniques:
  - T1012
tags:
  - SimuLand
  - ATR
  - AADInternals
query: |
  // AADJoined or Register Device Registry Keys
  let aadJoinRoot = "\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\CloudDomainJoin\\JoinInfo\\";
  let aadRegisteredRoot = "\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\WorkplaceJoin";
  // Transport Key Registry Key
  let keyTransportKey = "\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\Cryptography\\Ngc\\KeyTransportKey\\";
  (union isfuzzy=true
  (
  // Access to Object Requested
  SecurityEvent
  | where EventID == '4656'
  | where EventData has aadJoinRoot or EventData has aadRegisteredRoot
  | extend EventData = parse_xml(EventData).EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
  | evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
  | where ObjectType == 'Key'
  | where ObjectName startswith aadJoinRoot and SubjectLogonId != '0x3e7' //Local System
  | extend ProcessId = column_ifexists("ProcessId", ""), Process = split(ProcessName, '\\', -1)[-1],Account = strcat(SubjectDomainName, "\\", SubjectUserName)
  | join kind=innerunique (
      SecurityEvent
      | where EventID == '4656'
      | where EventData has keyTransportKey
      | extend EventData = parse_xml(EventData).EventData.Data
      | mv-expand bagexpansion=array EventData
      | evaluate bag_unpack(EventData)
      | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
      | evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
      | extend ObjectName = column_ifexists("ObjectName", ""),ObjectType = column_ifexists("ObjectType", "")
      | where ObjectType == 'Key'
      | where ObjectName startswith keyTransportKey and SubjectLogonId != '0x3e7' //Local System
      | extend ProcessId = column_ifexists("ProcessId", ""), Process = split(ProcessName, '\\', -1)[-1],Account = strcat(SubjectDomainName, "\\", SubjectUserName)
  ) on $left.Computer == $right.Computer and $left.SubjectLogonId == $right.SubjectLogonId and $left.ProcessId == $right.ProcessId
  | project TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, SubjectLogonId, ObjectName, tostring(Process), ProcessName, ProcessId, EventID
  ),
  // Accessing Object
  (
  SecurityEvent
  | where EventID == '4663'
  | where ObjectType == 'Key'
  | where (ObjectName startswith aadJoinRoot or ObjectName contains aadRegisteredRoot) and SubjectLogonId != '0x3e7' //Local System
  | extend Account = SubjectAccount
  | join kind=innerunique (
      SecurityEvent
      | where EventID == '4663'
      | where ObjectType == 'Key'
      | where ObjectName has keyTransportKey and SubjectLogonId != '0x3e7' //Local System
      | extend Account = SubjectAccount
  ) on $left.Computer == $right.Computer and $left.SubjectLogonId == $right.SubjectLogonId and $left.ProcessId == $right.ProcessId
  | project TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, SubjectLogonId, ObjectName, Process, ProcessName, ProcessId, EventID
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
  )
  )
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: SubjectUserName
      - identifier: NTDomain
        columnName: SubjectDomainName
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.6
kind: Scheduled

Stages and Predicates

Stage 0: let

let aadJoinRoot = "\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\CloudDomainJoin\\JoinInfo\\";
let aadRegisteredRoot = "\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\WorkplaceJoin";
let keyTransportKey = "\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\Cryptography\\Ngc\\KeyTransportKey\\";

Stage 1: union

union of 2 branches

Stage 2: source

SecurityEvent

Stage 3: where

| where EventID == '4656'

Stage 4: where

| where EventData has aadJoinRoot or EventData has aadRegisteredRoot

Stage 5: extend

| extend EventData = parse_xml(EventData).EventData.Data

Stage 6: mv-expand

| mv-expand bagexpansion=array EventData

Stage 7: evaluate

| evaluate bag_unpack(EventData)

Stage 8: extend

| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")

Stage 9: evaluate

| evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)

Stage 10: where

| where ObjectType == 'Key'

Stage 11: where

| where ObjectName startswith aadJoinRoot and SubjectLogonId != '0x3e7'

Stage 12: extend

| extend ProcessId = column_ifexists("ProcessId", ""), Process = split(ProcessName, '\\', -1)[-1],Account = strcat(SubjectDomainName, "\\", SubjectUserName)

Stage 13: join

| join kind=innerunique (
    SecurityEvent
    | where EventID == '4656'
    | where EventData has keyTransportKey
    | extend EventData = parse_xml(EventData).EventData.Data
    | mv-expand bagexpansion=array EventData
    | evaluate bag_unpack(EventData)
    | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
    | evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
    | extend ObjectName = column_ifexists("ObjectName", ""),ObjectType = column_ifexists("ObjectType", "")
    | where ObjectType == 'Key'
    | where ObjectName startswith keyTransportKey and SubjectLogonId != '0x3e7'
    | extend ProcessId = column_ifexists("ProcessId", ""), Process = split(ProcessName, '\\', -1)[-1],Account = strcat(SubjectDomainName, "\\", SubjectUserName)
) on $left.Computer == $right.Computer and $left.SubjectLogonId == $right.SubjectLogonId and $left.ProcessId == $right.ProcessId

Stage 14: project

project Account, Computer, EventID, ObjectName, ProcessId, ProcessName, SubjectDomainName, SubjectLogonId, SubjectUserName, TimeGenerated

Stage 15: source

SecurityEvent

Stage 16: where

| where EventID == '4663'

Stage 17: where

| where ObjectType == 'Key'

Stage 18: where

| where (ObjectName startswith aadJoinRoot or ObjectName contains aadRegisteredRoot) and SubjectLogonId != '0x3e7'

Stage 19: extend

| extend Account = SubjectAccount

Stage 20: join

| join kind=innerunique (
    SecurityEvent
    | where EventID == '4663'
    | where ObjectType == 'Key'
    | where ObjectName has keyTransportKey and SubjectLogonId != '0x3e7'
    | extend Account = SubjectAccount
) on $left.Computer == $right.Computer and $left.SubjectLogonId == $right.SubjectLogonId and $left.ProcessId == $right.ProcessId

Stage 21: project

project Account, Computer, EventID, ObjectName, Process, ProcessId, ProcessName, SubjectDomainName, SubjectLogonId, SubjectUserName, TimeGenerated

Stage 22: extend

extend DnsDomain, HostName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventDatamatch
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\CloudDomainJoin\JoinInfo\ transforms: term
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Cryptography\Ngc\KeyTransportKey\ transforms: term
  • \SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin transforms: term
field:"EventData" kind:match
EventIDeq
  • 4656 corpus 19 (splunk 15, kusto 4)
  • 4663 corpus 36 (splunk 31, kusto 5)
field:"EventID" kind:eq
ObjectNamecontains
  • \SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin
field:"ObjectName" kind:contains value:"\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin"
ObjectNamematch
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Cryptography\Ngc\KeyTransportKey\ transforms: term
field:"ObjectName" kind:match value:"\REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Cryptography\Ngc\KeyTransportKey\"
ObjectNamestarts_with
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\CloudDomainJoin\JoinInfo\
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Cryptography\Ngc\KeyTransportKey\
field:"ObjectName" kind:starts_with
ObjectTypeeq
  • Key corpus 4 (kusto 4)
field:"ObjectType" kind:eq value:"Key"
SubjectLogonIdne
  • 0x3e7
field:"LogonId" kind:ne value:"0x3e7"

Output fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
Computerproject
EventIDproject
ObjectNameproject
Processproject
ProcessIdproject
ProcessNameproject
SubjectDomainNameproject
SubjectLogonIdproject
SubjectUserNameproject
TimeGeneratedproject
DnsDomainextend
HostNameextend