- 网络安全
- 网络
- IDS
【免费下载链接】zeek
Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.
Zeek 内置的 Modbus 分析器用于监控工业控制系统中最常见的工控协议之一 Modbus/TCP。本文以 base/protocols/modbus/main.zeek 为核心骨架,完整讲解其端口注册、Modbus::Info日志记录结构、modbus.log日志流的创建、函数码/异常码的语义化映射,以及底层 binpac 解析器与 btest 测试的验证方式。读完本文,你将能够读懂并扩展 Zeek 的 Modbus 日志输出,掌握利用Modbus::log_policy钩子定制日志、结合Modbus::log_modbus事件做实时告警的方法。
一、模块概览与加载方式
Modbus 分析脚本位于 scripts/base/protocols/modbus/ 目录,属于 Zeek 的 base 脚本集,随 Zeek 默认加载。该目录的入口文件load.zeek 只有两行:
@load ./consts @load ./main即依次加载两个脚本:
- consts.zeek:定义 Modbus 函数码(function codes)与异常码(exception codes)的语义化名称表;
- main.zeek:核心分析脚本,负责注册端口、定义日志记录类型
Modbus::Info、创建modbus日志流,并消费底层分析器产生的事件。
main.zeek声明的命名空间为Modbus,并导入 consts.zeek。Zeek 官方文档对它的定位就是 "Base Modbus analysis script",即 Modbus 协议分析的基础支撑层。
二、端口注册:Modbus::ports
main.zeek中定义了一个可重定义(&redef)的常量,用于声明 Modbus 服务的已知端口:
## Well-known ports for Modbus. const ports = { 502/tcp } &redef;其完整类型为set[port],默认值为{ 502/tcp }。502 是 Modbus/TCP 的 IANA 标准端口。该选项是redefinable(可重定义)的,这意味着部署人员可以按需扩展监听端口集合,例如:
redef Modbus::ports += { 1502/tcp };在zeek_init()事件中,该端口集合被注册到 Zeek 的 TCP 应用层分析器分发机制:
event zeek_init() &priority=5 { Log::create_stream(Modbus::LOG, Log::Stream($columns=Info, $ev=log_modbus, $path="modbus", $policy=log_policy)); Analyzer::register_for_ports(Analyzer::ANALYZER_MODBUS, ports); }其中Analyzer::register_for_ports(Analyzer::ANALYZER_MODBUS, ports)告诉 Zeek 引擎:凡目的端口(或源端口)落在ports集合内的 TCP 连接,都需要尝试挂载名为MODBUS的应用层分析器。这与 Zeek 的动态协议检测(DPD)机制配合:即使流量没有落在 502 端口,只要 DPD 识别出报文特征符合 Modbus 报文结构,分析器同样会被激活(这一点有专门测试用例modbus_and_non_modbus_on_port_502.test验证端口与非 Modbus 流量的混用行为)。
三、日志记录结构:Modbus::Info
Modbus::Info是modbus.log的列定义,它是一个 record 类型,各字段均带&log属性。原文档将其列为唯一的 Types 项,逐字段说明如下:
| 字段 | 类型 | 属性 | 含义 |
|---|---|---|---|
ts | time | &log | 请求发生的时间 |
uid | string | &log | 连接的唯一标识符 |
id | conn_id | &log | 连接标识(四元组:源/目的 IP 与端口) |
tid | count | &log &optional | Modbus 事务 ID(Transaction ID) |
unit | count | &log &optional | 报文的目标单元标识符(Unit ID,即原 "slave address") |
func | string | &log &optional | 所发送功能报文的名称(如READ_HOLDING_REGISTERS) |
pdu_type | string | &log &optional | 该 PDU 是响应("RESP")还是请求("REQ") |
exception | string | &log &optional | 若响应为失败,记录异常名称 |
track_address | count | &default=0 &optional | 仅当加载了 track-memmap.zeek 策略脚本时出现,用于跟踪内存映射地址 |
其中track_address字段并不在main.zeek的定义中,而是由策略脚本通过redef record Modbus::Info += { track_address: count &default=0; }动态追加的,体现了 Zeek 脚本类型可扩展的设计。
源码中该记录的定义(main.zeek):
type Info: record { ts: time &log; uid: string &log; id: conn_id &log; tid: count &log &optional; unit: count &log &optional; func: string &log &optional; pdu_type: string &log &optional; exception: string &log &optional; };四、连接记录扩展与日志流创建
main.zeek还做了两处类型层面的"重定义"(Redefinitions):
- 扩展
Log::ID枚举:新增Modbus::LOG日志流标识:
redef enum Log::ID += { LOG };- 扩展
connection记录:为每个连接对象增加modbus字段,保存当前连接的 Modbus 状态信息:
redef record connection += { modbus: Info &optional; };modbus字段由事件处理代码在收到第一条 Modbus 报文时惰性创建,随连接生命周期保存tid、unit、func、pdu_type等状态,供后续日志写入使用。
在zeek_init()中,通过Log::create_stream注册日志流:
Log::create_stream(Modbus::LOG, Log::Stream($columns=Info, $ev=log_modbus, $path="modbus", $policy=log_policy));这里四个关键参数分别是:
$columns=Info:日志列即Modbus::Info记录;$ev=log_modbus:日志流与log_modbus事件绑定,脚本可以在事件中截获将要写入日志的记录;$path="modbus":日志文件名为modbus.log(默认写入 Zeek 的 logs 目录);$policy=log_policy:挂接一个Log::PolicyHook类型的钩子,用于在写入前对记录做过滤或修改。
五、事件与钩子:log_modbus 与 log_policy
原文档列出的两个可编程扩展点如下。
5.1 Modbus::log_modbus 事件
类型为event(rec: Modbus::Info),在每条 Modbus 记录被发送到日志框架时触发。利用它可以对日志做二次加工,例如补字段或分流:
event Modbus::log_modbus(rec: Modbus::Info) { if ( rec?$func && rec$func == "READ_HOLDING_REGISTERS" ) # 对读保持寄存器的事务做特殊处理 NOTICE([$note=Modbus_Read_Holding_Registers, $conn=rec$id]); }5.2 Modbus::log_policy 钩子
类型为Log::PolicyHook,是 Zeek 日志框架标准的策略钩子,可用来丢弃记录或改写字段。例如只保留请求方向的日志:
hook Modbus::log_policy(rec: Modbus::Info, id: Log::ID) { if ( rec?$pdu_type && rec$pdu_type != "REQ" ) break; }break会终止后续钩子并阻止该记录写入日志文件。
六、核心事件处理逻辑:请求/响应与异常
main.zeek通过两个优先级相反的事件处理程序完成日志组装,这是理解modbus.log行为的关键。
6.1 组装阶段(priority=5)
modbus_message事件由 C++ 分析器对每一条Modbus 报文触发(无论该功能码是否被进一步解析)。priority=5 的处理程序负责填充记录:
event modbus_message(c: connection, headers: ModbusHeaders, is_orig: bool) &priority=5 { if ( ! c?$modbus ) c$modbus = Info($ts=network_time(), $uid=c$uid, $id=c$id); c$modbus$ts = network_time(); c$modbus$tid = headers$tid; c$modbus$unit = headers$uid; c$modbus$func = build_func(headers$function_code); c$modbus$pdu_type = is_orig ? "REQ" : "RESP"; }headers是ModbusHeaders记录,包含tid(事务 ID)、pid(协议 ID)、uid(单元标识)、function_code(功能码)与len字段;is_orig表示报文方向:来自 TCP 发起方(client)的是请求"REQ",来自响应方(server)的是响应"RESP";build_func()负责把原始功能码转换为可读名称(见下节)。
6.2 写日志阶段(priority=-5)
同一个事件还挂了一个 priority=-5 的处理程序,负责真正落盘,并通过异常码位决定是否推迟写入:
event modbus_message(c: connection, headers: ModbusHeaders, is_orig: bool) &priority=-5 { # Don't log now if this is an exception (log in the exception event handler) if ( headers$function_code < 0x80 ) Log::write(LOG, c$modbus); }Modbus 协议约定:正常响应的功能码与其请求相同(0x00–0x7F),而异常响应的功能码会把最高位(0x80)置 1。因此:
- 若
function_code < 0x80,说明是正常请求或正常响应,立即写日志; - 若
function_code >= 0x80,说明是异常响应,暂不写日志,等modbus_exception事件把exception字段补全后再写。
6.3 异常处理
modbus_exception事件携带异常码,同样分为两个优先级:
event modbus_exception(c: connection, headers: ModbusHeaders, code: count) &priority=5 { c$modbus$exception = exception_codes[code]; } event modbus_exception(c: connection, headers: ModbusHeaders, code: count) &priority=-5 { Log::write(LOG, c$modbus); delete c$modbus$exception; }priority=5 把数值异常码查表转换为语义名称(如ILLEGAL_FUNCTION)写入exception字段;priority=-5 写入日志后删除该字段,避免影响同一连接后续报文的日志内容。测试用例 exception_handling.test 专门覆盖了异常码的解析路径。
6.4 函数码名称转换 build_func
function build_func(func: count): string { local masked = func & ~0x80; if ( func in function_codes || masked !in function_codes ) return function_codes[func]; local s = function_codes[masked]; # Suffix exceptions with _EXCEPTION. if ( func & 0x80 == 0x80 ) s += "_EXCEPTION"; return s; }逻辑说明:
- 若
func(原始功能码)本身在表中,直接返回其名称; - 若剥掉异常位后的
masked也不在表中(即完全未知的功能码),查表时触发&default分支,返回unknown-<数值>形式; - 否则,用
masked查到基础功能名,若func最高位为 1(异常响应),在名称后追加_EXCEPTION后缀。
因此异常响应日志中func字段会呈现类似READ_HOLDING_REGISTERS_EXCEPTION的形态,与exception字段共同刻画异常详情。
七、函数码与异常码常量表(consts.zeek)
consts.zeek 定义了main.zeek依赖的两张表,均由&default兜底(未知值显示为unknown-<数值>)并允许&redef扩展。
7.1 function_codes:Modbus 标准函数码
覆盖标准功能码与机器/厂商/网络专用功能码:
| 功能码 | 名称 | 说明 |
|---|---|---|
| 0x01 | READ_COILS | 读线圈 |
| 0x02 | READ_DISCRETE_INPUTS | 读离散输入 |
| 0x03 | READ_HOLDING_REGISTERS | 读保持寄存器 |
| 0x04 | READ_INPUT_REGISTERS | 读输入寄存器 |
| 0x05 | WRITE_SINGLE_COIL | 写单个线圈 |
| 0x06 | WRITE_SINGLE_REGISTER | 写单个寄存器 |
| 0x07 | READ_EXCEPTION_STATUS | 读异常状态 |
| 0x08 | DIAGNOSTICS | 诊断 |
| 0x0B | GET_COMM_EVENT_COUNTER | 获取通信事件计数器 |
| 0x0C | GET_COMM_EVENT_LOG | 获取通信事件日志 |
| 0x0F | WRITE_MULTIPLE_COILS | 写多个线圈 |
| 0x10 | WRITE_MULTIPLE_REGISTERS | 写多个寄存器 |
| 0x11 | REPORT_SLAVE_ID | 上报从站 ID |
| 0x14 | READ_FILE_RECORD | 读文件记录 |
| 0x15 | WRITE_FILE_RECORD | 写文件记录 |
| 0x16 | MASK_WRITE_REGISTER | 掩码写寄存器 |
| 0x17 | READ_WRITE_MULTIPLE_REGISTERS | 读改写多个寄存器 |
| 0x18 | READ_FIFO_QUEUE | 读 FIFO 队列 |
| 0x2B | ENCAP_INTERFACE_TRANSPORT | 封装接口传输(MEI) |
| 0x5B | OBJECT_MESSAGING | 对象消息 |
| 0x09/0x0A/0x0D/0x0E/0x12/0x13/0x28/0x29/0x5A/0x7D/0x7E/0x7F | PROGRAM_484等 | 机器/厂商/网络专用功能码 |
7.2 exception_codes:Modbus 异常码
| 异常码 | 名称 | 含义 |
|---|---|---|
| 0x01 | ILLEGAL_FUNCTION | 非法功能 |
| 0x02 | ILLEGAL_DATA_ADDRESS | 非法数据地址 |
| 0x03 | ILLEGAL_DATA_VALUE | 非法数据值 |
| 0x04 | SLAVE_DEVICE_FAILURE | 从站设备故障 |
| 0x05 | ACKNOWLEDGE | 已确认 |
| 0x06 | SLAVE_DEVICE_BUSY | 从站设备忙 |
| 0x08 | MEMORY_PARITY_ERROR | 存储器奇偶校验错误 |
| 0x0A | GATEWAY_PATH_UNAVAILABLE | 网关路径不可用 |
| 0x0B | GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND | 网关目标设备无响应 |
八、底层实现:binpac 解析器与 C++ 分析器
脚本层之上,Modbus 报文解析由位于 src/analyzer/protocol/modbus/ 的 binpac 描述文件与 C++ 代码完成(该分析器的开发得到了荷兰司法与安全部 Hermes、Castor、Midas 项目的资助,见源码文件头注释)。
8.1 TCP 报文头与 PDU 结构
modbus-protocol.pac 定义了 Modbus/TCP 的线格式。传输头为 7 字节大端序结构:
tid: uint16 # Transaction identifier(事务 ID) pid: uint16 # Protocol identifier(协议 ID,须为 0) len: uint16 # 其后负载长度(须 >= 2) uid: uint8 # Unit identifier(单元标识) fc: uint8 # MODBUS function code(功能码)对应的 C++ 侧转换函数HeaderToVal()位于 modbus-analyzer.pac,它把该头组装成 Zeek 侧的ModbusHeaders记录(字段依次为 tid、pid、uid、fc、len),供脚本层事件消费。
请求与响应分别按功能码分发:请求侧ModbusTCP_Request根据fc值选择对应的具体结构(如ReadCoilsRequest、ReadHoldingRegistersRequest);响应侧先看fc & 0x80—— 为 0 走ModbusTCP_NormalResponse,否则走ModbusTCP_ExceptResponse(只含一个code: uint8异常码字节),并在deliver_Exception中触发脚本事件modbus_exception。
8.2 协议确认(Confirmation)逻辑
modbus-analyzer.pac 中通过三个成员变量跟踪协议确认状态:
bool confirmed; // 是否已确认 bool orig_pdu; // 是否成功解析过发起方 PDU bool resp_pdu; // 是否成功解析过响应方 PDU只有当双向的完整 PDU 都被成功解析时,IsConfirmed()才返回真,分析器才会调用AnalyzerConfirmation()正式确认该连接为 Modbus。这一设计有效避免了把少量相似流量误判为 Modbus。
8.3 数据完整性校验
C++ 侧对多个功能码做了严格的长度/取值校验,违反时调用AnalyzerViolation(脚本层体现为weird.log中的bad_TCP_...类记录)或reporter->Weird,例如:
- 读保持寄存器/读输入寄存器响应的
byte_count必须为偶数(奇数时触发 violation); - 写单个线圈的值必须是
0x0000或0xFF00; - 诊断(FC=8)各子功能的数据长度与取值约束(如
RESTART_COMMUNICATIONS_OPTION只能是0x0000或0xFF00,其余大部分子功能数据须为0x0000,未知子功能触发modbus_diag_unknown_request_subfunction)。
九、事件体系:从 modbus_message 到功能级事件
由 events.bif 声明的 Modbus 事件构成两层体系:
- 通用事件:
modbus_message(任何 Modbus 报文,含未被深入解析的功能码)与modbus_exception(任何异常报文); - 功能级事件:按功能码细分的请求/响应事件,共 20 组,例如
modbus_read_coils_request/response、modbus_read_holding_registers_request/response、modbus_write_single_coil_request/response、modbus_read_file_record_request/response、modbus_mask_write_register_request/response、modbus_diagnostics_request/response、modbus_encap_interface_transport_request/response等。
功能级事件携带解析后的语义参数,例如modbus_read_holding_registers_request带start_address与quantity,modbus_write_single_coil_request的value已被转换为bool。线圈类数据被封装为ModbusCoils向量(由bytestring_to_coils()按位展开),寄存器类数据封装为ModbusRegisters向量。
测试脚本 events.zeek 逐个订阅了这些事件并打印参数,同时用grep "^event modbus_"统计覆盖度,验证轨迹modbus.pcap与modbus-eit.pcap能触发的事件数。你可以照此模式编写自己的 Modbus 事件处理脚本。
十、扩展实战:track-memmap 策略脚本
原文档在track_address字段处引用了策略脚本 scripts/policy/protocols/modbus/track-memmap.zeek。该脚本展示了对 base 脚本的典型扩展方式:
- 新增日志流
Modbus::REGISTER_CHANGE_LOG,输出到modbus_register_change.log; - 新增可配置选项
track_memmap: Host = ALL_HOSTS,可限定只跟踪特定从站地址; - 扩展
Modbus::Info,追加track_address字段; - 消费功能级事件:在
modbus_read_holding_registers_request中记录起始地址,在modbus_read_holding_registers_response中逐寄存器比对历史值,发现变化即触发Modbus::changed_register事件并写入日志,记录old_val、new_val与变化时间差delta。
启用方式为在local.zeek或命令行加载:
@load policy/protocols/modbus/track-memmap其记录结构MemmapInfo包含ts、uid、id、register(设备内存偏移)、old_val、new_val、delta七列,适合直接对接资产监控与工控异常告警场景。
十一、验证与测试
仓库在 testing/btest/scripts/base/protocols/modbus/ 提供了完善的测试用例:
- events.zeek:覆盖全部事件参数输出与覆盖率统计;
- coil_parsing_big.zeek / coil_parsing_small.zeek / register_parsing.zeek:验证线圈与寄存器的位级/字级解析;
- exception_handling.test:验证异常码处理与日志输出;
- length_mismatch.zeek:验证长度不匹配时的违规检测;
- modbus_and_non_modbus_on_port_502.test:验证 502 端口上 Modbus 与非 Modbus 流量的区分;
- policy.zeek:验证策略脚本行为。
本地复现事件测试的命令(见 events.zeek 的@TEST-EXEC行):
zeek -b -r testing/btest/Traces/modbus/modbus.pcap testing/btest/scripts/base/protocols/modbus/events.zeek运行后查看modbus.log,可以看到形如以下的记录(func为语义名称、pdu_type为REQ/RESP、异常时exception字段填充):
ts,uid,id,tid,unit,func,pdu_type,exception ...,...,...,...,...,READ_HOLDING_REGISTERS,REQ, ...,...,...,...,...,READ_HOLDING_REGISTERS,RESP, ...,...,...,...,...,READ_HOLDING_REGISTERS_EXCEPTION,RESP,ILLEGAL_DATA_ADDRESS十二、小结
从 main.zeek 这一份 "Base Modbus analysis script" 出发,可以完整还原 Zeek Modbus 分析的全链路:
- 端口触发:
Modbus::ports(默认502/tcp,可&redef扩展)注册分析器; - 报文解析:binpac 描述文件解析 Modbus/TCP 传输头与各功能码 PDU,双向成功解析后确认协议;
- 事件分发:
modbus_message/modbus_exception通用事件 + 20 组功能级请求/响应事件; - 状态组装:脚本层
build_func()完成功能码语义化,pdu_type区分请求/响应,异常码查表写入exception; - 日志输出:
modbus.log通过Log::create_stream注册,log_modbus事件与log_policy钩子提供写入前的定制入口; - 策略扩展:如 track-memmap.zeek 所示,可叠加寄存器变更跟踪等业务能力。
掌握这六个环节,你就拥有了从原始工控流量到结构化modbus.log、再到定制化告警与内存映射监控的完整工具箱。
- 网络安全
- 网络
- IDS
【免费下载链接】zeek
Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.
相关推荐
Zeek Modbus 协议分析指南:base/protocols/modbus 脚本包、事件体系与 modbus.log 日志实战
Zeek Modbus 协议分析指南:base/protocols/modbus 脚本包、事件体系与 modbus.log 日志实战 Modbus 是工业控制系
网络安全网络IDSZeek 的 Modbus 协议分析基础脚本解析:加载机制、日志字段与源码实现
Zeek 的 Modbus 协议分析基础脚本解析:加载机制、日志字段与源码实现 导读 本文围绕 Zeek 仓库中 doc/scripts/base/protoc
网络安全网络IDSZeek 中 HTTP 协议分析基础:从日志模型到源码级实现指南
Zeek 中 HTTP 协议分析基础:从日志模型到源码级实现指南 本文基于 Zeek 仓库中 scripts/base/protocols/http/main.
网络安全网络IDS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考