☰
加密恶意流量检测:基于机器学习的全流程项目实战
2026/9/26 19:14:22 网站建设 项目流程

简介:面向毕业设计与课程实践场景的机器学习加密恶意流量分析与检测项目,提供完整可运行的Python源码和配套文档说明。项目以CTU-13恶意流量和DoH加密DNS流量为数据基础,覆盖流量特征提取与相关性分析、Boruta特征筛选、多模型训练对比、结果评估与可视化展示等关键环节。代码内部包含较详细注释,文档对实验思路、数据处理过程和结果表现作了说明,适合具备基础Python能力的学生快速搭建环境并复现实验。压缩包共217个文件,主要包含log日志、html可视化结果页、csv特征与模型结果、py源码、pcap原始数据包、npy特征文件以及图片素材,整体大小25.6MB,目录层次分明,便于按模块学习和二次开发。已有187人学习下载,可作为毕业设计、期末大作业或课程设计的高分参考,直接用于方案复现、报告撰写与功能拓展。

1. 加密流量检测是一个被低估的机器学习落地点

加密流量在今天的网络上早已不是少数派,HTTPS、TLS 1.3、DoH 一普及,传统靠端口、特征字符串做检测的规则引擎基本失效,恶意流量开始大摇大摆地藏在加密信道里流动。这个基于机器学习的加密恶意流量分析与检测项目,把 CTU-13 僵尸网络流量和 DoH 流量两组标准公开数据拉进来,走完了从会话特征提取、相关性过滤、Boruta 特征选择到多模型训练对比、交互式可视化的全流程。它适合两类人:一类是正在做 Python 毕设或课程设计,需要一份能部署、能答辩、代码带注释的完整项目;另一类是刚接触流量安全的同学,想用最短路径看清机器学习模型在安全场景里的完整用法。跟着复现一遍,你基本就摸清了这类任务的套路。

2. 两条数据集线:CTU-13 与 DoH 的任务定义和标签体系

做加密恶意流量检测,第一步不是调模型,而是搞清楚数据集里的标签到底代表什么。这个项目没有只押在一份数据上,而是拉了两条线:CTU-13 和 DoH,分别对应两类形态完全不同的加密恶意流量场景。这个选择在答辩时也很讨巧——你能讲清楚"为什么需要两种数据来验证方法",而不是只在一个数据集上自说自话。

2.1 为什么必须同时拿 CTU-13 和 DoH 两个场景

CTU-13 是捷克理工大学公开的僵尸网络流量基准集,里面包含了 13 个不同的僵尸网络捕获场景,流量以会话流(flow)的形式组织,标签维度比较多。它的价值在于:僵尸网络流量虽然也走加密信道,但恶意行为特征(心跳、DGA 域名查询、定向外连)会显著改变流的统计形态,比如连接持续时间变长、分组到达时间间隔呈现固定节律,这些特征恰好是机器学习模型最容易捕捉的。

DoH 则是另一回事。DNS over HTTPS 把 DNS 查询整个塞进 TLS 加密流里,从报文载荷层面几乎无特征可用,只能靠流量行为、分组大小分布、时间间隔统计来鉴别。它比 CTU-13 更接近"纯加密"场景,检测难度更大。两组数据合在一起,项目就覆盖了从"加密与传统流量混杂"到"全加密服务"的谱系。你的模型如果两边的结果都说得过去,说明它不只是在某个数据集上过拟合。

在文件组织上,doh_corr_features.csv、doh_boruta_features.csv 对应 DoH 线,ctu13_corr_features.csv、ctu13_boruta_features.csv 对应 CTU-13 线。corr 后缀文件是相关性过滤后的特征矩阵,boruta 后缀文件是 Boruta 特征选择后的特征矩阵,后面的模型结果 csv 也是按这两条线分别输出的。

2.2 数据怎么读:先看形状、列名和标签分布

拿到 csv 后的第一件事不是急着训练,而是用 pandas 把文件读进来,看清行数、列数和标签分布。我一般会这样起手:

import pandas as pd # 先读 Boruta 筛选后的 CTU-13 特征文件 ctu = pd.read_csv("ctu13_boruta_features.csv") print("shape:", ctu.shape) print("columns:", ctu.columns.tolist()) # 自动找到标签列,看类别分布 label_col = [col for col in ctu.columns if "label" in col.lower() or "class" in col.lower()] print("label column:", label_col) print(ctu[label_col[0]].value_counts())

这段代码的逻辑很直接:先打印矩阵规模和列名,确认特征数量和标签列位置;再用 value_counts 统计类别分布,这一步能帮你判断数据是否平衡。需要注意,流特征文件通常是以 NetFlow 格式为底子生成的,行数可能上万,标签列名在不同数据集里不一样,所以用模糊匹配去定位标签列是更稳妥的习惯。

DoH 线文件的读取方式完全一样,两个文件的区别主要在特征集合上。CTU-13 的特征偏传统流统计——持续时长、收发分组、字节数、标志位分布;DoH 的特征则更关注 TLS 握手信息、分组长度分布和 DNS over HTTPS 流的时间结构。

2.3 标签体系里的“坑”要先拆干净

CTU-13 的标签不只有 benign 和 malicious,常见的还有 normal、botnet、background、flashcrowd 这几类。这里最容易翻车的地方是:background 代表的是背景噪声流量,它既不是恶意行为,也不是用户主动产生的正常业务,而是网络里的扫描、广播、探测等杂散流量;flashcrowd 是闪拥流量,也不属于攻击。如果一股脑把 background 当恶意样本塞进去,模型学到的就是"非正常流量即恶意"的错误边界。

我处理这类标签时通常会做归并:把 botnet 视为恶意样本,把 normal 和 flashcrowd 归为正常样本,background 选择丢弃或单独建模。如果你做的是二分类模型,推荐直接丢弃 background,因为它的语义太模糊,混进任何一侧都会污染决策边界。DoH 数据集则干净得多,通常就是 benign 和 malicious 二分类,直接可用。

下表是我在复现时梳理的对比,你可以拿来做答辩材料:

对比维度CTU-13DoH
场景类型僵尸网络混合流量(含加密)DNS over HTTPS 纯加密流量
标签体系normal / botnet / background / flashcrowdbenign / malicious
检测目标识别被控主机的命令与控制行为识别隐藏在 DoH 信道里的恶意查询
特征侧重点流统计、连接节奏、上下行比例TLS 元数据、分组长度分布、时间间隔

这份表格说来简单,但把两类数据的边界讲清楚,比直接贴训练代码更能体现你对项目的理解。

3. 特征工程三步走:从相关性过滤到 Boruta 特征筛选

流量数据的维度通常不会低,一次捕获能提取出几十个流统计特征,但里面真正有效的往往只有三分之一。这个项目的特征工程部分走了三步:先做相关性过滤把冗余列砍掉,再用 Boruta 算法对剩余特征做重要性筛选,最后对选中的特征做业务含义解释。三步下来,模型输入维度大幅下降,训练速度和可解释性同时提升,这也是它能拿高分的关键设计。

3.1 相关性过滤:先杀掉一批明显冗余的特征

加密流量特征之间存在很强的相关性,比如"总字节数"和"平均分组长度"往往高度相关,"上行分组数"和"下行分组数"也和流方向占比耦合。直接把所有特征喂给模型,树模型还好,线性模型会出现系数不稳定,更重要的是多个相关特征会稀释特征重要性判断。常见的做法是先计算相关性矩阵,把相关系数超过阈值的特征对中挑一个删掉。

import pandas as pd import numpy as np df = pd.read_csv("doh_corr_features.csv") # 假设标签列在最后一列 X = df.drop(columns=[df.columns[-1]]) # 计算特征矩阵的相关性绝对值矩阵 corr = X.corr().abs() # 取上三角,避免重复比对 upper = corr.where(np.triu(np.ones(corr.shape), k=1).astype(bool)) drop_set = set() for col in upper.columns: high_corr_cols = [c for c in upper.index if upper.loc[c, col] > 0.95] drop_set.update(high_corr_cols) X_reduced = X.drop(columns=drop_set) print("features before:", X.shape[1], "after:", X_reduced.shape[1]) print("dropped:", sorted(drop_set))

逻辑说明:np.triu 取相关矩阵的上三角,只保留每个特征对一次比对;遍历每一列,找出与该列相关系数超过 0.95 的其他特征,统一放入待删集合。参数上,阈值选 0.9 到 0.95 比较常见,0.95 更保守,适合特征总量不太多、不想误删有效信息的场景。如果你后续还要跑 Boruta,相关性过滤这步前置处理能帮 Boruta 省下大量迭代时间,这是我在实操里最推荐的一条流水线。

3.2 Boruta 特征选择:为什么比树的 feature_importance 更可靠

很多同学会直接拿随机森林的 feature_importance 排序来筛特征,但这条路有一个隐藏问题:树模型对高度相关的特征组会分散重要性,导致真正重要的特征排名被拉低。Boruta 的做法更稳:它在原始特征旁边生成一批影子特征,把原始特征的值随机打乱,然后让随机森林在所有特征(原始+影子)上训练,比较原始特征的重要性是否显著高于影子特征中的最优者;迭代多轮后,只有那些反复胜过影子特征的原始特征才会被保留。

from boruta import BorutaPy from sklearn.ensemble import RandomForestClassifier X = X_reduced.fillna(0).values y = df[df.columns[-1]].values # Boruta 内部每轮都要重训森林,n_estimators 太大会非常慢 rf = RandomForestClassifier( n_estimators=200, max_depth=8, random_state=42, n_jobs=-1 ) boruta = BorutaPy( estimator=rf, n_estimators="auto", max_iter=50, random_state=42, perc=100 ) boruta.fit(X, y) kept = X_reduced.columns[boruta.support_].tolist() print("Boruta kept:", kept)

参数说明:n_estimators 使用 "auto" 会依据特征数量自动确定内部迭代的树数量;max_iter=50 限制最大迭代轮数,避免长时间空转;perc=100 表示以影子特征的最大值作为比较基线,这是最严格的标准,如果你想放宽一点,可以调到 95 或 90。注意 BorutaPy 的 fit 方法接收的是 numpy 数组,所以我用 .values 传入,同时在全过程里保留原 DataFrame 的列名,方便最终调用 boruta.support_ 来映射哪些列被留下。还有一个实际坑:特征矩阵里不能有 NaN,我在 fit 前统一 fillna(0),遇到缺失严重的特征建议直接删列。

3.3 被保留下来的特征在业务上意味着什么

以 DoH 数据集为例,Boruta 一轮跑下来,通常留下的特征集中在分组长度均值、到达时间间隔方差、上下行分组数比这几类。为什么会是它们?因为恶意 DoH 流量多由自动化脚本产生,分组节奏均匀、长度分布集中,而真实用户的 DNS over HTTPS 查询有很强的随机性和间歇性。CTU-13 场景里,被保留下来的往往还有流持续时间、TCP 标志位分布、每流字节数方差——这些特征能捕捉到僵尸网络心跳连接与正常业务流的差异。

用表格归纳一下特征类型和业务含义:

特征类型典型特征恶意流量中的表现
时间结构到达时间间隔均值/方差心跳流量呈周期性,方差明显偏小
分组形态分组长度均值/标准差隧道流量长度分布集中,标准差异常
方向特征上下行分组数比、方向占比DGA 查询重上行,数据窃取重下行
连接节奏流持续时间、握手时间失陷主机维持长连接概率更高

这些解释是答辩时的加分项,也是你在写文档说明时能写出"个人手打"味道的关键——模型选中的特征不是黑匣子里的随机结果,而是能和攻击行为一一对应起来的。

4. 模型对比与结果解读:别只盯着 accuracy 一个数

特征工程做完,模型训练本身反而是流水线里最顺手的一环。项目里 doh_boruta_model_result.csv 和 ctu13_boruta_model_result.csv 两个结果文件存的就是不同模型在 Boruta 特征子集上的表现对比。这一章我把训练评估的完整套路拆开讲,包括分类器怎么选、评估代码怎么写、结果文件怎么解读。

4.1 分类器选型:为什么树模型是主力,线性模型做对照

流量特征普遍存在两个特点:一是特征尺度差异极大,字节数可能是千级,时间间隔是毫秒级;二是特征之间存在非线性交互,比如"小分组 + 短间隔"组合才是恶意行为的信号,单独看哪个都不明显。线性模型对尺度敏感,需要做标准化,而且很难自动捕捉交互效应;随机森林和 LightGBM 这类树模型天然适应这种场景。因此主模型推荐随机森林,再用逻辑回归做一个基准对照,这已是流量安全论文里最常见的组合。

随机森林作为主模型还有一个实际优势:它对高维稀疏特征不敏感,调参压力小,默认参数下也能出不错的分数,适合毕设这种时间有限的项目。LightGBM 更强,但调参成本高,如果你不熟悉的细节建议不要作为主模型,当作补充对比即可。

4.2 训练评估的标准流程

以 DoH 的 Boruta 特征文件为例,完整的训练评估代码长这样:

from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, roc_auc_score from sklearn.preprocessing import LabelEncoder import pandas as pd df = pd.read_csv("doh_boruta_features.csv") X = df.drop(columns=[df.columns[-1]]) y = df[df.columns[-1]] # 标签转 0/1,树模型需要数值型目标 le = LabelEncoder() y = le.fit_transform(y) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42 ) model = RandomForestClassifier( n_estimators=300, max_depth=10, min_samples_leaf=2, class_weight="balanced", random_state=42, n_jobs=-1 ) model.fit(X_train, y_train) y_pred = model.predict(X_test) y_prob = model.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred, digits=3)) print("AUC:", round(roc_auc_score(y_test, y_prob), 4))

逻辑说明:train_test_split 里 stratify=y 保证训练集和测试集的类别比例与原始分布一致,这在恶意流量占比偏小的数据集里是必须的,否则随机切分很容易让测试集里几乎没有恶意样本。model 里 class_weight="balanced" 让模型对少数类做权重补偿,解决样本不平衡问题。输出部分,classification_report 给出 precision、recall、F1,再加一个 AUC 覆盖概率排序质量。

这些指标会被汇总写入 doh_boruta_model_result.csv,文件在项目里就是一份多模型对比表。你可以把逻辑回归、随机森林、LightGBM 的准确率、精确率、召回率、F1、AUC 各列排开,行是模型名。注意 precision 高而 recall 低时,说明模型"宁缺毋滥",只抓最有把握的恶意样本;反之 recall 高 precision 低则模型偏激进,会把大量正常流量标成恶意。流量安全场景里,误报会导致安全运营人员疲于奔命,漏报则可能直接造成失陷失窃,所以需要结合成本权衡。

4.3 结果文件怎么比才公平

我的习惯是先把两个结果 csv 读成 DataFrame,然后以 F1 和 AUC 两个指标为准做排序,而不是看 accuracy。

import pandas as pd res = pd.read_csv("doh_boruta_model_result.csv") print(res.columns.tolist()) # 以 F1 排序,兼顾 AUC res_sorted = res.sort_values(["F1", "AUC"], ascending=False) print(res_sorted[["model", "accuracy", "precision", "recall", "F1", "AUC"]])

在 CTU-13 上,因为数据量更大、特征更丰富,随机森林和 LightGBM 的 F1 通常会比 DoH 场景高——这不是模型差异,而是两个场景的难度本身不同。读结果时先把两个 csv 分开看,再用同一个模型在两个场景里的表现做横向对比,比直接跨文件比数字更科学。

5. 常见问题排查:训练时最容易翻车的五个地方

流量检测项目跑起来会遇到一堆幺蛾子,很多问题不是模型本身导致的,而是数据处理和库版本层面的。这一章把我踩过的坑按"现象 → 原因 → 解决"写清楚,你照着排查能省下大半天。

5.1 一读数据集就内存溢出

现象:pd.read_csv 跑 ctu13_corr_features.csv 时进程直接被杀,或者内存占用瞬间涨到十几个 G。

原因:CTU-13 完整数据集如果合并成单张宽表,行数可能达到几十万甚至更多,默认 read_csv 会把所有列读成 int64/float64,内存开销翻倍。

解决:读取时只加载需要的列,并指定更紧凑的 dtype。

cols = ["duration", "bytes_in", "bytes_out", "label"] df = pd.read_csv("ctu13_corr_features.csv", usecols=cols, dtype={ "duration": "float32", "bytes_in": "float32", "bytes_out": "float32" })

float32 把精度从 64 位降到 32 位,对流量统计特征完全够用,内存直接减半。如果你的机器还是吃力,把 nrows 参数加上先读 10000 行确认流程能跑通,再放开全量。

5.2 Boruta 跑了好几个小时不出结果

现象:BorutaPy 的 fit 一跑就是几个小时,进度条看起来永远走不完。

原因:Boruta 内部要反复训练随机森林,每一轮迭代都会在全量特征上训练一次;如果特征有几十个、n_estimators 设了 1000、max_iter 又没限制,训练轮次就是天文数字。另外前置没做相关性过滤时,影子特征数量翻倍,更加剧了开销。

解决:先用相关性过滤把特征压到 30 个以内,再跑 Boruta;n_estimators 从 200 起步即可,不要一上来就追求精度。验证时可以设 max_iter=10 快速跑一遍,看代码没有报错后再加长迭代。

boruta = BorutaPy( estimator=rf, n_estimators=200, max_iter=20, # 先用小轮数试跑 random_state=42 )

5.3 accuracy 很高,但恶意样本一个都没抓到

现象:classification_report 里 accuracy 有 0.95,但恶意类别的 recall 是 0.0,precision 也是 0.0。

原因:数据集里恶意样本占比过低,比如只有 5%,模型只要全部预测为 normal 就能拿到 95% 的 accuracy,但它实际上什么都没学。这种情况在 CTU-13 里尤其常见,因为 background 流量占了一大块。

解决:先用 value_counts 看类别分布,再决定处理方式。如果恶意样本占比低于 10%,优先考虑 class_weight="balanced",其次做下采样让正负样本比例到 1:1 到 1:3 之间。评估时只看 F1 和 AUC,把 accuracy 直接忽略掉,否则会被它误导。

5.4 show_data_doh.html 打开是空白页面

现象:浏览器打开 show_data_doh.html 或 show_data_ctu-13.html,页面框架在但图表区域全是空白。

原因:这类 HTML 通常是 pyecharts 生成的,图表渲染依赖外部的 echarts.min.js 脚本。如果生成的 HTML 引用的是 CDN 地址,而你电脑在离线环境或 CDN 访问失败,图表就加载不出来。

解决:在生成 HTML 的环境里检查脚本引用方式。pyecharts 的 render 接口默认外链 CDN,离线场景可以下载 echarts.min.js 到本地目录,修改 HTML 的 script src 指向本地文件;如果只是自己看结果,直接在 Jupyter Notebook 里使用 render_notebook 输出图表也能正常显示。

5.5 复现结果和文档里的数值对不上

现象:自己跑出来的 F1 和下载包里的 doh_boruta_model_result.csv 对不上,差得还挺多。

原因:sklearn 版本不同会导致树模型的分裂策略细节变化,pandas 版本差异会影响填补缺失值的默认行为;另外随机种子没固定,每次运行结果都会浮动。

解决:模型里所有随机过程都固定 random_state=42,train_test_split、BorutaPy、随机森林一个都不能漏;用的时候在 requirements 里锁好关键库版本。树模型的可复现性本身就有点"玄学",跨版本跑数字微差是正常的,只要同版本、同种子下能复现就没问题。

6. 进阶验证:把训练好的模型接到新流量上做推理

模型训练完只是第一步,毕设里真正体现工程能力的是你把模型导出来、喂一条新的会话特征、得到可解释的判定结果。这一章讲怎么把训练好的模型落地到推理场景,并分享一个我常用的验证习惯。

6.1 模型导出与单条样本推理

用 joblib 把训练好的模型和标签编码器一起存成文件,以后做推理时加载。注意特征顺序必须和训练时完全一致,否则 predict_proba 的结果就是错的。

import joblib import pandas as pd # 训练完成后保存 joblib.dump({"model": model, "columns": X.columns.tolist(), "le": le}, "doh_model.pkl") # 推理时加载 pkg = joblib.load("doh_model.pkl") clf, cols, le = pkg["model"], pkg["columns"], pkg["le"] # 假设 new_flow 是从新捕获流量里提取的会话特征向量 # 必须按训练时的列顺序排列 new_flow = pd.DataFrame([new_flow_dict], columns=cols) prob_malicious = clf.predict_proba(new_flow)[0][1] label = le.inverse_transform(clf.predict(new_flow))[0] print(f"label={label}, malicious_prob={prob_malicious:.4f}")

逻辑说明:把列名列表和标签编码器一起存进 pkl,是为了在加载模型时强制校验特征顺序,避免推理时列名错位这种静默错误。生产环境里建议把概率阈值从默认的 0.5 调高到 0.7 或 0.8,因为恶意样本漏报的成本远高于误报,提高阈值可以只对高置信样本做拦截。

6.2 一个验证习惯:看概率分布,而不是只看汇总指标

我在拆这个项目时养成了一个习惯:测试集跑完后,不急着看报告,而是把真实标签为恶意的样本单独拎出来,看它们的 predict_proba 概率分布。如果大部分恶意样本的概率集中在 0.9 以上,说明模型决策果断;如果大量集中在 0.5 到 0.7 之间,说明决策边界附近有一批样本,模型对这些样本并不自信,这时需要回看特征工程是不是漏了什么。

import numpy as np # 取出恶意样本的概率分布 mal_prob = y_prob[y_test == 1] print("malicious prob quantiles:", np.percentile(mal_prob, [10, 50, 90]))

这个检查非常快,但能提前发现特征区分度不够的问题,比答辩前发现模型不靠谱要体面得多。我从那以后每次跑流量检测项目都强制走一遍这个动作,它能让我对模型边界心里有数,而不是靠 accuracy 自我感觉良好。希望这个习惯和上面的排查经验能帮到你的毕设。

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

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

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

立即咨询