用带标签的公开流量验证 NetSentry 的检测规则与算法。
从 NetSentry 的 tester/ 拆出来独立成项目(git 历史保留),在原有的协议流量生成器
之上,加了带真值的数据集回放、逐样本判定、漏报归因和规则静态体检。
NetSentry 有两套互不相同的检测架构,任何单一平面都覆盖不全:
| Plane A(回放) | Plane B(注入) | |
|---|---|---|
| 输入 | 真实带标签 pcap | 带标签的流/连接记录 |
| 路径 | tcpreplay → veth → Rust 采集 → agent → ClickHouse |
直接写 session_flows / dns_transactions |
| 能测 | 解析器、nDPI、Suricata 签名、JA3、证书审计、内存态检测器 | 10 个计划型 SQL 检测器的判定逻辑 |
| 不能测 | 需要大样本才有统计意义的规则 | 一切依赖报文内容的东西 |
关键事实:NetSentry 没有 pcap 文件输入。 Rust 采集侧只支持 AF_PACKET / AF_XDP
活口(probe/capture/src/source.rs)。/forensics/offline/upload 那条路是另一个
基于 gopacket 的浅层分析器,不跑任何检测规则,所以拿它验证规则是无效的。回放
必须过真实网卡,这就是 Plane A 要建 veth 的原因。
KDD 不能回放。 KDD Cup 99 / NSL-KDD 是 41 维特征的连接记录 CSV,发行版里没有
报文、没有时间戳、没有 IP 和端口。tcpreplay 没有东西可发。它只能走 Plane B。
发送(回放带标签 pcap | 合成用例)→ 上线 → NetSentry 处理
→ 逐用例判定:检出 / 未检出(各带样本)→ 总体评分
一切围绕 Case:一个带标签的行为 + 期望命中的能力 + 样本定位规则。 检测器视角的报告回答不了正确的问题——「brute_force 0 命中」分不清是漏了、 是流量里本来没有、还是检测器没开。用例视角就是直接回答这个。
./tt.py doctor # 依赖自检
./tt.py sources # 哪些抓包带标签、可以评分
./tt.py cases # 有哪些合成用例,各自期望命中什么
./tt.py send synth # 发送默认合成套件(10 个用例,约 2 分钟)
./tt.py send synth:brute-force-ssh,benign-smtp # 只跑指定用例
./tt.py send ctu13 # 真实带标签抓包
./tt.py slice ctu13 --minutes 6 --from-min 70 # 长抓包先切片
./tt.py audit # 规则体检(不需要数据集)控制台(发送端 + NetSentry 结果 + 逐用例样本同屏):
./tools/webctl.sh start # http://<host>:8910不要用裸 python3 web/app.py & —— 那个进程会被启动它的 shell 带走,
而 $! 拿到的是 setsid 的 PID 不是 python 的。webctl.sh 走 /proc 找进程。
send 只接受有 <名字>.labels.json 边车的抓包。没有标签的抓包做出来的
「检出/未检出」没有参照,评分也就没有含义——所以宁可拒绝,也不给一个看起来
像测量结果的数字。tt.py sources 会把无标签的文件也列出来并说明原因。
标签粒度会一起显示,因为它决定结论的强度:
| 粒度 | 含义 | 能支撑什么 |
|---|---|---|
constructed |
本工具生成,标签精确 | 召回率 + 精确率 |
file |
整文件同一标签(如 CTU-13 的纯僵尸抓包) | 只能支撑召回率 |
host |
该主机的流量属于此标签 | 召回率,误报要另找良性流量 |
flow |
逐流标注 | 两者都可以 |
./tt.py catalog 会调查 https://mcfp.felk.cvut.cz/publicDatasets/ 里约 90 份
抓包,不下载任何 pcap —— 每份都自带 README.md(写明恶意软件家族、
被感染主机、正常主机、记载的行为)与 .capinfos(包数/体积/时长),
所以标签来自作者而不是我编的。
三类,各自买到不同的东西:
| 类别 | 数量 | 买到什么 |
|---|---|---|
CTU-Normal-* |
4 | 真实良性流量 —— 唯一能在真实流量上度量精确率的途径 |
CTU-Malware-Capture-Botnet-* |
12 | Neris / Rbot / Virut / DonBot / Sogou / NSIS.ay 等家族,行为各不相同 |
CTU-Mixed-Capture-* |
2 | 同一抓包内含正常与感染阶段 |
标签是按主机级的(作者列出被感染/正常主机),所以对任意子窗口都成立 —— 这正是前缀下载与切片在这里安全的原因。
./tt.py catalog # 调查(只抓 README 与 capinfos)
./tt.py get CTU-Normal-32 --mb 0 # 整份取(1 MB)
./tt.py get CTU-Normal-22 --mb 24 # 大文件只取前 24 MB
./tt.py merge <僵尸抓包> <良性抓包> # 合并 → 一次运行同时给召回率与精确率前缀下载:档案里的真实抓包 0.5–2 GB,链路约 125 KB/s,整份要几小时。
pcap 的字节前缀仍然是一份 pcap(头 + 记录流),所以取前 N MB 就得到一段真实、
标签仍然正确的时间前缀,代价只有 N MB。下载会停在记录中间,
ttbench/pcaptrim.py 走一遍记录链切回最后一条完整记录。
需要注意前缀是开头:有些恶意抓包前期是背景流量、感染行为出现在后段,
此时前缀里的「未检出」不能当漏报 —— 工具会把这句话写进标签边车。
合并为什么值得:纯僵尸抓包只能给召回率(在它上面说「没有误报」是同义反复),
纯良性抓包只能给精确率。合并后一次运行两个数字都有。合并会做两件事:
拒绝地址空间重叠的来源(否则一个来源的用例会匹配另一个来源的流量,
精确率会算错),以及用 editcap -t 把各段时间戳重基到前一段之后 ——
这些抓包录制时间相隔数年(CTU-13 是 2011,某些正常抓包时间戳从 epoch 起算),
直接拼接会得到一个跨 41 年的文件,1x 回放要等四十年。
好几个检测器任何公开抓包都触达不到:exfiltration 要 1 小时 5 GiB、
portscan 要 60 秒内 20 个不同目标、amplification 要 20 个反射目标、
beaconing 要 20 次低抖动回连。合成配方能精确造出这些形状,标签由构造保证。
代价写在每个用例的 rationale 里:载荷是合成的。可以检验流形状与协议解析, 不能用来评价 Suricata 签名、nDPI 置信度或证书审计——那些需要真实软件产生的 字节,只能靠回放。两者互补,谁也替代不了谁。
每个用例一行,点开有它自己的流量样本——未检出的用例同样带样本,因为 分析漏报时第一个问题就是「流量到 NetSentry 手里的时候是什么样」:
── 检出 3 项 ──
[OK] brute-force-ssh SSH 凭证暴力破解
凭据: brute_force → {"attempts":50,"avg_bytes":4622,...}
样本: session_flows 85 行 / 25 连接
── 未检出 5 项 ──
[漏报] brute-force-telnet Telnet 凭证暴力破解
原因: outside_detector_row_filter — 流量不在检测器的行级过滤范围内
细节: brute_force 的端口表不含 23(表内:21,22,25,110,143,445,...)
样本: session_flows 97 行 / 25 连接
[缺口] ctu13-clickfraud HTTP 点击欺诈
NetSentry 无对应能力,不计入召回率
漏报归因是从真值本身推导的,不让人猜「0 检出」是什么意思:
detector_disabled / detector_query_error / traffic_never_arrived /
no_rows_in_input_table / outside_detector_row_filter /
replay_speed_destroyed_the_signal / threshold_not_reached。
总分 = 100 × 召回率 × (1 − 0.5 × 误报率)
公式与每一项输入都随结果返回,方便复核。四条口径约定:
expect为空的攻击用例是「能力缺口」,不进分母。 否则等于责怪探针没有它 从未声称的功能。- 没有良性用例时精确率报空。 在纯僵尸流量上讲「没有误报」是同义反复。
- 时序敏感用例在非原速回放下不计分,标为「本速度无法评估」——那是工具的限制。
- 归不到用例的检出算
unattributed,既不算命中也不算误报。
两个检测器读的是真实时间间隔,而且失效方向相反:
beaconing要求连接间隔 5s–7200s、抖动 ≤0.25。加速回放把间隔压到MinGapSeconds=5以下,全部被丢弃 → 必然漏报。inline:portscan是 60 秒墙钟滑窗内 20 个不同目标。加速回放把 10 分钟的目标基数 压进 1 分钟 → 凭空造出扫描告警。
所以不存在"对两者都安全"的加速倍率。Replayer.plan() 在时序敏感检测器进入范围时
强制 1x,并把这个决定写进运行产物。代价是:一小时的心跳样本要花一小时。
dirFor() 只在 src 和 dst 都落在 localIPs/localCIDRs 内才返回 internal,
否则是 unknown(dispatcher.go:932-948)。lateral.go:90 硬性要求
direction = 'internal',exfiltration 默认 external_only。
回放 CIC-IDS2017(192.168.10.0/24)或 UNSW-NB15(175.45.176.0/24)而不同步配
local_cidrs,这些检测器看到的是零行——和"没有横向移动"在页面上完全一样。
每个数据集的地址段登记在 ttbench/datasets/registry.py 的 internal_cidrs,
tt.py stack up --dataset <key> 会自动对齐。
agent 启动时 mmap SHM ring;采集侧重启会建新的 ring 文件,老映射还在,于是
agent 一帧也收不到,而且没有任何报错——ipc throughput 里的 pkt= 就不动了。
顺序永远是:采集侧先起,agent 后起。tt.py capture up && tt.py stack up 与
Web 控制台的"启动栈"都是这个顺序。
./tt.py planeb nsl-kdd --compare-modes --only brute_forcefaithful:一条连接一行。检测器作者脑子里的理想形态。snapshot:复现表里真实的形态。采集侧每flow_table.snapshot_interval_ms(生产 1000ms)给每条活流发一次 FlowSnapshot,OnFlowSnapshot每个快照落一行,字节/包数是累计值 (dispatcher.go:1598-1643)。而且流在表里能活到expire_seconds(300s)才被清,期间一直被重复快照。
按 flow_hash 归并后再统计的检测器不受影响;直接 count() 行数的会把一条连接
当成多次。同一份数据跑两遍、只改形态,差出来的就是这笔代价。
实测:3000 条流在 snapshot 形态下变成 9526 行(3.2x),brute_force 的检出从 1 个
实体变成 2 个。
ttbench/datasets/registry.py 是登记表,每条都写清了 plane(格式决定它能测
什么)、标签单位、体积和已知缺陷。
大文件放数据集主机,按需拉到本地缓存,理由是实测吞吐:
数据源 → 数据集主机 ~125 KB/s
数据源 → 本机 ~75 KB/s
数据集主机 → 本机 ~15 MB/s ← 快 200 倍
所以下载只做一次,之后走私网链路暂存;也避免多 GB 抓包挤占本机只剩 ~23 GB 的根盘。 认证只用密钥——配置文件里写密码会进 git。
发行版没有 IP、端口、时间戳。本工具重建它们,规则全部与标签无关:
- 端口来自 KDD 真实的
service字段 - 实体按
(protocol_type, service, flag)分组,善恶记录同一规则 - 时间在注入时均匀铺开,善恶记录同一规则
因此绝对速率没有意义,有意义的是"同样构造下,善恶能不能被分开"——那个分离只能
来自真实的逐记录特征(端口、字节数、时长)。规则记录在
GroundTruth.provenance 里,随每次运行落盘。
- 召回率只在 NetSentry 声称能检的攻击类别上算。 拿 KDD 的
teardrop去考一个 从来没有 teardrop 检测器的探针,会得到一个很难看但毫无意义的数字。CAPABILITY_MAP里显式为None的类别记进"无对应能力",不进召回率分母。 - CTU-13 的
Background永不参与评分。 作者明确说过这部分未经确认, 在上面报一次检出既不是 TP 也不是 FP。 - 兼有善恶流的实体算 TP,并单独报告数量。 悄悄选一边都会移动数字。
每条漏报都带归因(detector_disabled / no_eligible_rows / below_threshold /
direction_not_internal / needs_realtime_replay),这样"0 检出"能读成
"前提没满足"而不是留给人猜。
tt.py CLI
ttbench/
config.py config/default.yaml + local.yaml
groundtruth.py 统一真值模型 + 攻击类别→检测能力映射
detectors.py 15 项检测能力的测试矩阵(阈值均出自源码,标了出处)
ch.py ClickHouse HTTP 客户端(无编译依赖)
inject.py Plane B 注入(faithful / snapshot 两形态)
agent.py agent HTTP API 客户端
stack.py 本工具自有的测试 agent 生命周期
capture.py Rust 采集侧生命周期 + 指标
replay.py veth、tcpreplay、速度策略、发送端统计
planea.py / planeb.py 两个平面的编排
scoring.py 混淆矩阵 + 漏报归因
audit.py 静态 + 运行态规则体检
datasets/ 数据集登记表与适配器
web/ Flask 控制台
tools/make_probe_pcap.py 极小的定向验证抓包(隔离单个检测器行为)
pcaps/samples/ 继承下来的无标签协议样本,只做通路自检
本工具只写自己的 tenant_id / probe_id。所有检测器查询都带这两个条件,
retention janitor 也只裁自己的(internal/store/janitor.go:32-58),因此测试数据
与同库里的其它数据互不干扰。./tt.py(或控制台)的清空只删自己写的行。
不要把回放指向生产探针 192.168.9.254:那是在线抓包设备,攻击样本会写进真实 ClickHouse,上面的计划型检测器还会把测试流量和真实流量关联到一起。