Lesson 3 of 6 · 25 min read

DNS Anomaly Analysis — DGA, Tunnelling, and Fast-Flux

DNS is the phone book of the internet — and it was designed before anyone was thinking about security. Every malware family that needs to survive IP blocklists uses DNS to find its C2. Every attacker who needs to exfiltrate data from a network with strict egress filtering eventually tries DNS. Understanding what legitimate DNS looks like makes the malicious patterns obvious.


Normal vs. Anomalous DNS: The Baseline

Normal enterprise DNS profile:

Query volume: 100-500 queries/hour per host (varies by role)
Domain characteristics:
  - Human-readable names (google.com, office.com, cdn.example.com)
  - Known registration dates (years old for established services)
  - Low NXDOMAIN rate (< 2% of queries)
  - Query types: predominantly A and AAAA
  - TTL: typically 300-3600 seconds for stable services

Suspicious DNS profile:

Query volume: spikes to thousands/hour in short windows
Domain characteristics:
  - Random-looking character strings (high entropy)
  - Recently registered (days old)
  - High NXDOMAIN rate (> 20% of queries)
  - Query types: TXT or NULL for data domains
  - TTL: < 60 seconds (fast-flux) or > 86400 (exfil domain, no need for rotation)

DGA Detection

Domain Generation Algorithms create a stream of pseudo-random domain names. The malware and the attacker both run the same algorithm, seeded with the current date. The attacker registers whichever domain(s) are “active” that day; the malware queries hundreds of domains until it finds one that resolves.

From the defender’s perspective in dns.log:

# What DGA traffic looks like:
14:23:01  query=a8f3j2k1bc.com          rcode=NXDOMAIN
14:23:01  query=z9x1w2q4rt.com          rcode=NXDOMAIN
14:23:01  query=m4n8k3l2pq.net          rcode=NXDOMAIN
14:23:01  query=p7r3t9v2wx.org          rcode=NXDOMAIN
14:23:02  query=d2f6g1h9jk.com          rcode=NOERROR   ← TODAY'S ACTIVE DOMAIN
           answers=[185.220.101.5]

DGA indicators:

  1. Burst of NXDOMAIN responses in rapid succession from one host
  2. Domain names with high entropy (measure with Shannon entropy formula or tooling)
  3. No prior DNS history for any of these domains
  4. Sudden resolution of one domain to an IP not seen in your environment

KQL query for DGA detection:

Zeek_dns_CL
| where TimeGenerated > ago(1h)
| where rcode_name_s == "NXDOMAIN"
| summarize nxdomain_count = count(), 
            domain_list = make_set(query_s, 10)
  by id_orig_h_s, bin(TimeGenerated, 5m)
| where nxdomain_count > 30
| order by nxdomain_count desc

More than 30 NXDOMAIN responses in 5 minutes from one host → investigate.

Entropy calculation:

Domain names generated by DGA have higher Shannon entropy than human-readable names:

Threshold: query label entropy > 3.5 bits/char is suspicious. Automated tools: dnstwist, custom Python scripts using scipy.stats.entropy.


Fast-Flux Detection

Fast-flux domains rotate their IP addresses faster than legitimate CDNs. Legitimate CDNs do use multiple IPs, but they rotate on a longer timescale.

Fast-flux indicators in dns.log:

# Same domain, multiple short-TTL resolutions:
14:00:00  query=c2.evil.net  TTL=30  answers=[1.2.3.4]
14:00:35  query=c2.evil.net  TTL=30  answers=[5.6.7.8]
14:01:10  query=c2.evil.net  TTL=30  answers=[9.10.11.12]
14:01:50  query=c2.evil.net  TTL=30  answers=[13.14.15.16]

Four different IPs in 2 minutes for the same domain. TTL=30 means the record expires every 30 seconds — forcing re-resolution and IP rotation.

KQL for fast-flux:

Zeek_dns_CL
| where TimeGenerated > ago(24h)
| where rcode_name_s == "NOERROR"
| summarize 
    unique_answers = dcount(answers_s),
    min_ttl = min(toint(TTLs_s)),
    query_count = count()
  by query_s
| where unique_answers > 5 and min_ttl < 60
| order by unique_answers desc

More than 5 unique IPs for the same domain with TTL < 60 seconds = fast-flux investigation.


DNS Tunnelling Detection

DNS tunnelling requires:

  1. A malware client that encodes data as DNS queries
  2. An attacker-controlled authoritative DNS server that responds with encoded data

Pattern in dns.log:

# Tunnelling: base64 in subdomain, TXT query type
query=V2hhdCBpcyB0aGUgYW5zd2VyIHRvIGxpZmU=.tunnel.attacker.com  qtype=TXT
query=dGhlIHVuaXZlcnNlIGFuZCBldmVyeXRoaW5n.tunnel.attacker.com  qtype=TXT
query=Zm9ydHktdHdv.tunnel.attacker.com  qtype=TXT
answers=Y29tbWFuZDogbHMgLWxhIC9ldGMvcGFzc3dk  ← base64 C2 command in TXT response

Tunnelling detection KQL:

Zeek_dns_CL
| where TimeGenerated > ago(1h)
| where qtype_name_s in ("TXT", "NULL")  // primary tunnelling types
| extend label_len = strlen(tostring(split(query_s, ".")[0]))
| where label_len > 30                   // long first label = encoded data
| summarize 
    query_count = count(),
    avg_label_len = avg(label_len),
    sample_queries = make_set(query_s, 3)
  by id_orig_h_s, tostring(split(query_s, ".")[-2])  // group by registered domain
| where query_count > 20
| order by query_count desc