news 2026/10/23 11:44:37

Zeek Modbus 协议分析指南:base/protocols/modbus 脚本包、事件体系与 modbus.log 日志实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Zeek Modbus 协议分析指南:base/protocols/modbus 脚本包、事件体系与 modbus.log 日志实战
  • 网络安全
  • 网络
  • IDS

【免费下载链接】zeek

Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.

项目地址:https://gitcode.com/gh_mirrors/ze/zeek
点击查看免费下载

Modbus 是工业控制系统(ICS/SCADA)中最常见的应用层协议之一,对它的深度解析是网络流量安全监控的关键环节。本指南以 Zeek 仓库中 doc/scripts/base/protocols/modbus/index.rst 所描述的base/protocols/modbus脚本包为主线,完整讲解 Zeek 如何分析 Modbus/TCP 流量、定义 Modbus 函数码与异常码常量、产出modbus.log日志,以及如何利用脚本层事件实现自定义监控。读完本文,你将掌握 Zeek 中 Modbus 分析的完整数据流:从底层 binpac 协议解析器 → 中间层 BIF 事件 → 脚本层日志记录,并能独立编写针对 Modbus 主站/从站、寄存器读写、异常响应的检测脚本。

包结构与加载方式

base/protocols/modbus脚本包位于 scripts/base/protocols/modbus/,由三个脚本文件组成,包入口load.zeek 按顺序加载两个模块:

@load ./consts @load ./main
  • consts.zeek:定义 Modbus 标准函数码与异常码的常量映射表;
  • main.zeek:核心分析脚本,负责创建modbus日志流、注册默认端口、填充日志记录并输出日志。

在 Zeek 脚本中只需一行即可启用完整的 Modbus 分析能力:

@load base/protocols/modbus

该脚本包是基础(base)脚本,默认随 Zeek 的init-default.zeek加载,也就是说在标准安装下无需任何额外配置,Modbus 流量一旦出现就会被解析并记录。

底层协议解析器:从原始字节到 Modbus 结构

脚本层之上,Zeek 使用基于 binpac 生成的协议解析器完成二进制报文解析,源码位于 src/analyzer/protocol/modbus/,其中 modbus.pac 是解析器的总装入口,它声明了ModbusTCP分析器,并为双向流量各建立一个ModbusTCP_Flow(upflow/downflow分别对应请求与响应方向)。

Modbus/TCP 传输头(MBAP)

modbus-protocol.pac 定义了协议报文结构。每一个 Modbus/TCP PDU 由 7 字节的传输头加函数体组成,传输头字段如下:

字段宽度含义
tid2 字节事务标识符(Transaction identifier),用于请求/响应配对
pid2 字节协议标识符,Modbus 协议规定必须为 0(解析器通过&enforce(pid == 0)强制校验)
len2 字节后续字节长度,解析器要求len >= 2
uid1 字节单元标识符(Unit identifier,即传统意义上的从站地址)
fc1 字节功能码(Function code)

整体 PDU 的定义体现了"请求/响应方向决定结构"的建模思路:ModbusTCP_PDU(is_orig)依据is_orig选择解析为ModbusTCP_Request还是ModbusTCP_Response。值得注意的两点设计:

  1. 响应区分正常与异常:ModbusTCP_Response通过header.fc & 0x80判断——功能码最高位(0x80)被置位即为异常响应(ModbusTCP_ExceptResponse,仅含 1 字节异常码),否则走ModbusTCP_NormalResponse按功能码分发到具体响应结构。
  2. 未识别功能码兜底:请求与响应的 case 分发均有default -> unknown: bytestring &restofdata,对暂未建模的功能码,余下数据以原始字节保留,保证解析器不会因未知功能码而崩溃。

功能码覆盖范围

解析器在 function_codes 枚举中建模了以下功能码的请求/响应结构:READ_COILS (0x01)、READ_DISCRETE_INPUTS (0x02)、READ_HOLDING_REGISTERS (0x03)、READ_INPUT_REGISTERS (0x04)、WRITE_SINGLE_COIL (0x05)、WRITE_SINGLE_REGISTER (0x06)、DIAGNOSTICS (0x08)、WRITE_MULTIPLE_COILS (0x0F)、WRITE_MULTIPLE_REGISTERS (0x10)、READ_FILE_RECORD (0x14)、WRITE_FILE_RECORD (0x15)、MASK_WRITE_REGISTER (0x16)、READ_WRITE_MULTIPLE_REGISTERS (0x17)、READ_FIFO_QUEUE (0x18)、ENCAP_INTERFACE_TRANSPORT (0x2B)、OBJECT_MESSAGING (0x5B),以及厂商/机型相关的PROGRAM_*、POLL_*等专有功能码。READ_EXCEPTION_STATUS (0x07)、GET_COMM_EVENT_COUNTER (0x0B)、GET_COMM_EVENT_LOG (0x0C)、REPORT_SLAVE_ID (0x11)在枚举中被注释掉,未建立专门的解析结构。

解析结果的校验与确认机制

modbus-analyzer.pac 承担两层职责:

  • 协议确认:连接对象维护confirmed/orig_pdu/resp_pdu三个布尔状态。只有当双向各成功解析出至少一个完整 PDU(SetPDU置位对应方向标志)后,IsConfirmed()才返回真,此时才调用AnalyzerConfirmation()确认该连接为 Modbus。若后续报文解析失败,则会触发AnalyzerViolation。
  • 数据合法性校验:例如读保持寄存器/读输入寄存器响应的byte_count必须为偶数(寄存器 2 字节对齐),写单线圈的value只能是0x0000或0xFF00,多寄存器写入的byte_count必须与 quantity 匹配,否则报告AnalyzerViolation并拒绝该 PDU。诊断请求(FC=0x08)则依据 diagnostic_subfunctions 枚举进一步校验子功能码:数据长度必须是 2 的倍数,除 "Return Query Data (0x00)" 外其余子功能数据必须恰好 2 字节,各子功能的取值还有专门约束(如RESTART_COMMUNICATIONS_OPTION只允许0x0000/0xFF00),违规时通过 reporter 产生modbus_diag_invalid_request_data等 weird 日志。

事件体系:脚本层与解析器的接口

解析器解析出的每一个结构都会通过 events.bif 定义的 BIF 事件派发到脚本层。这些事件分为三类:

  • 通用事件(每条消息都会触发):
    • modbus_message(c, headers, is_orig):任何 Modbus 消息,无论该功能码是否被进一步支持;
    • modbus_exception(c, headers, code):任何 Modbus 异常响应。
  • 按功能码细分的请求/响应事件:modbus_read_coils_request/response、modbus_read_discrete_inputs_request/response、modbus_read_holding_registers_request/response、modbus_read_input_registers_request/response、modbus_write_single_coil_request/response、modbus_write_single_register_request/response、modbus_write_multiple_coils_request/response、modbus_write_multiple_registers_request/response、modbus_read_file_record_request/response、modbus_write_file_record_request/response、modbus_mask_write_register_request/response、modbus_read_write_multiple_registers_request/response、modbus_read_fifo_queue_request/response、modbus_diagnostics_request/response、modbus_encap_interface_transport_request/response。

所有事件均携带headers: ModbusHeaders参数,该记录由解析器在 modbus-analyzer.pac 的 HeaderToVal 中构造,包含tid、pid、uid、fc、len五个字段。线圈与寄存器值被转换为ModbusCoils(布尔向量)与ModbusRegisters(count 向量)类型的 Zeek 值:线圈数据按位解包——bytestring_to_coils中逐位取出(coils[i/8] >> (i % 8)) % 2转为布尔值;寄存器数据则按大端序拆为 16 位无符号整数。

脚本层核心:main.zeek 详解

main.zeek 是脚本包的主体,其核心逻辑分为四部分。

默认端口

const ports = { 502/tcp } &redef;

Modbus/TCP 的 IANA 标准端口为 502/TCP。zeek_init事件中通过Analyzer::register_for_ports(Analyzer::ANALYZER_MODBUS, ports)完成端口到分析器的注册。由于是&redef常量,你可以在本地脚本中扩展监听端口:

redef Modbus::ports += { 1502/tcp };

日志流与 Info 记录

脚本包redef enum Log::ID += { LOG }扩展了日志 ID 枚举,并在zeek_init中创建日志流:

Log::create_stream(Modbus::LOG, Log::Stream($columns=Info, $ev=log_modbus, $path="modbus", $policy=log_policy));

日志路径为modbus(即输出modbus.log)。Info记录的字段就是modbus.log的列,其完整定义如下:

字段类型说明
tstime请求发生时间
uidstring连接的唯一标识符
idconn_id连接四元组(orig_h/orig_p/resp_h/resp_p)
tidcount(可选)Modbus 事务标识符
unitcount(可选)单元(从站)标识符
funcstring(可选)发送的功能消息名称,如READ_HOLDING_REGISTERS
pdu_typestring(可选)REQ(请求)或RESP(响应)
exceptionstring(可选)响应失败时的异常名称

此外脚本包还通过redef record connection += { modbus: Info &optional; }在连接记录上挂载modbus字段,用于在连接生命周期内暂存待输出的记录(注意:track_address字段是在加载策略脚本 track-memmap.zeek 后才会出现)。

功能码名称映射:build_func

build_func(func: count): string函数负责把原始功能码数值翻译为可读字符串,其核心是处理异常响应标志位:

function build_func(func: count): string { local masked = func & ~0x80; # If the function code is in function_codes, use it. Also, # if the masked value isn't in function_codes, use function_codes # &default functionality. 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; }

逻辑要点:当功能码高位置位(异常响应,如0x83)但对应的正常功能码已知时,返回READ_HOLDING_REGISTERS_EXCEPTION这样的名称;当功能码完全未知时,返回function_codes表&default函数生成的unknown-<数值>字符串。

日志记录的两个优先级事件

modbus_message事件被声明了两次,利用事件优先级实现"先填充、后输出"的流水线:

  • &priority=5版本:若连接尚无modbus记录则初始化,随后依次写入ts(network_time())、tid(来自headers$tid)、unit(来自headers$uid)、func(build_func(headers$function_code))、pdu_type(is_orig ? "REQ" : "RESP")。注意这里请求/响应的判定依据是消息来源方向:来自 TCP 发起方即为请求。
  • &priority=-5版本:如果功能码小于0x80(正常响应或请求),调用Log::write(LOG, c$modbus)输出日志。

异常响应的记录则由modbus_exception事件承担:

  • &priority=5版本:把exception_codes[code]翻译后的异常名称写入c$modbus$exception;
  • &priority=-5版本:输出日志并delete c$modbus$exception清理字段,避免污染下一条消息。

因此,正常流量与异常响应最终都会进入modbus.log,区别在于异常条目多了一个exception列。测试 exception_handling.test 专门验证了模糊流量下异常处理路径的正确性——binpac 生成的解析器抛出的越界异常必须被正确捕获,且不影响 Zeek 稳定性。

函数码与异常码常量表:consts.zeek

consts.zeek 提供了两张可直接用于脚本编程的映射表,二者均带&default兜底函数与&redef属性,允许用户扩展。

Modbus::function_codes

标准 Modbus 功能码映射(节选核心部分):

功能码名称功能码名称
0x01READ_COILS0x10WRITE_MULTIPLE_REGISTERS
0x02READ_DISCRETE_INPUTS0x11REPORT_SLAVE_ID
0x03READ_HOLDING_REGISTERS0x14READ_FILE_RECORD
0x04READ_INPUT_REGISTERS0x15WRITE_FILE_RECORD
0x05WRITE_SINGLE_COIL0x16MASK_WRITE_REGISTER
0x06WRITE_SINGLE_REGISTER0x17READ_WRITE_MULTIPLE_REGISTERS
0x07READ_EXCEPTION_STATUS0x18READ_FIFO_QUEUE
0x08DIAGNOSTICS0x2BENCAP_INTERFACE_TRANSPORT
0x0BGET_COMM_EVENT_COUNTER0x5BOBJECT_MESSAGING
0x0CGET_COMM_EVENT_LOG
0x0FWRITE_MULTIPLE_COILS

同时包含厂商/机型专有功能码:PROGRAM_484 (0x09)、POLL_484 (0x0A)、PROGRAM_584_984 (0x0D)、POLL_584_984 (0x0E)、PROGRAM_884_U84 (0x12)、RESET_COMM_LINK_884_U84 (0x13)、PROGRAM_CONCEPT (0x28)、MULTIPLE_FUNCTION_CODES (0x29)、PROGRAM_UNITY (0x5A)、FIRMWARE_REPLACEMENT (0x7D)、PROGRAM_584_984_2 (0x7E)、REPORT_LOCAL_ADDRESS (0x7F)。这些厂商专用码来自公开的 Modbus 扩展资料与逆向分析(代码注释中标注了来源)。未知功能码通过&default=function(i: count):string { return fmt("unknown-%d", i); }呈现为unknown-<n>。

Modbus::exception_codes

Modbus 异常响应码(响应功能码最高位置位时携带的 1 字节异常码):

异常码名称异常码名称
0x01ILLEGAL_FUNCTION0x08MEMORY_PARITY_ERROR
0x02ILLEGAL_DATA_ADDRESS0x0AGATEWAY_PATH_UNAVAILABLE
0x03ILLEGAL_DATA_VALUE0x0BGATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND
0x04SLAVE_DEVICE_FAILURE
0x05ACKNOWLEDGE
0x06SLAVE_DEVICE_BUSY

未知异常码同样回退到unknown-<n>格式。由于两张表均为&redef,可以在本地策略脚本中为私有功能码补充名称:

redef Modbus::function_codes += { [0x6B] = "VENDOR_READ_REGISTERS" };

modbus.log 实战解读

以一段典型的"主站读取保持寄存器"交互为例,modbus.log中会产生两条记录:

#separator \x09 #fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p tid unit func pdu_type exception 192.168.1.10 CFG... 192.168.1.10 49152 192.168.1.20 502 4521 1 READ_HOLDING_REGISTERS REQ - 192.168.1.10 CFG... 192.168.1.10 49152 192.168.1.20 502 4521 1 READ_HOLDING_REGISTERS RESP -

若从站返回异常(例如功能码 0x83、异常码 0x02),则响应记录变为:

... tid=4521 unit=1 func=READ_HOLDING_REGISTERS_EXCEPTION pdu_type=RESP exception=ILLEGAL_DATA_ADDRESS

关键点:请求与响应的tid相同(事务配对依据),pdu_type区分方向;异常响应的func带_EXCEPTION后缀且exception列有值。这些字段组合可用于检测异常率过高、非法从站地址访问等 ICS 安全场景。

策略脚本:主从设备发现与寄存器内存映射跟踪

除了基础脚本包,scripts/policy/protocols/modbus/ 还提供了两个可选策略脚本,可按需@load。

known-masters-slaves.zeek:发现 Modbus 主站与从站

known-masters-slaves.zeek 监听modbus_message事件,将连接的发起方(c$id$orig_h)识别为MODBUS_MASTER、响应方(c$id$resp_h)识别为MODBUS_SLAVE,维护带 1 天过期时间的集合modbus_nodes,并将新发现的设备写入known_modbus.log(列:ts、host、device_type)。示例输出:

#fields ts host device_type 2026-10-08T19:40:59Z 192.168.1.10 MODBUS_MASTER 2026-10-08T19:40:59Z 192.168.1.20 MODBUS_SLAVE

track-memmap.zeek:跟踪保持寄存器内存映射变化

track-memmap.zeek 是更具纵深价值的脚本:它维护每个从站设备的寄存器内存映射device_registers: table[addr] of Registers(Registers为table[count] of RegisterValue,记录last_set时间与value)。工作流程如下:

  1. 在modbus_read_holding_registers_request事件中,用c$modbus$track_address = start_address+1记录本次读操作的起始地址(注意脚本从 1 开始计数,与协议中 0 基地址相差 1);
  2. 在modbus_read_holding_registers_response事件中,若响应方匹配track_memmap选项(默认ALL_HOSTS,可通过option track_memmap: Host = ...限制仅跟踪特定主机),则逐寄存器比对旧值与新值;
  3. 值发生变化时触发changed_register事件,计算变化时间间隔delta,写入modbus_register_change.log(列:ts、uid、id、register、old_val、new_val、delta)。

该脚本对于检测 PLC 寄存器被非法改写(如针对 RTU/PLC 的恶意写操作)非常实用,测试与日志样例可参考 testing/btest/scripts/base/protocols/modbus/ 目录下的用例。

自定义检测脚本示例

结合基础脚本包提供的事件与常量,可以快速编写针对性检测逻辑。以下示例检测对保持寄存器的异常写入(写单寄存器功能码 0x06),并输出 notice:

@load base/protocols/modbus event modbus_write_single_register_request(c: connection, headers: ModbusHeaders, address: count, value: count) { if ( address < 100 ) { NOTICE([$note=Weird::Activity, $msg=fmt("Modbus write to protected register %d = %d (unit %d)", address, value, headers$uid), $conn=c]); } }

而 testing/btest/scripts/base/protocols/modbus/events.zeek 展示了官方测试中如何逐一打印全部 Modbus 事件,适合作为学习事件参数的参考模板;coil_parsing_big.zeek 与 register_parsing.zeek 等用例则覆盖了线圈位解析、寄存器数组解析等边界场景。

小结

Zeek 对 Modbus 的分析是一条完整的流水线:binpac 解析器(src/analyzer/protocol/modbus/)把 Modbus/TCP 报文解析为带强校验的结构 → 解析结果经 events.bif 的 30 余个事件派发到脚本层 → main.zeek 与 consts.zeek 完成功能码/异常码翻译并输出modbus.log。在此之上,策略脚本 known-masters-slaves.zeek 与 track-memmap.zeek 提供了设备发现与寄存器变化跟踪能力,所有细节均可通过 testing/btest/scripts/base/protocols/modbus/ 下的测试用例验证。无论是常规流量审计还是 ICS 安全监控,这套脚本包都能直接作为基线使用,并可通过redef、事件处理与Log::PolicyHook(Modbus::log_policy)自由扩展。

  • 网络安全
  • 网络
  • IDS

【免费下载链接】zeek

Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.

项目地址:https://gitcode.com/gh_mirrors/ze/zeek
点击查看免费下载
上一篇:医学影像分割神器ITK-SNAP:3步学会精准分割脑部MRI图像
下一篇:如何永久保存微信聊天记录:WeChatMsg完整指南与实用技巧

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 8:02:20

基于Simulink的蓄电池与超级电容混合储能能量管理仿真模型解析

做混合储能仿真这几年&#xff0c;我最大的感受是&#xff1a;电池和超级电容的组合&#xff0c;本质上是一种工程上的“取长补短”。单一储能元件很难同时扛下长时功率支撑和短时冲击响应&#xff0c;而把两者混联再用一套靠谱的能量管理策略去分配功率&#xff0c;就能让电池…

作者头像 李华
网站建设 2026/10/21 10:11:12

基于Nexus 7000的数据中心网络建设方案:从vPC到安全域划分的落地实践

简介&#xff1a;这份《数据中心建设方案》文档面向网络工程师、系统架构师及信息化项目规划人员&#xff0c;系统讲解数据中心从架构设计到落地实施的关键环节&#xff0c;帮助读者理解如何构建高可用、可扩展且绿色节能的数据中心。资源包内含1个doc文件&#xff0c;大小约2.…

作者头像 李华
网站建设 2026/10/21 10:11:14

Playnite 主题定制指南:如何修改 XAML 给游戏库加上动效

Playnite 主题定制指南&#xff1a;如何修改 XAML 给游戏库加上动效 【免费下载链接】Playnite Video game library manager with support for wide range of 3rd party libraries and game emulation support, providing one unified interface for your games. 项目地址: h…

作者头像 李华
网站建设 2026/10/21 10:11:15

Django还是Flask?仓库库存管理系统实战选型与完整实现

做仓库库存管理系统的时候&#xff0c;我其实在Django和Flask之间纠结了整整一个晚上。后来想明白一件事&#xff1a;这不是“哪个框架更好”&#xff0c;而是“这个系统真正需要什么”。如果你也正准备用Python写一套仓库库存管理系统&#xff0c;或者正卡在Django/Flask选型上…

作者头像 李华
网站建设 2026/10/21 10:11:14

简单的yolo识别训练并移植到ROS1

第一步&#xff1a;本地环境检测与创建&#xff08;核心&#xff09;训练模型对显卡&#xff08;GPU&#xff09;有硬性要求。在本地 Dell 电脑上&#xff0c;我们需要建立一个支持 CUDA 加速的 PyTorch 环境。1.1 硬件与驱动检查在 Dell 电脑上打开普通的命令提示符&#xff0…

作者头像 李华