用大模型检测 DGA 恶意域名实战:当字典型 DGA 骗过熵值统计,LLM 语义研判如何补上这块短板
用大模型检测 DGA 恶意域名实战:当字典型 DGA 骗过熵值统计,LLM 语义研判如何补上这块短板
法律红线与适用范围声明:本文所有实验均在本地隔离环境与公开数据集上完成,仅限授权安全测试、自有环境研究与防御能力建设使用。文中的"字典型 DGA 生成器"仅用于在实验环境内生成检测样本语料,刻意不包含任何注册、外联与真实 C2 逻辑;不含可直接滥用的完整武器化代码。未经授权对他人系统、账号或网络实施文中任何技术,可能违反《刑法》第 285 条、第 286 条相关规定。请做安全的建设者。
0. 这篇文章写给谁
如果你是负责企业 DNS 安全、SOC 告警运营或反僵尸网络方向的安全工程师,已经用熵值 + 随机森林搭了一套 DGA 检测,却发现召回率在"新型家族"和"字典型 DGA"上断崖式下跌,又不想把 API 预算烧在逐条调 LLM 上——本文给你一条"统计初筛 + 家族隔离验证 + LLM 语义研判"的三层可复现路径,并用真实踩坑说明每一层为什么这样设计、边界在哪。阅读需要 Python 基础,不需要预先了解 DGA。
1. 背景:为什么 DGA 至今仍是 C2 通信的主力规避手段
域名生成算法(Domain Generation Algorithm,DGA)是僵尸网络用来对抗域名黑名单的经典技术,对应 MITRE ATT&CK 战术"Command and Control"下的技术条目 T1568.002(Dynamic Resolution: Domain Generation Algorithms)。其运行机制是一个"隔空对暗号"的过程:攻击者以日期、热词或随机数为种子,让恶意软件每天生成成千上万个候选域名,只注册其中极少数指向真实 C2;被感染主机用同样的种子生成同一批域名并逐个尝试解析,命中已注册的那个就建立 C2 通道。由于绝大多数生成域名未注册,网络里会出现典型的 NXDOMAIN(域名不存在)解析失败爆发,这也是防御方最可靠的检测信号之一——Conficker 每日生成 250 个候选域名、执法机构用"抢注/sinkhole"反制,就是这个博弈的早期版本(可参见 Conficker 工作组当年的公开处置报告)。
它至今活跃的原因很简单:黑名单追不上生成速度,而查封追不上重建速度。2024 年 5 月与 2025 年的 Operation Endgame 多国联合行动每次查封上千个恶意域名,但 Emotet、QakBot 等家族在历次打击后均数月内重建基础设施(其寻址方式里就有 DGA 与 fast-flux 的组合)。这意味着企业侧不能只依赖外部情报,必须有能力在自家 DNS 流量里"就地"识别 DGA 行为。
2. 原理剖析:统计检测为什么会有天花板
2.1 香农熵:第一代检测器的基石与盲区
传统 DGA 检测的核心假设是:算法生成的域名接近均匀随机分布,字符混乱度显著高于人类命名。量化这个"混乱度"的工具是香农熵:
$$H(X) = -\sum_{i=1}^{n} P(x_i) \log_2 P(x_i)$$
对 26 个字母的均匀随机字符串,熵值趋近 log2(26) ≈ 4.7;而英文单词因元音辅音交替、常见字母组合(th、er、ing)的存在,熵值通常在 2.5~4.0 之间。基于熵 + 数字占比 + 连续辅音长度这类词汇特征的传统机器学习,在 2016—2022 年间的公开评测中确实能到 95% 以上的准确率。先看一个最小可运行示例,理解这一层到底在算什么:
# entropy_features.py —— 统计特征层的最小可复现实现
import math
from collections import Counter
def shannon_entropy(s: str) -> float:
"""香农熵:衡量字符串字符分布的混乱程度"""
if not s:
return 0.0
counts = Counter(s) # 统计每个字符出现次数
n = len(s)
return -sum(c / n * math.log2(c / n) for c in counts.values())
def stat_features(domain: str) -> dict:
"""提取经典词汇统计特征,熵值只是其中一维"""
sld = domain.split('.')[0].lower() # 取二级域主体(去掉 TLD)
if not sld:
sld = domain.lower()
n = max(len(sld), 1)
vowels = sum(c in 'aeiou' for c in sld)
consonants = sum(c.isalpha() and c not in 'aeiou' for c in sld)
return {
'len': len(sld),
'entropy': round(shannon_entropy(sld), 4),
'digit_ratio': round(sum(c.isdigit() for c in sld) / n, 4),
'vowel_ratio': round(vowels / n, 4),
'uniq_ratio': round(len(set(sld)) / n, 4),
'hyphens': sld.count('-'),
}
if __name__ == '__main__':
for d in ['google', 'xjqkwzbnmgspv', 'impressiveideas', 'chncietyazlbbi']:
f = stat_features(d)
print(f"{d:16s} entropy={f['entropy']:.2f} vowel={f['vowel_ratio']}")
运行后你会看到 xjqkwzbnmgspv(典型算术型 DGA)熵值逼近 3.6 且元音占比极低,而 google 熵值仅 2.32——区分度非常明显。这段代码的价值在于把"直觉上的乱"变成可审计的数值,它是后面所有层的地基。
2.2 字典型 DGA:为什么熵值防线会整体失守
问题出在 DGA 的军备竞赛下半场。Highnam 等人在 SN Computer Science(2021)发表的 Bilbo 系列工作已经证明:基于词表拼接的 dictionary-based DGA(matsnu、gozi、suppobox 等家族)生成的域名,字符统计特征与人类命名几乎无法区分——因为它们本来就是"真单词的合法组合"。比如 impressiveideas.com(wordlist 拼接产物)与创业公司起名在统计上没有本质差异。2024 年 IEEE CICN 会议上 Yerima 等人(doi: 10.1109/CICN63059.2024.10847389)用 DistilBERT 嵌入替代手工特征做 DGA 检测,动机正是"统计特征对字典型 DGA 泛化失败";2025 年 11 月 arXiv:2512.07866 的 LSTM 工作同样报告了"熵分析对简单 DGA 有效、对复杂模式失效"的结论。
我们在授权实验环境里复现一个刻意削弱的字典型 DGA 样本生成器,用它产出对抗语料来检验自己的检测器(注意:它只生成字符串样本用于本地检测评测,没有任何注册、DNS 查询或网络行为):
# dict_dga_sim.py —— 字典型 DGA 的实验复现(仅用于生成检测语料,无任何外联逻辑)
import random
WORDLISTS = { # 词表来源参考公开 matsnu/gozi 家族研究,此处精简为示意
'adj': ['bright', 'silent', 'lucky', 'swift', 'golden', 'silent'],
'noun': ['market', 'forest', 'window', 'signal', 'harbor', 'meadow'],
'verb': ['share', 'build', 'track', 'lift'],
}
def dict_dga_sample(seed: int, n: int = 5) -> list[str]:
"""模拟字典型 DGA:从词表拼接出'像人话'的域名"""
rng = random.Random(seed) # 固定种子保证实验可复现
out = []
for _ in range(n):
parts = [rng.choice(WORDLISTS[k]) for k in ('adj', 'noun')]
if rng.random() < 0.4:
parts.append(rng.choice(WORDLISTS['verb'])) # 部分样本三词拼接
out.append(''.join(parts) + rng.choice(['.com', '.net', '.biz']))
return out
if __name__ == '__main__':
for d in dict_dga_sample(seed=20261105, n=6):
print(d)
# 预期输出示例:luckyharbor.com / brightsignaltrack.net / swiftmeadow.com ...
把这类样本喂给 2.1 的特征提取器,你会看到熵值、元音占比、唯一字符比全部落在正常域名的分布区间内——这就是"统计特征天花板"的直接证据。这也是本文引入大模型的根本原因:LLM 的预训练语料里包含了海量真实品牌名、人类命名习惯与已知威胁家族样本,它判断的不是"字符乱不乱",而是"这个组合像不像人类会起的名字、是否带已知家族的构词指纹"——恰好是统计特征的盲区。
2.3 信任模型与设计权衡
这里要先说清一个设计权衡:为什么不干脆"全用 LLM"?三个硬约束——时延(LLM 单次研判秒级,而 DNS 每秒查询量在企业网内是万级)、成本(逐条调用按 token 计费,全量研判会烧穿预算)、确定性(同样的域名两次调用可能得到不同结论,运维无法接受)。所以工程上的正确形态必然是漏斗:便宜的层处理 99% 的流量,贵的层只看 1% 的高价值样本。这个"小模型初筛 + 大模型研判"的分工,也是 2024—2026 年学界与工业界的共识路线(前述 CICN 2024 论文用轻量 transformer 嵌入替代大模型,本质上是在成本曲线上找另一个平衡点)。
3. 完整实战:三层检测流水线从零搭建
3.1 环境与数据
版本敏感项统一标注如下:Python 3.11、scikit-learn 1.5+、LightGBM 4.x、transformers 4.46+(2024 年末起对 pipeline 推理接口稳定)、一个 OpenAI 兼容 Chat API(本文示例以 2026 年常见的 qwen 系列 / gpt-4o-mini 档位模型为参照,任何兼容 /v1/chat/completions 的服务皆可)。数据集三件套:黑样本用 360 Netlab 公开 DGA 数据集或 Bambenek Consulting 的 DGA feed(覆盖数十个家族,含字典型);白样本用 Tranco Top 1M(比已停止维护的 Alexa Top 1M 更可信);带标签的学术数据可用 UMUDGA(50 个 DGA 家族、每类 5000 条)。
# 1. 建立隔离环境
python -m venv dga-venv && source dga-venv/bin/activate # Windows: dga-venv\Scripts\activate
# 2. 安装依赖(版本为 2026-10 实测可用组合)
pip install "scikit-learn>=1.5" "lightgbm>=4.5" "transformers>=4.46" requests pandas
# 3. 下载语料(示例:360 Netlab 每日 DGA feed 与 Bambenek 域名列表,均公开)
# curl -o dga_360.txt https://data.netlab.360.com/feeds/dga/dga.txt
# curl -o dga_bambenek.csv https://osint.bambenekconsulting.com/feeds/dga-feed.csv
# 白名单基线:curl -o tranco.csv https://tranco-list.eu/top-1m.csv 下载每日更新的 Tranco 列表
# 4. 运行本文流水线
python train_baseline.py && python serve_pipeline.py
3.2 第二层:统计模型初筛(含第一处踩坑)
第一层(黑白名单与 TTL/响应码的硬规则)略过——NXDOMAIN 突发、TTL 异常低这类信号值得单开一篇。直接看统计模型层,这里藏着 DGA 检测最著名的一个坑:家族泄漏(family leakage)。
❌ 错误写法:
# 错误示例:普通 K-Fold —— 同一 DGA 家族的样本被随机分进训练集和测试集
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True) # 模型在验证集上"作弊":它见过同家族的其他域名
# 结果:测试准确率 98%+,上线后遇到训练集外的新家族召回率可能不到 30%
✅ 正确写法:
# train_baseline.py —— 家族隔离交叉验证(Family-Stratified K-Fold)
import pandas as pd
from sklearn.metrics import classification_report
from lightgbm import LGBMClassifier
from sklearn.model_selection import GroupKFold
from entropy_features import stat_features # 复用 2.1 的特征提取器
def load_domains(path: str, label: int) -> pd.DataFrame:
"""读入域名样本,DGA 侧需带家族标签用于分组"""
rows = []
for line in open(path, encoding='utf-8'):
parts = line.strip().split(',')
if len(parts) >= 2: # DGA feed 格式:domain,family
rows.append({'domain': parts[0], 'family': parts[1], 'label': label})
elif parts[0]:
rows.append({'domain': parts[0], 'family': 'benign', 'label': label})
return pd.DataFrame(rows)
df = pd.concat([load_domains('dga_360.txt', 1), load_domains('tranco_sld.txt', 0)])
df = df.sample(frac=1.0, random_state=42).reset_index(drop=True) # 打乱,固定种子
X = pd.DataFrame([stat_features(d) for d in df['domain']])
gkf = GroupKFold(n_splits=5) # 按 family 分组整折隔离,杜绝家族泄漏
scores = []
for tr, te in gkf.split(X, df['label'], groups=df['family']):
clf = LGBMClassifier(n_estimators=300, learning_rate=0.08, num_leaves=31)
clf.fit(X.iloc[tr], df['label'].iloc[tr])
scores.append(clf.score(X.iloc[te], df['label'].iloc[te]))
print(f"fold score={scores[-1]:.4f}")
print(f"family-isolated CV mean = {sum(scores)/len(scores):.4f}")
# 预期输出:fold score ≈ 0.93~0.96(低于普通 KFold 的 0.98+,但这才是真实水位)
这段做了什么 / 为什么这么写:GroupKFold 保证同一家族(groups=df['family'])的所有样本整块进入同一折,模拟"线上只遇到没见过的新家族"的真实条件。我在自己的复现里,普通 K-Fold 报 98.7%、家族隔离后掉到 93~95%——这 4~6 个点就是以前自欺欺人的部分。低于 0.5 置信度的样本,正是下一层 LLM 的输入;初筛层的目标不是给出终审,而是把 LLM 的调用面从"全量"压到"可疑子集"。
3.3 第三层:LLM 语义研判(含第二处踩坑)
这一层解决两类问题:一是字典型 DGA 的统计盲区(2.2 已证);二是给告警配上人能读懂的证据链。同样先看坑——很多人第一版代码是这样的:
❌ 错误写法:
# 错误示例:全量域名直接丢给 LLM,自由文本提问
for domain in all_dns_queries: # 每天百万级 DNS 查询全部调用 API
resp = llm.chat(f"这个域名 {domain} 是不是恶意的?请回答是或不是")
# 问题1:成本失控(百万次调用 × 每次数百 token)
# 问题2:域名是攻击者可控的输入,未经清洗直接拼进提示词,存在提示注入面
# 问题3:自由文本输出无法被程序消费,告警系统解析不了"看起来有点可疑"
✅ 正确写法:
# llm_verdict.py —— 置信度门控 + 结构化输出的 LLM 研判层
import json, hashlib, time
import requests
CACHED: dict[str, dict] = {} # 进程内 TTL 缓存(生产换 Redis)
TTL_SECONDS = 86400 # 域名判定 24h 内缓存,重复查询零成本
JUDGE_PROMPT = """你是 DNS 安全分析师。判断该域名是否为算法生成(DGA)或仿冒域名。
只依据构词特征与你的语言知识判断,不要臆测无法验证的注册信息。
严格输出 JSON(不要任何多余文本):
{{"verdict":"malicious|benign|unknown","confidence":0-100,
"family_guess":"如 matsnu/gozi/未知","reasons":["一句话证据"]}}
域名:{domain}
初筛模型置信度:{ml_score}"""
def llm_judge(domain: str, ml_score: float) -> dict:
key = hashlib.sha1(domain.encode()).hexdigest() # 缓存键与提示词解耦
hit = CACHED.get(key)
if hit and time.time() - hit['ts'] < TTL_SECONDS: # 命中缓存直接返回
return hit['data']
# 关键行1:只允许 [a-z0-9.-] 通过,剥掉一切可能构成提示注入的字符
safe = ''.join(c for c in domain.lower() if c in 'abcdefghijklmnopqrstuvwxyz0123456789.-')
if not safe or safe != domain.lower():
return {'verdict': 'unknown', 'confidence': 0, 'reasons': ['域名含异常字符,转人工']}
payload = {'model': 'qwen-plus', 'temperature': 0.1, # 温度调低保证判定稳定
'messages': [{'role': 'user',
'content': JUDGE_PROMPT.format(domain=safe, ml_score=round(ml_score, 3))}]}
r = requests.post('http://llm-gw.internal/v1/chat/completions',
json=payload, timeout=20) # 内部网关,走统一鉴权与限额
text = r.json()['choices'][0]['message']['content']
data = json.loads(text) # 结构化输出:解析失败即降级
data['reasons'] = data.get('reasons', [])[:3] # 证据链截断,防 token 爆炸
CACHED[key] = {'ts': time.time(), 'data': data}
return data
为什么这么写:三个关键决策分别对应错误写法的三个问题——缓存(同一域名在 TTL 内只判一次,DGA 域名的生命周期恰好以天计,命中率很高)、输入白名单清洗(域名是 DNS 日志里的外部数据,把"数据"当"代码"拼进提示词就是提示注入的教科书场景,safe != domain 的兜底分支把含异常字符的样本直接转人工,与本文系列前面讲过的间接注入防御是同一原理)、结构化输出(JSON schema 固定,解析失败走降级路径,绝不阻塞主管线)。temperature=0.1 是为了让同一域名的多次研判尽量稳定——LLM 的非确定性在这个场景里是纯负债。
3.4 融合与证据链输出
最后把三层结果融合成 SIEM 可直接消费的告警。融合策略刻意保守:只放大不武断——统计层与语义层一致恶意才升级为 block 建议,任何冲突都保留不确定性:
# fuse.py —— 三层结果融合与 SIEM 告警输出
def fuse(domain: str, ml_score: float, llm_result: dict, ttl_anomaly: bool) -> dict:
"""统计分、语义判定、DNS 行为信号三源融合"""
sem = 1 if llm_result['verdict'] == 'malicious' else 0
# 融合规则:行为信号(NXDOMAIN 突发等)是强放大器,但单源不足以定罪
if ml_score >= 0.9 and sem == 1:
action, severity = 'block', 'high' # 双源一致:可建议阻断
elif ml_score >= 0.5 and (sem == 1 or ttl_anomaly):
action, severity = 'alert', 'medium' # 任一辅助源支持:告警人工复核
else:
action, severity = 'observe', 'low' # 其余一律先观察,绝不误杀
return {
'domain': domain, 'action': action, 'severity': severity,
'ml_score': round(ml_score, 3),
'llm_family': llm_result.get('family_guess', 'n/a'),
'evidence': llm_result['reasons'], # LLM 给出的构词证据,供分析师复核
'schema': 'dga-alert/1.0', # 固定 schema 方便 SIEM 端解析
}
if __name__ == '__main__':
# 端到端示例:一个字典型 DGA 样本走完三层
print(json.dumps(fuse('luckyharbor.com',
ml_score=0.42, # 统计层:低置信(字典型盲区)
llm_result=llm_judge('luckyharbor.com', 0.42),
ttl_anomaly=False), ensure_ascii=False, indent=2))
预期输出(LLM 研判命中构词指纹时):
{
"domain": "luckyharbor.com",
"action": "alert",
"severity": "medium",
"ml_score": 0.42,
"llm_family": "matsnu-like",
"evidence": ["形容词+名词无品牌语义的机械拼接,符合字典型 DGA 构词模式"],
"schema": "dga-alert/1.0"
}
注意这个样本统计层只有 0.42——低于常见 0.5 告警线,纯统计管线会漏掉它;是 LLM 层把它捞了回来,同时 family_guess 给了分析师一个可验证的归因线索(可对照 360 Netlab 的家族标注复核)。这一条输出就是整个三层架构存在意义的缩影。
4. 边界与权衡:这套东西不能用在哪
误报面:CDN 与云厂商的确定性子域名(如 a1b2c3.dualstack.cloudfront.net、对象存储桶域名)在统计层长得极像算术型 DGA。治理办法不是调阈值,而是先把内部递归解析器日志中的已知基础设施白名单化,否则 LLM 研判层会被这类样本打爆。短链域名、含 hash 的资源 URL 同理。
对抗面:攻防是双向的。防御方用 LLM 看构词语义,攻击方同样可以用 LLM 生成"更有人味"的域名(把品牌词、真实地名混入词表),甚至针对性构造让语义模型误判的样本。缓解思路有三:其一,不要放弃行为信号——无论域名起得多像人话,"大量 NXDOMAIN + 短 TTL + 查询源集中"的 rendezvous 行为模式是 DGA 的结构性代价,绕不开(2026 年 7 月公开的 BNDF 研究也再次验证:监督模型的判定结果与 NXDOMAIN 占比高度一致);其二,家族隔离评测要常态化,把"对抗集"纳入回归;其三,LLM 层结论只做告警增强,不做自动阻断的唯一依据。
成本与时延的真实数字:初筛层(LightGBM)单条亚毫秒、内存几百 MB,可以扛满量 DNS;LLM 层在 2026 年主流 API 档位下单次研判约 0.5~2 秒、按百万 token 计价。经验值:经置信度门控 + TTL 缓存 + 白名单后,LLM 层日调用量可压到全量查询的 0.1%~1%。如果连这个量也想省,可参考 CICN 2024 论文路线:用 DistilBERT 级别的轻量 transformer 嵌入做本地推理,把云端大模型留给归因与证据生成。
采集面:DoH/DoT 加密 DNS 的普及正在侵蚀"出口镜像看明文 DNS"的传统采集方式(中国互联网络信息中心 CNNIC 在 2025 年 11 月发布的《主动防御域名系统关键技术体系研究》中系统讨论了这个问题)。企业侧的对策是把检测点移到自己控制的递归解析器/代理上,或部署 PDNS(Protective DNS)类服务,本文管线不依赖采集方式,但输入数据源的可持续性需要在立项时回答。
不适用场景:终端数量 < 500 且没有统一 DNS 出口的小网络(流量太小,NXDOMAIN 基线建不起来);以及把本方案当成"装完就完事"的产品——没有每周一次的家族隔离回归评测,任何 DGA 检测都会在三个月内衰减。
5. 前沿动态与可核实来源
- MITRE ATT&CK T1568.002(Domain Generation Algorithms):技术条目与已知使用该技术的软件/组织清单,可查官方矩阵。
- Yerima, Vinod, Shaalan: A Transformer Embedding-Based Model for Malicious DGA-Generated Domain Detection, IEEE CICN 2024, doi: 10.1109/CICN63059.2024.10847389(配套数据集已公开,含 transformer 嵌入实验样本)。
- arXiv:2512.07866(2025-11-26 提交):C2 流量 DGA 检测的 LSTM 方案,明确报告"熵分析对简单 DGA 有效、复杂模式需要深度模型"。
- Highnam 等:Real-Time Detection of Dictionary DGA Network Traffic Using Deep Learning, SN Computer Science, 2021(Bilbo,字典型 DGA 检测的代表作)。
- CNNIC:主动防御域名系统关键技术体系研究,2025-11-07(PDNS、加密 DNS 与 DGA 检测的国家级视角)。
- 数据与情报源:360 Netlab 公开 DGA 数据集(data.netlab.360.com)、Bambenek Consulting DGA feed、Tranco Top 1M(tranco-list.eu,替代已停止维护的 Alexa 排名)、UMUDGA 学术数据集(50 家族);国际执法侧参考 Operation Endgame(2024-05 首轮、2025 年后续行动)公开通报。
6. 总结与延伸方向
回到开头的问题:字典型 DGA 骗过熵值统计之后,大模型补位的方式不是"取代统计层",而是把防线从"字符分布"推进到"构词语义 + 可解释证据链"。三层架构的本质是一份成本与确定性的预算表——亚毫秒的统计层守住吞吐,秒级的语义层守住召回,行为信号(NXDOMAIN/短 TTL)守住对抗底线,任何单层失效都有人接住。两条踩坑是整套流水线里最值钱的部分:家族隔离评测决定你敢不敢相信自己的召回率,缓存 + 输入清洗 + 结构化输出决定 LLM 层能不能活过第一周的运维评审。
延伸方向有三个:其一,家族归因的 RAG 化——把 360/Bambenek 的家族标注与历年威胁报告做成向量库,让 family_guess 从"猜测"变成"有出处引用的检索结论";其二,轻量模型蒸馏——用 LLM 批量研判的结果回填训练集,蒸馏出本地小模型把 LLM 调用面再压一个数量级;其三,DoH 时代的采集对抗——把检测点下沉到终端代理与 PDNS,这会是未来两年 DNS 安全的主战场。如果这篇对你有启发,下一篇我们聊聊"用 LLM 给勒索软件事件做时间线重建"——同样是 B 类战场,难度更高,坑也更多。
更多推荐



所有评论(0)