简介:一套基于Python的加密恶意流量智能检测系统源码,源自人工智能与大数据安全分析竞赛项目,面向信息安全、数据科学与大数据技术、人工智能等专业的在校学生、高校教师与行业从业者。系统设计具备较强的可扩展性,既能作为初学者的进阶学习材料,也可直接用于毕业设计、课程实验、大型作业及项目初期方案展示。压缩包共29个文件,以16个Python脚本为核心,覆盖数据预处理、训练与预测等主要环节,另有gz压缩模型、CSV数据文件、说明文档及备份文件等,总大小约4.02MB。目前已有54人学习下载。资源内含完整赛题实现思路、训练与预测代码及数据处理模块,便于使用者在此基础架构上进行二次开发与功能扩展,适合深入理解加密恶意流量检测技术。
1. 加密恶意流量智能检测:这套 Python 源码把训练和预测两条流水线都跑通了
网络流量全面加密之后,传统 DPI 深度包检测基本失效,恶意软件开始把 C2 通信、数据回传藏进正常的 HTTPS 加密隧道里。基于 Python 的加密恶意流量智能检测系统,就是在这个背景下把流量统计特征、TLS 握手元数据和机器学习模型结合在一起的解决方案。这套源码源自 AI 大数据安全分析竞赛,完整包含 data_preprocessing.py、main_train.py、main_predict.py、utils.py 以及多份备份工程,训练和预测两条链路都能直接跑。对做毕设、课程设计、或者准备接手一个可扩展检测框架的工程师来说,它比网上零散的博客代码更接近真实项目,也是理解加密流量检测全流程的一个不错的起点。
2. 系统架构与源码文件:先弄清楚训练、预测、预处理三条线
拿到源码包第一件事不是直接跑,而是先看文件结构。这套包里的文件命名很直白,train_code、predict_code、data_preprocessing.py、utils.py 各管一段,如果不先分清谁是谁,很容易在后续扩展时把代码改乱。我一般会先把每个文件的入口函数找出来,再顺着数据流向走一遍,这样后面调参、加特征、换模型时心里才有底。
2.1 从源码包文件看工程结构:哪份文件管训练,哪份文件管预测
以下是源码包里的核心文件清单,我按使用阶段做了分类,这样读起来更清晰。
| 文件/目录 | 作用 | 使用阶段 |
|---|---|---|
| train_code/main_train.py | 训练入口,负责加载处理后的样本、训练模型、保存权重 | 训练 |
| predict_code/main_predict.py | 预测入口,加载模型并对新流量输出恶意概率 | 推理 |
| data_preprocessing.py | 原始 pcap 解析、流切分、特征提取 | 数据处理 |
| utils.py | 公共工具函数:文件读取、标签映射、日志、指标计算 | 训练和预测共用 |
| tokens | 训练时生成的 token 映射文件,预测阶段必须保留 | 训练和预测共用 |
| addition_code | 附加代码,离线评估、特征统计、结果可视化 | 辅助 |
| finalB / processCode | 竞赛提交版处理脚本,通常是数据处理和预测的整合 | 辅助 |
| 备份文件.zip | 完成备份,防止某次改动把主流程改坏 | 辅助 |
这个结构对应两条流水线:第一条是数据预处理,把所有 pcap 或 CSV 样本统一成特征 DataFrame;第二条是模型训练与预测,训练时 main_train.py 吃特征表,输出模型文件,预测时 main_predict.py 加载模型文件,对新的流量数据做同样的特征提取后给出恶意概率。tokens 目录容易被忽略,但预测阶段如果少了它,模型加载或特征编码往往直接失败,后面避坑章节我会专门讲。
2.2 数据预处理模块:把 pcap 变成模型能吃的特征表
加密流量检测有个核心前提:不需要解密。因为解不开,所以模型只能从流的外部特征找规律。常见做法是两条腿走路,一条腿是流统计特征,包括包长分布、包方向、到达时间间隔、吞吐量、TCP 窗口大小;另一条腿是 TLS 握手元数据,包含 ClientHello 里的加密套件列表、SNI 域名、TLS 版本、证书长度等。
data_preprocessing.py 的核心工作就是把原始 pcap 切分成流,再从每条流里提取这些特征。常见实现是这样一段逻辑:
def build_features_from_pcap(pcap_file: str, label: int, min_packets=20): flows = {} for pkt in pcap_read(pcap_file): key = flow_key(pkt) # (src_ip, src_port, dst_ip, dst_port, protocol) if key not in flows: flows[key] = [] flows[key].append({ "ts": pkt.time, "length": pkt.len, "direction": 0 if pkt.src == src_ip else 1, "tls_info": parse_tls_handshake(pkt), }) rows = [] for key, packets in flows.items(): if len(packets) < min_packets: continue feats = extract_flow_features(packets) feats["label"] = label rows.append(feats) return pd.DataFrame(rows)这段代码里,flow_key根据五元组生成流的唯一 ID,pcap_read是底层解析函数,可以基于 scapy 或 dpkt 实现。min_packets=20用来过滤短连接,因为包太少的流特征不稳定,强行纳入只会给模型加噪声。extract_flow_features内部会统计包长均值、方差、首包方向、上行下行包数比等特征。需要注意的是,不要单纯按 pcap 里的顺序遍历直接生成 DataFrame,那样会把同一个五元组的不同包拆散。要先flows[key]聚合,再按流提取特征,否则后续训练测试集划分时必出数据泄漏问题。
2.3 模型选型:为什么先用随机森林当基线,再考虑深度模型
预处理产出的特征大多是以表格形式存在的数值特征,这类数据在中小规模数据集上,树模型天然占优势。竞赛和实际项目里,我一般会先用随机森林或 LightGBM 把全链路跑通,确认数据没问题、指标能达到基线,再去尝试 1D-CNN 或 Transformer。下面是几种常见选型的对比。
| 模型 | 输入 | 优点 | 主要风险 |
|---|---|---|---|
| RandomForest | 流统计特征向量 | 抗过拟合、训练快、特征重要性可解释 | 特征工程决定了效果上限 |
| LightGBM | 流统计特征向量 | 训练极快、对类别不平衡支持好 | 调参不当容易过拟合 |
| 1D-CNN | 包长序列 | 能自动学习时序局部模式 | 需要更多数据,训练时间长 |
| Transformer | 载荷字节序列 | 拟合能力强,上限高 | 算力要求高,小数据下严重过拟合 |
这套源码的 train_code 里保留了多种模型的接入方式,我建议新手先跑 RandomForest,把数据预处理到模型输出的整条链路跑通,再换 LightGBM 调参。直接上深度模型最容易翻车的地方不是网络结构,而是数据量不够、特征没有标准化,结果指标反而不如树模型。
3. 训练模块 main_train.py 实战:从特征列到可复用的模型文件
训练阶段的目标很明确:读入预处理好的特征表,划分训练集和验证集,训练模型,保存成可复用的文件。但这里有几个关键点没处理好,后续预测会连环踩坑。我把 main_train.py 里最常见的实现拆开讲,重点是训练入口、类别不平衡和指标选择。
3.1 训练入口怎么组织:配置、分层抽样和模型保存
main_train.py 的典型入口函数是这样组织的:
def main_train(): config = load_config("config.yaml") df = load_data(config["train_data"]) x_cols = [c for c in df.columns if c not in config["label_cols"] + config["id_cols"]] X, y = df[x_cols], df["label"] X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42, ) model = train_lightgbm(X_train, y_train, X_val, y_val) save_model(model, config["model_output"])逻辑说明:load_config读取配置文件,把样本特征列、标签列、ID 列分开。stratify=y保证训练集和验证集里恶意样本占比与原始数据一致,这在加密流量数据集里尤其重要。save_model一般会把模型文件和特征列顺序一起保存,因为预测阶段模型要求输入特征的顺序和训练时完全一致。
参数说明:test_size=0.2是一般默认值,如果数据量很小可以改成 0.3;random_state=42固定随机种子,方便复现。x_cols这一步要特别小心,ID 列和标签列必须排除干净,否则模型会学到“看 IP 就知道是不是恶意”的假规律。
3.2 类别不平衡:恶意样本占比过低时的三种解决顺序
加密恶意流量数据集里,恶意样本占比往往只有百分之几,有的甚至不到 1%。遇到这种数据,直接训练出来的模型会倾向于把所有流量预测成正常,因为这样整体准确率也很高。处理顺序我一般是先算类别权重,再考虑采样,最后才上 SMOTE。
from sklearn.utils.class_weight import compute_class_weight classes = np.unique(y_train) weights = compute_class_weight(class_weight="balanced", classes=classes, y=y_train) weight_dict = dict(zip(classes, weights)) model = lgb.LGBMClassifier( n_estimators=800, learning_rate=0.05, num_leaves=63, class_weight=weight_dict, )逻辑说明:compute_class_weight会根据每个类别的实际样本数自动计算权重,少数类权重高,多数类权重低。把权重传给class_weight之后,LightGBM 在计算损失时会放大少数类的误差,从而强迫模型关注恶意样本。
参数说明:n_estimators=800决定树的数量,加密流量特征通常几百维,800 棵足够;learning_rate=0.05是保守的学习率,小学习率配多树更稳;num_leaves=63控制单棵树复杂度,太大容易过拟合。如果类别不平衡特别严重,优先调class_weight,不要一上来就做 SMOTE,因为加密流量特征维度高、样本数量大,SMOTE 很容易生成噪声样本。
3.3 训练指标:加密流量检测场景下 AUC 和召回率比 accuracy 更诚实
很多初学项目的人只盯 accuracy,这在正常流量占 95% 的数据集上会得出非常误导的结论。比如模型把所有流量判为正常,accuracy 高达 95%,但恶意流量一条都没报出来。所以在验证阶段,至少要看 AUC、精确率、召回率和 F1。
from sklearn.metrics import roc_auc_score, f1_score pred_proba = model.predict_proba(X_val)[:, 1] auc = roc_auc_score(y_val, pred_proba) f1 = f1_score(y_val, pred_proba > 0.5)逻辑说明:predict_proba返回的是每个样本属于正常和恶意两个类别的概率,这里取[:, 1]得到恶意概率。roc_auc_score评估模型排序能力,f1_score综合精确率和召回率,两者结合才能看出模型在少数类上到底有没有效果。
我见过不少人在这个环节翻车:验证集 AUC 0.98,但把预测阈值调到 0.5 后召回率只有 0.1。这说明模型虽然能排序,但默认阈值不适合当前正负样本比例。后续预测时阈值需要根据实际场景重新调,不能机械地认为概率大于 0.5 就算恶意。
4. 预测模块 main_predict.py 实战:把训练产物变成可用的检测能力
训练完成后,真正的考验在预测阶段。模型文件只是产物,怎么让新流量走通和训练时一模一样的特征处理流程,才是线上能不能用的关键。这一章我把 main_predict.py 里最关键的三个点拆出来讲:特征对齐、概率阈值和单条流检测封装。
4.1 预测流程与特征对齐:训练时有哪些列,预测时一个都不能少
预测最常见的报错是ValueError: feature_names mismatch。原因是训练时特征 DataFrame 有 127 列,预测时新数据因为某一步预处理少了其中一列,或者列顺序和训练时不一样,模型直接罢工。避免这个问题的标准做法是把训练用的特征列顺序保存下来,预测时用reindex强制对齐。
def load_trained_artifacts(model_path, feature_path): model = load_model(model_path) feature_cols = load_json(feature_path)["feature_cols"] return model, feature_cols def predict_file(model, feature_cols, pcap_path): df_new = build_features_from_pcap(pcap_path, label=None) X_new = df_new.reindex(columns=feature_cols) proba = model.predict_proba(X_new)[:, 1] return df_new, proba逻辑说明:reindex(columns=feature_cols)会按照训练时保存的列顺序重新排列新数据,缺失的列会变成 NaN。如果build_features_from_pcap和训练时用的是同一份 data_preprocessing.py,一般不会缺列;万一缺了,这个步骤也会暴露问题。
参数说明:model_path和feature_path建议放在同一个输出目录下,部署时整个目录拷贝过去,避免漏文件。我在实际项目里会把模型文件命名成best_model_lgb.pkl,特征文件命名成feature_cols.json,这样一眼就能对应上。
4.2 概率阈值:恶意流量判定不是非黑即白,默认 0.5 往往不合适
模型输出的概率只是一个排序分数,到底在哪个阈值切分,取决于你更怕漏报还是误报。安全检测场景里,漏掉一个恶意流量可能意味着整个内网被渗透,所以我一般会把阈值适当调低,比如 0.3 或 0.2,宁可多查几个告警,也不能放走真正的恶意通信。
def decide(prob, threshold=0.3): if prob >= threshold: return "malicious" return "benign" for idx, p in enumerate(proba): result = decide(p, threshold=0.3) print(f"flow {idx}: prob={p:.3f}, result={result}")逻辑说明:decide函数把概率和阈值比较,得到最终判定。阈值调低会提高召回率,但精确率可能下降,告警变多;阈值调高则相反。具体数值没有标准答案,要根据验证集上误报率和漏报率的可接受范围来定。
在离线评估时,可以把不同阈值下的结果整理成一张表,方便业务方决策:
| 阈值 | 召回率 | 精确率 | 每天预计告警量 |
|---|---|---|---|
| 0.2 | 0.95 | 0.22 | 高 |
| 0.3 | 0.88 | 0.41 | 中 |
| 0.5 | 0.63 | 0.72 | 低 |
这张表的意义是让决策者直观看到“把阈值调低会换来多少检出率、代价是什么”,而不是拍脑袋定 0.5。
4.3 封装成单条流检测函数:从批量预测到实时接口
竞赛代码只需要对一批文件预测,但真实业务要给每个新流量实时判断。我会把预测逻辑封装成单条流函数,这样接到新的网络数据源时,直接按流对象调用即可。
def predict_single_flow(flow_json: dict) -> dict: flow_df = pd.DataFrame([flow_json]) flow_df = flow_df.reindex(columns=feature_cols) prob = model.predict_proba(flow_df)[0, 1] return { "malicious_prob": round(prob, 4), "level": "high" if prob >= high_threshold else "low", }逻辑说明:flow_json是已经按特征名组织好的单条流字典,函数内部复用训练时的特征列顺序,输出包含恶意概率和风险等级。风险等级可以设置两级或三级阈值,比如 0.7 以上高风险、0.3 到 0.7 中风险、低于 0.3 低风险。
这里有一个容易被忽略的细节:单条流和批量流要共用同一个预处理函数,绝不能为预测单独写一套特征提取逻辑。两套逻辑哪怕差一个特征,都会导致线上行为完全不可控。
5. 避坑指南:加密恶意流量检测从训练到部署最常见的 5 个坑
这套源码本身是竞赛工程的完整备份,流程能跑通,但真实落地时最容易出问题的往往不在模型算法,而在数据切分、特征工程和部署细节。我把自己踩过的坑和排查经验按“现象 → 原因 → 解决”整理成下面几条,希望能帮你少走弯路。
5.1 训练集和预测集特征不一致,模型直接报错
现象:训练好的模型在 predict 阶段报feature_names mismatch,或者预测结果全部是同一个值。
原因:训练时特征列有 127 列,预测时因为预处理脚本版本没同步,少了某几个特征列,或者多了几个临时统计列。sklearn 和 LightGBM 对特征名很敏感,列顺序一变,预测结果就是错的。
解决:训练结束前把feature_cols存成 JSON 文件,预测时用reindex(columns=feature_cols)强制对齐。这个操作要写进 main_predict.py 的固定流程里,不要靠肉眼比对列名。另外,训练和预测必须保证用的是同一份 data_preprocessing.py,不要各改各的。
5.2 数据泄漏:同一条 TCP 连接被切进训练集和测试集,成绩虚假
现象:离线验证 AUC 高达 0.99,但部署到真实网络环境后效果很差,误报漏报都严重。
原因:切流时只按包遍历,同一个五元组的包被均匀分配到训练集和测试集,模型记住了具体 IP 和端口组合,而不是学习恶意流量的本质特征。这种数据泄漏在竞赛和项目里最常见的翻车点。
解决:划分数据集时要按流 ID 分组,而不是按单个样本随机划分。可以用GroupShuffleSplit,把groups参数传成每个样本所属的流 ID,保证同一条流的样本只能出现在训练集或测试集中。这样评估出来的指标才是真实可预期的。
5.3 时间戳特征被当成“上线后才知道的上帝特征”
现象:模型在离线测试集表现不错,但上线后预测结果忽好忽坏,甚至出现同一时间段的流量全部被判成恶意。
原因:预处理时引入了“流首包时间”“周期编号”“当天小时”这类和时间点强相关的特征。离线数据是历史数据,模型可能学到了“某几天出现的流量是恶意”这种虚假规律,上线后时间一变,特征失效。
解决:删除绝对时间特征,保留相对时间特征,比如相邻包到达时间间隔、会话持续时间。常见做法是只看包间间隔和时长统计,不看具体几点几分。如果业务确实需要时间维度,也要用周期编码或差分特征,而不是把原文时间戳直接丢给模型。
5.4 tokens 目录缺失或版本不匹配,模型加载直接报错
现象:预测环境下只拷贝了模型 pkl 文件,没有带上 tokens 目录,运行时报No such file or directory: tokens/vocab.json,或者模型加载后预测结果和训练时完全不一致。
原因:源码头部的 token 映射文件是在训练时生成的,有些特征编码需要依赖它。预测环境如果只备份模型文件,而遗漏了 tokens 目录,模型运行时自然找不到映射关系。
解决:部署时把模型文件、feature_cols.json、tokens 目录打成一个整体文件夹,不要拆分拷贝。每部署一次,都用一条固定的 pcap 样本跑一遍 smoke test,对比预测概率和训练环境的输出,数值一致才认为部署成功。
5.5 离线均衡的好看模型上线后误报飙升
现象:训练时精确率、召回率都很好看,上线后安全分析师每小时收到几百条误报告警,正常流量大量被标记为恶意。
原因:训练数据来自竞赛环境或实验环境,正负样本比例相对均衡;真实网络里正常加密流量占比极高,模型见过的正常流量模式不够丰富,稍微偏移一点就会误判。
解决:上线前收集一段真实业务流量,用训练好的模型先做离线回放,观察误报率。如果误报太多,一方面增加真实正常流量样本到训练集,另一方面把预测阈值调高,用精确率换告警可读性。我一般会在告警系统里加一个“置信度过滤”阶段,只有概率超过阈值的流量才进入工单流。
6. 进阶:用 SHAP 验证模型到底在看什么,把黑匣子变成可解释告警
模型上线之后,安全分析师会问一个问题:为什么这条流量被判成恶意?如果答不上来,告警就很难被信任。这里我建议用 SHAP 来验证特征合理性,这一步既是模型验证,也是把检测结果落地成威胁情报的关键技巧。
6.1 用 SHAP 找出模型真正在看的特征
SHAP 适用于树模型,LightGBM、RandomForest 都可以直接使用。代码很简单:
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_val) shap.summary_plot(shap_values, X_val, feature_names=feature_cols)逻辑说明:shap_values是每个样本的每个特征对预测结果的贡献值,正值表示把结果往恶意方向推,负值表示往正常方向推。summary_plot会把所有特征按重要性排序展示,一眼就能看出模型主要依赖包长均值、TLS 加密套件数量还是流持续时间。
这一步对框架验证很有用。如果发现模型最看重的特征是“目标端口”,那要警惕它可能只是记住了训练集里的某个端口,而不是真正的恶意流量模式。
6.2 把预测结果输出成可读的告警单
SHAP 结果和单条预测结合起来,可以生成结构化的告警数据:
alert = { "src_ip": flow.src_ip, "dst_ip": stream.dst_ip, "malicious_prob": prob, "risk_level": level, "top_features": shap_rank[:5], }逻辑说明:top_features里放贡献值最高的五个特征及其取值,分析师看到的是“到达时间间隔异常短”而不是一串概率数字,调查效率会高很多。这个技巧在我实际项目中帮了大忙,安全团队终于不用对着黑盒模型挠头了。
从那以后,我每次训练完模型都会强制自己跑一遍 SHAP,再决定要不要上线。这个习惯帮我把很多只看准确率的假模型挡在了部署之前,也希望帮到你。
本文还有配套的精品资源,点击获取