简介基于机器学习的加密恶意流量检测毕业设计项目面向计算机相关专业学生及对网络空间安全、机器学习交叉领域感兴趣的开发者可适用于毕业设计、课程设计、大作业或工程实训及初期项目立项。项目采用web交互界面围绕CTU-13与DoH两个数据集依次实现数据展示、相关性分析、Boruta特征选择与特征轻量化的完整流程让使用者直观理解从pcap解析到恶意加密流量检测的建模链路。资源包共218个文件压缩后约25.73MB主要包含Python源码、HTML可视化页面、日志与CSV特征表、npy数据文件以及少量pcap样本另有辅助文档与图片素材其中log日志保留了中间解析结果csv和npy对应特征工程产物便于对照检查。已有564人学习/下载。借助该资源可快速部署本地演示环境复现Boruta特征选择与模型评估过程掌握加密流量检测实验的设计思路也可为毕业答辩展示论文实现框架提供完整素材。1. 加密恶意流量检测毕设从 pcap 到可答辩的完整链路流量一旦加密传统 IDS 基本等于瞎了一半。TLS 握手之后的载荷全黑入侵检测系统只能靠连接元数据猜而恶意程序恰恰最爱把 C2 通信、DNS 查询藏进加密隧道里。这套毕业设计做的事就是把加密流量检测变成一条可复现的机器学习流水线上传 pcap 包、解析成特征表、做相关性分析和 Boruta 特征选择、喂给随机森林等分类器、再用 Web 页面把每步结果展示出来。它不追求发明新算法而是把「加密流量到底能不能靠机器学习识别」这个问题做成一个能演示、能讲清楚、能应付答辩追问的完整项目。适合正在做毕设或课程设计的同学也适合想系统了解流量特征工程和特征选择实操的从业者。2. 双场景数据集选型为什么是 DoH 和 CTU-13以及 CSV 里存了什么2.1 两个场景代表两类典型恶意加密流量这个项目同时准备了两个数据集路径DoH 和 CTU-13。两者不是随便挑的它们代表了加密恶意流量检测里最常见的两个分支。DoHDNS over HTTPS是把 DNS 查询封装进 HTTPS 流量里传统基于 UDP 53 端口的 DNS 监控直接失效恶意软件可以用 DoH 隧道做隐蔽通信。这个场景的难点在于正常 DoH 查询和恶意 DoH 隧道在字节层面几乎一样只能靠时序、查询域名特征、TLS 握手特征去区分。CTU-13 是布拉格捷克理工大学发布的僵尸网络流量数据集包含多个 botnet 场景的 pcap 抓包。它和 DoH 的差别在于CTU-13 里有大量明文或低加密强度的 C2 通信特征更多体现在流级别——持续时长、包长分布、连接频率、端口行为。它的特征工程更偏传统 NetFlow 风格。所以这两个数据集一组合覆盖面就完整了一个有 TLS 加密隧道特征一个有经典流统计特征。做毕设答辩时你可以拿这两个场景强调「特征工程的可迁移性」——同一套相关性分析加 Boruta 的筛选流程不更换核心代码就能适配两种差异很大的加密流量。2.2 文件命名里的两层特征筛选逻辑注意看项目根目录下的 CSV 文件命名它本身就在暗示完整的特征处理流程doh_corr_features.csv 和 ctu13_corr_features.csv经过相关性分析筛选后的特征集doh_boruta_features.csv 和 ctu13_boruta_features.csv再经过 Boruta 特征选择后的特征集doh_boruta_model_result.csv 和 ctu13_boruta_model_result.csvBoruta 特征集输入模型后的结果记录这个顺序很关键。相关性分析解决的是「特征之间高度共线」的问题比如平均包长和总字节数往往强相关两个都进模型会放大冗余Boruta 解决的是「特征与标签是否真的有关」的问题它用随机森林的影子特征做假设检验剔除掉那些纯粹靠噪声混进来的变量。两个步骤一前一后是这套项目特征工程的主线。2.3 先读数据再谈建模一上来就干模型的多半要返工我拿到这类项目的第一件事不是急着看模型代码而是先把 CSV 读进来看规模、看标签分布、看字段类型。因为你得先确认这两个数据集是平衡样本还是极度偏斜特征列是连续型为主还是有大量类别型模型结果 CSV 里记录的评价指标是准确率还是 F1import pandas as pd # 以 DoH 的 Boruta 特征集为例 doh pd.read_csv(data/doh_boruta_features.csv) print(样本量和特征数:, doh.shape) print(\n标签分布:) print(doh[label].value_counts(normalizeTrue)) print(\n特征类型分布:) print(doh.dtypes.value_counts()) # 快速看一下特征列名确认是流特征还是载荷特征 feature_cols [c for c in doh.columns if c ! label] print(\n前20个特征列:) print(feature_cols[:20])正常读完之后你会看到两类信息。一是标签分布如果恶意样本占比只有 5%那准确率就是废指标必须看 AUC 或 F1这也解释了为什么模型结果 CSV 里不能只记录准确率。二是特征构成如果字段名里有duration、avg_packet_size、tcp_flag_count这类说明走的是流统计路线如果有dns_query_len、tls_sni_len、response_time这类说明走的是隧道行为路线。这决定了后面相关性分析时用 Pearson 还是 Spearman。提示数据目录下如果同时存在doh_corr_features.csv和doh_boruta_features.csv建议分别读一次并打印列名肉眼对比一下 Boruta 到底砍掉了哪些特征。这个对比结论写进论文里是很好的特征选择效果证据。3. Boruta 特征选择实战从影子特征到轻量化特征集3.1 Boruta 的核心思想拿随机森林做裁判先讲清楚 Boruta 的原理不然你没法跟答辩老师解释。Boruta 的做法是对于每一个真实特征构造一个对应的「影子特征」即把该特征列随机打乱破坏它与标签的相关性。然后训练随机森林计算每个真实特征和影子特征的重要性得分拿真实特征的最大得分去跟影子特征的最大得分比。如果某个真实特征的重要性显著高于影子特征的最好水平说明它携带了标签信息标记为「确认」hit如果显著低于说明它可能只是噪声标记为「拒绝」。这个流程会迭代很多轮每一轮重新打乱、重新训练、重新比较最终保留下来的就是真正对分类有贡献的特征。这个方法的裁判是随机森林所以它不依赖线性假设也不要求特征标准化——加密流量特征量纲差异很大包长是字节、时长是毫秒、计数是整数如果先做标准化再选特征反而可能丢失树模型对原始分布的敏感性。这是我个人用 Boruta 而不只用卡方检验或互信息法的核心理由非线性特征交互下相关性过滤会误杀很多有效特征。3.2 跑一轮 Boruta参数从哪里调起先把环境装好这个项目既然用了 Boruta 特征集代码里大概率已经依赖了 Boruta 库pip install boruta然后按下面的方式跑一次注意代码里的参数并不是默认值每一行都有调整空间from sklearn.ensemble import RandomForestClassifier from boruta import BorutaPy import pandas as pd df pd.read_csv(data/ctu13_boruta_features.csv) X df.drop(columns[label]).values y df[label].values # 随机森林作为 Boruta 的基础评估器 rf RandomForestClassifier( n_estimators500, # 决策树数量加密流量特征通常几百维500 起步 max_depth10, # 限制深度防止单棵树过拟合到某个流的极端模式 n_jobs-1, random_state42 ) boruta BorutaPy( estimatorrf, n_estimatorsauto, # 继承 rf 里的 n_estimators max_iter100, # 最大迭代轮次100 轮足够让 hit 比例收敛 random_state42, verbose2 ) boruta.fit(X, y) # 特征筛选结果 selected [df.columns[i] for i in range(len(df.columns) - 1) if boruta.support_[i]] tentative [df.columns[i] for i in range(len(df.columns) - 1) if boruta.support_weak_[i]] print(确认保留的特征:, selected) print(待定特征:, tentative)max_iter是迭代轮数上限不是树的数量。Boruta 每一轮都会重新计算 hit 次数默认跑到 100 轮绝大多数特征会在这之前就收敛到确认或拒绝。support_表示严格确认的特征support_weak_表示 Boruta 认为「有可能有用但证据不足」的 tentative 特征。实务里我会把 tentative 特征也放进后续模型评估因为它们往往在真实测试集上有意外表现。跑完后你会看到 verbose 输出的 hit 比例变化曲线——这是答辩时可以拿出来讲的东西特征从最初的杂乱分布随着迭代轮次增加确认特征不断累积最终趋于稳定。这个趋势图比任何文字都说明白「为什么用 Boruta」。3.3 特征轻量化从两百维到几十维性能不能掉Boruta 选完特征下一步是轻量化验证。作者在项目里把boruta_features.csv作为模型输入但作为工程师我应该确认一件事Boruta 选出来的特征集跟原始全量特征相比在模型表现上到底有没有掉点如果掉了那是 Boruta 参数没调好如果没掉说明这套轻量化是成功的论文里「在特征维度降低 60% 的情况下保持了同等分类性能」这种话才有根据。常见的做法是把三套特征集——全量、相关性筛选后、Boruta 筛选后——分别输入同一个模型记录指标变化from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score, StratifiedKFold import pandas as pd # 两个特征集做对比 corr_df pd.read_csv(data/ctu13_corr_features.csv) boruta_df pd.read_csv(data/ctu13_boruta_features.csv) X_corr corr_df.drop(columns[label]) X_boruta boruta_df.drop(columns[label]) y corr_df[label] rf RandomForestClassifier(n_estimators300, max_depth12, n_jobs-1, random_state42) cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) corr_score cross_val_score(rf, X_corr, y, cvcv, scoringf1_macro) boruta_score cross_val_score(rf, X_boruta, y, cvcv, scoringf1_macro) print(f相关性筛选特征集: 特征数 {X_corr.shape[1]}, F1 {corr_score.mean():.4f}) print(fBoruta 特征集: 特征数 {X_boruta.shape[1]}, F1 {boruta_score.mean():.4f})这里我用f1_macro做评分是因为加密流量数据集里恶意样本占比通常低于正常流量F1 比准确率更能反映真实分类能力。StratifiedKFold保证每一折里正负样本比例与全量一致防止某折样本全来自单一攻击场景导致评估虚高。项目里的doh_boruta_model_result.csv和ctu13_boruta_model_result.csv就是记录这类对比结果的。实际跑完你会发现 Boruta 特征集在维度大幅下降的同时F1 往往和全量特征持平甚至略有上升——这正是论文最需要的结论。提示Boruta 跑起来有一定玄学成分max_depth10和max_depthNone的结果可能差很多。建议先跑一版max_depthNone看确认特征数再跑一版max_depth10对比选特征数适中、模型表现稳定的一组参数写进论文。4. 从 pcap 到特征表上传解析与永久缓存的运行逻辑4.1 文件流转链路uploads、logs、data 三者的分工这个项目的 Web 交互逻辑是典型的「上传-解析-持久化-建模」四层结构。页面上传 pcap 包后文件先落在uploads文件夹下随后解析程序把它转成特征 CSV写入logs文件夹但因为作者直接把解析结果预生成在data文件夹下所以实际读数是走data路径。这里有个容易混乱的点uploads、logs、data三个目录的定位是不同的。uploads是原始 pcap 的暂存区logs是每次解析的中间产物data是整理好的最终特征集。项目为了演示稳定性把data目录里的特征文件当作唯一数据源所以你在读 CSV 时路径要统一指向data/下的文件而不是logs/下的解析输出。4.2 永久缓存机制省时间的双刃剑项目里设置了缓存且缓存时限是永久。这意味着第一次运行某个数据集的分析流程后后续再访问同一个页面会直接读取缓存的中间结果不再重新做相关性矩阵计算和 Boruta 迭代。演示时这是加分项——答辩现场网络不稳定重算时间长容易翻车缓存保证页面秒开。但它也是双刃剑。如果你改了特征工程代码重新运行却看不到任何变化不用怀疑代码写错了先去查缓存。常见做法是代码里会按数据文件路径做缓存 key只要路径不变缓存永远不会失效。你需要改的是缓存时限或者手动清缓存。4.3 改缓存时限与手动清理三分钟学会永久缓存的可维护性问题很简单找到缓存初始化或读取的代码把有效期从永久改成按小时或按天计算。如果你不想改代码更粗暴但有效的办法是直接删缓存目录。我一般是这样处理的# 先找到缓存目录常见命名是 cache/ 或 tmp_cache/ find . -type d \( -name *cache* -o -name *tmp* \) -maxdepth 3 # 确认路径后删除缓存以常见项目结构为例 rm -rf app/cache/*如果你不想每次手动删可以在项目的入口文件里把缓存时限改成动态的。实际项目中常见的配置写法是这样# 项目入口或配置文件中把永久缓存改成按需刷新 import time CACHE_TTL 60 * 60 * 6 # 6 小时过期单位秒 def get_cache(cache_key): cache_path fcache/{cache_key}.json if not os.path.exists(cache_path): return None # 超出 TTL 就认为缓存失效重新计算 if time.time() - os.path.getmtime(cache_path) CACHE_TTL: return None return json.load(open(cache_path))把CACHE_TTL设成 6 小时既能保证答辩演示时快速加载又不会在你调试特征工程时被陈旧结果坑半天。注意getmtime是文件最后修改时间也就是说用这种方式每次重新生成缓存文件后计时自动重置逻辑比固定时间戳更可靠。# 如果不想手工删缓存文件可以在入口再加一段启动时清理超过 TTL 的缓存 import os, time def clean_expired_cache(cache_dircache): now time.time() for fname in os.listdir(cache_dir): fpath os.path.join(cache_dir, fname) if os.path.isfile(fpath) and now - os.path.getmtime(fpath) 3600: os.remove(fpath)这段代码可以挂到 Web 应用启动函数里每次程序启动自动清理过期缓存。清理逻辑用文件修改时间判断比在缓存内容里存时间戳要容易落地。一句话永久缓存在交付演示时是优点在开发调试时是坑改 TTL 或删目录二选一即可别懒得动。5. 避坑笔记缓存失效、路径错位与特征泄漏排查5.1 缓存导致结果不更新代码改了等于没改现象修改了特征选择的参数重新运行 Web 页面展示的相关性热力图和特征重要性和修改前一模一样。原因项目的永久缓存按数据文件路径做 key只要data/下的 CSV 文件名没变它永远命中缓存根本不执行新的计算逻辑。解决优先改CACHE_TTL为按小时计算让调试阶段缓存自动过期。如果不想改代码直接rm -rf app/cache/*再重启服务。注意如果缓存目录不在应用根目录下用find . -type d -name *cache*先定位别把所有叫 cache 的目录都删了以免误伤模型持久化文件。5.2 路径错位读 logs 还是读 data对不上就报错现象页面选择 pcap 文件后特征展示区域空白或者报FileNotFoundError提示找不到形如logs/xxx.csv的文件。原因项目说明里写得很清楚——上传解析的结果在logs/下但作者为了演示直接把最终特征文件放进了data/。如果你把解析模块的输出路径硬编码成logs/而解析模块又被改成直接读data/路径就不匹配。这是这套项目最容易踩的坑我第一次跑就折在这里。解决先确认你启动项目时用的数据读取路径在哪。如果是按 README 跑直接读data/下的 CSV如果是自己改过解析流程保持「解析写logs/、建模读logs/」一致不要让两条读取逻辑混用。调试时打印一行os.path.abspath(data/doh_boruta_features.csv)确认当前工作目录往往比对着代码猜更快。5.3 Boruta 跑出全保留影子特征的节奏乱了现象Boruta 迭代结束后几乎所有特征都被标记为确认support_全为 True特征维度没降轻量化失败。原因两个常见诱因。一是max_iter太小Boruta 还没来得及拒绝弱特征就提前结束弱特征因为早期波动被误判为确认。二是随机森林的max_depth设太深模型过度拟合训练集中个别流量的极端模式部分噪声特征的重要性被抬得过高影子特征无法超越它。解决把max_iter从默认值提高到 100 以上观察 verbose 输出的 hit 比例是否在后期趋于平缓。如果 100 轮后还有大量特征处于待定状态把max_depth从None改为 10 左右再看一轮。记住规律Boruta 保留特征太多不是好事那说明基础评估器过拟合了而不是特征真的都有用。5.4 特征泄漏解析时用了事后统计量成绩虚高现象CTU-13 的模型结果 CSV 里 F1 很高但你把同一套特征拿到自己抓的 pcap 上测试性能明显下滑。原因典型的特征泄漏——解析 pcap 时用了全流的统计量。比如计算某个流的「平均包长」用了整个流生命周期内的所有包。这在一个完整 pcap 里没问题但如果是实时检测场景你只能看到流的前几个包全流平均值在检测时刻根本不可能得到。模型在「全知视角」的特征上学到了规律在「实时视角」的特征上就失效。解决按「前 N 个包窗口」重新计算流特征比如前 10 个包的平均大小、前 5 个包的到达间隔标准差。做完这步你会发现 F1 下降一截但这才是一个在真实环境能站得住的检测模型。论文里如果只报全流特征的指标答辩老师一问「你的特征在检测时刻能拿到吗」就会暴露。5.5 模型结果 CSV 与复现结果对不上现象你自己跑了一遍模型得到的 F1 或 AUC 和项目里model_result.csv里记录的对不上误差超过合理范围。原因最常见的是随机种子没固定或者数据划分方式不一致。你的train_test_split用了默认随机种子项目里用的是另一种。Boruta 本身有随机性如果特征集每次都完全重选模型指标有波动是正常的。解决先在代码里全局固定随机种子import random import numpy as np random.seed(42) np.random.seed(42)再检查交叉验证的折数是否一致。如果项目记录的是 5 折均值你用 3 折对不上是必然的。还有一个容易忽略的点确认label的编码是否一致——有的项目把恶意标记为 1有的标记为 0F1 的宏平均结果会受类别顺序影响。提示如果以上都查了还是对不上直接看model_result.csv里有没有记录参数信息。作者如果留下了n_estimators、max_depth、cv_folds这些字段按它的参数重跑一般能对齐。6. 答辩前的一遍完整验证把演示顺序走成说服链这个项目最高价值的地方是它把「数据展示 → 相关性分析 → Boruta 特征选择 → 特征轻量化 → 模型评估」编排成了一个顺序固定的 Web 演示流程。这个顺序本身就是一条说服链先让答辩老师看到原始数据长什么样再从相关系数矩阵说明为什么要筛特征接着用 Boruta 的迭代过程证明「我不是拍脑袋选特征」最后用轻量化后的模型结果收尾。现场演示的时候建议卡在两步上一是相关性热力图中颜色最深的几对特征直接指出它们的具体含义比如包长和字节数的相关系数接近 0.9说明这类强共线特征会拖慢模型训练且不影响精度二是 Boruta 特征选择界面如果 Web 页面能显示确认特征比例曲线就把这个图点开用 it 的趋势来回答「为什么信任这个筛选结果」。这两步能撑起至少五分钟的深度讲解比干巴巴报告准确率更有说服力。我建议在答辩前跑一遍下面的验证脚本确认整个链路的技术细节自己心里有数import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score, StratifiedKFold # 用 Boruta 后的特征集做一次最终验证 df pd.read_csv(data/doh_boruta_features.csv) X df.drop(columns[label]) y df[label] rf RandomForestClassifier(n_estimators500, max_depth12, n_jobs-1, random_state42) cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(rf, X, y, cvcv, scoringf1_macro) print(各折 F1:, [round(s, 4) for s in scores]) print(平均 F1:, round(scores.mean(), 4))跑完后把平均结果与model_result.csv对比误差在 1 个百分点内就说明你完全掌握这套项目了。如果差太多回到第五章的检查清单优先排查随机种子和数据划分方式。我曾经带过一个学员答辩前夜发现自己的复现结果和项目 CSV 差了五个百分点慌到凌晨两点。后来发现他把data/下的 corr 特征集当成 boruta 特征集喂进了模型特征列数差了将近一半等于拿旧数据跑新模型。从那以后我每次碰这套项目都强制走一遍「确认文件名 → 检查特征列数 → 固定随机种子 → 对齐评估指标」的流程再没在演示时翻过车。这份项目本身代码量不大但每一步都值得你亲手敲一遍希望帮到你。本文还有配套的精品资源点击获取