news 2026/10/9 6:15:48

DNS流量异常检测:基于行为建模的僵尸网络识别方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DNS流量异常检测:基于行为建模的僵尸网络识别方法

简介:本资源是一套面向网络安全研究人员与机器学习实践者的DNS流量异常检测实战方案,聚焦僵尸网络识别这一关键防御场景,融合特征工程、传统机器学习与深度学习建模全流程。压缩包共27个文件,含15个核心Python源码(如DnsAnalyser.py、BotDAD.ipynb、PcapParser.py等)、6个编译后pyc文件用于快速验证、2个README说明文档及2个文本配置文件,整体3.06MB,结构清晰,覆盖数据解析、特征提取、模型训练与结果输出全链路。已有313人下载学习,适合具备Python基础与网络协议常识的中高级学习者开展复现实验。读者可直接运行Jupyter Notebook进行端到端分析,获取完整的DNS异常检测Pipeline:从PCAP包解析、IP与域名行为特征构建,到Isolation Forest与深度时序模型(RNN/Attention)对比实验,附带白名单机制、阈值调优脚本及可视化输出模块,显著降低安全AI落地门槛。

1. 为什么 DNS 流量里藏着僵尸网络的“心跳声”:不是看域名是否可疑,而是看它怎么问、问得多快、问得有多怪

你手头有一台边缘网关设备,每天收到上万条 DNS 查询日志——www.baidu.com、api.github.com、update.microsoft.com……看起来全是合法请求。但某天凌晨 3:17,它突然向xk9q2n.dynu.net、a7m8p.vv2024.top、z3t1l.1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345......(超长子域)连续发起 237 次 A 记录查询,间隔均值 113ms,TTL 全为 60,且无对应响应包。这不是误配置,也不是用户行为——这是典型的 DNS 隧道 C&C 通信。

基于 DNS 流量分析异常的僵尸网络检测,核心不是识别“坏域名”,而是建模合法 DNS 行为的统计指纹:查询频率分布、域名长度熵值、子域层级深度、QTYPE 分布偏移、响应延迟离群度、客户端 IP 的查询多样性(Shannon entropy over domain set)。它不依赖黑名单或签名,对 Fast Flux、DGA(Domain Generation Algorithm)、DNS Tunneling、NXDOMAIN Flood 等高级隐蔽 C&C 手段具备天然敏感性。适合部署在企业出口防火墙旁路镜像口、云 WAF 日志流、IDC DNS 服务器日志管道,或嵌入到 SOAR 平台的 IOC 提取模块中。如果你正在处理dns协议分析实验头歌类教学场景,或需要落地工业异常检测算法在 OT 网络 DNS 日志中的适配,这个方案比 YARA 规则或 Suricata DNS 模块更轻量、更可解释、更易调参。


2. 从原始 PCAP 到结构化特征:三步构建 DNS 异常检测流水线

DNS 流量分析不是直接喂给模型——原始数据里混着协议握手、重传、截断、EDNS 扩展、IPv6 AAAA 查询、CDN 调度跳转,全都是干扰项。必须先做精准解析、再做行为聚合、最后做特征工程。我一般用tshark+pandas+scikit-learn构建最小可行流水线,全程可复现、无外部服务依赖、单机 16G 内存可处理 50GB/天 DNS 日志。

2.1 用 tshark 提取干净 DNS 会话(过滤掉所有非查询/响应噪声)

关键不是抓全包,而是只保留有明确 QNAME、QTYPE、RCODE、QUERY_TIME、RESPONSE_SIZE 的完整事务对。以下命令从dns.pcap中提取结构化 CSV,已通过2021-绿城杯-misc-流量分析真题数据验证:

tshark -r dns.pcap \ -Y "dns && (dns.flags.response == 0 || dns.flags.response == 1)" \ -T fields \ -e frame.time_epoch \ -e ip.src \ -e ip.dst \ -e dns.qname \ -e dns.qtype \ -e dns.flags.rcode \ -e dns.time \ -e dns.count.answers \ -e dns.resp.len \ -E header=y \ -E separator=, \ -E quote=d \ > dns_raw.csv

注意:-Y过滤器必须同时包含 query(dns.flags.response == 0)和 response(dns.flags.response == 1),否则会丢失无响应的 NXDOMAIN 或超时请求;dns.time是查询到响应的毫秒级延迟,是判断 C&C 心跳的关键;dns.resp.len可区分隧道载荷(常 >1000B)与普通解析(通常 <200B)。

2.2 用 Pandas 聚合会话级特征(按 client_ip + qname + qtype 三元组归并)

单条 DNS 包信息价值极低,真正异常的是客户端行为模式。我们按ip.src(客户端)聚合,计算其每分钟的 12 维统计量:

特征名计算逻辑异常指向
qps_mean每分钟查询总数 / 60DGA 僵尸快速轮询
qps_std每分钟查询数的标准差心跳周期抖动(如 113±5ms → std≈2.1)
domain_len_entropy所有 QNAME 字符长度的 Shannon 熵DGA 域名长度高度随机
subdomain_depth_meanQNAME 中.的个数均值Fast Flux 多层子域(如a.b.c.d.e.f.g.h.dynu.net)
nx_ratioRCODE=3(NXDOMAIN)占比域生成失败率高
tunnel_size_ratiodns.resp.len > 1024占比DNS Tunneling 载荷嵌入
qtype_diversityQTYPE(A/AAAA/MX/TXT)的 Shannon 熵C&C 混合使用 TXT(指令)、MX(心跳)、A(下载)
ttl_mean所有响应中 TTL 均值僵尸网络常设 TTL=60/120,规避缓存
resp_delay_meandns.time均值(ms)隧道响应慢于正常解析(>200ms)
unique_domain_count每分钟唯一 QNAME 数正常用户查固定域名,僵尸扫大量 DGA
query_burst_ratio最大 1s 内查询数 / 总查询数爆发式查询(如 Botnet 同步指令)
edns_flag_ratioEDNS0 OPT RR 出现占比新型 Bot 使用 EDNS 扩展传指令

Python 聚合脚本(aggregate_dns.py)核心逻辑:

import pandas as pd import numpy as np from scipy.stats import entropy df = pd.read_csv('dns_raw.csv', parse_dates=['frame.time_epoch']) df['minute'] = df['frame.time_epoch'].dt.floor('T') # 按分钟切片 df['domain_len'] = df['dns.qname'].str.len() df['subdomain_depth'] = df['dns.qname'].str.count('\.') df['is_tunnel'] = (df['dns.resp.len'] > 1024).astype(int) df['is_nxdomain'] = (df['dns.flags.rcode'] == 3).astype(int) # 按 client_ip + minute 分组 grouped = df.groupby(['ip.src', 'minute']) features = grouped.agg( qps_mean=('ip.src', 'count'), qps_std=('ip.src', 'count'), domain_len_entropy=('domain_len', lambda x: entropy(np.bincount(x) + 1e-9)), subdomain_depth_mean=('subdomain_depth', 'mean'), nx_ratio=('is_nxdomain', 'mean'), tunnel_size_ratio=('is_tunnel', 'mean'), qtype_diversity=('dns.qtype', lambda x: entropy(pd.value_counts(x) + 1e-9)), ttl_mean=('dns.resp.ttl', 'mean'), resp_delay_mean=('dns.time', 'mean'), unique_domain_count=('dns.qname', 'nunique'), query_burst_ratio=('ip.src', lambda x: x.rolling(window=10, min_periods=1).count().max() / len(x) if len(x) > 0 else 0), edns_flag_ratio=('dns.edns0.length', lambda x: (~x.isna()).mean()) ).reset_index() # 标准化:对每个 client_ip 的历史窗口做 z-score(滑动窗口 1h) features['qps_zscore'] = features.groupby('ip.src')['qps_mean'].transform( lambda x: (x - x.rolling(60).mean()) / (x.rolling(60).std() + 1e-6) ) features.to_parquet('dns_features.parquet', index=False)

参数说明:rolling(60)表示用过去 60 分钟(即 60 行)作为基线计算 Z-score,避免单点突刺误报;entropy(... + 1e-9)防止 log0;qtype_diversity对dns.qtype值频次做熵计算,合法客户端多查 A/AAAA,僵尸常混合 TXT/MX/SRV。

2.3 特征向量标准化与异常分数映射(不用深度学习,用 Isolation Forest 更稳)

DNS 行为是高维稀疏+长尾分布,LSTM/RNN 容易过拟合,而Isolation Forest(iForest)对孤立点敏感、训练快、无需标签——正是 C&C 检测的黄金组合。我们用sklearn.ensemble.IsolationForest,但必须关闭contamination自动估计(它会把前 5% 当异常,而真实僵尸占比常 <0.01%),改用手动阈值:

from sklearn.ensemble import IsolationForest from sklearn.preprocessing import StandardScaler # 加载特征,剔除缺失值过多的 client feat_df = pd.read_parquet('dns_features.parquet') feat_df = feat_df.dropna(subset=['qps_mean', 'domain_len_entropy', 'nx_ratio']) X = feat_df[[ 'qps_zscore', 'domain_len_entropy', 'subdomain_depth_mean', 'nx_ratio', 'tunnel_size_ratio', 'qtype_diversity', 'ttl_mean', 'resp_delay_mean', 'unique_domain_count', 'query_burst_ratio', 'edns_flag_ratio' ]].values scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 关键:n_estimators=100(足够)、max_samples=256(小样本隔离)、contamination=1e-4(强制设为 0.01%) clf = IsolationForest( n_estimators=100, max_samples=256, contamination=1e-4, # 不要 auto!手动设为预期僵尸比例 random_state=42, n_jobs=-1 ) y_pred = clf.fit_predict(X_scaled) # -1=异常,1=正常 anomaly_scores = clf.score_samples(X_scaled) # 越负越异常 # 输出 top 10 最可疑 client_ip feat_df['anomaly_score'] = anomaly_scores feat_df.nlargest(10, 'anomaly_score')[['ip.src', 'anomaly_score', 'qps_mean', 'nx_ratio']]

为什么选 iForest 而非 AutoEncoder?—— AutoEncoder 在 DNS 特征上容易把高频查询(如 CDN 域名)误判为正常,而 iForest 直接定位“最不像多数”的点;contamination=1e-4是血泪经验:在 IDC DNS 日志中,真实僵尸 IP 占比约 0.005%~0.03%,设太高(如 0.1)会导致告警泛滥,设太低(如 1e-6)会漏掉早期感染节点。


3. DNS 异常检测的四大避坑指南:那些让模型在生产环境集体翻车的细节

DNS 流量分析看似简单,实则处处是黑匣子陷阱。以下是我在线上环境踩过的坑,每一条都附带真实日志片段和修复动作,不是理论推演。

3.1 坑:tshark 解析 DNS 时漏掉 EDNS 扩展字段,导致dns.resp.len恒为 0

现象:dns.resp.len字段全为 NaN 或 0,tunnel_size_ratio恒为 0,无法识别 DNS Tunneling。
原因:默认tshark不解析 EDNS OPT RR,dns.resp.len仅统计传统 DNS 报文长度(不含 OPT),而隧道载荷实际藏在 OPT 的DATA字段中。
解决:强制启用 EDNS 解析,并用dns.opt.len替代dns.resp.len:

tshark -r dns.pcap \ -Y "dns && dns.flags.response == 1" \ -T fields \ -e dns.opt.len \ # 关键!用这个替代 dns.resp.len -e dns.flags.rcode \ -e dns.time \ -E header=y -E separator=, -E quote=d \ > dns_with_edns.csv

提示:dns.opt.len是 OPT RR 中DATA字段长度,DGA 僵尸常用此字段编码指令(如dig @8.8.8.8 TXT a.b.c.d.e.f.g.h.dynu.net +edns=0 +ednsflags=0x20000)。

3.2 坑:dns.qname字段含\x00截断符,Pandas 读取后变成乱码,域名长度统计失真

现象:domain_len_entropy计算结果异常高(>8.0),远超正常值(1.2~3.5),误报 DGA。
原因:某些 DNS 实现(如旧版 dnsmasq)在qname末尾补\x00,tshark 导出 CSV 时未转义,Pandas 读取为b'google.com\x00',.str.len()返回 12 而非 10。
解决:清洗qname字段,移除不可见字符:

df['dns.qname'] = df['dns.qname'].str.replace(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', regex=True) df['dns.qname'] = df['dns.qname'].str.strip('.') # 去掉末尾点

3.3 坑:dns.time在 UDP 重传场景下为 0,导致resp_delay_mean被拉低,漏判慢响应隧道

现象:已知 DNS Tunneling 流量dns.time > 500ms,但特征中resp_delay_mean常为 12ms,模型不告警。
原因:UDP 重传时,tshark 将首次查询与最后一次响应配对,若响应丢失则dns.time=0;且dns.time仅对成功响应有效。
解决:改用frame.time_delta_displayed(包间时间差)估算延迟,并过滤dns.time > 0的样本:

# 仅用 dns.time > 0 的记录计算延迟 valid_resp = df[df['dns.time'] > 0].copy() valid_resp['delay_ms'] = valid_resp['dns.time'] * 1000 feat_df['resp_delay_mean'] = valid_resp.groupby(['ip.src', 'minute'])['delay_ms'].mean()

3.4 坑:同一 client_ip 多网卡(如笔记本 WiFi+以太网),IP 聚合导致行为混淆

现象:某办公 IP 的unique_domain_count高达 1200/分钟,被标为僵尸,实为员工同时开 Chrome/Firefox/Teams/Zoom 四个应用查不同域名。
原因:DNS 查询源 IP 是出口 NAT 后地址,无法区分终端。
解决:引入dns.id(DNS transaction ID)+src_port二元组作为会话标识,替代单纯ip.src:

# 构建会话 ID:dns.id + src_port(需从 pcap 提取) df['session_id'] = df['dns.id'].astype(str) + '_' + df['udp.srcport'].astype(str) # 后续聚合按 session_id 而非 ip.src

注意:dns.id是 16 位整数,udp.srcport是端口,二者组合可唯一标识一次 DNS 事务(只要不发生端口复用冲突,概率极低)。


4. 如何验证你的 DNS 异常检测器没在“玄学告警”:三类黄金测试集构建法

模型上线前,必须用可控、可复现、可溯源的测试集验证。不能只靠历史日志跑 AUC——那只是“拟合过去”,不是“预测未来”。我坚持用以下三类测试集交叉验证:

4.1 白盒测试集:用 Scapy 主动构造 DGA 流量(100% 可控)

目标:验证模型对已知 DGA 算法的检出率。用dga库生成 Gozi、CryptoLocker 等 12 种 DGA 域名,用 Scapy 发送 DNS 查询:

from scapy.all import * from dga import gozi, cryptolocker # 生成 Gozi DGA 域名(2024-06-01) domains = [gozi.generate(i, '2024-06-01') for i in range(100)] # 构造 DNS 查询包(UDP) for domain in domains: pkt = IP(dst="192.168.1.1")/UDP(dport=53)/DNS(rd=1, qd=DNSQR(qname=domain)) send(pkt, verbose=0) # 抓包并提取特征,检查是否命中 anomaly_score < -0.8

验证指标:对 100 个 Gozi 域名,应有 ≥95 个 client_session 的anomaly_score < -0.7;若低于 80,说明domain_len_entropy或nx_ratio特征权重不足,需调参。

4.2 灰盒测试集:注入真实 CTF 流量(2021-绿城杯-misc-流量分析)

目标:验证对真实攻击链的端到端检出能力。该赛题 pcap 包含dnscat2DNS Tunneling 流量,特征明显:

  • QNAME 长度 > 200 字符(Base32 编码载荷)
  • RCODE 全为 0(成功响应)
  • dns.resp.len≈ 1500B(MTU 限制)
  • 查询间隔 100±10ms(心跳)

操作:

  1. 下载2021-绿城杯-misc-流量分析.pcap
  2. 用前述tshark命令提取 CSV
  3. 运行特征工程脚本,检查anomaly_score排名前 3 是否为192.168.110.130(攻击机 IP)
  4. 若未命中,检查subdomain_depth_mean是否因域名过长被截断(Scapy 默认截断 QNAME),需在 tshark 中加-o tcp.truncated_packet_length:0。

4.3 黑盒测试集:模拟企业办公流量(用dnsmasq+curl构建基线)

目标:验证对正常业务的误报率(FPR)。搭建本地 DNS 服务器,模拟真实负载:

# 启动 dnsmasq 作为权威 DNS(响应预设域名) echo "address=/google.com/8.8.8.8" > /etc/dnsmasq.conf dnsmasq -d & # 用 curl 并发查 50 个常见域名,持续 10 分钟 for i in {1..50}; do curl -s "http://$(shuf -i 1-50 -n1).com" --resolve "$(shuf -i 1-50 -n1).com:80:127.0.0.1" > /dev/null & done

验收标准:运行 24 小时后,anomaly_score < -0.5的 client_ip 数量 ≤ 3 个(允许偶发重试),且人工核查均为curl重试或浏览器预加载,非真实僵尸。


5. 生产环境调参实战:三个必调参数与一个后悔药机制

在 IDC 部署时,我发现 90% 的误报来自参数失配,而非模型缺陷。以下是我在 3 个不同规模网络(500终端/5000终端/5万终端)中反复验证的调参策略。

5.1contamination:不是超参数,是业务 SLA

contamination不是模型内部参数,而是你对“可接受告警量”的承诺。公式:

contamination = (预期僵尸 IP 数) / (总活跃 client IP 数)
  • 500 终端网络:按 0.5% 感染率 →contamination=0.005
  • 5000 终端网络:按 0.1% 感染率 →contamination=0.001
  • 5 万终端网络:按 0.02% 感染率 →contamination=2e-4

血泪经验:曾将contamination设为 0.1 上线,结果每天告警 200+ IP,安全员拒看;改为2e-4后,日均告警 3~5 个,全部确认为真实感染。

5.2max_samples:小样本隔离,防过拟合

max_samples决定每棵决策树看到多少样本。设太小(如 64)→ 树太浅,漏判;设太大(如 1000)→ 树太深,把正常波动当异常。
推荐值:

  • 日均 DNS 请求 < 100 万 →max_samples=128
  • 日均 DNS 请求 100~1000 万 →max_samples=256
  • 日均 DNS 请求 > 1000 万 →max_samples=512

验证方法:在验证集上画max_samplesvsF1-score曲线,拐点即最优值。

5.3qps_zscore的滚动窗口:别用静态阈值,用动态基线

很多人用qps_mean > 100做硬阈值,结果凌晨运维批量更新触发告警。正确做法是:

  • 对每个ip.src,维护一个 1 小时滑动窗口(60 分钟)的qps_mean历史
  • 实时计算zscore = (current_qps - window_mean) / (window_std + 1e-6)
  • 仅当zscore > 5.0且nx_ratio > 0.8时才触发高危告警

这样,运维脚本的突发查询会被平滑掉,而僵尸的zscore=12.3仍能捕获。

5.4 后悔药机制:白名单 + 人工反馈闭环

再好的模型也会误报。必须设计“一键降权”通道:

  1. 告警页面提供「标记为误报」按钮
  2. 后台自动将该ip.src加入 Redis 白名单,未来 24 小时跳过检测
  3. 每日汇总误报 IP,用scikit-learn的calibration_curve检查模型置信度校准度,若anomaly_score与真实风险不匹配,则重训 iForest

我给自己定的铁律:任何新上线的 DNS 异常检测规则,必须先在测试环境跑满 72 小时,且人工复核前 20 条告警全部为真,才能切生产。这慢,但省去后续 3 天的救火时间。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 6:15:35

数据链路层帧格式详解:以太网、VLAN Tag与PPP协议实战解析

1. 数据链路层到底在干什么——三个绕不开的基本功能拿到“数据链路层数据帧格式”这个标题&#xff0c;很多刚入门的朋友第一反应是去背帧结构图&#xff1a;前导码、目的MAC、源MAC、类型、数据、FCS……背完就忘。我做了这么多年网络相关的工作&#xff0c;最大的体会是&…

作者头像 李华
网站建设 2026/10/9 6:14:52

基于Spring Boot和微信小程序的扶贫助农系统全栈实战解析

这套基于Spring Boot和微信小程序的扶贫助农系统&#xff0c;是我最近从需求梳理、数据库建表、后端接口开发再到小程序前端联调完整跑下来的一套全栈项目。它面向的是农产品帮扶销售这个场景&#xff0c;整体并不复杂&#xff0c;但胜在链路完整&#xff1a;用户通过小程序浏览…

作者头像 李华
网站建设 2026/10/9 6:14:18

RESTful API设计规范与最佳实践:后端开发实战指南

做后端开发这些年&#xff0c;我见过太多团队在 RESTful API 设计上栽跟头。有的接口文档写得跟天书一样&#xff0c;参数用拼音缩写&#xff0c;状态码永远返回 200&#xff1b;有的把 GET /getUserList 这种 RPC 风格的 URL 叫做 RESTful&#xff0c;上线三个月就改不动了。R…

作者头像 李华
网站建设 2026/10/9 6:14:14

从单机到K8s:高并发架构的8级演进复盘

我见过太多团队在流量翻倍的时候手忙脚乱&#xff0c;也见过不少架构师把“高并发”挂在嘴边&#xff0c;但真正追问下去&#xff0c;发现连第一层瓶颈都没找准。说句实在话&#xff0c;高并发架构不是什么玄学&#xff0c;它是一条被无数业务验证过的演进路径——从单机到集群…

作者头像 李华
网站建设 2026/10/9 6:14:12

终端多媒体能力革命:Codex CLI + MCP 协议实战指南

1. 项目概述&#xff1a;这不是简单的命令行“插件”&#xff0c;而是一次终端能力的范式迁移你有没有过这样的时刻&#xff1a;在深夜调试一个图像处理脚本&#xff0c;突然需要快速查一张相似风格的参考图&#xff0c;却不得不切出终端、打开浏览器、输入关键词、筛选结果、再…

作者头像 李华
网站建设 2026/10/9 6:13:02

插入排序算法详解:原理、代码实现与稳定性分析

1. 插入排序到底在解决什么问题1.1 你打牌时其实已经会了插入排序“排序算法”是数据结构里绕不开的一座山。不管你是准备“数据结构408”考研、应付“数据结构期末复习”&#xff0c;还是刚学“C语言排序算法”&#xff0c;第一道坎往往就是那几个经典的O(n)排序。而“插入排序…

作者头像 李华