☰
LSTM DGA域名识别实战:Python源码解析与避坑指南
2026/10/1 11:56:15 网站建设 项目流程

简介:一套基于LSTM的DGA域名识别Python源码及文档说明,是面向网络安全方向学生、科研人员及爱好者的完整项目资源。项目针对恶意软件利用DGA算法生成随机域名以绕过黑名单检测的问题,采用360公开的百万级DGA域名数据作为负面样本,结合长短期记忆网络实现域名恶意性识别。资源共7个文件,包含3个Jupyter Notebook(数据探索、模型训练与结果展示)、2个Python脚本(数据预处理与逻辑实现)、1个txt域名样本数据及1个README说明文档,压缩包仅594KB,结构清晰,便于快速下载学习。目前已有101人学习下载,适合作为毕设、课程设计或入门实践项目。代码均经过测试运行成功,涵盖从数据加载、特征处理到LSTM模型训练与评估的完整流程,读者可直接复现实验,也可在此基础上调整网络结构或数据源,用于验证LSTM在DGA检测任务中的效果或进一步拓展为恶意流量检测系统。

1. 从DGA域名识别说起:为什么LSTM是抓域名黑匣子的首选

打开一台被感染主机的DNS日志,你大概率会看到一类诡异的查询记录:gx8s7d2kq9.example.com、y3n4m5b6v7.cn——它们没有语义、拼写生僻,但周而复始地出现在凌晨三点。这就是DGA(Domain Generation Algorithm)域名生成算法生成的伪随机域名,僵尸网络控制的C2服务器靠它们在黑名单和信誉系统之外动态切换。传统基于黑名单和规则的检测在这里几乎失效,因为攻击者只需要换一个随机种子,就生成一批全新域名。

而LSTM恰恰擅长处理这种场景:域名本身是一段有长度限制的字符串序列,字符之间存在长短不一的时序依赖,而LSTM能记住序列中哪些字符组合更像随机生成、哪些像人工注册的可读域名。这份基于LSTM的DGA域名识别Python源码,正是用40万级的DGA负样本和正常域名构成训练集,把字符序列喂进Embedding + LSTM网络,训练出一个能对域名做二分类的检测模型。适合三类人:做安全运营想给DNS日志加一道AI检测规则的安全工程师,做毕业设计需要完整深度学习流程参考的在校学生,以及刚入门NLP序列建模、想找一个非文本领域落地案例的Python开发者。接下来我从数据文件开始拆解这份源码包,把每一步该怎么跑、参数怎么调、坑在哪里讲明白。

2. 数据准备:zeus_dga_domains.txt与360数据集的分工

2.1 先理清目录里每个文件的角色

下载解压后你会在DGA_domain_test-master目录下看到这些文件:zeus_dga_domains.txt是Zeus家族的DGA样本集,data.py负责数据加载和预处理,LSTM_train.py是模型训练脚本,train_model.ipynb和prodect_test.ipynb分别是训练和测试的Jupyter Notebook,README.md记录了使用说明,Untitled.ipynb是作者调试过程的残余文件。动手之前先打开README.md,确认代码依赖的Python版本和第三方库,一般就是numpy、pandas、tensorflow或keras、sklearn这几个。

zeus_dga_domains.txt每一行一个纯域名,格式类似gx8s7d2kq9.ru。数据量并不大,Zeus家族样本大约几千条,真正支撑模型训练的是摘要中提到的360公开的DGA域名数据,那是100万量级的负面样本。也就是说,你在源码包里看到的zeus_dga_domains.txt更多是验证集或补充样本,训练时你需要按README中的脚本去加载那份百万级的DGA域名数据。如果你打算完全复现训练过程却找不到那份360数据集,可以用zeus_dga_domains.txt加上自己收集的DGA家族样本凑数,但最终精度会有明显差距。

2.2 正常域名从哪里来:必须补齐的另一半样本

DGA域名识别本质是二分类,只有DGA正样本没有正常域名负样本,模型根本训练不起来。常见做法是用Alexa Top 100万域名作为正常域名样本,这些域名是真实注册、有业务含义的,例如google.com、taobao.com。安全领域很多开源DGA检测项目都默认加载Alexa列表,你可以在网上找到导出好的CSV,或者用DNS历史数据自己做一份。

训练之前,先把正负样本数量对齐。如果DGA样本有100万条,正常域名也取100万条。如果只有几万条DGA数据,正常域名就也取几万条。正负样本比例失衡会直接把模型拉偏,我见过一个项目里负样本是正样本的20倍,训练出的模型把所有域名都预测成正常域名,准确率看似高达95%,但那只是分布偏差造成的假象。经验做法:正负样本比控制在1:1到1:2之间,如果实际收集到的DGA样本不够,就用过采样或人工构造变体来补充。

2.3 data.py里的数据清洗逻辑

data.py处理的核心是域名文本清洗。域名序列里包含小写字母、数字、连字符和点,但DGA生成的域名往往没有点(顶级域之外的部分才是算法生成的主体)。很多DGA检测只取域名主机的部分,也就是去掉顶级域.com、.cn、.ru等后缀之后的那一串。

在实际预处理中,我一般会按这样的清洗流程走:

import re def clean_domain(domain: str) -> str: # 只保留二级域名之前的部分,不包含顶级域 domain = domain.strip().lower() domain = re.split(r'[.\/]', domain)[0] # 过滤掉纯数字或长度过短的域名 if len(domain) < 5 or domain.isdigit(): return None return domain

逻辑很简单:先统一小写,再用正则把域名按点分割,取最前面一段作为特征序列。纯数字域名要过滤掉,因为正常数字域名太少,混进去会让模型学到“含数字就是正常”这种错误规律。长度小于5的域名也过滤掉,这类样本几乎没有判别信息,反而增加Padding负担。data.py里如果只是读取文件后原样输出,你就需要自己补上这一步清洗逻辑。

清洗完成后,把DGA域名标注为1,正常域名标注为0,构成(domain, label)的配对列表。下一步是字符级编码,这一步直接决定后续LSTM输入的质量。

3. 特征编码:字符映射、Padding与序列长度的取舍

3.1 字符级编码:为什么DGA识别不适合词向量

正常NLP任务用词向量是因为词有语义,域名却是离散的字符组合。DGA域名是算法生成的伪随机字符串,不存在“词”层面的语义信息,字符本身才是最小特征单元。所以这里对域名做字符级编码,把每个字符映射成一个整数索引。这种处理方式能得到更细粒度的特征,LSTM可以在字符层面捕捉“连续辅音字母簇”或“数字与字母交替出现”这些DGA域名常见的高熵模式。

先定义一个字符表,覆盖字母、数字和特殊符号,然后每个域名转成整数数组:

char_list = "abcdefghijklmnopqrstuvwxyz0123456789-." char_to_idx = {ch: i + 1 for i, ch in enumerate(char_list)} idx_to_char = {i + 1: ch for i, ch in enumerate(char_list)} # 0 保留给 Padding def encode_domain(domain: str, max_len: int = 75): domain = clean_domain(domain) if domain is None: return None indices = [char_to_idx.get(ch, 0) for ch in domain] # 未知字符映射为 0 if len(indices) > max_len: indices = indices[:max_len] else: indices += [0] * (max_len - len(indices)) return indices

参数说明:max_len=75是经验值。域名最长可达253个字符,但主流DGA算法生成的子域名长度集中在10到60之间,取75既能覆盖绝大多数样本,又不会让序列过长拖慢训练速度。char_to_idx.get(ch, 0)这行的作用是容错:域名的随机组合里可能出现字符表之外的Unicode字符,未知字符统一映射为0,而这个0在模型里对应Embedding中全零向量,相当于告诉LSTM“这里有未知字符,忽略它”。尾部用0补齐到固定长度,这样所有输入向量形状一致,才能批量送入LSTM。

3.2 序列长度:过短截断信息,过长浪费算力

序列长度是这个项目里最值得手动调的超参数之一。把max_len设成30,你会发现很多正常域名如internationalcommercialbank.com被截断后只剩下internationalcommercialba,模型根本看不到完整的域名模式,误报率直接上升。设成120,训练参数没增加多少,但每个Batch的Padding计算量变大,而且大部分位置都是零填充,LSTM在长序列上更容易遗忘早期字符信息。

调参参考:如果你的样本集里域名长度分布偏短,比如DGA家族是Zeus,那就用max_len=45;如果包含Cryptolocker这类生成较长域名的家族,用到max_len=75。不确定的时候可以跑一段统计代码看看训练集域名长度的分位数,取95分位数的值作为max_len最稳妥。

3.3 训练集与验证集划分:按域名家族而非按域名随机切

DGA检测项目里有一个隐蔽的坑:如果用train_test_split(domain_list, test_size=0.2)直接随机切分,同一个家族生成的DGA域名会同时出现在训练集和验证集里,验证集loss看起来很低,但换一个新的DGA家族域名来做测试时,准确率立刻暴跌。正确做法是按域名来源的家族划分:把所有DGA域名按家族标签分组,一部分家族进训练集,另一部分家族进验证集。

如果样本只有zeus_dga_domains.txt这一个来源,至少要把从不同域名后缀和不同时间段的样本错开。更实际的做法是训练时留出10%的样本做验证,同时单独准备另一份不是同一家族DGA域名来做最终评估。这份源码包里只有Zeus一个家族的数据,意味着模型对Zeus之外的家族泛化能力未知,你在用它生产之前一定要补上跨家族测试这一步。

4. LSTM模型构建与训练:从搭建到收敛判断

4.1 模型结构:Embedding + LSTM + 二分类输出

模型结构是标准的序列二分类架构,分三层。Embedding层把字符索引映射成稠密向量,解决字符稀疏编码的问题;LSTM层接收向量序列并提取时序特征;Dense层加sigmoid输出0到1之间的概率值。这套结构对DGA识别来说足够,不需要上Bidirectional LSTM或Attention——DGA域名长度短,前向LSTM已经能捕获序列中的随机模式,加双向结构只是让训练时间翻倍而精度提升有限。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout def build_model(vocab_size: int, max_len: int = 75, embedding_dim: int = 64, lstm_units: int = 128): model = Sequential() model.add(Embedding(input_dim=vocab_size + 1, output_dim=embedding_dim, input_length=max_len)) model.add(LSTM(units=lstm_units, return_sequences=False)) model.add(Dropout(0.5)) model.add(Dense(1, activation='sigmoid')) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) return model model = build_model(vocab_size=len(char_list) + 1) model.summary()

参数说明:vocab_size传入的是字符表长度加1,因为索引0被保留给Padding位置,例如字符表有28个字符,vocab_size就是29。embedding_dim设64,字符映射成64维向量,维度太低表达力不足,太高则训练参数膨胀。lstm_units设128,这是LSTM隐藏状态维度,直接决定模型容量,DGA二分类任务用128足够了。Dropout(0.5)放在LSTM输出之后,防止全连接层过拟合——DGA训练数据量大时模型容易把训练集里的域名模式背下来,Dropout打断这种死记硬背。batch_size在compile里没有指定,训练时用128或256。

4.2 训练脚本拆解:epochs、batch_size与学习率怎么配合

LSTM_train.py的主流程一般是加载数据、预处理、划分训练集、构建模型、model.fit()训练、保存权重。训练参数决定模型收敛质量和训练耗时,这一节值得细看。

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint X_train, y_train, X_val, y_val = load_and_preprocess() X_train = np.array(X_train, dtype='int32') y_train = np.array(y_train, dtype='float32') early_stop = EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True) checkpoint = ModelCheckpoint('best_dga_model.keras', monitor='val_loss', save_best_only=True) model.fit( X_train, y_train, batch_size=256, epochs=20, validation_data=(X_val, y_val), callbacks=[early_stop, checkpoint], verbose=1 )

关于epochs=20,不是说要跑满20轮。加了EarlyStopping之后,当val_loss连续3轮不再下降就自动停止并回滚到验证集上表现最好的权重,这是防止过拟合的关键配置。实际项目里我跑DGA模型,通常在5到10轮之间就触发早停,20只是上限。batch_size=256在显存充足的GPU上没问题,如果是用CPU训练就改成64,防止一次加载太多样本导致内存溢出,但注意batch_size太小会让梯度震荡,训练过程更不稳定。restore_best_weights=True这行很多人忽略,不设置的话,早停返回的是最后一轮的权重而不是最优权重,模型精度可能差几个百分点。

4.3 训练观察:loss曲线和准确率的联动判断

训练时重点看两个指标:loss(二分类交叉熵损失)和accuracy(准确率)。对DGA识别项目来说,验证集loss的重要性高于准确率,因为类别通常均衡,准确率容易虚高。如果训练loss持续下降而验证loss在第4轮掉头上涨,说明模型开始过拟合,此时看EarlyStopping是否触发,没触发就手动调低lstm_units至64,或调高Dropout至0.6。如果训练loss降不下去,卡在0.3左右波动,常见原因是embedding_dim太小或者max_len设置不合理,把字符表里未知字符映射到0导致大量无意义输入干扰模型。

训练完成后,会生成一个权重文件,通常命名为best_dga_model.keras或dga_model.h5。这份源码的字里行间会说明权重存放路径,如果没说明,注意训练脚本代码里的model.save调用,保存位置决定了后续预测脚本从哪加载模型。

4.4 从Notebook运行还是从脚本运行

train_model.ipynb和prodect_test.ipynb提供了两种使用方式。Notebook的优点是每一步输出可视化,方便观察中间结果,适合调试和毕设展示;LSTM_train.py则是把整个流程串成一条命令,适合服务器上后台运行。我的建议是先在Jupyter Notebook里用小批量数据跑通流程——比如只加载1万条样本、epochs=2——确认数据管道和模型没有报错,再跑到LSTM_train.py里做全量训练。这样报错时定位快,不用盯着空跑的脚本干等。

5. 避坑:DGA识别项目里最常见的五个翻车现场

5.1 验证集准确率高,线上识别全是误报

现象:模型在验证集上准确率达到0.97,部署到生产环境检测真实DNS流量时,大量正常域名被判定为DGA,误报率高到没法用。

原因:训练数据里DGA样本来源单一,来自Zeus家族,正常域名用Alexa Top百万,但线上访问的长尾域名很多不在Alexa列表里,例如企业自建域名、泛解析域名,这些域名在模型看来“不够正常”,于是被误判。

解决:收集正常域名样本时加入更多来源,包括本机构的历史DNS流量域名、其它公开的域名黑白名单,让正常域名的分布更贴近真实场景。另外,把每个家族的DGA样本单独留一部分作为测试集,确保模型对没见过的DGA家族也能正确识别。

5.2 预测概率集中在0.5附近,模型跟抛硬币一样

现象:模型预测输出的概率值大多在0.45到0.55之间,无论阈值怎么调都不好用。

原因:字符级编码把域名的原始顺序打乱了,或者max_len设置过短,导致LSTM收到的序列信息严重不足。还可能是Embedding层没有训练好,字符之间的相似关系没有学到。

解决:回到数据预处理,检查是否有把域名反转或乱序的代码误留在流程里。然后调大embedding_dim从64到128,让每个字符有更充分的向量表达;同时把max_len从75提高到100,观察验证集loss是否有明显下降。

5.3 训练时显存溢出,Notebook直接崩溃

现象:加载了100万条DGA域名和100万条正常域名后,训练还没开始,GPU显存就被占满了。

原因:没有把数据转成int32或int8类型,Python列表直接塞进NumPy数组,每个元素占用大量内存;同时batch_size设得太大,每轮迭代加载的样本数量爆炸。

解决:数据喂给模型之前统一转成np.array(dtype='int32'),标签转成float32。在model.fit里把batch_size从256降到64。如果还溢出,把训练数据按10万条分批载入,用model.fit里的steps_per_epoch控制每轮迭代数,数据全部驻留内存的做法在大数据集下就是自找麻烦。

5.4 模型权重文件加载后预测结果全为0

现象:用load_model加载训练好的模型文件,输入一个明显是DGA的域名,输出概率是0.0,模型完全不工作。

原因:权重文件加载成功,但模型重建时词汇表大小不一致。例如训练时vocab_size=29,预测脚本里重建Model时传了vocab_size=30,Embedding维度对不上,或者字符表顺序不一致,导致同一个字符在训练和预测时映射到不同索引。

解决:把字符表char_list和char_to_idx保存成JSON文件,训练脚本和预测脚本都从这个文件读取映射关系。预测时重建模型传参必须严格保证vocab_size和max_len与训练时一致。

5.5 中文域名直接报错

现象:输入中国移动.cn这类国际化域名时,预处理正则报错或预测结果不可解释。

原因:字符表里没有中文字符,encode_domain函数把中文字符全映射成了0,LSTM看到的是连串的Padding填充,丢失了全部有效信息。

解决:DGA识别项目面向英文域名,中文域名需要先做Punycode转码,例如中国移动.cn转成xn--fiqs8s.cn,然后再进入字符编码流程。数据清洗阶段加入domain.encode('idna').decode('utf-8')统一转换最稳妥。

6. 进阶:模型导出、批量检测与阈值校准

训练好的模型不能只躺在Notebook里跑一次测试,实际使用需要导出成一个稳定的可调用模块。这里分享一套我常用的落地做法。

第一步是把模型导出为完整文件,包含网络结构和权重,并记录词汇表和超参数。可以用model.save('final_dga_model.keras'),同时把char_to_idx和max_len写进一个JSON文件。第二步是封装一个预测函数,支持单条域名检测、批量域名检测和阈值调节:

import json import numpy as np from tensorflow.keras.models import load_model class DGADetector: def __init__(self, model_path="./final_dga_model.keras"): self.model = load_model(model_path) self.char_to_idx = json.load(open("./char_to_idx.json")) self.max_len = 75 self.vocab_size = len(self.char_to_idx) + 1 def _encode(self, domain: str): domain = clean_domain(domain) if domain is None: return None indices = [self.char_to_idx.get(ch, 0) for ch in domain[:self.max_len]] indices += [0] * (self.max_len - len(indices)) return np.array([indices], dtype='int32') def predict(self, domain: str, threshold: float = 0.5): X = self._encode(domain) if X is None: return False, 0.0 prob = self.model.predict(X, verbose=0)[0][0] return prob >= threshold, prob detector = DGADetector() is_dga, prob = detector.predict("gx8s7d2k9q.example.com") print(f"DGA: {is_dga}, probability: {prob:.4f}")

最值得做的校准工作是阈值选择。模型输出的原始概率不能直接当作最终判定标准,用测试集画出不同阈值下的精确率和召回率曲线,选一个符合业务场景的阈值——安全场景宁可误报多也要召回高,选0.3左右;对可用性要求高的业务场景,把阈值调到0.7,容忍部分DGA漏过。这一步看起来简单,实际对上线效果影响巨大,甚至比调网络结构还明显。

另外一个实用技巧是把域名按长度做分段预测。短域名如abc.io本身信息量少,模型预测不可靠,此时应该输出“未知”而不是强制判定,否则会制造大量误报。你可以这样做:对长度小于8的域名单独设一个阈值下限,低于0.3就放行,同时记录日志留待后续人工复核。这样的设计才能让同一个模型在真实DNS流量里真正站住脚。

这套代码里最让我印象深刻的坑,是跨家族DGA测试——同一套模型,在Zeus上识别率99%,换到Conficker家族就掉到70%。从那以后我每次训练DGA模型都会强制准备至少三个不同家族的样本做独立测试,达不到阈值就不算完成任务。希望这篇拆解帮你把这个流程少走一段弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询