4、DNS行为分析:DNS隧道检测、DGA域名识别、异常DNS请求频率分析
DNS,说白了就是网络世界的电话本。但攻击者偏偏喜欢在这本电话本里做手脚。
我做了这么多年威胁狩猎,发现一个规律:僵尸网络可以藏流量、藏文件、藏进程,但很难藏住DNS行为。为什么?因为只要它想和C2通信,就一定会产生DNS请求。这是躲不掉的。
这一章,我们就来聊聊怎么从海量的DNS日志里,把那些潜伏的“僵尸”揪出来。
4.1 DNS隧道检测:当DNS不再是DNS
DNS隧道,说白了就是把数据包塞进DNS查询里。攻击者用这个手法绕过防火墙,窃取数据,或者建立隐蔽通道。
我记得有一次,客户说内网有台服务器流量异常小,但CPU一直高负载。我查了DNS日志,发现这台机器每秒都在向一个陌生域名发起TXT记录查询。每条TXT响应都带着一段Base64编码的数据。嗯,典型的DNS隧道。
检测特征
- 查询类型异常:正常客户端很少用TXT、NULL、MX记录。如果某个IP大量查询这些类型,要警惕。
- 域名长度异常:正常域名平均20-30个字符。隧道域名经常超过50个字符,甚至上百。
- 子域名层级过多:正常域名3-4级。隧道域名可能有6-8级子域名。
- 熵值过高:隧道域名通常由随机字符组成,字符分布均匀,熵值远高于正常域名。
检测脚本示例
# 检测DNS隧道:基于查询类型和域名长度
import pandas as pd
import numpy as np
def detect_dns_tunnel(df):
# df 包含字段:timestamp, src_ip, query, query_type, response_size
suspicious = []
# 特征1:查询类型为TXT或NULL
type_anomaly = df[df['query_type'].isin(['TXT', 'NULL', 'MX'])]
# 特征2:域名长度 > 50
type_anomaly['domain_len'] = type_anomaly['query'].str.len()
long_domain = type_anomaly[type_anomaly['domain_len'] > 50]
# 特征3:子域名层级 > 5
long_domain['sub_level'] = long_domain['query'].str.count('\.')
deep_domain = long_domain[long_domain['sub_level'] > 5]
# 聚合:按源IP统计
result = deep_domain.groupby('src_ip').agg(
query_count=('query', 'count'),
avg_len=('domain_len', 'mean')
).reset_index()
return result[result['query_count'] > 10]
4.2 DGA域名识别:算法生成的“随机”并不随机
DGA(Domain Generation Algorithm)是僵尸网络的“狡兔三窟”。感染了DGA的机器,会每天生成成千上万个候选域名,然后尝试连接其中几个。这样安全团队很难通过封禁单个域名来阻断通信。
我曾经处理过一个案例:某公司内网2000多台机器,每天产生300万条DNS请求。其中有一台机器,每隔5分钟就尝试解析一个看起来完全随机的域名。我查了那台机器的进程,发现它后台跑着一个DGA生成器。嗯,抓了个正着。
DGA域名的典型特征
| 特征维度 | 正常域名 | DGA域名 |
|---|---|---|
| 字符分布 | 元音字母多,可读性强 | 辅音字母多,无意义组合 |
| 域名长度 | 8-20字符为主 | 20-40字符常见 |
| N-gram频率 | 符合自然语言分布 | 均匀分布,无规律 |
| TTL值 | 通常较长(3600s+) | 通常很短(60-300s) |
| 解析成功率 | 高(90%+) | 低(大部分域名未注册) |
基于熵值的DGA检测
# 计算域名的信息熵
import math
from collections import Counter
def shannon_entropy(domain):
# 去掉顶级域名
main_part = domain.split('.')[0]
if not main_part:
return 0
freq = Counter(main_part)
length = len(main_part)
entropy = -sum((count/length) * math.log2(count/length)
for count in freq.values())
return entropy
# 判断是否为DGA域名
def is_dga_domain(domain, threshold=3.5):
entropy = shannon_entropy(domain)
# 正常域名熵值通常在2.0-3.0之间
# DGA域名熵值通常在3.5-4.5之间
return entropy > threshold
4.3 异常DNS请求频率分析:时间维度上的“心跳”
僵尸网络有个特点:它会定期向C2服务器发送“心跳”包,告诉服务器“我还活着”。这个心跳,在DNS层面就表现为固定频率的DNS请求。
你想想看,正常用户访问网站,DNS请求的时间间隔是随机的。但僵尸网络不一样,它可能每5分钟、每30分钟、每1小时准时发起一次查询。这种规律性,就是我们的突破口。
频率分析的核心方法
- 时间序列分析:将DNS请求按时间戳排序,计算相邻请求的时间间隔。
- 周期性检测:使用自相关函数或FFT,检测是否存在固定周期。
- 突发检测:检测短时间内请求量突然暴增(如从每分钟1次暴增到每分钟100次)。
- 对比基线:与历史同期数据对比,发现异常波动。
周期性检测代码
# 检测DNS请求的周期性
import numpy as np
from scipy.signal import find_peaks
def detect_periodicity(timestamps):
# timestamps: 时间戳列表(秒级)
if len(timestamps) < 10:
return None
# 计算时间间隔
intervals = np.diff(timestamps)
# 去除异常值(间隔超过3600秒的)
intervals = intervals[intervals < 3600]
if len(intervals) < 5:
return None
# 计算间隔的直方图
hist, bins = np.histogram(intervals, bins=50)
# 找到峰值
peaks, _ = find_peaks(hist, height=len(intervals)*0.1)
if len(peaks) > 0:
# 峰值对应的间隔就是可能的周期
period = bins[peaks[0]]
return period
return None
4.4 综合检测框架:把三招合在一起
单独用哪一招都可能漏报。我建议把三种方法结合起来,形成一个完整的检测流水线。
综合检测流程
- 数据采集:收集全量DNS日志,包括成功和失败的解析请求。
- 特征提取:对每个域名计算长度、熵值、子域名层级、查询类型分布。
- 并行检测:同时运行隧道检测、DGA检测、频率分析三个模块。
- 关联分析:如果一个IP在三个模块中都被标记,那基本可以实锤了。
- 告警输出:生成可疑IP和域名列表,供进一步调查。
4.5 实战案例:一次完整的DNS狩猎
最后,分享一个我亲身经历的案例。
某天,客户反馈内网有台服务器网络延迟忽高忽低。我查了DNS日志,发现这台机器每隔15分钟准时向 xyz123.abc.def.xyz 发起一次A记录查询。域名长度47个字符,熵值3.8,子域名层级5级。嗯,三个特征全中。
我顺着这个IP查进程,发现它后台跑着一个名为“svchost.exe”的进程(注意,真正的svchost在System32目录下,这个在Temp目录里)。进一步分析,确认是某个DGA家族的变种。
处理方案很简单:封禁那个DGA域名,然后在内网防火墙阻断该IP的所有外联。之后用EDR工具清理了那台机器。整个过程从发现到处置,不到2小时。