Detection rules › Kusto
Detect Suspicious ncrypt.dll usage with RDP connections to unmanaged or 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 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 cli_tools = dynamic(["powershell", "python"]);
// Get suspicious ncrypt.dll usage via WDAC audit policy
let time_lookback = 1h;
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)
// 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)
| 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 = 1h;
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)
// 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)
| 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 cli_tools = dynamic(["powershell", "python"]);
let time_lookback = 1h;
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 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 no_tpm_devices
Stage 2: source
let no_tpm_device_info
Stage 3: source
let dangerous_rdp_sessions
Stage 4: source
let nonce_requests
Stage 5: source
DeviceEvents
Stage 6: where
where Timestamp > ago(3600s)
Stage 7: where
where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
Stage 8: join
join kind=inner (nonce_requests) on InitiatingProcessId, DeviceId
Stage 9: where
where /* macro: (Timestamp between (todatetime((NonceRequestTimestamp - 10m)) .. todatetime((NonceRequestTimestamp + 10m)))) */
Stage 10: join
join kind=inner (dangerous_rdp_sessions) on DeviceId
Stage 11: where
where not (InitiatingProcessFileName in~ ("backgroundtaskhost.exe", "svchost.exe"))
Stage 12: extend
extend WdacPolicyName
Stage 13: project
project ActionType, DeviceName, FileName, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP, InitiatingProcessSHA1, NonceRequestTimestamp, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteDeviceOnboardingStatus, RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported, RdpRemoteMacAddress, RdpTimeGenerated, Timestamp, WdacPolicyName
Stage 14: 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq value:"ConnectionSuccess" |
ActionType | starts_with |
| field:"ActionType" kind:starts_with value:"AppControl" |
FileName | eq |
| field:"file_name" kind:eq value:"ncrypt.dll" |
InitiatingProcessFileName | ne |
| field:"parent_process_name" kind:ne value:"microsoft.tri.sensor.exe" |
NodeLabel | eq |
| field:"NodeLabel" kind:eq value:"device" |
OnboardingStatus | ne |
| field:"OnboardingStatus" kind:ne value:"Onboarded" |
RemotePort | eq |
| field:"DestinationPort" kind:eq value:"3389" |
RemoteUrl | eq |
| field:"DestinationHostname" kind:eq value:"login.microsoftonline.com" |
TpmActivated | ne |
| field:"TpmActivated" kind:ne value:"true" |
TpmEnabled | ne |
| field:"TpmEnabled" kind:ne value:"true" |
TpmSupported | ne |
| field:"TpmSupported" kind:ne value:"true" |
type | eq |
| field:"type" kind:eq value:"DeviceInventoryId" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | summarize |