Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-SkillsFramework mappings
MITRE ATT&CK
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:
- High Shannon entropy in query names (> 3.5 for subdomain labels)
- Unusually long query names (> 50 characters)
- High volume of TXT record requests to a single domain
- High unique subdomain count per parent domain
- 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 bitsDNS 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
- scapy: https://scapy.readthedocs.io/en/latest/
- DNS tunneling detection: https://www.sans.org/white-papers/dns-tunneling/
- iodine: https://github.com/yarrick/iodine
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