$npx -y skills add killvxk/cybersecurity-skills-zh --skill analyzing-dns-logs-for-exfiltration分析 DNS 查询日志,利用熵值分析、查询量异常检测和子域名长度检测,在 SIEM 平台中检测 DNS 隧道数据外泄、DGA 域名通信和隐蔽 C2 信道。适用于 SOC 团队识别绕过传统网络安全控制的 DNS 威胁。
| 1 | # 分析 DNS 日志中的数据外泄 |
| 2 | |
| 3 | ## 适用场景 |
| 4 | |
| 5 | 在以下情况下使用本技能: |
| 6 | - SOC 团队怀疑通过 DNS 隧道进行数据外泄以绕过防火墙/代理控制 |
| 7 | - 威胁情报显示对手使用基于 DNS 的 C2 信道(例如 Cobalt Strike DNS Beacon) |
| 8 | - UEBA 检测到特定主机存在异常 DNS 查询量 |
| 9 | - 恶意软件分析揭示具有 DNS-over-HTTPS(DoH)或 DNS 隧道能力 |
| 10 | |
| 11 | **不适用于**标准 DNS 故障排除或可用性监控——本技能专注于与安全相关的 DNS 滥用检测。 |
| 12 | |
| 13 | ## 前置条件 |
| 14 | |
| 15 | - 已启用 DNS 查询日志记录(Windows DNS Server、Bind、Infoblox 或 Cisco Umbrella) |
| 16 | - DNS 日志已摄取到 SIEM(Splunk 的 `Stream:DNS`、`dns` 数据源或 Zeek DNS 日志) |
| 17 | - 用于历史域名解析分析的被动 DNS 数据 |
| 18 | - 正常 DNS 行为基线(查询量、域名分布、TXT 记录频率) |
| 19 | - Python(含 `math` 和 `collections` 库)用于熵值计算 |
| 20 | |
| 21 | ## 工作流程 |
| 22 | |
| 23 | ### 步骤 1:通过子域名长度分析检测 DNS 隧道 |
| 24 | |
| 25 | DNS 隧道将数据编码在子域名标签中,产生异常长的查询: |
| 26 | |
| 27 | ```spl |
| 28 | index=dns sourcetype="stream:dns" query_type IN ("A", "AAAA", "TXT", "CNAME", "MX") |
| 29 | | eval domain_parts = split(query, ".") |
| 30 | | eval subdomain = mvindex(domain_parts, 0, mvcount(domain_parts)-3) |
| 31 | | eval subdomain_str = mvjoin(subdomain, ".") |
| 32 | | eval subdomain_len = len(subdomain_str) |
| 33 | | eval tld = mvindex(domain_parts, -1) |
| 34 | | eval registered_domain = mvindex(domain_parts, -2).".".tld |
| 35 | | where subdomain_len > 50 |
| 36 | | stats count AS queries, dc(query) AS unique_queries, |
| 37 | avg(subdomain_len) AS avg_subdomain_len, |
| 38 | max(subdomain_len) AS max_subdomain_len, |
| 39 | values(src_ip) AS sources |
| 40 | by registered_domain |
| 41 | | where queries > 20 |
| 42 | | sort - avg_subdomain_len |
| 43 | | table registered_domain, queries, unique_queries, avg_subdomain_len, max_subdomain_len, sources |
| 44 | ``` |
| 45 | |
| 46 | ### 步骤 2:检测高熵域名查询(DGA 检测) |
| 47 | |
| 48 | 域名生成算法(DGA)产生看似随机的域名: |
| 49 | |
| 50 | ```spl |
| 51 | index=dns sourcetype="stream:dns" |
| 52 | | eval domain_parts = split(query, ".") |
| 53 | | eval sld = mvindex(domain_parts, -2) |
| 54 | | eval sld_len = len(sld) |
| 55 | | eval char_count = sld_len |
| 56 | | eval vowels = len(replace(sld, "[^aeiou]", "")) |
| 57 | | eval consonants = len(replace(sld, "[^bcdfghjklmnpqrstvwxyz]", "")) |
| 58 | | eval digits = len(replace(sld, "[^0-9]", "")) |
| 59 | | eval vowel_ratio = if(char_count > 0, vowels / char_count, 0) |
| 60 | | eval digit_ratio = if(char_count > 0, digits / char_count, 0) |
| 61 | | where sld_len > 12 AND (vowel_ratio < 0.2 OR digit_ratio > 0.3) |
| 62 | | stats count AS queries, dc(query) AS unique_domains, values(src_ip) AS sources |
| 63 | by query |
| 64 | | where unique_domains > 10 |
| 65 | | sort - queries |
| 66 | ``` |
| 67 | |
| 68 | **基于 Python 的 DNS 查询香农熵(Shannon Entropy)计算:** |
| 69 | |
| 70 | ```python |
| 71 | import math |
| 72 | from collections import Counter |
| 73 | |
| 74 | def shannon_entropy(text): |
| 75 | """计算字符串的香农熵""" |
| 76 | if not text: |
| 77 | return 0 |
| 78 | counter = Counter(text.lower()) |
| 79 | length = len(text) |
| 80 | entropy = -sum( |
| 81 | (count / length) * math.log2(count / length) |
| 82 | for count in counter.values() |
| 83 | ) |
| 84 | return round(entropy, 4) |
| 85 | |
| 86 | # 测试示例 |
| 87 | normal_domain = "google" # 低熵 |
| 88 | dga_domain = "x8kj2m9p4qw7n" # 高熵 |
| 89 | tunnel_subdomain = "aGVsbG8gd29ybGQ.evil.com" # Base64 编码的数据 |
| 90 | |
| 91 | print(f"正常: {shannon_entropy(normal_domain)}") # ~2.25 |
| 92 | print(f"DGA: {shannon_entropy(dga_domain)}") # ~3.70 |
| 93 | print(f"隧道: {shannon_entropy(tunnel_subdomain)}") # ~3.50 |
| 94 | |
| 95 | # 阈值:子域名熵值 > 3.5 = 可能是隧道/DGA |
| 96 | ``` |
| 97 | |
| 98 | **Splunk 熵值评分实现:** |
| 99 | |
| 100 | ```spl |
| 101 | index=dns sourcetype="stream:dns" |
| 102 | | eval domain_parts = split(query, ".") |
| 103 | | eval check_string = mvindex(domain_parts, 0) |
| 104 | | eval check_len = len(check_string) |
| 105 | | where check_len > 8 |
| 106 | | eval chars = split(check_string, "") |
| 107 | | stats count AS total_chars, dc(chars) AS unique_chars by query, src_ip, check_string, check_len |
| 108 | | eval entropy_estimate = log(unique_chars, 2) * (unique_chars / check_len) |
| 109 | | where entropy_estimate > 3.5 |
| 110 | | stats count AS high_entropy_queries, dc(query) AS unique_queries by src_ip |
| 111 | | where high_entropy_queries > 50 |
| 112 | | sort - high_entropy_queries |
| 113 | ``` |
| 114 | |
| 115 | ### 步骤 3:检测异常 DNS 查询量 |
| 116 | |
| 117 | 识别产生异常 DNS 流量的主机: |
| 118 | |
| 119 | ```spl |
| 120 | index=dns sourcetype="stream:dns" earliest=-24h |
| 121 | | bin _time span=1h |
| 122 | | stats count AS queries, dc(query) AS unique_domains by src_ip, _time |
| 123 | | eventstats avg(queries) AS avg_queries, stdev(queries) AS stdev_queries by src_ip |
| 124 | | eval z_score = (queries - avg_queries) / stdev_queries |
| 125 | | where z_score > 3 OR queries > 5000 |
| 126 | | sort - z_score |
| 127 | | table _time, src_ip, queries, unique_domains, avg_queries, z_score |
| 128 | ``` |
| 129 | |
| 130 | **检测 TXT 记录滥用(常见隧道方法):** |
| 131 | |
| 132 | ```spl |
| 133 | index=dns sourcetype="stream:dns" query_type="TXT" |
| 134 | | stats count AS txt_queries, dc(query) AS unique_txt_domains, |
| 135 | values(query) AS domains by src_ip |
| 136 | | where txt_queries > 100 |
| 137 | | eval suspicion = case( |
| 138 | txt_queries > 1000, "CRITICAL — 可能是 DNS 隧道", |
| 139 | txt_queries > 500, "HIGH — 可能是 DNS 隧道", |
| 140 | txt_queries > 100, "MEDIUM — 异常 TXT 查询量" |
| 141 | ) |
| 142 | | sort - txt_queries |
| 143 | | table src_ip, txt_queries, unique_txt_domains, suspicion |
| 144 | ``` |
| 145 | |
| 146 | ### 步骤 4:检测已知 DNS 隧道工具 |
| 147 | |
| 148 | 搜索常见 DNS 隧道工具的特征: |
| 149 | |
| 150 | ```spl |
| 151 | index=dns sourcetype="stream:dns" |
| 152 | | eval query_lower = lower(query) |
| 153 | | where ( |
| 154 | match(query_lower, "\.dnscat\.") OR |
| 155 | match(query_lower, "\.dns2tcp\.") OR |
| 156 | match(query_lower, "\.iodine\.") OR |
| 157 | match(query_lower, "\.dnscapy\.") O |