security operations

Performing DNS Tunneling Detection

Detects DNS tunneling by computing Shannon entropy of DNS query names, analyzing query length distributions, inspecting TXT record payloads, and identifying high subdomain cardinality. Uses scapy for packet capture analysis and statistical methods to distinguish legitimate DNS from covert channels. Use when hunting for data exfiltration.

dns-analysisdns-tunnelingexfiltration-detectionsecurity-operationsshannon-entropythreat-detection
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When conducting security assessments that involve performing dns tunneling detection
  • When following incident response procedures for related security events
  • When performing scheduled security testing or auditing activities
  • When validating security controls through hands-on testing

Prerequisites

  • Familiarity with security operations concepts and tools
  • Access to a test or lab environment for safe execution
  • Python 3.8+ with required dependencies installed
  • Appropriate authorization for any testing activities

Instructions

Analyze DNS traffic for indicators of DNS tunneling using entropy analysis and statistical methods on query name characteristics.

import math
from collections import Counter
 
def shannon_entropy(data):
    if not data:
        return 0
    counter = Counter(data)
    length = len(data)
    return -sum((c/length) * math.log2(c/length) for c in counter.values())
 
# Legitimate domain: low entropy (~3.0-3.5)
print(shannon_entropy("www.google.com"))
# DNS tunnel: high entropy (~4.0-5.0)
print(shannon_entropy("aGVsbG8gd29ybGQ.tunnel.example.com"))

Key detection indicators:

  1. High Shannon entropy in query names (> 3.5 for subdomain labels)
  2. Unusually long query names (> 50 characters)
  3. High volume of TXT record requests to a single domain
  4. High unique subdomain count per parent domain
  5. Non-standard character distribution in labels

Examples

from scapy.all import rdpcap, DNS, DNSQR
packets = rdpcap("dns_traffic.pcap")
for pkt in packets:
    if pkt.haslayer(DNSQR):
        query = pkt[DNSQR].qname.decode()
        entropy = shannon_entropy(query)
        if entropy > 4.0:
            print(f"Suspicious: {query} (entropy={entropy:.2f})")
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md1.6 KB

API Reference: Performing DNS Tunneling Detection

scapy (DNS Packet Analysis)

from scapy.all import rdpcap, DNS, DNSQR, DNSRR, sniff
 
# Read from PCAP
packets = rdpcap("traffic.pcap")
for pkt in packets:
    if pkt.haslayer(DNSQR):
        qname = pkt[DNSQR].qname.decode()
        qtype = pkt[DNSQR].qtype  # 1=A, 16=TXT, 28=AAAA
 
# Live capture
def dns_callback(pkt):
    if pkt.haslayer(DNSQR):
        print(pkt[DNSQR].qname)
 
sniff(filter="udp port 53", prn=dns_callback, count=100)

Shannon Entropy Calculation

import math
from collections import Counter
 
def shannon_entropy(data):
    counter = Counter(data)
    length = len(data)
    return -sum((c/length) * math.log2(c/length)
                for c in counter.values())
 
# Normal domain: ~2.5-3.5 bits
# DNS tunnel:    ~4.0-5.0 bits

DNS Tunneling Indicators

Indicator Threshold Rationale
Subdomain entropy > 3.8 Encoded/encrypted data
Query length > 50 chars Payload in subdomain
TXT queries/domain > 20/hour Data channel
Unique subdomains > 50/parent Encoded sessions
Digit ratio > 0.4 Base64/hex encoding

Common DNS Tunnel Tools

Tool Encoding Record Types
iodine Base128 NULL, TXT, CNAME
dnscat2 Hex/CNAME TXT, MX, CNAME
dns2tcp Base64 TXT, KEY

References

Scripts 1

agent.py6.2 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for detecting DNS tunneling via entropy and statistical analysis."""

import json
import math
import argparse
from collections import Counter, defaultdict
from datetime import datetime

from scapy.all import rdpcap, DNS, DNSQR


def shannon_entropy(data):
    """Calculate Shannon entropy of a string."""
    if not data:
        return 0.0
    counter = Counter(data)
    length = len(data)
    return -sum((count / length) * math.log2(count / length) for count in counter.values())


def extract_dns_queries(pcap_path):
    """Extract DNS queries from a PCAP file using scapy."""
    packets = rdpcap(pcap_path)
    queries = []
    for pkt in packets:
        if pkt.haslayer(DNSQR):
            qname = pkt[DNSQR].qname.decode().rstrip(".")
            qtype = pkt[DNSQR].qtype
            src_ip = pkt.src if hasattr(pkt, "src") else ""
            queries.append({
                "query": qname,
                "qtype": qtype,
                "src_ip": src_ip,
                "timestamp": float(pkt.time),
            })
    return queries


def analyze_entropy(queries, threshold=3.8):
    """Flag queries with high Shannon entropy in subdomain labels."""
    suspicious = []
    for q in queries:
        domain = q["query"]
        labels = domain.split(".")
        if len(labels) < 2:
            continue
        subdomain = ".".join(labels[:-2])
        if not subdomain:
            continue
        entropy = shannon_entropy(subdomain)
        if entropy > threshold:
            suspicious.append({
                "query": domain,
                "subdomain": subdomain,
                "entropy": round(entropy, 3),
                "length": len(subdomain),
                "src_ip": q.get("src_ip", ""),
            })
    return sorted(suspicious, key=lambda x: x["entropy"], reverse=True)


def analyze_query_lengths(queries, length_threshold=50):
    """Detect queries with unusually long domain names."""
    long_queries = []
    for q in queries:
        if len(q["query"]) > length_threshold:
            long_queries.append({
                "query": q["query"],
                "length": len(q["query"]),
                "src_ip": q.get("src_ip", ""),
            })
    return long_queries


def analyze_txt_records(pcap_path):
    """Detect high volume of TXT record queries to single domains."""
    packets = rdpcap(pcap_path)
    txt_counts = defaultdict(int)
    for pkt in packets:
        if pkt.haslayer(DNSQR) and pkt[DNSQR].qtype == 16:
            domain = pkt[DNSQR].qname.decode().rstrip(".")
            parent = ".".join(domain.split(".")[-2:])
            txt_counts[parent] += 1
    suspicious = [
        {"domain": d, "txt_query_count": c}
        for d, c in txt_counts.items() if c > 20
    ]
    return sorted(suspicious, key=lambda x: x["txt_query_count"], reverse=True)


def analyze_subdomain_cardinality(queries):
    """Detect domains with high unique subdomain count (tunneling indicator)."""
    parent_subdomains = defaultdict(set)
    for q in queries:
        labels = q["query"].split(".")
        if len(labels) >= 3:
            parent = ".".join(labels[-2:])
            subdomain = ".".join(labels[:-2])
            parent_subdomains[parent].add(subdomain)
    high_cardinality = []
    for parent, subs in parent_subdomains.items():
        if len(subs) > 50:
            high_cardinality.append({
                "parent_domain": parent,
                "unique_subdomains": len(subs),
                "sample_subdomains": list(subs)[:5],
            })
    return sorted(high_cardinality, key=lambda x: x["unique_subdomains"], reverse=True)


def analyze_character_distribution(queries):
    """Detect non-standard character frequency in query labels."""
    suspicious = []
    for q in queries:
        labels = q["query"].split(".")
        subdomain = ".".join(labels[:-2])
        if len(subdomain) < 10:
            continue
        alpha_count = sum(1 for c in subdomain if c.isalpha())
        digit_count = sum(1 for c in subdomain if c.isdigit())
        total = len(subdomain.replace(".", ""))
        if total == 0:
            continue
        digit_ratio = digit_count / total
        if digit_ratio > 0.4 or (alpha_count / total) < 0.5:
            suspicious.append({
                "query": q["query"],
                "digit_ratio": round(digit_ratio, 3),
                "subdomain_length": len(subdomain),
            })
    return suspicious


def main():
    parser = argparse.ArgumentParser(description="DNS Tunneling Detection Agent")
    parser.add_argument("--pcap", required=True, help="Path to PCAP file")
    parser.add_argument("--entropy-threshold", type=float, default=3.8)
    parser.add_argument("--output", default="dns_tunnel_report.json")
    parser.add_argument("--action", choices=[
        "entropy", "length", "txt", "cardinality", "full_analysis"
    ], default="full_analysis")
    args = parser.parse_args()

    report = {"pcap": args.pcap, "generated_at": datetime.utcnow().isoformat(),
              "findings": {}}

    queries = extract_dns_queries(args.pcap)
    report["total_queries"] = len(queries)
    print(f"[+] Extracted {len(queries)} DNS queries from {args.pcap}")

    if args.action in ("entropy", "full_analysis"):
        high_entropy = analyze_entropy(queries, args.entropy_threshold)
        report["findings"]["high_entropy"] = high_entropy
        print(f"[+] High entropy queries: {len(high_entropy)}")

    if args.action in ("length", "full_analysis"):
        long_q = analyze_query_lengths(queries)
        report["findings"]["long_queries"] = long_q
        print(f"[+] Long queries (>50 chars): {len(long_q)}")

    if args.action in ("txt", "full_analysis"):
        txt = analyze_txt_records(args.pcap)
        report["findings"]["txt_anomalies"] = txt
        print(f"[+] TXT record anomalies: {len(txt)}")

    if args.action in ("cardinality", "full_analysis"):
        cardinality = analyze_subdomain_cardinality(queries)
        report["findings"]["high_cardinality"] = cardinality
        print(f"[+] High cardinality domains: {len(cardinality)}")

    with open(args.output, "w") as f:
        json.dump(report, f, indent=2, default=str)
    print(f"[+] Report saved to {args.output}")


if __name__ == "__main__":
    main()
Keep exploring