
简介面向网络安全学习者、渗透测试工程师及高校相关专业学生的一套基于贝叶斯理论的恶意流量检测可视化程序。程序以贝叶斯分类器为核心涵盖数据预处理、特征提取、模型训练与实时预测环节内置图形界面展现流量时序变化、类型占比和告警信息便于快速定位异常访问。压缩包内共三十五个文件整体体积约五千字节核心为两个Python脚本主程序与界面逻辑另附三十个PHP、两个ASP、一个JSP样本覆盖正常与恶意两类代码可用作WebShell检测测试或入侵模拟验证。资源已有四百九十五人学习下载适合作为毕业设计、课程设计或安全测试的参考资料。读者可获取一套可扩展的贝叶斯检测原型同时借助多种脚本语言的样本集直观了解各语言中典型危险函数与攻击载荷特征为后续自研或改进检测系统提供基础。1. 恶意流量检测为什么选贝叶斯先看概率再看结果做渗透测试和防御的人都有一个感受恶意流量检测最有价值的部分不是报警而是“报警之后拿得出证据”。规则引擎只能告诉你命中了一条特征但这条特征为什么可信、误报边界在哪它说不清。贝叶斯模型的思路不一样它输出的是条件概率——一条流量属于恶意类别的概率是多少属于良性类别的概率是多少这两个数字本身就是可解释的判定依据。这套“基于贝叶斯的恶意流量检测可视化程序”做的事情很简单采集流量特征用朴素贝叶斯训练分类器把检测结果实时写库再用 Flask ECharts 把概率、流量趋势、攻击类型分布画成面板。适合做安全运营的工程师、想验证自己抓包数据的渗透测试人员以及刚接触机器学习流量检测、不想一上来就上深度学习的学生。不需要 GPU一台 8GB 内存的普通机器就能跑完整个流程。2. 贝叶斯模型选型与特征工程从朴素贝叶斯到半朴素贝叶斯的取舍2.1 朴素贝叶斯用在流量检测上的模型假设朴素贝叶斯的基础是贝叶斯公式在给定一条流量的特征向量 X 时它属于类别 C 的后验概率等于 P(C) 乘以 P(X|C) 再除以 P(X)。实际做分类时P(X) 对所有类别是常数所以只需要比较每个类别的分子大小。这就是这个程序里“恶意概率”的来源P(恶意|特征) 等于先验概率 P(恶意) 乘以该恶意类别下观察到这种特征的概率再做归一化。这里的“朴素”主要指条件独立假设给定流量类别后各个特征之间互不影响。可实际上流量的平均包长和包长方差高度相关一个长连接往往同时有更大的包长和更高的方差源端口和目标端口也常常成对出现Web 流量基本是 50362 的源端口配 80 的目标端口。这种相关性会破坏朴素贝叶斯的理论基础但工程上的结论是在样本量有限、特征维度不高的情况下朴素贝叶斯依然能给出足够稳的基线结果而且训练时间可以忽略不计。我用这个模型时踩过的最大的坑是连续特征的分布假设。流量数据的偏度很大SYN 泛洪的包长度集中在 40~60 字节偶尔几个大数据包会把方差拉得很大直接用高斯分布建模容易让均值失真。所以我在这个项目里做了一个折中把连续特征按分位数离散成 5~10 个区间再交给离散型朴素贝叶斯处理。这样等于人为降低了对分布假设的依赖模型对异常值更鲁棒。想要更高的准确率则可以把模型升级成半朴素贝叶斯比如 SPODE 或 TAN允许部分相关性强的特征组共享条件概率这是后话第 6 章会再提。2.2 特征构造从 PCAP 到可用于训练的特征向量流量检测的特征不能直接拿原始报文喂给模型需要先从 PCAP 或在线抓包中提炼出“一条连接”的统计量。我这里把一次会话定义为五元组相同的双向包集合源 IP、目标 IP、协议、源端口、目标端口。对每个会话算九个基础特征协议类型、源端口、目标端口、包总数、总字节数、平均包长、包长标准差、SYN 包占比、ACK 包占比。有时间维度需求还可以再加 TCP 握手时延和连接持续时间。下面是我用 Scapy 离线解析 PCAP 生成特征的代码from scapy.all import rdpcap, IP, TCP, UDP from collections import defaultdict import statistics def extract_flow_features(pcap_path): packets rdpcap(pcap_path) flows defaultdict(list) for pkt in packets: if IP not in pkt: continue src pkt[IP].src dst pkt[IP].dst proto pkt[IP].proto sport pkt[TCP].sport if TCP in pkt else pkt[UDP].sport dport pkt[TCP].dport if TCP in pkt else pkt[UDP].dport key (src, dst, proto, sport, dport) flows[key].append(pkt) features [] for key, pkt_list in flows.items(): src, dst, proto, sport, dport key pkt_sizes [len(p) for p in pkt_list] syn_count sum(1 for p in pkt_list if TCP in p and p[TCP].flags.S) ack_count sum(1 for p in pkt_list if TCP in p and p[TCP].flags.A) avg_pkt_len statistics.mean(pkt_sizes) std_pkt_len statistics.pstdev(pkt_sizes) if len(pkt_sizes) 1 else 0.0 features.append({ protocol: int(proto), src_port: int(sport), dst_port: int(dport), pkt_count: len(pkt_list), total_bytes: sum(pkt_sizes), avg_pkt_len: round(avg_pkt_len, 2), std_pkt_len: round(std_pkt_len, 2), syn_ratio: round(syn_count / len(pkt_list), 4), ack_ratio: round(ack_count / len(pkt_list), 4) }) return features这段代码把每个会话压缩成一行特征记录。注意syn_ratio和ack_ratio这类的比例特征要防止除零UDP 流量没有 SYN 和 ACK 标志会得到全 0这也是合法样本不要丢弃。Scapy 解析大 PCAP 时比较慢几十万个包可能要跑几分钟但在离线训练阶段这个速度可以接受。如果要上实时检测建议换成 nprint 或 tshark 的-T fields做流聚合吞吐会高很多。2.3 数据与标签公开数据集和自建流量的处理训练贝叶斯模型时标签质量直接决定模型成败。推荐优先用公开数据集做基线验证最常用的是 NSL-KDD 和 UNSW-NB15。这两者的差异在于NSL-KDD 偏老攻击类型集中在 DoS、Probe、R2L、U2R 四类特征里离散型居多和朴素贝叶斯契合度不错UNSW-NB15 更新包含 Shellcode、Worms、Backdoor、Analysis 等类型流量统计特征更接近真实环境但类别不平衡更严重。这份程序我按 UNSW-NB15 的结构组织特征文件把标签映射成四类0 表示良性1 表示恶意2 表示可疑。可疑类单独拎出来是为了让可视化面板能展示“无法判定但值得跟踪”的中间状态。贝叶斯分类天然适合这种多类输出因为每个类别都会得到一个概率不需要额外训练多个二分类器。如果是自建流量我一般的做法是开一台虚拟机跑攻击工具生成恶意样本用 tcpdump 抓包得到 PCAP再从正常办公网段抓一段时间的流量作为良性样本。然后按上一节的特征提取脚本批量转换。自建数据的关键是时间跨度要够同一类攻击至少覆盖不同时段的背景流量否则训练集和验证集会因为时序相近造成数据泄漏。3. 训练与检测实现从数据集到可落地的判定接口3.1 数据预处理与训练代码含拉普拉斯平滑拿到特征 CSV 后第一步是检查有没有缺失值和极端值。离散特征里的端口号建议先做分桶大于 1024 的端口统一归入 1024避免模型把高位端口的具体数字当成有意义的证据。连续特征用分位数离散化这一步能大幅减少零概率的产生。训练代码我用 sklearn 的CategoricalNB而不是GaussianNB因为前面已经做了离散化import pandas as pd from sklearn.model_selection import train_test_split from sklearn.naive_bayes import CategoricalNB from sklearn.preprocessing import LabelEncoder from sklearn.metrics import classification_report, confusion_matrix df pd.read_csv(traffic_features.csv) # 离散化连续特征按分位数切成5个区间 for col in [pkt_count, total_bytes, avg_pkt_len, std_pkt_len]: df[col] pd.qcut(df[col], q5, labelsFalse, duplicatesdrop) # 端口分桶 df[src_port] df[src_port].apply(lambda x: x if x 1024 else 1024) df[dst_port] df[dst_port].apply(lambda x: x if x 1024 else 1024) feature_cols [protocol, src_port, dst_port, pkt_count, total_bytes, avg_pkt_len, std_pkt_len, syn_ratio, ack_ratio] X df[feature_cols] y LabelEncoder().fit_transform(df[label]) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) model CategoricalNB(alpha1.0) model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred, target_names[benign, malicious, suspicious]))这段代码里最值得解释的就是alpha1.0这是拉普拉斯平滑系数。它的作用是给所有特征取值组合一个最小概率防止训练集里没出现过的组合在预测时直接得到概率 0。流量场景里广域网扫描流量经常带来一种训练集里完全没见过的协议与端口的组合如果不加平滑模型会把后验概率打成 0从而强行判定为良性这正好和直觉相反是新手最容易翻车的点。stratifyy参数保证三个类别在训练集和测试集里的比例一致避免恶意样本占比太小时被随机切分落到测试集之外。如果你是做在线学习更新则把test_size改成 0.3并固定按时间切后面避坑章节会细讲。3.2 模型导出与实时检测的阈值控制训练完成后程序会保存模型文件和一个阈值配置文件。阈值是贝叶斯检测里比模型本身更重要的参数模型输出的是三类概率最终显示成“恶意”“可疑”“良性”靠的是阈值而不是argmax。我默认的阈值是恶意概率大于等于 0.9 判定为恶意0.6 到 0.9 之间判定为可疑低于 0.6 判定为良性。但这组阈值在不同网络环境里差异很大需要按实际误报成本调整。import joblib import numpy as np joblib.dump(model, nb_traffic_model.joblib) def predict_flow(feature_vector): probs model.predict_proba([feature_vector])[0] # probs 顺序: [benign, malicious, suspicious] malicious_prob probs[1] if malicious_prob 0.9: return malicious, float(malicious_prob) elif malicious_prob 0.6: return suspicious, float(malicious_prob) else: return benign, float(malicious_prob)predict_proba是贝叶斯模型最应该使用的接口它返回的是所有类别的概率向量而不是一个生硬的预测标签。在告警页面里我会同时展示malicious_prob和suspicious_prob因为真正需要关注的是那些两个概率都非常高的流量它们往往意味着特征不够充分需要补充更多维度的特征再判断。阈值本身还可以用贝叶斯优化思路去搜索比如固定一个误报率上界在这个约束下最小化漏报率构建目标函数后遍历阈值空间这比人拍脑袋填写数字要可靠得多。3.3 检测结果落库与日志格式设计可视化面板的数据不能从模型内存里直接读因为前端需要的是历史趋势和分布统计。程序在检测进程里收到一条待判定会话后先查模型得到概率再写 SQLite 和 JSON 日志双通道。SQLite 供面板查询JSON 日志作为审计证据。import sqlite3 import json from datetime import datetime conn sqlite3.connect(detect_log.db) conn.execute( CREATE TABLE IF NOT EXISTS detections ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, src_ip TEXT, dst_ip TEXT, protocol INTEGER, src_port INTEGER, dst_port INTEGER, malicious_prob REAL, suspicious_prob REAL, label TEXT )) def log_detection(flow_info, probs, label): ts datetime.utcnow().isoformat() conn.execute( INSERT INTO detections (timestamp, src_ip, dst_ip, protocol, src_port, dst_port, malicious_prob, suspicious_prob, label) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?), (ts, flow_info[src_ip], flow_info[dst_ip], flow_info[protocol], flow_info[src_port], flow_info[dst_port], probs[1], probs[2], label) ) conn.commit() log_entry { timestamp: ts, flow: flow_info, probabilities: {benign: probs[0], malicious: probs[1], suspicious: probs[2]}, label: label } with open(detect_audit.jsonl, a, encodingutf-8) as f: f.write(json.dumps(log_entry) \n)SQLite 的写入和 JSON 文件的追加在低并发场景下足够用不需要引入消息队列。但要注意SQLite 写入不应放在抓包线程里否则高流量时会阻塞采集。我一般开一个独立的检测进程用队列把原始会话塞进去检测和落库都在消费者端完成。日志里记录probabilities全量而不是只记录最终标签是为了后面排查误报时能回放模型的判断依据。4. 可视化面板把模型输出变成能看懂的攻击态势4.1 技术栈选型为什么用 Flask ECharts可视化层我选了 Flask 做后端、ECharts 做前端图表原因只有一个在安全分析场景里后端需要频繁对接内网数据源和认证系统Flask 的轻量路由写起来最快而 ECharts 的折线图、热力图、关系图都内置了成熟的交互缩放不需要前端团队介入。整个面板就是三个页面总览页、流向分析页、单条会话详情页单机部署时直接跑一个 Python 进程不依赖 Node.js 环境。这个选型的边界是如果并发访问量超过 20 人或者需要做复杂的用户权限分级Flask 自带开发服务器就不够用了得换成 gunicorn 加 Nginx。但就一个安全检测小组内部使用瓶颈根本不在这里。4.2 后端接口与前端刷新的数据流设计面板采用“后端查库、前端轮询”的模式。检测进程负责写库Web 服务只读库两者解耦。下面是最核心的两个接口一个返回最近一小时的检测计数一个返回最近一段时间恶意流量趋势。from flask import Flask, jsonify import sqlite3 app Flask(__name__) app.route(/api/summary) def summary(): conn sqlite3.connect(detect_log.db) cur conn.cursor() total cur.execute(SELECT COUNT(*) FROM detections).fetchone()[0] malicious cur.execute( SELECT COUNT(*) FROM detections WHERE labelmalicious ).fetchone()[0] suspicious cur.execute( SELECT COUNT(*) FROM detections WHERE labelsuspicious ).fetchone()[0] conn.close() return jsonify({ total: total, malicious: malicious, suspicious: suspicious }) app.route(/api/trend) def trend(): conn sqlite3.connect(detect_log.db) cur conn.cursor() rows cur.execute( SELECT substr(timestamp, 1, 16) AS minute, label, COUNT(*) FROM detections GROUP BY minute, label ORDER BY minute DESC LIMIT 60 ).fetchall() conn.close() return jsonify(rows)前端的核心逻辑是setInterval定时刷新我用 3 秒刷新一次。这里有个原则前端永远不要直接调用模型推理接口只读库。推理过程可能耗时几十毫秒如果前端每次刷新都触发一次推理高流量场景下会把服务拖死。把“写入时推理”和“读取时聚合”分开整个链路才稳定。setInterval(() { fetch(/api/trend) .then(res res.json()) .then(data { trendChart.setOption({ xAxis: { data: data.map(d d.minute) }, series: [ { name: 恶意, data: data.filter(d d.label malicious).map(d d[COUNT(*)]) }, { name: 可疑, data: data.filter(d d.label suspicious).map(d d[COUNT(*)]) } ] }); }); }, 3000);前端代码里要注意map(d d[COUNT(*)])这种写法依赖 SQL 别名我建议在服务端直接把聚合结果改造成{ minute, label, count }这样的标准结构前端处理起来更清晰。3 秒刷新频率对内网数据库压力很小但如果 SQLite 中积累了上亿条记录必须把GROUP BY的查询限制在最近 7 天否则页面会越用越卡。常见的做法是单独建一张每小时汇总表用定时任务做降精度前端查询汇总表而不是明细表。4.3 可视化指标哪些图是给分析人员用的哪些是给汇报用的这个程序的可视化面板里不同角色的关注点完全不同。分析人员要的是单条会话的概率向量和原始特征能用于确认攻击行为管理层要的则是今天的恶意流量占比和趋势曲线。所以我在面板里做了两套视图。分析视图放三类图恶意概率分布直方图、被判定为可疑的会话列表、源 IP 与目标端口的关系图汇报视图只放两类图每小时的恶意流量计数折线图、攻击类型占比饼图。指标 | 图表类型 | 使用角色 | 说明 恶意概率分布 | 柱状图 | 分析人员 | 观察概率集中在 0.5~0.6 的所谓“灰色地带”有多大 可疑会话列表 | 表格 | 分析人员 | 每行展示概率、五元组、关键特征支持按阈值筛选 源 IP 与目标端口关系图 | 关系图 | 分析人员 | 看到同一个源 IP 是否在批量扫描多个端口 每小时恶意流量计数 | 折线图 | 管理层 | 对比不同时间段攻击强度 攻击类型占比 | 饼图 | 管理层 | 只显示前三类和“其他”图中的颜色也有讲究。恶意用深红色可疑用橙色良性用灰色。ECharts 默认调色板偏鲜艳适合商业大屏但安全运维场景看久了容易疲劳所以我手动指定了一套低饱和度的颜色。可视化不是越花哨越好一套能在一屏内读完的颜色体系才是真正有用的。5. 贝叶斯流量检测避坑指南五条实测踩坑记录这一章是血泪经验。我在这个项目迭代过程中踩过的坑几乎全部来自“看起来模型很准但实际部署后就不是那么回事”。下面五条按严重程度排序每一条都按现象、原因、解决三个部分展开。5.1 零概率直接把预测打死现象模型在训练集上准确率超过 97%但拿真实流量测试时大量良性流量被判定成恶意而且误判集中在异常端口组合上比如“UDP 高位端口 高 SYN 占比”。排查发现这些样本在某个特征维度上落入了一个训练集中完全空白的区间后验概率被算成 0。原因训练集没有覆盖全部特征取值组合但离散型朴素贝叶斯在计算 P(特征|恶意) 时只要某个特征组合在恶意类别的样本里出现次数为 0整条样本的分子就会变成 0。这是朴素贝叶斯最著名的短板和模型是否过拟合无关。解决给模型加拉普拉斯平滑alpha取 1.0 是默认值但我实际调下来 0.5 到 1.0 之间效果更好。更大的alpha会把所有概率往均匀方向拉降低模型的区分能力。另外一个补充手段是训练前检查特征组合的覆盖度如果发现某些离散区间样本量太少就把区间合并宁少勿滥。5.2 先验概率用错了地方现象某天凌晨面板突然开始把全网大量流量判定为恶意误报率从平时的 2% 直接冲到 30%。当天上午核查发现是因为这台机器连着的一个网段正在被某业务系统做全端口扫描恶意流量占比短时间内从 5% 飙到 40%。原因sklearn 的CategoricalNB在fit时会自动学习训练集的先验概率 P(恶意)。当线上流量分布和训练集分布不一致时后验概率会被先验带偏。也就是模型并没有变差变的是输入分布。在流量检测场景里“脱网扫描”和“正常业务活动”是随时波动的静态先验很难撑住。解决我的做法是训练时不依赖 sklearn 默认先验而是手动指定一个贴近业务判断的先验比如 P(良性)0.85、P(恶意)0.10、P(可疑)0.05。更靠谱的办法是定期用最近一周的真实标签统计分布重新调整先验概率。这比重新训练整个模型成本低得多但效果立竿见影。5.3 特征相关性太强朴素贝叶斯的独立性假设失效现象误报集中在文件传输类流量上比如 FTP 和 SMB 的大文件拷贝。这类流量的包长均值、包长方差、总字节数三个特征全部偏高模型看到这三个特征同时大就判定为恶意但事实上它们描述的是同一个事实“这是一个大文件传输”。原因这三个特征实际强相关而朴素贝叶斯把他们当作三个独立证据统计上相当于把同一个证据重复用了三次放大了 P(特征|恶意) 的乘积结果。独立性假设被破坏得越严重模型的置信度就越失真。解决一个是从特征源头做处理用卡方检验或者互信息法筛选特征把相关性超过 0.7 的组合只保留一个另一个是把模型升级成半朴素贝叶斯比如 TAN树增强朴素贝叶斯它允许特征之间有树形依赖关系能部分缓解重复计数问题。我实际用的是前者因为 TAN 的训练时间和实现复杂度高出不少在特征维度只有 9 个的场景里收益有限。5.4 随机切分导致“假 AUC”现象模型验证时 AUC 达到 0.97但上线第一天就被运维投诉误报刷屏。回去复查才发现用train_test_split随机切分时同一个 TCP 会话的多个包在抓包文件里是连续的它们天然会同时落入训练集和测试集。原因流量样本不是独立同分布的。同一时段的同一条连接、同一个 IP 的相似行为在时间轴上高度自相关。随机切分等于让模型提前偷看了未来数据AUC 虚高是典型的数据泄漏表现。解决切分必须按时间维度比如用前 7 天做训练、第 8 天做测试而不是随机抽样。评估指标也不能只看 AUC还要看每天的误报绝对数和漏报绝对数。在流量检测这种类别不平衡的场景里AUC 会把大量“容易判对”的良性样本和“容易判对”的恶意样本都算进去真实难点永远在边界样本上。5.5 可视化面板时间滞后与“假实时”现象流量监控面板显示“最近一小时恶意流量 1200 条”但运维按时间戳去翻原始日志发现最新记录还停留在 10 分钟前。前端明明 3 秒刷新一次为什么数据不更新原因检测进程和抓包进程之间用了同一个线程池抓包耗时长时阻塞了检测和写库。SQLite 的commit是同步写磁盘的抓包高峰时每一批流量都要排队写库前端轮询看到的永远是旧数据。这不是可视化代码的问题而是管道设计的问题。解决把抓包、检测、写库拆成三段独立进程或线程之间用队列缓冲。面板查询的聚合表只做“追加”不做“更新”避免数据库写锁竞争。同时在记录里加process_time和ingest_time两个时间戳面板显示ingest_time分析人员查问题时用process_time对比延迟。从那以后我上线任何检测程序都会先看数据管道各环节的积压量和延迟再谈面板效果。6. 模型上线后的验证套路用自己抓的流量反向校验模型6.1 回放验证用 PCAP 构造反向测试链路训练时的测试集再准也代替不了真实上线验证。我的习惯是拿模型对自己现场抓的 PCAP 做一次离线回放先用tcpreplay把历史攻击流量重放到镜像端口观察检测系统能不能在几秒内打点、落库、上屏。这一步能同时验证模型效果和整个数据管道的吞吐能力。6.2 误报率观察从混淆矩阵到阈值的换算回放验证后重点不是看总准确率而是看误报绝对数。我一般把模型输出落在混淆矩阵上然后把阈值从 0.9 依次降到 0.8、0.7、0.6记录每档的误报数变化。如果阈值从 0.9 降到 0.8 时误报数翻倍说明大量样本集中在 0.8 到 0.9 的置信区间这个区间的特征几乎无法区分良性和恶意需要补特征而不是继续调阈值。6.3 后续迭代从朴素贝叶斯到贝叶斯网络当误报集中在边界区间时最终解法是升级模型。动态贝叶斯网络可以引入状态转移把“上一帧的概率”作为下一帧的先验适合检测慢速隐蔽扫描贝叶斯 CUSUM 则擅长时间序列上的突变点检测可以作为现有分类器的前置过滤器。但这些都是后话基础版本的朴素贝叶斯做好特征、阈值、日志三板斧已经能覆盖大多数安全分析场景。从那以后我每次训练完新模型都会强制自己走一遍离线回放和阈值遍历流程确认输出可信度再交给可视化面板。希望帮到你。本文还有配套的精品资源点击获取