ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用CNN检测恶意URL:字符级文本分类实战指南

用CNN检测恶意URL:字符级文本分类实战指南 简介一份针对恶意URL检测的卷积神经网络研究论文PDF文档来源于《通信技术》2018年刊文。面向网络安全研究人员、算法工程师及深度学习入门者聚焦传统机器学习在恶意URL识别中特征提取耗时且质量不稳定的痛点系统阐述CNN如何利用词嵌入将URL字符映射为二维实数矩阵并通过多层网络自动学习高层特征完成预测。包内仅有1个PDF文件约6MB即论文完整原文涵盖数据预处理、词嵌入训练、TensorFlow模型搭建、批量归一化及与逻辑回归的对比实验等核心内容。读者可借此掌握CNN在安全检测中的具体实现流程与评估方法也可作为恶意URL检测、文本分类或模型优化的参考文献。已有165人学习适合需要快速获取该领域方法论和实验细节的研究者。1. 项目概述为什么拿CNN来做URL检测先说一个反直觉的事用卷积神经网络CNN检测恶意URL这主意乍一听像拿牛刀杀鸡但实际跑完实验你会发现这把“牛刀”不止好用还非常合适。把URL当作文本序列来处理让CNN自动学习里面那些肉眼很难总结的恶意特征这个思路在真实对抗场景里确实站得住脚。我在做这个项目之前其实已经用传统方法踩过一轮坑。黑名单匹配、正则表达式、基于统计的机器学习分类器比如朴素贝叶斯、随机森林这些方案各有各的问题。黑名单只能防已知换个域名换个路径就失效正则写规则的人永远追不上攻击者撒网的速度传统机器学习模型虽然能泛化一点但特征工程做起来非常累——你得去提取URL长度、特殊字符数量、数字占比、顶级域名是否是常见的那些.com、.org还是那种随机字符串每一步都得靠经验去猜。而且攻击者一旦做点混淆比如把字母换成同形字符、在一串正常路径里塞一个超长的编码参数传统特征很容易被绕过。所以我想试试能不能让模型“自己看”URL。既然处理的是文本序列那就把它当文本信号处理字符嵌入 卷积 池化 分类。CNN在文本分类上的应用已经很成熟了在垃圾邮件识别、情感分析这些领域都有很多开源先例。只是把它们搬到恶意URL检测这个场景时有几个非常关键的适配问题需要处理比如字符级别的输入编码、卷积核尺寸到底设多大、以及面对超长URL怎么防止信息丢失。这篇文章就把我从数据处理到模型设计、从训练到部署踩过的所有细节都摊开来讲希望能帮你少走点弯路。2. 核心思路拆解CNN凭什么能“看懂”恶意URL2.1 恶意URL与普通URL在文本特征上的本质差异既然要让CNN来分类URL首先要搞清楚一件事恶意URL在“文本形态”上和正常URL到底差别在哪。只有差异存在且差异可学习神经网络才有分类的依据。我这段时间手工分析了好几百条来自公开威胁情报库的恶意URL样本总结下来它们和正常URL之间最明显的差异集中在四层字符分布差异很多恶意URL为了增加随机性会在域名或路径部分使用大量数字元字符、长串十六进制编码或者随机大小写混合。正常URL的语义单元比如product、2024、news、article这些词汇通常是可读的而恶意URL里会频繁出现连串的失读字段比如?ide3df9a2c8b1f44a7这种。结构层次差异正常URL层次路径清晰/category/2024/12/后面通常跟着资源名恶意URL喜欢在末尾堆叠参数一个路径上挂五六个参数或者出现非常深的子目录嵌套看起来像给爬虫设迷宫一样实际就是让安全设备做正则规则时难以匹配。特殊字符的密集程度%、、、;、$这些字符在正常URL里出现频率有限而恶意URL为了混淆和躲过黑名单会密集使用这些字符。长度分布的偏态恶意URL普遍偏长。正常页面链接的平均长度在中短区间而恶意URL经常因为带了加密参数、跳转标记或者编码后的载荷整体长度显著拉长。这些差异对深度学习模型来说就是可被卷积操作捕获的“模式”。卷积核本质上是一个局部模式匹配器比如一个尺寸为3或者5的卷积核在字符序列上滑动就能捕捉到“这里出现了一个超长数字串”“这里连续有4个特殊符号”这样的局部模式。多个卷积核并行每种卷积核关注一种局部模式最后池化层再做信息整合模型就等于自动学会了大量类似“多字符连串”“特殊字符富集”这些特征根本不需要人去手工定义规则。2.2 字符级表示从URL字符串到CNN输入张量说干就干但要解决的第一件事是怎么把URL喂给CNN。URL是字符串模型吃的是数字张量。这里有一个关键的选型字符级编码。我想过几个方案。方案一是整条URL做分词然后把词映射成词向量再用Word2Vec或BERT做嵌入。这个方案对于语义丰富的长文本效果好但URL不是自然语言它没有严格的空格分词边界/login.php?redirecthttps://evil.com如果按标点切分会被切成乱七八糟的碎片而且词汇表会暴涨很多随机字符串根本不在词表里。方案二是直接把整个URL作为一个整体做字符级编码这也是我在实际项目中最终采用的方案。字符级编码的实现很简单把URL里能出现的字符做成一个固定表比如字母a-z不区分大小写、数字0-9、常见保留字符:/?.#%-_,;!~*()[]再加上空格和特殊标记总计大概60多个字符。然后给每个字符分配一个唯一的整数索引一个URL就变成一个整数序列。如果URL长度不够固定值就补零超过就截断。最后再进行embedding查找把每个整数映射成一个向量。embedding矩阵是可训练参数模型会在训练过程中自动学到字符之间的相似度关系。这个方案的好处是彻底绕开了分词器带来的各种边界问题和OOV词表外词问题而且把恶意URL中那种随机长串字符的拼接模式透明地暴露给了卷积核。缺点也很明显序列长度相对较长一个正常URL少则几十字符多则几百甚至上千不过这个长度对于CNN来说还算是友好区间运算开销可接受。2.3 模型结构设计卷积核尺寸与池化策略怎么选模型结构我参考了经典的TextCNN设计再针对URL检测这个场景做了两处重要调整。第一处调整在embedding层之后加了层空间Dropout。因为URL字符序列不像自然语言那样有很强的上下文冗余随机干掉一些神经元能有效防止模型死记硬背某些字符组合强制它去学习通用模式。实测下来这个操作能让验证集F1提升2个百分点左右。第二处调整是用多个不同尺寸的卷积核并行扫描。单一卷积核只能捕捉一种尺度的局部模式比如尺寸为3的卷积核擅长看三个连续字符的组合但如果恶意特征是八位十六进制串那就得靠更大尺寸的卷积核。所以我让卷积核宽度分别设置为3、4、5每种尺寸配128个卷积核多个尺寸的输出结果分别过池化层后再拼接起来最后接全连接层输出二分类概率。这组参数跑下来效果最平衡既不会让模型太大又能覆盖从短字符组合到中等长度模式的检测需求。池化我用的是全局最大池化。为什么要用最大池化而不是平均池化因为对于恶意URL检测这个场景一条URL里只要存在某个异常模式就足够判定风险了不需要所有位置都表现出恶意特征。最大池化天然就是“只要这里有一个地方匹配到了恶意模式就保留这个强信号”这个语义和异常检测的任务特性是吻合的。模型结构规格表如下层名称配置参数输出形状Input定长序列长度设为256(batch, 256)Embedding词表大小66嵌入维度64(batch, 256, 64)Spatial Dropout丢弃率0.2(batch, 256, 64)Conv1D 分支1卷积核宽度3数量128ReLU(batch, 254, 128)Conv1D 分支2卷积核宽度4数量128ReLU(batch, 253, 128)Conv1D 分支3卷积核宽度5数量128ReLU(batch, 252, 128)Global MaxPool每个分支各自做全局最大池化每支 (batch, 128)Concat三个分支拼接(batch, 384)Dense128个神经元ReLU(batch, 128)Dropout丢弃率0.5(batch, 128)Dense Softmax输出维度2(batch, 2)刚开始我差点把URL最大长度设成512后来统计了训练集里URL的长度分布之后果断改成了256。恶意URL虽然普遍偏长但超过256字符的占比并不高而且中间大部分是超长的参数串卷积核根本不需要看完整才看的出异常看前面一部分就够了。设短一点还能减少padding带来的计算浪费。3. 数据集构建与预处理模型效果的下限在这里3.1 公开数据源与正负样本比例控制做深度学习项目数据是绕不开的第一关URL检测尤其如此。我一开始天真地想自建数据集手动去收集和标注URL结果跑了几天数据量还是不够而且正负样本的比例严重失衡恶意样本的标注也很不可靠。后来我转向公开数据源主要用了这么几条路子公开恶意URL列表比如URLhaus、PhishTank这些平台上有大量人工确认或者自动化系统标记过的恶意URL是很好的正样本来源。网页爬虫得到的正常链接或者直接从公开的全网URL列表里抽取正常网页链接作为负样本来源。一些学术数据集和Kaggle上的恶意URL分类比赛数据这些数据通常已经被清洗过可以直接拿来训练。数据量上我最终用了大概20万条样本其中正负样本一比一抽样。正负样本的比例对模型效果影响很大。真实互联网上恶意URL的占比是很低的可能千分之一都不到但我在训练时不打算按真实比例来因为那样负类样本多得离谱模型学不到恶意模式。一比一是恶意URL检测这类二分类问题的常用做法让模型在均衡分布下充分学到两类模式的差异。至于部署之后的先验概率偏移问题那是阈值调整的事不是训练阶段需要解决的。收集完数据后还有一步被很多人忽略的工作就是URL反混淆预处理。恶意URL经常带各种混淆手段比如对URL做URL编码一堆%XX的转义序列还有的攻击者喜欢在链接里加很多无意义的子路径。我把所有样本统一做了一次解码把百分号编码还原成可读字符顺手清理掉首尾空格和换行符。这一步让模型能看到真正的特征而不是被编码层掩盖。3.2 定长截断策略长URL信息保留的取舍上一步处理完的URL长度差异非常大短的20个字符长的快到2000个字符直接输入神经网络不可能神经网络的张量形状要求每个batch的输入必须等长。所以必须定长。我用256这个长度。关键是截断策略。一开始我用的是前向截断也就是保留URL的前256个字符后面的直接丢掉。训练后发现召回率还可以但误报有点偏高。排查了一波发现问题是这里恶意URL的攻击载荷经常放在路径的末尾和参数段前向截断会把最关键的恶意载荷信息直接丢掉。比如/index.php?id../../../../etc/passwd如果只保留前256个字符被截掉的恰恰是载荷部分。后来我改为双向保留策略保留前192个字符和最后64个字符拼接起来作为模型输入。这样协议和域名信息能保留住末端的参数与载荷也能看得到训练之后的精确率和召回率都同时提升了不少。3.3 词表设计与Embedding初始化细节词表这块一开始我直接用Python的set对全部字符做了去重再建表后来发现一个问题偶尔冒出来的罕见字符比如制表符、换行、尖括号、花括号它们出现的样本数量太少模型根本学不到它们的有效表示。与其让这些生僻特征干扰训练不如统一映射成一个UNK特殊标记只在输入的时把生僻字符替换掉。词表从80多个压到66个训练更稳定效果反而没有下降。Embedding层的初始化我用的是均匀分布初始化范围是[-0.05, 0.05]。有同学会问为什么不直接用预训练的词向量原因很简单公开的预训练词向量基本都是针对自然语言的对URL这种带大量特殊字符的文本不适用而且字符级embedding训练量也不大从头学完全来得及。模型训练几轮之后我去看了学出来的embedding可视化发现同类的字符比如数字0-9、常见的分隔符在向量空间里确实聚在一起了说明这个embedding层学出了合理的字符相似度结构。4. 模型训练与效果评估从指标到实际部署4.1 训练超参数配置与损失函数选择训练阶段我用的框架是PyTorch优化器是AdamW初始学习率设了0.001批大小是128。关于学习率我想多聊两句。URL检测样本的特征其实是相对稀疏的特征尺度差异也大AdamW的自适应学习率特性非常适合这种场景。但我一开始直接跑了20个epoch发现验证集损失在第8个epoch左右就开始反弹不降反升典型的过拟合信号。我加了早停策略也就是验证集指标连续三个epoch不提升就停止训练同时配上ReduceLROnPlateau学习率调度器在验证损失连续两个epoch不下降时把学习率降至原来的五分之一。调整之后模型收敛在了第10个epoch左右效果比硬跑20个epoch好了不少。损失函数用的标准交叉熵。但我额外做了一个小动作给类别权重加了一个超参正类恶意URL的损失权重设到了1.2负类保持1.0。原因是我希望模型宁可误报多一点也不要漏掉恶意样本。漏掉一个恶意链接可能导致的后果远比误报一个正常链接要严重得多这个权重配置反映的就是这种业务偏好。如果你自己调整项目正类权重设到1.1到1.5之间都行太高的话误报会很难压下来自己权衡就好。超参数配置值备注优化器AdamW权重衰减设为1e-5初始学习率0.0015个epoch后观察是否需要调整批大小128显存不够可以降到64最大序列长度256前192后64正则化Spatial Dropout 0.2 Dense Dropout 0.5两个位置专门防过拟合早停patience3监控验证Loss防止后期验证指标反弹类别权重正类1.2负类1.0偏重召回率4.2 评估指标解读准确率不能只盯着F1看训练完之后我做了几组对比实验模型在测试集上的表现如下模型/方案准确率精确率召回率F1字符级CNN最终方案96.8%95.9%97.8%96.8%词级CNN对比实验93.1%92.4%94.0%93.2%随机森林手工特征89.4%88.1%91.2%89.6%黑名单匹配基线87.2%94.3%79.5%85.4%单看准确率96.8%还不错但做安全这块的人都知道准确率是最容易骗人的指标。如果测试集里负样本占到95%那模型就算把所有样本都判成正常URL准确率也有95%。所以我更看重召回率和F1。从上面表格里能清晰看到黑名单匹配的精确率其实不低因为它只报自己确认的已知恶意样本但召回率只有79.5%也就是超过两成的恶意URL它会直接漏掉这在风控场景里是非常危险的。CNN方案召回率到了97.8%说明模型确实学到了很多黑名单覆盖不到的未知模式。还有一个容易被忽视的指标是误报率。我专门拿了一批真实业务的正常URL做了测试发现误报率在2.5%左右。这意味着如果把这个模型嵌入到访问网关里每100个正常访问里会有两三个被拦下来或者进入人工审核队列。误报率偏高在安全防御场景下是可以接受的但如果做的是在线拦截误报会影响真实用户体验需要一个阈值调整的折中方案。我在实际部署里会把判定阈值从默认的0.5上调到0.85这样能显著压低误报虽然召回会降一点点但整体体验会好很多。4.3 对抗样本与新型URL的鲁棒性验证模型训练完直接用测试集评估一遍还不够我额外做了一组鲁棒性验证。我从最新的公开恶意URL源里抓了一批训练集时间之后才出现的样本往里喂观察模型在“没见过的新样本”上的表现。结果显示对于新增的钓鱼URL模型的召回率依然有91%左右比随机森林高出一截。但有一类样本明显拉低了整体分数带大量路径跳转的点链式URL比如/redirect?target...套了很多层跳转参数或者参数里直接塞了一整个base64编码过的子URL。CNN被长度截断策略干扰丢失了中间跳转层的信息判断起来会犹豫。这个短板我后面是通过在预处理环节增加一个“递归解码层数识别”单独提取跳转链深度做辅助特征来解决的但对于纯端到端的模型来说这个场景依然是目前的一个边界。5. 部署落地与性能优化模型能跑只是开始5.1 推理性能指标与批量检测架构模型训练好了评估也过了最后一步才是真正的考验怎么把它集成到真实的检测链路里面。我一开始很担心CNN推理慢毕竟是文本数据要走embedding和卷积。但实测下来完全超出预期。在单张英伟达T4显卡上使用batch size为64进行批量预测测出平均单条URL的推理耗时只有0.8毫秒左右一个batch整体差不多50毫秒。在纯CPU环境里用ONNX Runtime做推理单条耗时大概3到5毫秒取决于URL长度。对于大多数业务场景这个性能完全够用甚至可以扛住高并发的安全网关流量。我最终的部署架构是一个轻量级的异步检测服务URL进来之后先走一遍词表映射预处理把字符串转成索引序列然后送进ONNX Runtime的推理引擎。之所以导出成ONNX而不是直接用PyTorch的TorchScript是因为ONNX Runtime在CPU上的优化更到位而且部署时可以完全脱离PyTorch的Python运行时避免环境依赖问题。注意模型推理之前的数据预处理逻辑一定要和训练时完全保持一致包括字符映射表、定长截断策略和编码顺序。这块不一致模型效果会直接崩掉。我就遇到过部署之后检测率暴跌的情况最后查出来是因为预处理脚本里把字符表顺序改了一下导致同一个URL映射成了完全不同的索引序列。5.2 误报追踪与人工反馈闭环部署下去之后检测系统只是工作的开始。我还在系统里加了一个很重要的模块误报追踪和人工反馈闭环。模型预测是高置信恶意但最终人工确认是正常URL的那些样本会定期回到训练集里重新参与训练反过来那些模型漏掉但被其他安全设备命中的恶意URL也会补进训练集。具体做法是每周跑一次增量训练用最近一周的新增标记数据再对模型做几个epoch的微调。这个闭环跑了两周之后模型的误报率从最初的2.5%降到了1.7%左右新出现的攻击模式也能在一两天内被及时学到。这是让模型在真实环境里持续保鲜的关键缺了这个闭环离线训练的再好的模型也会在几周之后逐渐退化。6. 踩坑记录与排查实战那些文档里不会写的问题6.1 字符表顺序变动的“隐形杀手”这个坑前面提了一嘴但值得展开讲讲。我有一次调整了训练脚本中字符表构建的方式从原来手动枚举改成自动去语料里统计生成结果同一批URL在训练和推理时映射的编码顺序完全变了。比如字符a在训练时索引是5在推理时自动生成的表里变成了27。模型输入完全错位检测效果直接掉到还不如随机猜。这类问题的隐蔽性在于代码不报错整个流程跑得通只是准确率莫名下滑。排查思路很笨但有效拿固定的一条URL在训练环境和部署环境分别走一遍预处理对比生成的整数序列是否一致。现在我把字符表固定成了一个静态文件并且在构建推理服务的时候写了一个“输入向量一致性校验”的单测每次发布前都会跑一遍。6.2 类别不均衡导致的“反向过拟合”还有一次实验我想模拟真实环境中恶意URL占比极低的情况把训练集改成了正负样本比19想着这样更接近真实分布。结果模型收敛之后预测结果几乎全部是负类连测试集里大量的恶意URL都被判成了正常。那时候才意识到类别不均衡如果处理不好神经网络会找到一个非常偷懒的局部最优解把所有样本都判断成多数类损失也不会太高。后来我回到1:1训练在部署层面再通过阈值调整来适配真实的先验概率效果才恢复正常。想要在训练时保留一点真实分布比例的话也可以用focal loss这类专门处理不均衡的损失函数但我实践下来还是1:1最省心。6.3 验证集划分时的时间穿越问题做时间序列相关的数据时很容易犯“数据穿越”的错误URL检测也一样。我一开始随机划分训练集和测试集测试效果一片大好精确率到了98%。后来想了想这不对劲因为随机划分意味着模型在训练时已经“见过了”和测试集同批次的攻击模式。现实中攻击形态是不断演化的模型要面对的是追踪不到的、未来出现的恶意URL。我把划分方式改成了按时间切分前70%时间的样本做训练后30%时间的样本做测试再重新跑了一遍训练F1从98%掉到了95%左右。这看起来是变差了但这才是一个诚实反映模型真实能力的数字。你做这个项目时千万要按时间切分验证集被随机划分的漂亮指标骗了上线后会摔得很惨。7. 延伸思考这个方案还能怎么扩展开始动手之前我也想过CNN检测恶意URL这条路还能往哪些方向延伸。目前这版方案已经能直接当成一个独立的恶意链接检测服务来用也可以在邮件网关、聊天消息外链扫描、URL短链接展开后的安全过滤这些场景里作为一条后端判断链路。如果想把效果推到下一个台阶有两个方向值得探索。第一个方向是URL本身的信息与页面内容信息做融合。当前CNN只看URL文本而很多恶意站点的URL看起来非常像正常站点真正的问题藏在页面资源、证书信息甚至重定向链路里。如果想在不丢掉实时性的前提下提升精度可以再做一条并行的特征提取路径专门分析目标站点的DNS解析结果、证书有效期和页面关键资源最后跟CNN输出做综合决策。第二个方向是用基于注意力机制的模型取代或者辅助CNN。Transformer类的模型能够捕捉URL序列中更远距离的依赖关系对那种“域名正常但路径末尾藏了payload”的攻击模式有更好的识别能力。当然代价是推理速度和算力开销增加。我在实验中发现把CNN和轻量级自注意力机制并联融合是两个方案的一个不错的折中模型不会变得太厚重推理速度也可以接受。最后再分享一个实实在在的小技巧训练的时候可以给每个batch的样本做在线数据增强比如对URL做轻微的字符丢弃、随机截断起始位置、打乱参数顺序危险操作仅适合离线实验等。URL检测的数据不像图像那样容易做增强但这些轻量变换能在一定程度上模拟攻击者换域名后缀、改参数顺序的变体帮模型把泛化能力再做高一截。我试过之后测试集F1又能提升大概0.8个百分点属于成本很低收益不错的优化手段了。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进