资讯动态

LSTM实战:识别DGA恶意域名全流程解析

发布时间:2026/10/9 6:32:15 来源:尧图企业网站定制
简介针对DGA域名检测任务的实战项目资源提供基于LSTM的完整Python实现与文档说明。项目采用360公开的100万DGA域名数据作为负面样本数据预处理、LSTM训练、测试评估等环节均有对应代码支撑适合作业、课设、毕设或算法复现。压缩包共7个文件包含3个Jupyter Notebook用于分步实验和结果可视化2个Python脚本负责数据加载与模型训练另有1个域名样本数据文件和1个README说明文档整体大小仅594KB轻量易上手。已有101人学习浏览代码在测试运行成功后才上传可远程教学指导。下载后参照README即可快速复现流程也可基于现有结构扩展改进是学习DGA域名识别与恶意域名检测的实用参考。1. 一条乱码域名背后LSTM 凭什么识别 DGA威胁情报平台弹出告警DNS 日志里一条qw8x2kf4on99p.com持续请求失败却不在任何黑名单里。这类域名不是人起的是 DGA 算法生成的伪随机串恶意软件每天换一批静态黑名单根本封不完。LSTM 这类序列模型能直接学域名里字符的转移规律——DGA 生成串的统计分布和正常域名明显不同所以域名识别本质是个二分类输入域名串输出 DGA 概率。这套基于 LSTM 的 Python 源码把全链路串好了data.py处理数据负面样本用 360 公开的 100 万条 DGA 域名LSTM_train.py负责训练两个 notebook 分别用来看训练过程和跑推理。适合三拨人想快速落地安全检测小工具的从业者、拿 LSTM 做毕设的在校学生、想理解序列模型怎么处理变长文本的初学者。2. 数据工程把 100 万条 DGA 域名变成 LSTM 能吃的张量2.1 源码包里每个文件在干什么拿到压缩包解压后第一件事不是急着跑训练而是把文件分工理清楚。这套源码的文件不多但每个角色都很明确README.md是入口文档说明数据来源和运行顺序zeus_dga_domains.txt是 DGA 域名样本data.py负责把文本域名转成数值张量LSTM_train.py是训练主脚本train_model.ipynb是训练过程的可视化记录prodect_test.ipynb是训练完之后的推理演示。文件职责关键点README.md项目说明与运行指引建议先读里面有数据格式说明zeus_dga_domains.txt恶意域名样本一行一个纯域名无协议头data.py文本转张量的预处理模块字符表映射、定长填充LSTM_train.py模型定义与训练主循环带 checkpoint 和早停train_model.ipynb训练曲线可视化观察过拟合和收敛prodect_test.ipynb推理脚本演示单条域名输出 DGA 概率zeus_dga_domains.txt这种格式是最常见的 DGA 数据集形态一行一个域名不带http://前缀也不带www。DGA 家族有很多Zeus、Conficker、Necurs 这些老牌家族的域名生成规律各不相同有的喜欢夹杂数字有的干脆全是随机字母组合。这套项目里的负面样本用的是 360 公开的 100 万条 DGA 域名数据覆盖面足够当训练集用。正面样本正常域名源码包里没附需要自己准备。常见做法是导出 Alexa Top 100 万或者从公司 DNS 日志里捞正常访问域名。我一般会用公共知名域名列表然后按二级域名去重比如map.baidu.com和news.baidu.com只留baidu.com不然正常样本里同一个主域的变体会大量重复模型会学偏。2.2 data.py 的预处理核心逻辑域名是变长字符串LSTM 吃不了字符串必须先做数值化。data.py做的就是三件事建立字符表、把域名映射成索引序列、统一长度做 padding。下面这段是这套源码里最核心的预处理逻辑我用可读性更好的写法还原出来。# data.py 的核心逻辑根据资源内脚本结构还原 import numpy as np from tensorflow.keras.preprocessing import sequence CHARS abcdefghijklmnopqrstuvwxyz0123456789-. char_to_idx {c: i 1 for i, c in enumerate(CHARS)} # 0 留给 padding VOCAB_SIZE len(CHARS) 1 # 37 个字符 1 个 pad MAX_LEN 75 def load_domains(path, label): domains, labels [], [] with open(path, r, encodingutf-8) as f: for line in f: d line.strip().lower() if not d or len(d) 4: # 过滤空行和过短记录 continue domains.append(d) labels.append(label) return domains, labels def domain_to_seq(domain, max_lenMAX_LEN): raw [char_to_idx.get(c, 0) for c in domain] # 字符表外的统一置 0 return sequence.pad_sequences([raw], maxlenmax_len, paddingpost)[0] def build_dataset(pos_file, neg_file): pos_domains, pos_y load_domains(pos_file, 0) # 正常域名标 0 neg_domains, neg_y load_domains(neg_file, 1) # DGA 域名标 1 domains pos_domains neg_domains labels np.array(pos_y neg_y) X np.array([domain_to_seq(d) for d in domains]) return X, labels这段代码的要点在于字符表设计和 padding 策略。字符表只包括 26 个小写字母、10 个数字、以及-和.两个符号共 37 个字符加上一个 padding 占位就是 38。域名进来先lower()统一小写遇到字符表外的字符用 0 代替保证不会因为特殊符号直接抛异常。domain_to_seq里的sequence.pad_sequences是 Keras 自带的填充函数maxlen75表示超过 75 个字符截断不足 75 补 0。为什么是 75RFC 里域名字段的理论上限是 253 个字符但实际观测里 95% 的正常域名和 DGA 域名长度都在 75 以内超过这个长度的大多是攻击者故意构造的噪声样本。取值太大会让矩阵膨胀太小又丢失长域名特征75 是个实测平衡点。2.3 标签与数据集划分别让验证集泄漏标签构造没有悬念正常域名标 0DGA 域名标 1。真正容易翻车的是数据集划分这一步。很多人直接把所有样本丢给train_test_split看起来 8:2 划分没问题但 DGA 域名之间是有家族关系的同一个家族用同一个种子算法生成文本模式高度相似。如果某个家族的域名同时出现在训练集和验证集模型等于提前看到了答案验证集的 AUC 会虚高得离谱。from sklearn.model_selection import train_test_split # 先按域名去重避免同一域名双集合出现 unique_domains list(set(domains)) labels_unique [labels[domains.index(d)] for d in unique_domains] X_seq np.array([domain_to_seq(d) for d in unique_domains]) X_train, X_val, y_train, y_val train_test_split( X_seq, np.array(labels_unique), test_size0.2, stratifylabels_unique, random_state42)stratifylabels_unique保证切分后训练集和验证集里正常域名与 DGA 域名的比例一致避免验证集全是 DGA 导致 loss 曲线乱跳。我一般还会再多做一步把域名按注册商的公开后缀分组同一主域只保留一条这能进一步抑制泄漏。这套源码里没做这么细但你自己复现时值得加上。3. LSTM_train.py 训练实战超参、早停和收敛判断3.1 网络结构为什么这么定这套项目用的网络结构很克制Embedding 层 单层 LSTM Dense 输出层。没有上双向 LSTM也没有堆叠多层原因在于域名序列本身很短单层 128 单元已经能捕捉字符间的转移规律堆叠层数只会增加过拟合风险。层参数说明Embedding输入 38输出 128每个字符映射成 128 维向量LSTM128 单元dropout0.5学习字符序列依赖Dense1 单元sigmoid输出 DGA 概率为什么用 Embedding 而不是 one-hotone-hot 会给每个字符一个 38 维稀疏向量75 × 38 2850维输入参数爆炸而且完全丢掉了字符之间的相似性。Embedding 让模型自己学字符的分布式表示比如数字和字母在 DGA 语境下的语义距离这是字符级序列建模的主流做法。单层 LSTM 就够用的判断依据是 DGA 域名的模式强度伪随机串的字符转移概率和正常域名差异显著不需要深层网络去提取抽象特征。如果你拿到的训练集覆盖了十几种 DGA 家族而验证集看到明显欠拟合再加一层 LSTM 不迟先跑通单层永远是最稳的起点。3.2 训练脚本的关键代码# LSTM_train.py 的核心训练流程 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint model Sequential([ Embedding(input_dimVOCAB_SIZE, output_dim128, input_lengthMAX_LEN), LSTM(128, dropout0.5, recurrent_dropout0.2), Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) cp ModelCheckpoint(best_dga.h5, save_best_onlyTrue, monitorval_loss, verbose1) es EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue) model.fit(X_train, y_train, validation_data(X_val, y_val), batch_size512, epochs30, callbacks[cp, es])两个细节值得展开。第一损失函数用binary_crossentropy而不是categorical_crossentropy因为这是二分类输出层只有一个 sigmoid 单元用多分类交叉熵会把问题复杂化训练也不稳定。第二recurrent_dropout0.2是 LSTM 内部状态上的 dropout配合常规 dropout 一起防过拟合。batch_size512在百万级样本量下是合理的显存占用中等每个 epoch 的迭代次数不至于太多。epochs30不是硬跑满EarlyStopping连续 3 个 epoch 验证集 loss 不下降就自动停restore_best_weights会把模型参数回滚到验证集最优的那个位置。模型权重只保存在验证集 loss 最低的时刻防止过拟合后的模型被意外保存下来。3.3 在 train_model.ipynb 里看 loss 曲线train_model.ipynb的作用不是跑训练而是把训练过程可视化出来判断模型有没有真正在学。打开后你会看到两条曲线训练集 loss/accuracy 和验证集 loss/accuracy。第一次跑的时候重点关注三个信号。训练集 accuracy 高但验证集 accuracy 明显低是过拟合优先加大 dropout。两条曲线锯齿状抖动是学习率偏大把 Adam 默认的lr0.001降到0.0005。验证集 loss 先降后升、训练集 loss 一路走低这是最典型的过拟合拐点EarlyStopping 会在拐点附近截停但你得在图上看到它确认patience3没白设。跑完数据后顺手输出一份分类报告别只看 accuracy——如果数据集里正常域名和 DGA 域名比例是 9:1accuracy 99% 也可能一条 DGA 都没抓出来。4. 避坑实录DGA 识别最常见的五个问题4.1 训练集准确率 100%推理却全是 0现象训练跑完验证集准确率极高但拿一条全新的 DGA 域名去预测输出概率全部集中在 0 附近。原因数据集划分泄漏。DGA 域名按家族聚类同一个家族的域名字符模式高度相似如果划分时没有保证同一家族的样本只在训练集或只在验证集模型等于提前见过答案。更隐蔽的情况是数据里有重复行某个域名同时出现在两个集合里。解决read_csv后先drop_duplicates()再用域名主域去重如果知道家族标签用GroupShuffleSplit按家族划分。推理前用一条训练时没见过的 DGA 域名做 sanity check这是最直接的验证方式。4.2 换台机器复现结果对不上现象自己在本地训练好的模型效果很好换到服务器或者同事的机器上重新训练同样的脚本和数据集准确率和 AUC 掉了好几个点。原因TensorFlow 版本不一致是最常见的元凶。TensorFlow 1.x 和 2.x 的 LSTM 默认参数有差异tf.keras和单独安装的keras混用会导致权重初始化方式不同。其次Embedding 层和 LSTM 的权重初始化是随机的没有固定随机种子两次训练结果天然不同。解决写死随机种子。脚本开头加tf.random.set_seed(42)、np.random.seed(42)同时把PYTHONHASHSEED0加进环境变量。项目文档里应该写明推荐用的 TensorFlow 版本复现的时候先对齐版本再谈其他。保存模型参数用best_dga.h5这种独立文件别依赖 pickle 保存整个训练对象。4.3 padding 方向不一致线上表现漂移现象训练集和验证集效果都正常部署到推理脚本后同一批已知 DGA 域名的预测分数明显下降。原因训练时pad_sequences的padding参数用的是post尾部补 0但推理脚本里图省事没复用data.py的预处理函数自己手写了一段填充默认用了pre头部补 0。LSTM 是按顺序读取时间步的尾部补 0 和头部补 0 对最后一个隐藏状态的影响完全不同——补在尾部LSTM 多读了好几个无意义的 0补在头部0 只影响初期状态。解决推理脚本直接from data import domain_to_seq强制复用训练时同一套预处理函数。不要自己重新实现哪怕只是改了一个填充方向模型的输入分布就变了。4.4 精确率高召回率低阈值不是 0.5现象模型在验证集上 precision 很高但真正的高风险域名大量漏报。安全运营的人拿着名单来看发现抓到的都是 DGA 特征最明显的长随机串短一点的域名全漏了。原因默认阈值 0.5 对不平衡数据不友好。正常域名和 DGA 域名的预测概率分布在 0.3 到 0.7 之间重叠区域很大卡在 0.5 会把很多召回机会切掉。安全场景下漏掉一个恶意域名比误报一个正常域名的代价高阈值应该往低调。解决在验证集上输出 ROC 曲线和 PR 曲线找 F1 最大的点或者按业务容忍度定阈值。我一般会把prodect_test.ipynb里输出概率的代码改成输出概率分布直方图肉眼就能看到 0.3 到 0.7 区间挤了多少样本。如果 F1 最高的点在 0.35就把阈值定位 0.35而不是死守 0.5。4.5 大小写和特殊字符把字符表打穿现象推理阶段拿到一个包含大写字母或者下划线的域名domain_to_seq报错或者输出 0 占位符预测概率和真实值完全脱节。原因训练数据里全是小写域名字符表没留大写映射而线上流量里的域名经常带着大写字母还有一部分包含_或~这类符号。字符表外字符统一映射成 0结果 0 既代表 padding 又代表未知字符模型对这两类输入的内部表示被打乱。解决domain_to_seq里字符映射改成char_to_idx.get(c, UNK_IDX)单独给未知字符一个专属索引和 padding 的 0 区分开。推理入口先强制domain.lower()。这样即便字符表外字符反复出现模型也只会把它当成一种固定噪声模式不会和 padding 混淆。5. 上线前的小习惯用 prodect_test.ipynb 做一次完整推断5.1 推理脚本必须和训练共用一个预处理prodect_test.ipynb是整套源码里最后一个落地环节。训练完的模型要接进真实流量就得先跑通单条域名推断。这里的核心原则是推理脚本直接import训练时的预处理函数绝不自己重写第二份。# prodect_test.ipynb 的推理逻辑 from tensorflow.keras.models import load_model from data import domain_to_seq, MAX_LEN model load_model(best_dga.h5) test_domains [ baidu.com, qw8x2kf4on99p.com, secure-update-service.net ] for d in test_domains: seq domain_to_seq(d.lower()) p model.predict(seq.reshape(1, -1), verbose0)[0][0] label DGA if p 0.5 else normal print(f{d:30s} 概率{p:.4f} 判定{label})这段代码逻辑不复杂关键是seq.reshape(1, -1)这一步——domain_to_seq返回的是一维序列但模型要求二维输入(batch_size, sequence_length)reshape 成(1, 75)才能喂进去。你以为predict会自动处理维度实际上它只会默默报错而且错误信息相当隐晦。5.2 阈值先拿出来看一眼别直接用 0.5跑完推理我建议你顺手把验证集所有样本的概率分布打出来而不是只看几条测试域名。如果验证集里大多数正常域名的概率在 0.1 以下大多数 DGA 域名在 0.9 以上0.5 作为阈值很安全。但如果分布显示两堆样本有大量重叠说明模型还没训好调阈值只是治标。安全场景我习惯先看召回率再定阈值先保证 DGA 域名被筛出来再让运营同学人工复核误报。5.3 一个我踩过三次的教训有一次我把模型跑完验证集 AUC 0.98兴致勃勃准备部署结果三天后接到反馈说线上告警量暴涨。排查了一圈发现训练时用的data.py里MAX_LEN是 75新流量里有个别正常域名超过 75 字符被截断截断后剩下的字符序列特征完全变了概率全堆在 0.9 以上。从那以后我每次训练完都会做两件事把最佳阈值和验证集概率分布存成.npy归档再用几条超过MAX_LEN的长域名做边界测试。这些看起来无关紧要的习惯能让你三个月后回看这个项目时不用重新跑一遍训练就能说清楚当时的判定标准。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑