分析DNS日志检测数据外泄 是做什么的?
何时使用
在以下场景使用此技能:
- SOC团队怀疑存在通过DNS隧道绕过防火墙/代理控制的数据外泄
- 威胁情报显示攻击者使用基于DNS的C2信道(例如Cobalt Strike DNS Beacon)
- UEBA检测到来自特定主机的异常DNS查询量
- 恶意软件分析显示存在DNS-over-HTTPS (DoH) 或DNS隧道能力
请勿用于标准DNS故障排除或可用性监控——此技能专注于安全相关的DNS滥用检测。
前置条件
- 已启用DNS查询日志记录(Windows DNS Server、BIND、Infoblox或Cisco Umbrella)
- DNS日志已导入SIEM(Splunk中的
Stream:DNS、dnssourcetype,或Zeek DNS日志) - 被动DNS数据用于历史域名解析分析
- 正常DNS行为的基线(查询量、域名分布、TXT记录频率)
- 安装了
math和collections库的Python环境,用于熵值计算
工作流
步骤1: 通过子域名长度分析检测DNS隧道
DNS隧道将数据编码在子域名标签中,导致查询异常长:
index=dns sourcetype="stream:dns" query_type IN ("A", "AAAA", "TXT", "CNAME", "MX")
| eval domain_parts = split(query, ".")
| eval subdomain = mvindex(domain_parts, 0, mvcount(domain_parts)-3)
| eval subdomain_str = mvjoin(subdomain, ".")
| eval subdomain_len = len(subdomain_str)
| eval tld = mvindex(domain_parts, -1)
| eval registered_domain = mvindex(domain_parts, -2).".".tld
| where subdomain_len > 50
| stats count AS queries, dc(query) AS unique_queries,
avg(subdomain_len) AS avg_subdomain_len,
max(subdomain_len) AS max_subdomain_len,
values(src_ip) AS sources
by registered_domain
| where queries > 20
| sort - avg_subdomain_len
| table registered_domain, queries, unique_queries, avg_subdomain_len, max_subdomain_len, sources
步骤2: 检测高熵域名查询(DGA检测)
域名生成算法会产生看似随机的域名:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval sld = mvindex(domain_parts, -2)
| eval sld_len = len(sld)
| eval char_count = sld_len
| eval vowels = len(replace(sld, "[^aeiou]", ""))
| eval consonants = len(replace(sld, "[^bcdfghjklmnpqrstvwxyz]", ""))
| eval digits = len(replace(sld, "[^0-9]", ""))
| eval vowel_ratio = if(char_count > 0, vowels / char_count, 0)
| eval digit_ratio = if(char_count > 0, digits / char_count, 0)
| where sld_len > 12 AND (vowel_ratio < 0.2 OR digit_ratio > 0.3)
| stats count AS queries, dc(query) AS unique_domains, values(src_ip) AS sources
by query
| where unique_domains > 10
| sort - queries
基于Python的DNS查询香农熵计算:
import math
from collections import Counter
def shannon_entropy(text):
"""计算字符串的香农熵"""
if not text:
return 0
counter = Counter(text.lower())
length = len(text)
entropy = -sum(
(count / length) * math.log2(count / length)
for count in counter.values()
)
return round(entropy, 4)
# 测试示例
normal_domain = "google" # 低熵
dga_domain = "x8kj2m9p4qw7n" # 高熵
tunnel_subdomain = "aGVsbG8gd29ybGQ.evil.com" # Base64编码数据
print(f"正常: {shannon_entropy(normal_domain)}") # ~2.25
print(f"DGA: {shannon_entropy(dga_domain)}") # ~3.70
print(f"隧道: {shannon_entropy(tunnel_subdomain)}") # ~3.50
# 阈值:子域名熵值 > 3.5 = 可能存在隧道/DGA
Splunk中的熵值评分实现:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval check_string = mvindex(domain_parts, 0)
| eval check_len = len(check_string)
| where check_len > 8
| eval chars = split(check_string, "")
| stats count AS total_chars, dc(chars) AS unique_chars by query, src_ip, check_string, check_len
| eval entropy_estimate = log(unique_chars, 2) * (unique_chars / check_len)
| where entropy_estimate > 3.5
| stats count AS high_entropy_queries, dc(query) AS unique_queries by src_ip
| where high_entropy_queries > 50
| sort - high_entropy_queries
步骤3: 检测异常DNS查询量
识别产生异常DNS流量的主机:
index=dns sourcetype="stream:dns" earliest=-24h
| bin _time span=1h
| stats count AS queries, dc(query) AS unique_domains by src_ip, _time
| eventstats avg(queries) AS avg_queries, stdev(queries) AS stdev_queries by src_ip
| eval z_score = (queries - avg_queries) / stdev_queries
| where z_score > 3 OR queries > 5000
| sort - z_score
| table _time, src_ip, queries, unique_domains, avg_queries, z_score
检测TXT记录滥用(常见隧道方法):
index=dns sourcetype="stream:dns" query_type="TXT"
| stats count AS txt_queries, dc(query) AS unique_txt_domains,
values(query) AS domains by src_ip
| where txt_queries > 100
| eval suspicion = case(
txt_queries > 1000, "CRITICAL — 疑似DNS隧道",
txt_queries > 500, "HIGH — 可能DNS隧道",
txt_queries > 100, "MEDIUM — TXT数量异常"
)
| sort - txt_queries
| table src_ip, txt_queries, unique_txt_domains, suspicion
步骤4: 检测已知DNS隧道工具
搜索常见DNS隧道工具的签名:
index=dns sourcetype="stream:dns"
| eval query_lower = lower(query)
| where (
match(query_lower, "\.dnscat\\.") OR
match(query_lower, "\.dns2tcp\\.") OR
match(query_lower, "\.iodine\\.") OR
match(query_lower, "\.dnscapy\\.") OR
match(query_lower, "\.cobalt.*\.beacon") OR
query_type="NULL" OR
(query_type="TXT" AND len(query) > 100)
)
| stats count by src_ip, query, query_type
| sort - count
检测绕过本地DNS的DNS over HTTPS (DoH):
index=proxy OR index=firewall
dest IN ("1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4",
"9.9.9.9", "149.112.112.112", "208.67.222.222")
dest_port=443
| stats sum(bytes_out) AS total_bytes, count AS connections by src_ip, dest
| where connections > 100 OR total_bytes > 10485760
| eval alert = "可能DoH绕过 — 通过HTTPS向公共解析器发送DNS查询"
| sort - total_bytes
步骤5: DNS发现与端点数据关联
将可疑DNS与进程数据交叉引用:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| stats count AS dns_queries, earliest(_time) AS first_query, latest(_time) AS last_query
by src_ip, query
| join src_ip [
search index=sysmon EventCode=3 DestinationPort=53 Computer="WORKSTATION-042"
| stats count AS connections, values(Image) AS processes by SourceIp
| rename SourceIp AS src_ip
]
| table src_ip, query, dns_queries, first_query, last_query, processes
步骤6: 估算数据外泄量
估算DNS查询中编码的数据量:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| eval domain_parts = split(query, ".")
| eval encoded_data = mvindex(domain_parts, 0)
| eval encoded_bytes = len(encoded_data)
| eval decoded_bytes = encoded_bytes * 0.75 -- Base64解码因子
| stats sum(decoded_bytes) AS total_bytes_estimated, count AS total_queries,
earliest(_time) AS first_seen, latest(_time) AS last_seen
| eval estimated_kb = round(total_bytes_estimated / 1024, 1)
| eval estimated_mb = round(total_bytes_estimated / 1048576, 2)
| eval duration_hours = round((last_seen - first_seen) / 3600, 1)
| eval rate_kbps = round(estimated_kb / (duration_hours * 3600) * 8, 2)
| table total_queries, estimated_mb, duration_hours, rate_kbps, first_seen, last_seen
关键概念
| 术语 | 定义 |
|---|---|
| DNS隧道 | 将数据编码在DNS查询/响应中,通过DNS进行数据外泄或建立C2信道的技术 |
| DGA | 域名生成算法 — 恶意软件生成伪随机域名以实现C2弹性的技术 |
| 香农熵 | 字符串随机性的数学度量 — 域名中高熵值(>3.5)表明DGA或隧道 |
| TXT记录滥用 | 使用DNS TXT记录(设计用于文本数据)作为高带宽数据隧道信道 |
| DNS over HTTPS (DoH) | 通过HTTPS(端口443)加密的DNS查询,绕过传统DNS监控 |
| 被动DNS | 历史DNS解析记录,显示域名随时间解析到的IP地址 |
工具与系统
- Splunk Stream:网络流量捕获插件,为SIEM分析提供解析后的DNS查询数据
- Zeek (Bro):网络安全监控器,生成详细的DNS事务日志用于分析
- Cisco Umbrella (OpenDNS):云DNS安全平台,阻止恶意域名并记录查询数据
- Infoblox DNS防火墙:DNS层安全,提供基于RPZ的拦截和详细查询日志
- Farsight DNSDB:被动DNS数据库,用于历史域名解析查询和基础设施映射
常见场景
- Cobalt Strike DNS Beacon:检测向C2域名的周期性TXT查询,其中包含编码的有效载荷
- 数据外泄:大量独特子域名查询,使用Base64/十六进制编码窃取数据
- DGA恶意软件:检测对算法生成域名的DNS查询(高熵,无Web内容)
- DNS-over-HTTPS绕过:员工使用DoH绕过公司DNS过滤和监控
- 慢速滴灌外泄:低量DNS隧道保持低于阈值告警(需要基线比较)
输出格式
DNS外泄分析 — WORKSTATION-042
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间范围: 2024-03-14 至 2024-03-15
来源: 192.168.1.105 (WORKSTATION-042, 财务部)
发现:
[严重] 检测到针对 evil-tunnel[.]com 的DNS隧道
查询量: 18小时内12,847次查询
平均子域名长度: 63个字符(正常:<20)
平均熵值: 3.82(阈值:3.5)
查询类型: TXT (89%), A (11%)
估算数据: ~4.7 MB 通过DNS外泄
速率: 0.58 kbps(慢速滴灌模式)
[高] 解析了类似DGA的域名
唯一DGA域名: 已解析247个域名
模式: 15字符随机字母数字.xyz TLD
熵值范围: 3.6 - 4.1
进程归因:
进程: svchost_update.exe(伪装 — 非合法svchost)
PID: 4892
父进程: explorer.exe
哈希: SHA256: a1b2c3d4... (VT: 34/72恶意 — Cobalt Strike信标)
遏制措施:
[已完成] 通过EDR隔离主机
[已完成] 域名 evil-tunnel[.]com 已添加至DNS黑洞
[已完成] 已创建事件单 IR-2024-0448