Tools · SIEM and detection engineering · 09 / 24
Hunting in DNS logs: newly seen domains, DGA-like names, and tunneling signs
Use DNS query logs from a resolver or endpoint to find rare domains, long random-looking names, high query volume to one domain, and unusual record types.
Where DNS logs come from
Useful sources include Windows DNS Server analytical logs, Sysmon Event ID 22 on endpoints, resolver logs from Unbound or BIND, protective DNS services, and cloud resolver logs such as Route 53 Resolver query logs. Endpoint sources tie a query to the process that made it, which is very helpful for triage.
Signals worth hunting
Newly observed domains that only one host queries. Long labels with high character randomness, typical of domain generation algorithms. A very large number of unique subdomains under one parent domain, which suggests data encoded in queries. Heavy TXT or NULL record queries. Regular beacon-like intervals to the same domain.
None of these are proof on their own: CDNs, telemetry services, and anti-virus cloud lookups also produce long random-looking names. Use allow lists of known services and pivot to the process.
Lab: rank suspicious domains
Export a day of DNS queries from your lab resolver or a public sample dataset to a CSV with timestamp, client, and query name. Use a short Python script to compute, per registered domain, the number of unique subdomains, the average label length, and the Shannon entropy of the leftmost label. Sort and review the top entries.
import csv, math, collections
def entropy(s):
counts = collections.Counter(s)
return -sum(c / len(s) * math.log2(c / len(s)) for c in counts.values()) if s else 0
stats = collections.defaultdict(lambda: {'subs': set(), 'ent': []})
with open('dns.csv') as f:
for row in csv.DictReader(f):
name = row['query'].rstrip('.').lower()
parts = name.split('.')
if len(parts) < 2:
continue
parent = '.'.join(parts[-2:])
stats[parent]['subs'].add(name)
stats[parent]['ent'].append(entropy(parts[0]))
rows = [(p, len(v['subs']), sum(v['ent']) / len(v['ent'])) for p, v in stats.items()]
for parent, subs, ent in sorted(rows, key=lambda r: (r[1], r[2]), reverse=True)[:20]:
print(f'{parent:40} unique_subdomains={subs:6} avg_entropy={ent:.2f}')Triage
For each candidate, identify which hosts queried it, which process made the query if you have endpoint data, when it started, and whether the domain is newly registered. Block confirmed malicious domains at the resolver, and keep the hunting query as a scheduled detection with your allow list.
Diagram
DNS hunting signals
Each is a lead, not a verdict.
- Rare domain
Queried by one host, first seen today.
- High entropy label
Random-looking names such as x7qk2v9pz.example.
- Many unique subdomains
Thousands of distinct names under one parent.
- TXT-heavy traffic
Unusual volume of TXT or NULL queries.
- Beaconing
Queries at fixed intervals around the clock.
Check yourself
1. Which Sysmon event records DNS queries with the requesting process?
2. Which pattern most suggests data being carried in DNS queries?
3. What commonly causes false positives in entropy-based DNS hunts?