Finding DNS tunnelling in a 4 GB capture without a signature
Entropy, subdomain cardinality and query-rate shape — three statistics that find a covert channel faster than any rule matching a known tool.
- Filed under
- dnspcapzeektunnellingstatisticssuricata
Signature-based detection for DNS tunnelling works right up to the moment someone changes their tooling. The statistics do not care what tool you used, because they are properties of the channel itself: you are pushing bytes through a field designed for hostnames, and hostnames do not look like that.
Three passes, no signature, about six minutes of wall clock on a 4 GB capture.
Do not open the PCAP
The first mistake is loading it into a GUI. Convert to Zeek logs and work on text.
$ zeek -r gateway-2026-07-26.pcap LogAscii::use_json=F
$ wc -l dns.log conn.log
2314887 dns.log
184220 conn.log
2.3 million queries is a normal day. It is also far too many to look at, which is why every pass below reduces before it inspects.
Pass 1 — subdomain cardinality per registered domain
A tunnel encodes data into the leftmost labels, so every query is unique. Sort by distinct subdomains per parent domain and the channel rises to the top on its own:
$ zeek-cut query < dns.log \
| awk -F. 'NF>2 { parent = $(NF-1)"."$NF; print parent"\t"$0 }' \
| sort -u \
| cut -f1 \
| uniq -c \
| sort -rn \
| head -10
184922 cdn-telemetry.example
9114 googleapis.com
4402 akamaiedge.net
1980 windowsupdate.com
712 office365.com
Pass 2 — entropy of the leftmost label
Cardinality alone flags some legitimate services. Encoded payload is near-uniform over its alphabet; a real hostname is not. Shannon entropy per character separates them cleanly:
import math, sys
from collections import Counter
def entropy(s: str) -> float:
if not s:
return 0.0
counts = Counter(s)
n = len(s)
return -sum((c / n) * math.log2(c / n) for c in counts.values())
for line in sys.stdin:
query = line.strip()
label = query.split(".", 1)[0]
if len(label) < 12: # short labels are noisy at this scale
continue
h = entropy(label)
if h > 3.6: # empirically ~3.2 for English-ish hostnames
print(f"{h:.2f}\t{len(label):3d}\t{query}")
$ zeek-cut query < dns.log | python3 label_entropy.py | sort -rn | head -4
4.61 58 kNzQ4YjFhLWQ3MmYtNGQ4ZS1hMzJlLTk5.cdn-telemetry.example
4.58 61 ZjM5YmM2NDMtODk0YS00YjIxLWFmZjItYWJ.cdn-telemetry.example
4.55 60 OWY4ZTIxMGEtMzMzYy00YTMwLWI0NTgtMzk.cdn-telemetry.example
3.71 22 d2h5LWFyZS15b3UtaGVyZQ.cdn-telemetry.example
Base32-shaped, 58–61 characters, sitting just under the 63-byte label limit — which is itself a signature of a tool that maximises throughput per query.
Pass 3 — rule out the boring explanation
Two things still fit the evidence: a covert channel, or a badly-designed
telemetry SDK. conn.log settles it.
$ zeek-cut id.orig_h query ts < dns.log \
| grep 'cdn-telemetry.example' \
| awk '{print $1}' | sort | uniq -c | sort -rn | head -3
184901 10.30.4.87
14 10.30.4.12
7 10.30.9.31
One host, 99.99% of the traffic, and the query rate is metronomic — a jitter of ±0.4 s around a 2 s beacon:
$ zeek-cut ts query < dns.log | grep 'cdn-telemetry.example' \
| awk 'NR>1 { printf "%.1f\n", $1-prev } { prev=$1 }' \
| sort | uniq -c | sort -rn | head -5
91204 2.0
42881 1.8
38664 2.2
6109 1.6
5844 2.4
Telemetry SDKs batch and back off. They do not hold a 2-second cadence for eleven hours.
The rule that came out of it
Written on the shape, not on the tool, so retooling does not evade it:
alert dns $HOME_NET any -> any 53 (msg:"POLICY DNS query with oversized encoded label"; \
dns.query; content:"."; \
pcre:"/^[A-Za-z0-9+\/=_-]{40,63}\./"; \
threshold:type both, track by_src, count 50, seconds 60; \
classtype:policy-violation; sid:9000117; rev:2;)
For the same shape in Sigma, against Zeek dns.log in a SIEM:
title: High Distinct Subdomain Cardinality Per Parent Domain
logsource:
product: zeek
service: dns
detection:
selection:
query|re: '^[A-Za-z0-9+/=_-]{40,63}\.'
timeframe: 5m
condition: selection | count(query) by src_ip > 200
falsepositives:
- DNS-based antivirus cloud lookups (scope out the vendor's parent domain)
- Some CDN cache-key schemes
level: high
What I would keep from this
Cardinality, entropy and inter-arrival regularity. All three are cheap, all
three are computable from dns.log with tools that ship with the operating
system, and none of them care which tunnelling utility was used. A signature
would have told me the tool's name and nothing else — and only if it happened
to be a tool someone had already written a signature for.
Discussion
GuidelinesSign in to comment. Corrections and additions are the point — this is a working document.