Detection rules › Kusto

Detect excessive NXDOMAIN DNS queries - Anomaly based (ASIM DNS Solution)

Status
available
Severity
medium
Time window
14d
Group by
SrcIpAddr, exists, sumexist
Source
github.com/Azure/Azure-Sentinel

This rule makes use of the series decompose anomaly method to generate an alert when client requests excessive amount of DNS queries to non-existent domains. This helps in identifying possible C2 communications. It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Telemetry coverage

ProviderRecord / event type
SysmonEvent ID 22: DNSEvent (DNS query)

Rule body

id: 02f23312-1a33-4390-8b80-f7cd4df4dea0
name: Detect excessive NXDOMAIN DNS queries - Anomaly based (ASIM DNS Solution)
description: |
  'This rule makes use of the series decompose anomaly method to generate an alert when client requests excessive amount of DNS queries to non-existent domains. This helps in identifying possible C2 communications. 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: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1568
  - T1008
query: |
  let threshold = 2.5;
  let min_t = ago(14d);
  let max_t = now();
  let dt = 1d;
  let summarizationexist = (
    union isfuzzy=true 
        (
        DNS_Summarized_Logs_ip_CL
        | where EventTime_t > ago(1d) 
        | project v = int(2)
        ),
        (
        print int(1) 
        | project v = print_0
        )
    | summarize maxv = max(v)
    | extend sumexist = (maxv > 1)
    );
  let allData = union isfuzzy=true
        (
        (datatable(exists: int, sumexist: bool)[1, false]
        | join (summarizationexist) on sumexist)
        | join (
            _Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(min_t), endtime=max_t)
            | summarize Count=count() by SrcIpAddr, bin(TimeGenerated, 1h)
            | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
            )
            on exists
        | project-away exists, maxv, sum*
        ),
        (
        DNS_Summarized_Logs_ip_CL
        | where EventTime_t > min_t and EventResultDetails_s == 'NXDOMAIN'
        | summarize Count=toint(sum(count__d)) by SrcIpAddr=SrcIpAddr_s, bin(EventTime=EventTime_t, 1h)
        );
  allData
  | make-series EventCount=sum(Count) on EventTime from min_t to max_t step dt by SrcIpAddr
  | extend (anomalies, score, baseline) = series_decompose_anomalies(EventCount, threshold, -1, 'linefit')
  | mv-expand anomalies, score, baseline, EventTime, EventCount
  | extend
    anomalies = toint(anomalies),
    score = toint(score),
    baseline = toint(baseline),
    EventTime = todatetime(EventTime),
    Total = tolong(EventCount)
  | where EventTime >= ago(dt)
  | where score >= threshold * 2
  | join kind=inner(_Im_Dns(responsecodename='NXDOMAIN', starttime=ago(dt), endtime=max_t)
    | summarize DNSQueries = make_set(DnsQuery) by SrcIpAddr)
    on SrcIpAddr
  | project-away SrcIpAddr1
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  DNSQueries: DNSQueries
  AnomalyScore: score
  baseline: baseline
  Total: Total
alertDetailsOverride:
  alertDisplayNameFormat: "[Anomaly] Excessive NXDOMAIN DNS Queries has been detected from client IP: '{{SrcIpAddr}}'"
  alertDescriptionFormat: "This client is generating excessive amount of DNS queries for non-existent domains. This can be an indication of possible C2 communications.\n\nBaseline for 'NXDOMAIN' error count for this client: '{{baseline}}'\n\nCurrent 'NXDOMAIN' error count for this client: '{{Total}}'\n\nDNS queries requested by the client include:\n\n'{{DNSQueries}}'"
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 0: let

let threshold = 2.5;
let min_t = ago(14d);
let max_t = now();
let dt = 1d;
let summarizationexist = (
  union isfuzzy=true 
      (
      DNS_Summarized_Logs_ip_CL
      | where EventTime_t > ago(1d) 
      | project v = int(2)
      ),
      (
      print int(1) 
      | project v = print_0
      )
  | summarize maxv = max(v)
  | extend sumexist = (maxv > 1)
  );
let allData = union <inlined as stages below>;

Stage 1: source

let allData

Stage 2: union

union of 2 branches

Stage 3: source

datatable

Stage 4: join

join (summarizationexist) on sumexist

Stage 5: join

join (_Im_Dns) on exists

Stage 6: project-away

project-away exists, maxv, sum*

Stage 7: source

DNS_Summarized_Logs_ip_CL

Stage 8: where

where EventResultDetails_s =~ "NXDOMAIN" and EventTime_t > min_t

Stage 9: summarize

summarize Count, SrcIpAddr by SrcIpAddr

Stage 10: summarize

summarize EventCount by SrcIpAddr

Stage 11: extend

extend anomalies, baseline, score

Stage 12: mv-expand

mv-expand anomalies

Stage 13: extend

extend EventTime, Total, anomalies, baseline, score

Stage 14: where

where EventTime >= ago(86400s)

Stage 15: where

where score >= 5

Stage 16: join

join kind=inner (_Im_Dns) on SrcIpAddr

Stage 17: project-away

project-away SrcIpAddr1

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventResultDetails_seq
  • NXDOMAIN corpus 2 (kusto 2)
field:"EventResultDetails_s" kind:eq value:"NXDOMAIN"
EventTime_tcross_field_compare
  • min_t transforms: op:gt corpus 4 (kusto 4)
field:"EventTime_t" kind:cross_field_compare value:"min_t"
scorege
  • 5 corpus 6 (kusto 6)
field:"score" kind:ge value:"5"

Output fields

These fields are emitted when the rule matches.

FieldSource
EventCountsummarize
SrcIpAddrsummarize
anomaliesextend
baselineextend
scoreextend
EventTimeextend
Totalextend