Detection rules › Kusto

Detect DNS queries reporting multiple errors from different clients - Static threshold based (ASIM DNS Solution)

Status
available
Severity
medium
Time window
10m
Group by
DnsQuery, DomainName, Dvc, IPCountthreshold, ResourceId, SrcIP, SrcIPs, TotalIPs
Source
github.com/Azure/Azure-Sentinel

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

Event coverage

ProviderEventTitle
SysmonEvent ID 22DNSEvent (DNS query)

Rule body kusto

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)
Threshold
ge 2

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 =
ifDomainIndex != -1substring(Dvc, (DomainIndex + 1))
elseDvc

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
EventResultDetailsmatch
  • NXDOMAIN
  • REFUSED
  • SERVFAIL
SrcIPsge
  • 2 transforms: array_length

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
DnsQuerysummarize
DomainNamesummarize
Dvcsummarize
Endsummarize
IPCountthresholdsummarize
ResourceIdsummarize
SrcIPsummarize
SrcIPssummarize
Startsummarize
TotalIPssummarize
DomainIndexextend
HostNameextend
HostNameDomainextend