多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

网络流量异常检测系统搭建:从会话化到孤立森林

网络流量异常检测系统搭建:从会话化到孤立森林 简介这是一份NEMEA网络测量分析系统的源码压缩包面向网络流量分析、异常检测方向的开发与研究人员。NEMEA采用模块化架构检测器负责识别DNS隧道、DoS、扫描等恶意流量工具模块承担数据预处理与聚合框架层提供统一通信接口适合理解或二次开发基于流的实时检测系统。压缩包共46个文件约200KB以shell脚本、Markdown文档、Automake配置及Vagrantfile为主另含Dockerfile、Jenkinsfile等构建与部署辅助文件方便在本地或虚拟环境快速搭建验证。内部目录覆盖框架、检测器、模块、用例与文档并附有打包配置和启动脚本便于对照源码梳理NEMEA的运行流程。目前已有515人学习适合具备Linux基础、希望深入NEMEA框架原理或扩展自定义检测模块的技术人员。1. Nemea 到底在解决什么问题网络流量里的异常为什么这么难抓流量分析这件事真正上手做过的人都会有一个共识抓包不难难的是从海量流量里找出那几条真正有问题的。我们常见的监控系统能告诉你带宽满了、连接数暴增但说不出是哪台主机在什么时间段、和谁建立了什么特征的连接更说不出这是端口扫描、数据回传还是某种爬虫行为。Nemea 这类「网络流量分析和异常检测系统」的切入点就是把原始流量转成结构化的流记录再在这个基础上做统计建模和离群检测从而把「看起来不对劲」变成「可量化、可定位、可复现」的结论。这套思路对两类人最有用。一类是刚接手内网安全监控的运维工程师手里只有交换机镜像口和一张网卡想快速搭出一个能用的检测基线另一类是做攻防研究的开发者需要一套不依赖商业设备的开源方案用于分析模拟攻击流量、验证检测规则。它的核心价值不是做得比商业 NDR 更花哨而是把流量分析的每个环节拆到你能理解和修改采集什么、怎么会话化、提取哪些特征、用什么算法判定异常。需要先说明的是本文不会假装手上有某个现成发布包或者源码仓库而是沿着「Nemea 这套命名所代表的思路」——流记录采集、特征提取、无监督异常检测、告警输出——一步步把可落地的方案讲清楚。你不需要特殊硬件一台普通的 x86 服务器、一张支持 AF_PACKET 的网卡、Python 3.8 以上环境就能把这套系统在自己网络里跑起来。2. 流量采集与会话化异常检测的地基在这里2.1 采集位置选不对后面所有分析都是空谈做流量分析的第一步不是写代码而是想清楚从哪里拿流量。常见做法是接交换机的镜像端口把核心交换机的流量复制一份到分析主机的网卡上。这里有两个容易犯的错一是镜像口带宽不足高峰期直接丢包导致检测结果失真二是分析主机和业务流量共用物理链路抓包本身影响了业务。我一般会先确认三个参数镜像口的方向只收入口流量还是收发都收、速率是否匹配分析网卡、分析主机是否关掉不必要的服务。网卡建议选择 Intel 82599 或类似支持多队列的型号开 RSS 让多个 CPU 核分担收包压力。如果流量峰值超过 1Gbps采集程序这一步就会成为瓶颈要么上 PF_RING 之类的加速方案要么把采样率从 1:1 降到 1:4先保证检测逻辑跑通再考虑全量覆盖。采集程序这里选 libpcap 的封装库比如 Python 的scapy或者更轻量的pyshark。但高流量下scapy的用户态解析会把 CPU 打满实际项目中我更倾向于先用tcpdump落盘成 pcap 文件再用分析脚本读文件。这样采集和分析解耦排错时也方便回放。# 在分析主机上抓取镜像口流量按 60 秒切分文件 tcpdump -i eth0 -G 60 -w /data/pcaps/traffic_%Y%m%d_%H%M%S.pcap \ -Z root -z /usr/local/bin/rotate_cleanup.sh \ not port 22 and not port 514这里-G 60表示每 60 秒生成一个新文件配合-z在文件轮转后执行清理脚本避免磁盘被 pcap 填爆。过滤条件排除了 SSH 和 syslog 流量这两个在我们的环境里是管理面流量噪声很大留着只会干扰后续异常检测的基线。2.2 数据链路五元组标识只有包不够得有会话拿到 pcap 之后下一个关键动作是「会话化」。网络异常检测的基本单元不是单个数据包而是一条条流记录。比如端口扫描的特征是短时间内大量不同目标 IP 的 SYN 包只在包级别看很难形成统计意义但聚合成流以后很容易算出「源 IP 在 10 秒内发起了 5000 条只含 SYN 的流」。会话划分的标准动作是用五元组加时间窗口。五元组就是源 IP、源端口、目的 IP、目的端口、传输层协议再加上协议状态来判断流的开始和结束。这里我推荐使用已有的流处理库而不是自己维护哈希表pyshark能直接从 pcap 文件里读取五元组并自带超时管理直接把每条流的时间和上下行字节数给出来。import pyshark def extract_flows(pcap_path, timeout_seconds30): cap pyshark.FileCapture( pcap_path, use_jsonTrue, include_rawFalse, only_summariesTrue, ) flows {} for pkt in cap: try: key (pkt.ip.src, pkt[pkt.transport_layer].srcport, pkt.ip.dst, pkt[pkt.transport_layer].dstport, pkt.transport_layer) except AttributeError: continue # 非 IP 包或解析失败直接跳过 ts float(pkt.frame_info.time_relative) size int(pkt.length) if key not in flows: flows[key] {start: ts, bytes_up: 0, bytes_down: 0, packets: 0} entry flows[key] if ts - entry[start] timeout_seconds: # 流超时落盘并新建 yield key, entry flows[key] {start: ts, bytes_up: 0, bytes_down: 0, packets: 0} entry[packets] 1 if pkt.ip.src key[0]: entry[bytes_up] size else: entry[bytes_down] size for key, entry in flows.items(): yield key, entry cap.close()这段代码的关键在于只保留 flow 级别的摘要信息不保留包级内容内存占用可控。timeout_seconds30是个合理的流超时参数太大的话会把两个独立的短连接拼成一条流太小则会把一条长连接拆散。对于 HTTP 类的短连接30 秒足够对于数据库长连接后面会单独配置不拆分的白名单。3. 异常检测的方法选型为什么说无监督建模是更稳的路线3.1 深度学习模型在流量异常检测里的现实困境很多文章一上来就推深度学习模型比如自编码器、LSTM 等等。真实落地的时候你会发现训练数据的标注是永远的痛。正常的网络流量好找异常流量怎么定义端口扫描、暴力破解、数据回传这些标签谁来打自己模拟攻击打出来的数据跟真实环境里的攻击流特征差距又很大模型在生产环境的误报率会高到让人想关掉整个系统。我的做法是绕开标签问题直接走无监督路线。通过「找离群点」的方式判断异常。流记录的特征向量分布在正常的区域异常流记录会偏离这个区域。最常用的是孤立森林不需要训练标签计算开销小单机上几万条流记录几秒钟就能跑完而且对特征维度的扩展很友好。from sklearn.ensemble import IsolationForest def detect_anomalies(feature_matrix, contamination0.01): model IsolationForest( n_estimators200, contaminationcontamination, random_state42, n_jobs-1, ) preds model.fit_predict(feature_matrix) scores model.score_samples(feature_matrix) return preds, scorescontamination参数表示预期异常比例这个值设成 1% 还是 5%直接影响检测阈值。它不是让模型去找「最异常的 1%」而是让算法在训练时按这个比例校准异常分数切分点。生产环境里可以先跑一周正常流量观察模型输出的分数分布再反推一个合理的 contamination 值。n_estimators200是精度与速度的折中树太少分离不稳定树太多在流数据上没必要。3.2 从流记录中提取特征哪些维度能真正区分异常孤立森林需要数值特征矩阵所以从流记录中提特征的质量直接决定检测效果。最常见也最有效的维度是四个方向时间维度、流量维度、连接模式维度、协议分布维度。每条流提取以下特征源端口号、目的端口号、上行总字节数、下行总字节数、总包数、流持续时间、每秒上行包速率、每秒下行包速率、TCP 标志位分布SYN 占比、ACK 占比、上下行字节比。端口扫描的特征是大量低包量、SYN 占比高、流持续时间短的记录数据回传的特征是上下行字节比严重悬殊、持续时长稳定的连接暴力破解的特征是同一目的 IP 和目的端口的大量短连接源端口各不相同。import numpy as np def build_feature_vector(flow_key, flow_entry): duration flow_entry[end] - flow_entry[start] if duration 0: duration 0.001 up_rate flow_entry[bytes_up] / duration down_rate flow_entry[bytes_down] / duration syn_ratio flow_entry.get(syn_count, 0) / max(flow_entry[packets], 1) size_ratio flow_entry[bytes_up] / max(flow_entry[bytes_down], 1) return np.array([ int(flow_key[1]), # 源端口 int(flow_key[3]), # 目的端口 flow_entry[bytes_up], flow_entry[bytes_down], flow_entry[packets], duration, up_rate, down_rate, syn_ratio, size_ratio, ])端口号直接做特征有个坑不同端口号数值范围差异极大80 和 443 很接近但 8080 和 8443 也很大孤立森林基于随机切分对这类数值不敏感所以端口特征我一般只保留目的端口且先做对数变换np.log1p(port)再进模型。特征标准化问题在孤立森林里影响不大树模型对单调变换不敏感但如果后面要换用距离类算法就需要统一做标准化。4. 完整搭建一套最小可用的检测闭环代码与参数全解4.1 离线检测读 pcap、出特征、标异常一气呵成先把离线流程跑通。这一步的目标是输入任意一个 pcap 文件输出每个五元组流及其异常分数排名方便人工核验。这里把前面的代码串成完整脚本用命令行参数控制输入输出和检测灵敏度。import sys import json import numpy as np import pyshark from sklearn.ensemble import IsolationForest def flow_generator(pcap_path): cap pyshark.FileCapture(pcap_path, use_jsonTrue, only_summariesTrue) flows {} for pkt in cap: try: src pkt.ip.src dst pkt.ip.dst proto pkt.transport_layer sport int(pkt[pkt.transport_layer].srcport) dport int(pkt[pkt.transport_layer].dstport) except AttributeError: continue ts float(pkt.frame_info.time_relative) length int(pkt.length) if proto TCP: syn 1 if int(pkt[pkt.transport_layer].flags, 16) 0x02 else 0 else: syn 0 key (src, sport, dst, dport, proto) if key not in flows: flows[key] { start: ts, end: ts, bytes_up: 0, bytes_down: 0, packets: 0, syn_count: 0, } f flows[key] f[end] ts f[packets] 1 f[syn_count] syn if src key[0]: f[bytes_up] length else: f[bytes_down] length for key, f in flows.items(): yield key, f cap.close() def main(pcap_path, contamination): X [] keys [] for key, entry in flow_generator(pcap_path): keys.append(key) duration max(entry[end] - entry[start], 0.001) up_rate entry[bytes_up] / duration down_rate entry[bytes_down] / duration syn_ratio entry[syn_count] / max(entry[packets], 1) size_ratio entry[bytes_up] / max(entry[bytes_down], 1) X.append([ np.log1p(int(key[3])), np.log1p(entry[bytes_up]), np.log1p(entry[bytes_down]), entry[packets], duration, up_rate, down_rate, syn_ratio, size_ratio, ]) X np.array(X) model IsolationForest( n_estimators300, contaminationcontamination, random_state42, ) preds model.fit_predict(X) scores model.score_samples(X) results [] for idx, key in enumerate(keys): results.append({ flow: key, anomaly: int(preds[idx]), score: round(float(scores[idx]), 4), }) results.sort(keylambda r: r[score]) print(json.dumps(results[:50], indent2)) if __name__ __main__: main(sys.argv[1], float(sys.argv[2]))运行方式python detect_offline.py /path/to/traffic.pcap 0.02。脚本输出异常分数最低的 50 条流记录数越小越异常。这里的score_samples输出的是对数似然形式的分数值越小代表越偏离正常分布。我在实际使用中会直接看排序结果的前 50 条人工核对源 IP 和目的端口模式确认哪些可以加入告警白名单。4.2 参数调优三板斧contamination、窗口长度和特征取舍第一次跑完你会发现两个问题。一是异常列表里有大量 DNS 流量因为 mDNS 和 NBNS 这类广播协议的流特征和普通业务差太远孤立森林很容易把它们标出来。解决办法是在采集层就把这些协议的端口加进过滤条件或者在特征提取时单独标记协议类型给模型一个参考维度而不是直接去掉。二是因为 time window 跨度太长一个持续了一整天的长连接跟其他短连接站在一起很容易产生误报。我常用的三个调节手段如下调大contamination会让系统挑出更多候选流适合安全人员和运营人员人工排查的场景推荐先用 0.02 跑一周观察误报数量再逐步降回 0.005。缩短流超时时间是控制特征分布的有效方式把timeout_seconds从 30 改成 5长连接会被拆成多个小段特征更均匀但代价是同一个 IP 的多段记录会带来碎片化告警。特征取舍方面先看shape的分布比如bytes_up的方差很大而syn_ratio的取值集中在 0 或接近 1直接把方差过大的特征做对数变换把方差过小的特征删除掉。最实用的验证办法是回放找一台测试主机跑一次模拟扫描生成 pcap 后输入脚本观察输出里这些模拟行为是否排在前 20 位。如果排不进说明特征提取或者参数有问题优先检查流是否被正确聚合其次是确认模拟流量没有被采集层的tcpdump过滤条件误伤。5. 避坑部署流量异常检测系统最常见的五个翻车现场5.1 抓包进程自己被系统 OOM Kill 了现象跑了一晚上第二天发现抓包进程没了pcap 文件只有前几个小时的数据。原因tcpdump -w落盘时流量高峰期 buffered 数据在内存里积压配合系统默认的 vm 参数当内存压力增大内核优先杀掉大内存进程。另一个常见原因是分析脚本里pyshark读 pcap 时没有及时释放句柄文件打开数超过 ulimit 限制。解决给抓包进程加ulimit -l unlimited和ulimit -n 65535同时用 systemd 单元把抓包配置成服务加入自动重启。落盘层改用-U参数让 tcpdump 无缓冲写入牺牲一点 IO 吞吐换来进程稳定性。# /etc/systemd/system/pcap_capture.service 核心配置 [Unit] DescriptionPcap capture service Afternetwork-online.target [Service] Userroot LimitNOFILE65535 LimitRTPRIO99 ExecStart/usr/sbin/tcpdump -i eth0 -U -G 60 \ -w /data/pcaps/capture_%Y%m%d_%H%M%S.pcap \ -z /usr/local/bin/cleanup_old.sh Restartalways RestartSec105.2 交换机镜像口丢包导致流数量对不上现象pcap 里统计的总流量和交换机流量监控面板对不上少了 30% 以上。原因镜像口带宽小于被镜像的多个端口之和或者镜像口本身工作在共享模式高峰期丢包。另一种可能是交换机镜像配置里漏掉了双向流量只镜像了入方向。解决先确认交换机的 mirror 配置里是否同时包含 rx 和 tx。如果确认配置没问题就在分析主机上跑ethtool -S eth0看网卡统计的rx_missed和rx_dropped计数。有计数持续增长说明入口丢包要么协商提升镜像口速率要么降低采样率。这里没有银弹丢包问题只能从链路容量上解决。5.3 模型把所有大流量连接都标成异常现象公司内部在做全量数据库备份时整个备份时段内所有相关流都被判定为异常告警淹没在误报里。原因孤立森林对特征的全局分布敏感大流量备份连接的bytes_up、packets等数值远远偏离日常分布的均值在特征空间里自然成为离群点。模型本身并不知道「这个时间段的带宽暴涨是计划内的」。解决在特征矩阵里加入时间窗口特征把流记录按小时分段每段单独建模。或者维护一个动态基线用前 7 天同时段的特征均值做归一化把bytes类特征替换成「相对基线倍数」。我采用的是第二种方式归一化后的特征分布更平稳计划内的带宽峰值不会轻易越过异常边界。5.4 UDP 流量检测结果一片空白现象抓到大量 UDP 流量但会话化之后发现很多条流只有上行没有下行而且源端口一直在变单独看每条流都像异常实际是正常的 QUIC 或 DTLS 流量。原因UDP 没有连接状态无法像 TCP 那样用 FIN/RST 判断流结束完全依赖超时阈值拆流。QUIC 连接因为端口和连接 ID 的随机性经常被拆成多条假流特征被搅乱。解决UDP 的流标识改成「四元组 超时无活动即结束」并把超时时间提到 60 秒。另外把 UDP 和 TCP 分开建模两类流的特征分布差异太大放一起只会互相污染。QUIC 还在演进明确针对它的检测要先解析 QUIC 连接 ID目前阶段可以先观察或者跳过。5.5 同一个源 IP 被拆出上千条短流特征矩阵爆炸现象某个主机在跑 P2P 下载连接数到了上万脚本的内存和 CPU 全部打满分析任务迟迟跑不完。原因短连接数量太多流超时判定频繁触发哈希表里堆积大量陈旧流记录。P2P 流量本身就是高连接数低字节数的模式单独一条流没有明显异常但聚合到 IP 层面才有意义。解决先按源 IP 聚合统计连接数和上下行字节再决定是否做流级分析。或者给流记录加一个总量上限超过上限时按 LRU 策略淘汰最旧条目。实际部署中我给分析脚本加了max_flows100000的限制超出后按「开始时间最早且包数最少」优先淘汰保证最长运行时间内处理完主干流量边缘流量可以后续专门分析。6. 从离线到在线怎么让它真正跑进监控体系6.1 定期任务扫描新 pcap自动化报警输出离线脚本跑通之后下一步是让它自动化。常见做法是把 pcap 采集和检测拆成两个 cron 任务采集每 60 秒一个文件检测每 5 分钟扫描一次新文件。我用一个简单的 Python 调度器循环检测 pcap 目录发现新文件就处理并写入结果表。import time import glob import os import subprocess def watch_and_detect(pcap_dir, result_dir): seen set() while True: for pcap in sorted(glob.glob(os.path.join(pcap_dir, *.pcap))): if pcap in seen: continue out os.path.join(result_dir, os.path.basename(pcap) .json) subprocess.run([ python, detect_offline.py, pcap, 0.01, out, ]) seen.add(pcap) time.sleep(30) if __name__ __main__: watch_and_detect(/data/pcaps, /data/results)这里的告警不追求实时30 秒到 60 秒的延迟对异常分析来说完全可以接受。结果 JSON 里包含流五元组和异常分数推送给告警平台或者存到 Elasticsearch 系系统里做后续搜索。我通常会把 50 条候选结果按分数分段只把前 10 条推给值班人员后面 40 条留作审计日志这样告警不会被刷屏。6.2 把检测结果聚合到主机维度流级告警最大的问题是碎片化。一个被攻破的主机对外发起扫描时会产生成百上千条异常流值班人员不可能逐条看。我在结果层加了一个聚合逻辑对异常分数排名前 50 的流记录按源 IP 分组统计每组异常流数量、涉及目标 IP 数量、平均异常分数得到一个「主机风险分」。风险分超过阈值就出一条主机级告警里面带上 TOP 流数据字段。这个聚合思路后来演变成我自己的一个习惯用法所有网络检测尽量不直接上报流级数据而是先归一到主机或子网维度。这跟误报处理方式是相辅相成的流级误报会被聚合稀释真正大规模的行为异常反而因为聚合被放大更容易一眼看出来。这套系统的价值在于你不需要商业设备就能获得一条完整的数据通路镜像流量、会话化、特征提取、孤立森林、聚合告警。调参数的感觉确实有点像玄学contamination 设 0.02 和设 0.01结果差异很大。我的做法是把参数文件和检测脚本一起纳入版本管理每次调参都留记录方便回溯。最开始我会觉得异常检测是黑匣子跑出异常列表就要人工排查。做得久了才意识到检测模型只是辅助真正的判断还是要靠对业务流量的理解。比如内网里某台机器凌晨三点和外部地址通信模型标不标异常你都得关注标出来能帮你缩小范围不标也不代表没问题。希望这套搭建流程里的细节和坑位能帮你在自己的环境里少走些弯路更快跑出一版可用的检测系统。本文还有配套的精品资源点击获取
返回列表