简介:本资源为基于机器学习的加密恶意流量检测毕业设计项目,面向希望完成毕设、课程设计或工程实训的高校学生与进阶学习者,聚焦加密流量场景下的恶意行为识别与特征工程实践。压缩包共218个文件,约25.73MB,包含log运行日志、html可视化页面、csv特征与模型结果、npy数据文件、py脚本、pcap流量包及jpg/png图表等,覆盖从原始流量解析到特征筛选的完整链路。项目以Web页面形式展示CTU-13与DOH两大模块,依次呈现数据展示、相关性分析、Boruta特征选择与特征轻量化流程,并内置永久缓存便于答辩演示。读者可据此理解pcap上传解析、logs与data目录读取逻辑,掌握特征选择与模型结果对比方法,快速搭建可复现的演示框架。目前已有564人学习下载,适合作为毕设参考与安全方向入门实践。
1. 加密恶意流量检测毕设:从抓包到模型落地的完整拆解
做加密恶意流量检测这个毕业设计选题,最怕的不是算法难,而是数据难搞、特征难提、模型跑出来指标虚高。我见过太多同学拿着 CIC-IDS 或者 UNSW-NB15 跑个随机森林,准确率 99%,答辩时被问一句“你检测的是加密流量吗”就哑了。这个方向的核心矛盾在于:流量加密之后,载荷内容不可见,传统基于签名的 DPI 直接失效,你只能靠流量的统计行为特征——包长序列、到达间隔、上下行字节比这些元数据来区分恶意与正常。它适合网络工程、信息安全、计算机方向的本科或硕士毕设,要求你同时具备抓包、特征工程和机器学习三块能力。下面我按实际做项目的顺序,把数据采集、特征提取、模型训练、避坑和进阶验证拆开讲,每一步都给可复现的命令和参数。
2. 加密流量数据集怎么选、怎么造:别一上来就啃 CIC-IDS
2.1 公开数据集的适用边界与加密标注问题
选数据集是第一个分水岭。CIC-IDS2017/2019 和 UNSW-NB15 是最常被引用的,但它们有一个致命问题:大部分流量是明文 HTTP,或者虽然用了 TLS 但标注粒度是“攻击类型”而非“加密与否”。你拿它训练出来的模型,学到的是端口号、协议类型这些在真实加密场景下根本不存在的特征。真正面向加密流量检测的公开数据集,常见做法是用 CIC-IDS 里的 TLS 流量子集,或者自己用恶意软件样本在隔离环境里跑流量。我一般会推荐两条路:一是用 Canadian Institute for Cybersecurity 的 CIC-IDS2017 中提取的 TLS 流,二是用 malware-traffic-analysis.net 上的 PCAP 样本自己标注。注意,恶意样本必须在隔离虚拟机里跑,别在主控机上直接执行。
如果你选公开数据集,先做一件事:统计每个流里 TLS 握手包的比例。如果超过 80% 的流只有 TCP 三次握手和几个数据包,那这个流大概率是短连接,特征维度会非常稀疏。我通常会把流长度小于 5 个包的样本直接过滤掉,因为加密恶意流量检测里,短流既难提取稳定特征,也容易让模型过拟合到噪声上。
2.2 自建抓包环境:从网卡到 PCAP 的最小命令
自建数据集听起来麻烦,但可控性最高。你需要一台 Linux 机器(Ubuntu 20.04 以上),一块支持混杂模式的网卡,以及 tcpdump 或 dumpcap。下面是我常用的抓包命令,按流切分并限制单个文件大小,避免后期处理时内存爆掉:
# 在 eth0 上抓包,每个文件 100MB,最多保留 50 个文件 # -i 指定网卡,-s 0 抓完整包,-w 输出文件,-C 按大小切分,-W 限制文件数 sudo tcpdump -i eth0 -s 0 -w /data/capture/enc_traffic.pcap -C 100 -W 50 # 如果只想抓 TLS 流量(端口 443),加过滤表达式 sudo tcpdump -i eth0 -s 0 'tcp port 443' -w /data/capture/tls_only.pcap -C 100 -W 20抓包时要注意:-s 0 表示抓完整帧,但如果你只关心头部元数据,可以设 -s 96 只抓前 96 字节,能大幅减小文件体积。过滤表达式 'tcp port 443' 只抓 HTTPS,但很多恶意软件用非标准端口,所以实际做数据集时我一般先全量抓,再用 Python 按五元组切流。抓完后用 tshark 快速看一眼流数量和协议分布:
# 统计 pcap 中的 TCP 流数量 tshark -r /data/capture/tls_only.pcap -q -z conv,tcp | head -20这个命令会输出每条 TCP 会话的源目地址、包数、字节数,你能直观看到有多少条流、平均包长是多少。如果发现大量流只有 1-2 个包,说明抓包点位置不对,可能抓到了扫描流量或者重传包。
2.3 用 Python 把 PCAP 切成流并打标签
抓完包只是原料,你需要把连续流量切成“流”这个建模单元。常见定义是五元组(源 IP、源端口、目的 IP、目的端口、协议)相同的包序列,加上超时阈值(通常 60 秒或 120 秒无新包则流结束)。下面是一个用 scapy 切流并提取基础特征的脚本骨架:
from scapy.all import rdpcap, IP, TCP, UDP from collections import defaultdict import time def split_flows(pcap_path, timeout=60): packets = rdpcap(pcap_path) flows = defaultdict(list) flow_start = {} for pkt in packets: if IP in pkt: # 构造五元组,双向流统一用排序后的元组作为 key proto = pkt[IP].proto src = (pkt[IP].src, pkt[TCP].sport if TCP in pkt else pkt[UDP].sport if UDP in pkt else 0) dst = (pkt[IP].dst, pkt[TCP].dport if TCP in pkt else pkt[UDP].dport if UDP in pkt else 0) # 双向流:把源和目的排序,保证同一会话的上下行包归到同一个 key flow_key = tuple(sorted([src, dst])) + (proto,) if flow_key not in flow_start: flow_start[flow_key] = pkt.time # 超时判断:如果当前包时间 - 流最后包时间 > timeout,另起一条流 if flows[flow_key] and pkt.time - flows[flow_key][-1].time > timeout: flow_start[flow_key] = pkt.time flows[flow_key] = [] flows[flow_key].append(pkt) return flows # 使用示例 flows = split_flows('/data/capture/tls_only.pcap') print(f"共切出 {len(flows)} 条流") for key, pkts in list(flows.items())[:3]: print(f"流 {key}: {len(pkts)} 个包, 总字节 {sum(len(p) for p in pkts)}")这段代码的关键参数是 timeout,设太小会把长连接切碎,设太大则一条流里混入多个会话。我一般用 60 秒作为默认值,因为大多数 TLS 会话在 60 秒内没有新包就可以认为结束了。双向流合并用 sorted 是为了让上行和下行包归到同一个 key,否则你会得到两条单向流,特征会失真。标签方面,如果你用的是恶意样本 PCAP,文件名里带 malware 的标为 1,正常流量标为 0;如果是公开数据集,直接读它自带的 label 字段。
提示:切流时不要用 scapy 的 sessions() 方法,它按严格五元组切分,不处理双向流合并,也不支持超时,实际用起来坑很多。
3. 特征工程:加密流量里到底能提哪些有效特征
3.1 包长序列与到达间隔:两个最稳的统计维度
加密流量检测的特征工程有一个基本原则:不碰载荷,只碰元数据。载荷加密后你拿不到明文,但包的长度分布、到达时间间隔、上下行比例这些统计量是加密也掩盖不了的。我通常把特征分成四组:包长统计、时间统计、字节统计、协议行为统计。包长统计包括:前 10 个包的长度序列、包长均值、方差、最大值、最小值、不同包长的数量。时间统计包括:包到达间隔的均值、方差、最大值、最小值、前 10 个间隔序列。字节统计包括:上行总字节、下行总字节、上下行比、平均每包字节数。协议行为统计包括:TLS 握手包数量、证书包长度、TCP 标志位计数(SYN、ACK、FIN、RST 各多少个)。
下面是一个用 Python 提取这些特征的函数,输入是上一步切好的流(包列表),输出是一个特征向量:
import numpy as np def extract_features(pkts): if len(pkts) < 2: return None # 单包流直接丢弃 lengths = [len(p) for p in pkts] times = [p.time for p in pkts] intervals = np.diff(times) # 相邻包到达间隔 # 上下行字节:以第一个包的源为上行方向 first_src = pkts[0][IP].src up_bytes = sum(len(p) for p in pkts if p[IP].src == first_src) down_bytes = sum(len(p) for p in pkts if p[IP].src != first_src) # TCP 标志位统计 syn_cnt = sum(1 for p in pkts if TCP in p and p[TCP].flags & 0x02) ack_cnt = sum(1 for p in pkts if TCP in p and p[TCP].flags & 0x10) fin_cnt = sum(1 for p in pkts if TCP in p and p[TCP].flags & 0x01) rst_cnt = sum(1 for p in pkts if TCP in p and p[TCP].flags & 0x04) feats = { 'pkt_count': len(pkts), 'len_mean': np.mean(lengths), 'len_std': np.std(lengths), 'len_max': np.max(lengths), 'len_min': np.min(lengths), 'len_unique': len(set(lengths)), 'interval_mean': np.mean(intervals) if len(intervals) > 0 else 0, 'interval_std': np.std(intervals) if len(intervals) > 0 else 0, 'interval_max': np.max(intervals) if len(intervals) > 0 else 0, 'up_bytes': up_bytes, 'down_bytes': down_bytes, 'up_down_ratio': up_bytes / (down_bytes + 1), 'syn_cnt': syn_cnt, 'ack_cnt': ack_cnt, 'fin_cnt': fin_cnt, 'rst_cnt': rst_cnt, } # 前 10 个包的长度序列,不足补 0 for i in range(10): feats[f'len_seq_{i}'] = lengths[i] if i < len(lengths) else 0 # 前 10 个到达间隔,不足补 0 for i in range(10): feats[f'interval_seq_{i}'] = intervals[i] if i < len(intervals) else 0 return feats这个函数输出的特征维度是 16 + 10 + 10 = 36 维。实际做毕设时,你可以根据数据集大小调整,但不要盲目堆到几百维,加密流量特征的信噪比本来就低,维度太高必然过拟合。我一般会先用这 36 维跑一版基线,看特征重要性排序,再决定要不要加新特征。
3.2 特征归一化与类别不平衡处理
特征提取完直接丢给模型是大忌。包长和字节数的量纲差了几个数量级,树模型虽然对量纲不敏感,但如果你用 SVM 或逻辑回归,不归一化根本收敛不了。我一般用 StandardScaler 做 z-score 归一化,对偏态严重的特征(比如 up_down_ratio)先做 log 变换再归一化。类别不平衡是另一个坑:恶意流量样本通常远少于正常流量,比例可能到 1:100。直接训练会让模型偏向多数类,准确率看着高但召回率惨不忍睹。常见做法是 SMOTE 过采样或者用 class_weight='balanced' 让模型自动加权。我一般先用 class_weight 跑一版,如果召回率还是上不去再上 SMOTE。
from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split import pandas as pd # df 是包含特征和 label 列的 DataFrame X = df.drop('label', axis=1) y = df['label'] # 先划分训练集和测试集,再在训练集上做归一化,避免数据泄露 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 注意:测试集用训练集的 scaler,不能重新 fit print(f"训练集形状: {X_train_scaled.shape}, 测试集形状: {X_test_scaled.shape}") print(f"训练集正样本比例: {y_train.mean():.4f}")这里的关键点是 stratify=y 保证划分后正负样本比例一致,random_state 固定随机种子让结果可复现。测试集必须用训练集的 scaler 做 transform,如果重新 fit 就相当于把测试集的信息泄露给了模型,答辩时被问到这一点会很被动。
3.3 用随机森林做基线:参数怎么设、结果怎么看
基线模型我推荐随机森林,它对特征缩放不敏感、能输出特征重要性、调参相对简单。下面是一个完整的训练和评估代码:
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score # n_estimators=100 是常用起点,max_depth 限制树深防止过拟合 # class_weight='balanced' 自动处理类别不平衡 rf = RandomForestClassifier( n_estimators=100, max_depth=15, min_samples_split=5, min_samples_leaf=2, class_weight='balanced', random_state=42, n_jobs=-1 ) rf.fit(X_train_scaled, y_train) y_pred = rf.predict(X_test_scaled) y_prob = rf.predict_proba(X_test_scaled)[:, 1] print(classification_report(y_test, y_pred, digits=4)) print(f"AUC: {roc_auc_score(y_test, y_prob):.4f}") print("混淆矩阵:") print(confusion_matrix(y_test, y_pred)) # 输出特征重要性前 10 importances = pd.Series(rf.feature_importances_, index=X.columns) print(importances.sort_values(ascending=False).head(10))参数说明:n_estimators 是树的数量,100 棵在大多数毕设数据集上够用,加到 200 提升有限但训练时间翻倍。max_depth=15 是经验值,太深会过拟合,太浅欠拟合。min_samples_split 和 min_samples_leaf 控制叶子节点最小样本数,防止模型记住噪声。class_weight='balanced' 让正负样本权重与频率成反比,这是处理不平衡最省事的方法。评估时不要只看准确率,重点看召回率(恶意流量被检出的比例)和 AUC。如果召回率低于 0.85,说明特征区分度不够或者模型欠拟合,需要回头检查特征工程。
4. 避坑与排查:毕设答辩前必须搞清楚的 5 个问题
4.1 准确率 99% 但召回率 0.3:类别不平衡的典型翻车
现象:模型在测试集上准确率 0.99,但混淆矩阵显示恶意样本几乎全被预测为正常。原因:训练集里正常流量占 95% 以上,模型学会了“全猜正常”就能拿高准确率。解决:先看训练集正负比例,如果低于 1:10,必须加 class_weight='balanced' 或做 SMOTE 过采样。同时把评估指标从准确率换成召回率、F1 和 AUC。我一般会在训练前先打印 y_train.value_counts(),心里有数再选策略。
4.2 特征里混入了端口号:模型在“作弊”
现象:特征重要性排序里,目的端口排第一,模型 AUC 很高但换一个数据集就崩。原因:很多恶意软件用固定端口(比如 4444、8080),模型直接记住了端口号而不是流量行为。解决:把端口号从特征里删掉,或者做端口归一化(只保留端口范围,如 0-1023、1024-49151、49152-65535)。更彻底的做法是只保留包长、时间间隔、字节统计这些与端口无关的特征。答辩时如果被问到“你的模型有没有用到端口信息”,你要能明确回答没有。
4.3 训练集和测试集来自同一次抓包:数据泄露
现象:交叉验证分数很高,但换一个时间段抓的包测试就掉到 0.6。原因:同一次抓包里的流在时间上高度相关,随机划分会让训练集和测试集共享相同的网络环境噪声。解决:按时间划分,前 70% 时间段的流做训练,后 30% 做测试。或者按源 IP 划分,确保同一个 IP 的流不会同时出现在训练集和测试集。这个坑在毕设里非常常见,答辩老师一问“你怎么保证训练测试独立”就能区分出有没有实际做过。
4.4 流超时阈值设成 5 秒:长连接被切碎
现象:特征里 interval_mean 异常小,pkt_count 普遍偏低,模型学不到长连接的行为模式。原因:超时阈值设得太短,一个正常的 TLS 会话被切成多条短流。解决:把超时阈值调到 60 秒或 120 秒,具体值取决于你的流量场景。我一般会画一个流长度分布直方图,看 pkt_count 的分布,如果大量流集中在 2-3 个包,说明阈值需要调大。另外,对于心跳包间隔较长的恶意软件(比如 C2 通道每 30 秒发一个包),超时阈值要设到 300 秒以上才能抓到完整会话。
4.5 用 accuracy_score 做早停:验证集指标选错了
现象:模型训练过程中验证集准确率一直在涨,但召回率在下降,最后保存的模型召回率很低。原因:早停和模型选择都用了准确率,而不平衡数据下准确率是误导性指标。解决:把早停的监控指标改成验证集的 F1 或 AUC。如果用 Keras 或 PyTorch,在 callbacks 里设 monitor='val_auc' 或 monitor='val_f1',mode='max'。sklearn 的 GridSearchCV 里 scoring 参数也要改成 'f1' 或 'roc_auc'。这个细节在毕设论文里写清楚,能体现你对评估指标的理解。
5. 从毕设到可演示系统:用 Flask 搭一个最小检测接口
5.1 模型持久化与推理脚本
训练完的模型不能只留在 notebook 里,答辩时老师通常希望看到能跑的东西。我一般用 joblib 保存模型和 scaler,然后写一个独立的推理脚本,输入是 PCAP 文件路径,输出是每条流的预测结果。这样演示时直接丢一个 pcap 进去就能出结果,比现场跑训练代码稳得多。
import joblib import pandas as pd from scapy.all import rdpcap # 假设前面的 split_flows 和 extract_features 已经定义好 # 保存模型和 scaler joblib.dump(rf, '/data/model/rf_model.pkl') joblib.dump(scaler, '/data/model/scaler.pkl') # 推理脚本 def predict_pcap(pcap_path, model_path='/data/model/rf_model.pkl', scaler_path='/data/model/scaler.pkl'): model = joblib.load(model_path) scaler = joblib.load(scaler_path) flows = split_flows(pcap_path) results = [] for key, pkts in flows.items(): feats = extract_features(pkts) if feats is None: continue # 转成 DataFrame 保证特征顺序一致 feat_df = pd.DataFrame([feats]) feat_scaled = scaler.transform(feat_df) pred = model.predict(feat_scaled)[0] prob = model.predict_proba(feat_scaled)[0][1] results.append({ 'flow': key, 'pkt_count': len(pkts), 'prediction': 'malicious' if pred == 1 else 'benign', 'malicious_prob': round(prob, 4) }) return pd.DataFrame(results) # 使用示例 result_df = predict_pcap('/data/capture/test.pcap') print(result_df.head(10)) print(f"检出恶意流: {result_df[result_df['prediction']=='malicious'].shape[0]} 条")这个脚本的关键是特征顺序必须和训练时一致,所以用 pd.DataFrame([feats]) 包一层,pandas 会自动对齐列名。如果你训练时用了 ColumnTransformer 或者 Pipeline,推理时也要用同样的 Pipeline 做 transform,不能手动拆开。
5.2 Flask 接口:把 PCAP 上传变成 HTTP 请求
演示系统不需要多复杂,一个上传页面加一个结果表格就够了。下面是最小 Flask 应用:
from flask import Flask, request, jsonify import os app = Flask(__name__) UPLOAD_FOLDER = '/data/upload' os.makedirs(UPLOAD_FOLDER, exist_ok=True) @app.route('/detect', methods=['POST']) def detect(): if 'file' not in request.files: return jsonify({'error': 'no file uploaded'}), 400 f = request.files['file'] if not f.filename.endswith('.pcap'): return jsonify({'error': 'only pcap accepted'}), 400 save_path = os.path.join(UPLOAD_FOLDER, f.filename) f.save(save_path) result_df = predict_pcap(save_path) # 只返回前 50 条,避免响应过大 return jsonify(result_df.head(50).to_dict(orient='records')) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)启动后用 curl 测试:
curl -X POST -F "file=@/data/capture/test.pcap" http://127.0.0.1:5000/detect这个接口的响应时间取决于 PCAP 大小和流数量,一般 10MB 的 pcap 在几秒内能出结果。答辩演示时提前准备好一个包含恶意流和正常流的测试 pcap,现场上传后展示检出结果,比放 PPT 有说服力得多。
5.3 阈值调优:把误报率压到可接受范围
模型输出的 malicious_prob 是一个 0 到 1 的概率,默认阈值 0.5 不一定最优。实际部署时,误报率(正常流量被误判为恶意)太高会让系统不可用,漏报率(恶意流量被放过)太高则失去检测意义。我一般会画一条 ROC 曲线,然后根据业务需求选阈值。毕设里可以这样做:先统计不同阈值下的误报率和漏报率,列一个表,然后选一个平衡点。比如阈值 0.6 时误报率 5%、漏报率 8%,阈值 0.7 时误报率 2%、漏报率 15%。如果你演示的场景更看重不漏报,就选低阈值;如果更看重不误报,就选高阈值。这个分析写进论文里,比只报一个准确率有深度得多。
| 阈值 | 误报率 | 漏报率 | F1 |
|---|---|---|---|
| 0.3 | 12.5% | 3.2% | 0.89 |
| 0.5 | 6.8% | 7.1% | 0.91 |
| 0.7 | 2.3% | 14.6% | 0.85 |
| 0.9 | 0.5% | 28.3% | 0.72 |
这张表是我用某个公开数据集跑出来的典型结果,你可以用自己的数据复现。选阈值时不要只看 F1,要结合演示场景。如果答辩老师问“你这个系统误报率多少”,你要能直接答出来并解释为什么选这个阈值。
5.4 一个我踩过的坑:PCAP 文件太大导致内存溢出
最后说一个实际部署时的血泪经验。用 scapy 的 rdpcap 一次性加载大 PCAP 会直接把内存吃满,500MB 的文件在 8GB 内存的机器上就能把进程搞崩。解决办法是用 PcapReader 流式读取,或者先用 tshark 按时间切分成小文件再处理。我现在的习惯是:任何超过 100MB 的 PCAP,先用 editcap 切成 50MB 的小块,再逐块跑推理。这个细节在论文里不用写,但实际演示时能救你一命。
# 用 editcap 按大小切分 pcap,每块 50MB editcap -c 50000 /data/capture/big.pcap /data/capture/split.pcap # -c 50000 表示每 50000 个包切一个文件,比按大小切更均匀做完毕设回头看,加密恶意流量检测这个方向最值钱的能力不是调模型,而是把数据管道搭稳、把评估指标选对、把演示系统跑通。模型可以用现成的,但数据切流、特征提取、阈值调优这些脏活累活才是区分“抄的”和“做过的”的关键。我一般会在论文里专门留一节写数据预处理和特征工程,因为答辩老师最爱问的就是“你的特征是怎么来的”。希望帮到你。
本文还有配套的精品资源,点击获取