Detection rules › Kusto
Detect DNS queries reporting multiple errors from different clients - Static threshold based (ASIM DNS Solution)
This rule creates an alert when multiple clients report errors for the same DNS query. This helps in identifying possible similar C2 communications originating from different clients. It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 22: DNSEvent (DNS query) |
Rule body
id: 5b8344eb-fa28-4ac3-bcff-bc19d5d63089
name: Detect DNS queries reporting multiple errors from different clients - Static threshold based (ASIM DNS Solution)
description: |
'This rule creates an alert when multiple clients report errors for the same DNS query. This helps in identifying possible similar C2 communications originating from different clients. It utilizes [ASIM](https://aka.ms/AboutASIM) normalization and is applied to any source that supports the ASIM DNS schema.'
severity: Medium
status: Available
tags:
- Schema: ASimDns
SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1568
- T1573
- T1008
query: |
let lookback=1h;
let threshold = 2;
let errors = dynamic(['NXDOMAIN', 'SERVFAIL', 'REFUSED']);
_Im_Dns(starttime=ago(lookback))
| where EventResultDetails has_any (errors)
| summarize SrcIPs = make_set(SrcIpAddr, 100), Dvcs = make_set(Dvc, 100), ResourceIds = make_set(_ResourceId, 100) by DnsQuery, bin(TimeGenerated, 10min)
| where array_length(SrcIPs) >= threshold
| extend TotalIPs = array_length(SrcIPs),IPCountthreshold = threshold
| extend DomainName = strcat(split(DnsQuery, ".")[1], ".", split(DnsQuery, ".")[2])
| mv-expand SrcIPs
| extend SrcIP = tostring(SrcIPs)
| mv-expand Dvcs
| extend Dvc = tostring(Dvcs)
| mv-expand ResourceIds
| extend ResourceId = tostring(ResourceIds)
| extend Dvc = strcat(split(Dvc, ".")[0])
| summarize Start=min(TimeGenerated), End=max(TimeGenerated) by SrcIP, Dvc, ResourceId, DnsQuery, DomainName, SrcIPs = tostring(SrcIPs), IPCountthreshold = threshold, TotalIPs
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DnsQuery
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIP
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: ResourceId
- entityType: Url
fieldMappings:
- identifier: Url
columnName: DnsQuery
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
eventGroupingSettings:
aggregationKind: SingleAlert
customDetails:
SrcIPs: SrcIPs
IPCountthreshold: IPCountthreshold
TotalIPs: TotalIPs
alertDetailsOverride:
alertDisplayNameFormat: "[Static threshold] Multiple errors for the same DNS query has been detected - '{{DnsQuery}}'"
alertDescriptionFormat: "Multiple errors were detected on different clients for the same DNS query. These unsuccessful responses can be an indication of C2 communication. \n\nThreshold for total clients reporting errors: '{{IPCountthreshold}}'\n\nCurrent count of clients reporting errors for this DNS query: '{{TotalIPs}}'\n\nClients requesting this DNSQuery include:\n\n'{{SrcIPs}}'"
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 1h;
let threshold = 2;
let errors = dynamic(['NXDOMAIN', 'SERVFAIL', 'REFUSED']);
Stage 1: source
_Im_Dns(starttime=ago(lookback))
Stage 2: where
| where EventResultDetails has_any (errors)
Stage 3: summarize
| summarize SrcIPs = make_set(SrcIpAddr, 100), Dvcs = make_set(Dvc, 100), ResourceIds = make_set(_ResourceId, 100) by DnsQuery, bin(TimeGenerated, 10min)
Stage 4: where
| where array_length(SrcIPs) >= threshold
Stage 5: extend
| extend TotalIPs = array_length(SrcIPs),IPCountthreshold = threshold
Stage 6: extend
| extend DomainName = strcat(split(DnsQuery, ".")[1], ".", split(DnsQuery, ".")[2])
Stage 7: mv-expand
| mv-expand SrcIPs
Stage 8: extend
| extend SrcIP = tostring(SrcIPs)
Stage 9: mv-expand
| mv-expand Dvcs
Stage 10: extend
| extend Dvc = tostring(Dvcs)
Stage 11: mv-expand
| mv-expand ResourceIds
Stage 12: extend
| extend ResourceId = tostring(ResourceIds)
Stage 13: extend
| extend Dvc = strcat(split(Dvc, ".")[0])
Stage 14: summarize
| summarize Start=min(TimeGenerated), End=max(TimeGenerated) by SrcIP, Dvc, ResourceId, DnsQuery, DomainName, SrcIPs = tostring(SrcIPs), IPCountthreshold = threshold, TotalIPs
Stage 15: extend
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
Stage 16: extend
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
HostNameDomain =if
DomainIndex != -1substring(Dvc, (DomainIndex + 1))else
DvcIndicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventResultDetails | match |
| field:"Status" kind:match |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsQuery | summarize |
DomainName | summarize |
Dvc | summarize |
End | summarize |
IPCountthreshold | summarize |
ResourceId | summarize |
SrcIP | summarize |
SrcIPs | summarize |
Start | summarize |
TotalIPs | summarize |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |