能使用 flowbits set、isset、toggle 在不同包之间传递状态,构建多阶段关联检测。
上一课我们靠 flow 关键字限定匹配方向(to_server / to_client)和会话状态(established),让引擎跳过大量无效匹配、降低了负载。但方向与状态约束仍然是在"看单个包":每个包要么匹配、要么不匹配,引擎依旧记不住这条流之前发生过什么。而一次入侵很少只靠一个包。攻击者先登录网站,再注入命令,最后连回反弹 shell。三个阶段,分散在多个包、多个时刻。只看单个包,每一步都像普通的 HTTP 请求。单包规则要么大量误报,要么漏掉后半程。你需要记住"这条流之前发生过什么"。比如一段被攻陷的 WordPress 站点流量:先出现登录请求,随后是含命令的注入请求,最后目标主动外连反弹 shell。多阶段攻击说明:判断不能只看最后一个包,还要看这条流之前发生过什么。
flowbits 就是用来记住这件事的关键字。它把状态写在流上,让后到的规则检查先到的证据。适合用它的场景:同一条连接里,只有先完成动作 A、后出现动作 B 才算可疑,单独看 B 会误报。
验证方法不变:用之前学过的 -T 测试模式校验语法,用 -r path-to-pcap 回放本地 PCAP,用 fast.log 核对告警。先用一个类比理解 flowbits。
核心概念
flowbits:流上的状态贴纸
生活类比:前台有一本访客登记簿,每条网络连接占一页。第一次看到可疑动作,你在这页贴上“已登记”的贴纸;几分钟后,同一页出现第二个动作,你翻回这页,看见贴纸还在,于是判定这是有前置步骤的事件。贴纸只在这页有效,换一页就看不见了。
技术定义:flowbits 是 Suricata 的流状态关键字,在同一条流上保存具名布尔状态。flowbits:set,名字; 写入状态;flowbits:isset,名字; 检查状态是否已写入;flowbits:toggle,名字; 翻转状态(已写变未写,未写变已写)。一个包上的 set 写入,稍后同一条流里另一个包上的 isset 读取——状态就这样在包之间传递。状态属于整条流,流的两个方向都能读到,在流的生命周期内持续有效;同一条流可以同时保存多个不同名字的状态。flowbits:noalert; 让规则只记录状态而不产生告警,常用于静默记录前置条件。isset 只回答“写没写过”,不回答“写过几次”;同一条流里多次触发 set,状态依然是同一个布尔值。名字是自定义字符串,多条规则之间必须完全一致。flowbits 的完整关键字列表在 Suricata 用户手册的 Flow Keywords 章节有说明。
下面用两阶段例子展示完整写法。两条规则的分工:阶段一规则负责记录,阶段二规则负责告警。拆开写的意义在于——阶段二的特征单独出现时可能是正常行为,只有同一条流里先出现阶段一特征时才算可疑。这比单条规则更精准,也更好维护。
阶段一规则匹配登录入口的访问,只写状态、不告警:
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"记录:访问登录入口"; flow:established,to_server; content:"wp-login.php"; http.uri; flowbits:set,auth.login; flowbits:noalert; sid:1000001; rev:1;)阶段二规则匹配登录之后的管理操作,只有状态存在才告警:
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"登录后访问上传接口"; flow:established,to_server; content:"POST"; http.method; content:"/admin/upload"; http.uri; flowbits:isset,auth.login; sid:1000002; rev:1;)注意顺序:阶段一规则的 set 先于阶段二规则的 isset 匹配到流量。如果同一条流里先出现上传、后出现登录,阶段二不会告警——这正是“前置条件”的含义。两条规则里的 flowbits 名字必须一模一样:auth.login 写错一个字符,关联就断了。
一条规则也可以既告警又写状态:去掉 flowbits:noalert;,匹配时既产生告警又写入状态,后续规则继续 isset。noalert 只影响本规则是否输出告警,不影响状态写入。
toggle 用于交替逻辑,每次匹配翻转一次状态:
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"翻转示例"; flow:established; content:"|0d 0a|"; flowbits:toggle,probe.state; flowbits:noalert; sid:1000003; rev:1;)示例里的 content 是占位内容,实际使用时按你自己的流量调整。msg、sid、rev、content、http.uri、http.method、flow 都是之前学过的关键字,这里只新增 flowbits。
flowbits 名字是规则之间的接口,不是给 Suricata 看的,是给你和同事看的。起名要短、有含义、带统一前缀,例如 auth.login、http.upload、scanner.scan。同一个检测场景的多条规则必须使用同一个名字。规则多了以后,统一命名能大幅减少拼写不一致导致的静默失效;规则集升级时,一致的命名也让合并与删除可审计。
flowbits 不能跨连接关联。贴纸贴在这一页,新连接是新的一页,看不到旧页的贴纸。要关联的是同一条流内的先后包或先后请求。另一个常见坑是名字不一致:set 写 auth.login,isset 查 auth_log,永远匹配不上。如果阶段二先于阶段一到达,isset 也读不到状态——顺序本身就是条件的一部分。-T 只校验语法,不会替你发现这类逻辑错误。
动手验证 flowbits 关联检测
- 1
把上面两条规则(sid 1000001 与 1000002)追加到你之前练习用的自定义规则文件末尾。
- 2
用之前学过的测试模式校验语法,确认没有解析错误。
bashsuricata -T - 3
准备一段本地授权流量:同一条 TCP 连接里先访问登录入口、再出现后续请求的 HTTP 交互。没有现成 PCAP 时,使用你之前练习的本地文件,不要对生产网络抓包。
- 4
用离线回放模式回放这段 PCAP。
bashsuricata -r path-to-pcap - 5
检查 fast.log:只应出现阶段二规则(sid 1000002)的告警,阶段一规则因为 noalert 不告警。
- 6
把阶段一规则里的 auth.login 改成 auth.log,重新回放,确认阶段二不再告警;再改回来,确认恢复。
- 7
每次修改规则后重新运行
-T,确认没有引入语法错误。
你应该看到
suricata -T 无语法错误;回放后 fast.log 只出现 sid 1000002 的告警;把 flowbits 名字改错后告警消失、改回后恢复,证明 isset 依赖的是与 set 完全一致的名字。
遇到问题:阶段二规则永远不告警,但 PCAP 里阶段一的特征确实出现过。
可能原因
- set 与 isset 的 flowbits 名字不一致:大小写、点号或下划线拼写不同。
- 阶段一和阶段二出现在不同连接里。flowbits 是流内状态,跨连接读不到。
- 阶段一规则没有被加载:没写进规则文件、被注释,或语法错误被丢弃。
- 包顺序反了:isset 先于 set 求值,状态尚未写入。
排查步骤
- 核对两条规则里的 flowbits 名字逐字节一致。
- 在 Wireshark 里确认阶段一与阶段二属于同一条 TCP 连接(同一五元组)。
- 用测试模式确认阶段一规则被加载,并检查回放日志里的规则加载信息。
- 调整 PCAP 顺序,让阶段一特征先出现;或重新设计规则触发时机。
自检:规则 A 匹配登录请求,写 flowbits:set,stage1; flowbits:noalert;。规则 B 匹配下载请求,写 flowbits:isset,stage1;。同一条流里先登录、后下载,会发生什么?先下载、后登录呢?
先登录后下载:规则 A 写入 stage1 且不告警;规则 B 匹配下载时 isset,stage1 成立,产生告警。先下载后登录:下载出现时 stage1 尚未写入,规则 B 不匹配、不告警;之后登录写入 stage1,但下载包已经处理完毕,规则 B 不会再次求值。顺序敏感性正是 flowbits 表达前置条件的方式。