Detection rules › Kusto
Detect Suspicious ncrypt.dll usage on admin device with RDP connections to non TPM protected device
This detection rule uses a WDAC audit policy to ingest missing DeviceImageLoad events in MDE, and check for suspicious processes using the ncrypt.dll and admin devices performing RDP connection to unmanaged or non-TPM devices. More information on the attack scenario this is detection is applicable for can be found in the references.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Lateral Movement |
References
Telemetry coverage
Rule body
let time_lookback = 30d;
let admin_users = toscalar(
IdentityInfo
| where Timestamp > ago(7d)
| where CriticalityLevel != "" or AccountDisplayName contains "Admin"
| summarize make_set(AccountDisplayName)
);
let devices_with_admin_accounts = (
ExposureGraphEdges
// Get edges where source is a device and destination is a admin user
| where SourceNodeLabel == "device" and TargetNodeLabel == "user"
| where TargetNodeName in (admin_users)
// Check which devices have the credentials of the admin user
| make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
| graph-match (SourceNode)-[hasCredentialsOf]->(TargetNode)
project IncomingNodeName = SourceNode.NodeName, OutgoingNodeName = TargetNode.NodeName, CriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, CriticalityRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames
| summarize make_set(IncomingNodeName)
);
let no_tpm_devices = (
ExposureGraphNodes
// Get device nodes with their inventory ID
| where NodeLabel == "device"
| mv-expand EntityIds
| where EntityIds.type == "DeviceInventoryId"
// Get interesting properties
| extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
DeviceId = tostring(EntityIds.id)
// Search for distinct devices
| distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
// Get Unmanaged devices and device not supporting a TPM
| where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
| extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
DeviceNetworkInfo
| where Timestamp > ago(7d)
// Get latest network info for each device ID
| summarize arg_max(Timestamp, *) by DeviceId
| mv-expand todynamic(IPAddresses)
| extend IPAddress = tostring(IPAddresses.IPAddress)
// Find no TPM devices and join with their network information
| join kind=inner no_tpm_devices on DeviceId
| project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
let dangerous_rdp_sessions = (
DeviceNetworkEvents
| where Timestamp > ago(time_lookback)
// Only flag admin devices
| where DeviceName in (devices_with_admin_accounts)
// Exclude MDI RDP Connections (known for NNR)
| where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
// Search for RDP connections to non-tpm devices
| where ActionType == "ConnectionSuccess"
| where RemotePort == 3389
| join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
| project-rename RemoteDeviceId = DeviceId1,
RdpRemoteDeviceName = DeviceName1,
RdpRemoteMacAddress = MacAddress,
RdpRemoteDeviceOnboardingStatus = OnboardingStatus,
RdpRemoteDeviceTpmActivated = TpmActivated,
RdpRemoteDeviceTpmEnabled = TpmEnabled,
RdpRemoteDeviceTpmSupported = TpmSupported,
RdpTimeGenerated = Timestamp,
RdpInitiatingProcessFileName = InitiatingProcessFileName
| project-away IPAddress
);
// Get all possible nonce requests
let nonce_requests = (
DeviceNetworkEvents
| where Timestamp > ago(time_lookback)
| where ActionType == "ConnectionSuccess"
| where RemoteUrl =~ "login.microsoftonline.com"
| project-rename NonceRequestTimestamp = Timestamp
);
// Get suspicious ncrypt.dll usage via WDAC audit policy
DeviceEvents
| where Timestamp > ago(time_lookback)
// Only flag admin devices
| where DeviceName in (devices_with_admin_accounts)
| where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
// Check if the same initiating process is doing a nonce request
| join kind=inner nonce_requests on InitiatingProcessId, DeviceId
// Only flag when nonce was request 10min before of after ncrypt usage
| where Timestamp between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m))
// Check if the same device is doing RDP Connections
| join kind=inner dangerous_rdp_sessions on DeviceId
// Whitelist known good processes
| where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe")
// Project interesting columns
| extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"]
| project Timestamp, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName,
InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP,
NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus,
RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported
let time_lookback = 30d;
let admin_users = toscalar(
IdentityInfo
| where TimeGenerated > ago(7d)
| where CriticalityLevel != "" or AccountDisplayName contains "Admin"
| summarize make_set(AccountDisplayName)
);
let devices_with_admin_accounts = (
ExposureGraphEdges
// Get edges where source is a device and destination is a admin user
| where SourceNodeLabel == "device" and TargetNodeLabel == "user"
| where TargetNodeName in (admin_users)
// Check which devices have the credentials of the admin user
| make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
| graph-match (SourceNode)-[hasCredentialsOf]->(TargetNode)
project IncomingNodeName = SourceNode.NodeName, OutgoingNodeName = TargetNode.NodeName, CriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, CriticalityRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames
| summarize make_set(IncomingNodeName)
);
let no_tpm_devices = (
ExposureGraphNodes
// Get device nodes with their inventory ID
| where NodeLabel == "device"
| mv-expand EntityIds
| where EntityIds.type == "DeviceInventoryId"
// Get interesting properties
| extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
DeviceId = tostring(EntityIds.id)
// Search for distinct devices
| distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
// Get Unmanaged devices and device not supporting a TPM
| where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
| extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
DeviceNetworkInfo
| where TimeGenerated > ago(7d)
// Get latest network info for each device ID
| summarize arg_max(TimeGenerated, *) by DeviceId
| mv-expand todynamic(IPAddresses)
| extend IPAddress = tostring(IPAddresses.IPAddress)
// Find no TPM devices and join with their network information
| join kind=inner no_tpm_devices on DeviceId
| project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
let dangerous_rdp_sessions = (
DeviceNetworkEvents
| where TimeGenerated > ago(time_lookback)
// Only flag admin devices
| where DeviceName in (devices_with_admin_accounts)
// Exclude MDI RDP Connections (known for NNR)
| where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
// Search for RDP connections to non-tpm devices
| where ActionType == "ConnectionSuccess"
| where RemotePort == 3389
| join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
| project-rename RemoteDeviceId = DeviceId1,
RdpRemoteDeviceName = DeviceName1,
RdpRemoteMacAddress = MacAddress,
RdpRemoteDeviceOnboardingStatus = OnboardingStatus,
RdpRemoteDeviceTpmActivated = TpmActivated,
RdpRemoteDeviceTpmEnabled = TpmEnabled,
RdpRemoteDeviceTpmSupported = TpmSupported,
RdpTimeGenerated = Timestamp,
RdpInitiatingProcessFileName = InitiatingProcessFileName
| project-away IPAddress
);
// Get all possible nonce requests
let nonce_requests = (
DeviceNetworkEvents
| where TimeGenerated > ago(time_lookback)
| where ActionType == "ConnectionSuccess"
| where RemoteUrl =~ "login.microsoftonline.com"
| project-rename NonceRequestTimestamp = TimeGenerated
);
// Get suspicious ncrypt.dll usage via WDAC audit policy
DeviceEvents
| where TimeGenerated > ago(time_lookback)
// Only flag admin devices
| where DeviceName in (devices_with_admin_accounts)
| where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
// Check if the same initiating process is doing a nonce request
| join kind=inner nonce_requests on InitiatingProcessId, DeviceId
// Only flag when nonce was request 10min before of after ncrypt usage
| where TimeGenerated between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m))
// Check if the same device is doing RDP Connections
| join kind=inner dangerous_rdp_sessions on DeviceId
// Whitelist known good processes
| where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe")
// Project interesting columns
| extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"]
| project TimeGenerated, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName,
InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP,
NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus,
RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported
Stages and Predicates
Parameters
let time_lookback = 30d;
Let binding: admin_users
let admin_users = toscalar(
IdentityInfo
| where Timestamp > ago(7d)
| where CriticalityLevel != "" or AccountDisplayName contains "Admin"
| summarize make_set(AccountDisplayName)
);
Let binding: devices_with_admin_accounts
let devices_with_admin_accounts = (
ExposureGraphEdges
| where SourceNodeLabel == "device" and TargetNodeLabel == "user"
| where TargetNodeName in (admin_users)
| make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
| graph-match (SourceNode)-[hasCredentialsOf]->(TargetNode)
project IncomingNodeName = SourceNode.NodeName, OutgoingNodeName = TargetNode.NodeName, CriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, CriticalityRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames
| summarize make_set(IncomingNodeName)
);
Let binding: no_tpm_devices
let no_tpm_devices = (
ExposureGraphNodes
| where NodeLabel == "device"
| mv-expand EntityIds
| where EntityIds.type == "DeviceInventoryId"
| extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
DeviceId = tostring(EntityIds.id)
| distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
| where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
| extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
Let binding: no_tpm_device_info
let no_tpm_device_info = (
DeviceNetworkInfo
| where Timestamp > ago(7d)
| summarize arg_max(Timestamp, *) by DeviceId
| mv-expand todynamic(IPAddresses)
| extend IPAddress = tostring(IPAddresses.IPAddress)
| join kind=inner no_tpm_devices on DeviceId
| project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
Let binding: dangerous_rdp_sessions
let dangerous_rdp_sessions = (
DeviceNetworkEvents
| where Timestamp > ago(time_lookback)
| where DeviceName in (devices_with_admin_accounts)
| where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
| where ActionType == "ConnectionSuccess"
| where RemotePort == 3389
| join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
| project-rename RemoteDeviceId = DeviceId1,
RdpRemoteDeviceName = DeviceName1,
RdpRemoteMacAddress = MacAddress,
RdpRemoteDeviceOnboardingStatus = OnboardingStatus,
RdpRemoteDeviceTpmActivated = TpmActivated,
RdpRemoteDeviceTpmEnabled = TpmEnabled,
RdpRemoteDeviceTpmSupported = TpmSupported,
RdpTimeGenerated = Timestamp,
RdpInitiatingProcessFileName = InitiatingProcessFileName
| project-away IPAddress
);
Let binding: nonce_requests
let nonce_requests = (
DeviceNetworkEvents
| where Timestamp > ago(time_lookback)
| where ActionType == "ConnectionSuccess"
| where RemoteUrl =~ "login.microsoftonline.com"
| project-rename NonceRequestTimestamp = Timestamp
);
Stage 1: source
let devices_with_admin_accounts
Stage 2: source
let no_tpm_devices
Stage 3: source
let no_tpm_device_info
Stage 4: source
let dangerous_rdp_sessions
Stage 5: source
let nonce_requests
Stage 6: source
DeviceEvents
Stage 7: where
where Timestamp > ago(2592000s)
Stage 8: where
where DeviceName =~ "devices_with_admin_accounts"
Stage 9: where
where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
Stage 10: join
join kind=inner (nonce_requests) on InitiatingProcessId, DeviceId
Stage 11: where
where /* macro: (Timestamp between (todatetime((NonceRequestTimestamp - 10m)) .. todatetime((NonceRequestTimestamp + 10m)))) */
Stage 12: join
join kind=inner (dangerous_rdp_sessions) on DeviceId
Stage 13: where
where not (InitiatingProcessFileName in~ ("backgroundtaskhost.exe", "svchost.exe"))
Stage 14: extend
extend WdacPolicyName
Stage 15: project
project ActionType, DeviceName, FileName, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP, InitiatingProcessSHA1, NonceRequestTimestamp, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteDeviceOnboardingStatus, RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported, RdpRemoteMacAddress, RdpTimeGenerated, Timestamp, WdacPolicyName
Stage 16: summarize aggregation inside the join branch
summarize by DeviceId
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
InitiatingProcessFileName | in | backgroundtaskhost.exe, svchost.exe | excludes:InitiatingProcessFileName field:"InitiatingProcessFileName" value:"backgroundtaskhost.exe" field:"InitiatingProcessFileName" value:"svchost.exe" |
Indicators
These rows show field, operator, and value matches.
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | summarize |