Modbus协议取证指南:从报文结构到现场实战 📅 发布时间:2026/9/15 6:56:53 👁 浏览次数: 1. 从取证视角重读 Modbus为什么越简单的协议越能说明问题入行头几年我对 Modbus 协议的印象就是老1979 年定型的串行通信协议报文格式简单得近乎朴素。直到后来做工业现场的安全事件分析我才意识到正是在这种朴素之下Modbus 协议反而成了取证分析里最容易还原的一类协议。它没有认证、没有加密甚至没有复杂的会话协商所有控制动作都以固定字段平铺在报文里主站是谁、从站是谁、读写哪个寄存器、写入什么值一读便知。这篇学习笔记想讲清楚三件事Modbus 协议本身的字节级结构取证时如何处理这些报文以及流量之外还有哪些痕迹值得收集。适合正在做工控安全、蓝队分析、OT 运维以及刚接触 Modbus 想建立整体认识的开发同学。简单说这不是一篇教你写 Modbus 程序的开发教程而是一份站在事后还原角度去理解协议的记录。1.1 取证要回答的三个问题正好对应报文的三个字段做取证本质上是回答三个问题谁在什么时间对什么设备做了什么操作。这话听着空但放到 Modbus 报文里几乎是一一对应的。谁对应源 IP、源端口、单元标识符Unit ID。在 Modbus TCP 里源 IP 就是主站地址端口通常是 502 或临时端口单元标识符则指向网关后面的具体从站。什么时间对应抓包时间戳 frame.time_epoch同时也体现在事务标识符的递增规律里。同一主站连续发起请求时事务标识符通常是顺序递增的这种序号本身就是时间轴上的一种旁证。对什么设备做了什么操作对应功能码、寄存器地址和数据值。功能码告诉你读还是写寄存器地址告诉你操作对象的编号数据值告诉你最终落进去的字节。这就是我为什么反复强调从协议本身开始学取证——其他协议你还要处理加密、分片、会话状态Modbus 几乎把答案直接写在明面上。但反过来它也有绕人的地方因为一个 IP 下可能挂着多个串口设备而单元标识符是可以被改的所以单纯看 IP 并不能确认物理设备必须把网关拓扑理解清楚。1.2 开发视角和取证视角的差异很多工程师写 Modbus 程序的时候只关心两件事怎么组帧能通过对方校验怎么解帧能拿到自己想要的字段。开发时收到一帧 CRC 错的报文通常会选择丢掉然后等下一帧没人会追究它为什么错。取证视角完全相反。CRC 错误的帧不是垃圾它可能说明串口链路有干扰、某个从站波特率配置不一致甚至是有第三方设备在抢总线。异常响应帧也不是协议错误那么简单0x83、0x90 这类响应往往记录了从站对非法请求的直接拒绝这本身就是行为证据。开发时可以把单帧提取出来做单元测试取证时则必须把几千帧请求放在同一条时间线上看事务标识符有没有异常跳跃、看重传是不是集中在某个时刻、看请求频率符不符合正常的工艺节拍。所以带着取证目的重读一遍 Modbus 协议很多以前觉得无关紧要的字段现在全都有意义了。2. 三种 Modbus 形态的字节级拆解RTU、ASCII、TCPModbus 实际上是一个应用层协议它可以用在串行链路RS-232/RS-485也可以直接跑在 TCP/IP 之上。串行链路上又有 RTU 和 ASCII 两种编码方式。现场取证时最常见的是 Modbus TCP 和 Modbus RTUASCII 多见于老设备但偶尔也会遇到。2.1 Modbus TCP先看 MBAP 头Modbus TCP 的报文由 MBAP 头加 PDU 组成。MBAP 头一共 7 个字节我直接用一个实际抓包里的读保持寄存器请求来说明12 34 00 00 00 06 FF 03 00 6B 00 03拆开来看字段长度示例值含义事务标识符2 字节0x1234用于请求-响应配对协议标识符2 字节0x00000 表示 Modbus长度2 字节0x0006后续字节数单元标识符1 字节0xFF下游从站地址功能码1 字节0x03读保持寄存器起始地址2 字节0x006B寄存器偏移寄存器数量2 字节0x0003读取 3 个寄存器这里最容易被忽视的是事务标识符。在同一个 TCP 连接里Modbus 允许主站连续发多个请求而不等响应响应返回时怎么知道对应哪个请求就是靠事务标识符配对。取证时如果某个事务标识符只出现了请求没有响应那就要特别注意可能主站没收到响应就超时重发也可能从站在处理过程中重启了。另外再说一个容易搞混的地方单元标识符并不总是等于从站地址。在纯 Modbus TCP 设备上它常常是 0xFF 或者 0x01在网关后面挂串口从站的场景里它才真正对应 RS-485 上的从站地址。分析的时候先搞清楚现场拓扑再下结论。2.2 Modbus RTU帧边界和 CRC 是取证分水岭Modbus RTU 没有 MBAP 头它的帧结构更紧凑01 03 00 6B 00 03 CRC_L CRC_H1 字节从站地址1 字节功能码N 字节数据2 字节 CRC16低字节在前RTU 的帧边界靠静默时间界定帧与帧之间至少要间隔 3.5 个字符时间超过 1.5 个字符时间就认为一帧结束。这个特性在串口取证里非常重要——如果你拿到一段原始的串口字节流必须先按时间间隔把帧切出来再校验 CRC才能开始解析功能码和数据。我见过不少人在这一步出错直接用工具强制按固定长度切帧结果把两个相邻请求切进同一帧里CRC 永远算不对最后怀疑设备坏了。实际上只要把抓包工具的采样时间戳打出来按 3.5 字符时间作为分隔点重新切片大部分所谓乱帧都能恢复正常。CRC16 的计算多项式是 0xA001初始值是 0xFFFF。取证时如果只需要批量验证帧完整性一个小脚本就能解决def modbus_crc(data: bytes) - int: crc 0xFFFF for b in data: crc ^ b for _ in range(8): if crc 1: crc (crc 1) ^ 0xA001 else: crc 1 return crc注意 CRC 发送时是低字节在前所以上面示例里的CRC_L CRC_H顺序不能反。这个细节看起来基础但现场因为字节序看反导致误判的情况特别多。2.3 Modbus ASCII还在喘气的老爷车Modbus ASCII 的帧以冒号:开头以回车换行结束中间每个字节用两个 ASCII 十六进制字符表示校验方式是 LRC纵向冗余校验。它比 RTU 冗长一倍传输效率低但现在还有一部分老旧 SCADA 系统在用。取证时遇到 Modbus ASCII好消息是它直接可读坏消息是如果你在网络上抓到的已经是解码后的 ASCII 流那就需要先做一层字符还原把十六进制字符转回原始字节再按协议字段切开。老系统的资料往往不全这时候最好的参考就是设备铭牌和组态软件里的通信参数波特率、数据位、校验位都会影响还原结果。2.4 寄存器模型和地址偏移的坑Modbus 的数据对象分四类对象功能码地址区读写属性线圈0x01 读 / 0x05 写单个 / 0x0F 写多个00001 起可读可写离散输入0x0210001 起只读输入寄存器0x0430001 起只读保持寄存器0x03 读 / 0x06 写单个 / 0x10 写多个40001 起可读可写这里的地址编号逻辑是一个著名的大坑。组态软件界面上显示的 40069对应到协议报文里的起始地址往往不是 69而是 680x0044。更麻烦的是有些软件会直接显示协议偏移地址有些显示的是带区号的绝对地址中间可能还差着 10001、30001、40001 这些偏移。我在现场见过有人拿着抓包文件里的 0x006B十进制 107直接去对照组态软件的 40069怎么都对不上。正确做法是一切以报文里的 16 进制地址为准然后再按厂商手册做一次偏移换算。此外Modbus 默认大端字节序也就是说一个 16 位寄存器的高字节在前。但很多设备实现 32 位浮点时会用 CDAB 甚至 BADC 的字节序这在没有设备手册时很容易把正常数据误判成异常写入。我的习惯是先用一组已知物理量反推字节序比如液位计的当前液位值读出来应该是多少米再对照报文里四个字节的排列就能确定这台设备到底怎么排的。3. 流量还原实操如何从抓包里复原一次完整控制操作这一章是动手部分。前面把协议字段讲清楚了现在说说实际取证流程中我会怎么做以及哪些细节最耽误时间。3.1 抓包之前镜像端口、pcapng 和最小干扰取证抓包之前先想清楚两个问题抓哪个位置以及怎么抓不干扰生产。在交换机组网的环境里首选交换机镜像端口把连接 PLC、RTU、历史数据库的端口流量复制一份出来。如果没有镜像端口也可以串接一个 TAP 或者用工业串口分流器但任何物理串接都必须先在停机窗口验证一次别让取证动作本身造成业务中断。抓包文件格式我坚持用 pcapng。它比 pcap 多了接口信息和注释字段能在文件里记录抓包主机名、接口名、操作系统版本。这些元信息在证据移交时很重要Pcap 格式存不了这些。抓完之后第一步是计算 SHA256 哈希再把原始文件做只读备份之后所有分析都基于副本。现场采集我通常用 dumpcap 而不是 Wireshark 图形界面因为它开销小、稳定、适合长时间运行dumpcap -i eth0 -b filesize:102400 -b files:20 -w fw_capture.pcapngfilesize单位是 KB102400就是 100MB 一个文件环形保留最近 20 个文件。这个配置跑几天都不会把磁盘写满而且事故发生后能保证拿到事故窗口前后的数据。3.2 用 Wireshark 过滤器定位写操作打开 pcapng 之后第一件事不是翻报文而是用过滤器缩小范围。基础过滤是tcp.port 502但在混合流量的 OT 环境里我习惯把过滤器写成这样只看 Modbus 协议modbus只看写操作modbus.func_code 0x06 || modbus.func_code 0x10只看异常响应modbus.func_code 0x80只看某个从站modbus.unit_id 0x01有一点要提醒不同 Wireshark 版本的字段族可能是modbus旧版本里也出现过mbtcp的情况。批量导出前先花十秒钟确认字段名比导出后发现全是空值再回头查要快得多。命令是tshark -G fields | grep -i modbus批量提取写操作并导出 CSV我常用的命令是这个模板tshark -r fw_capture.pcapng \ -Y tcp.port 502 (modbus.func_code 6 || modbus.func_code 16) \ -T fields \ -e frame.time_epoch -e ip.src -e ip.dst \ -e tcp.srcport -e tcp.dstport \ -e modbus.unit_id -e modbus.regnum -e modbus.value \ -E headery -E separator, write_ops.csv导出后我会用 Excel 或 pandas 按时间排序看有没有集中的、异常频率的写操作窗口。这一步往往比逐个翻包高效得多。3.3 用事务 ID 还原请求-响应配对Modbus TCP 允许同一连接并发多个请求所以判断这次写入是否成功不能只看请求帧还要找到对应的响应帧。配对依据是四元组源 IP、源端口、目的 IP、目的端口加事务标识符。举个例子主站发了一个写多个保持寄存器的请求12 35 00 00 00 0B 01 10 00 6B 00 03 06 00 01 00 02 00 03字段拆开事务标识符 0x1235协议标识符 0x0000长度 0x000B单元标识符 0x01功能码 0x10写多个保持寄存器起始地址 0x006B数量 3字节数 6数据 0x0001、0x0002、0x0003。正常响应应该是12 35 00 00 00 06 01 10 00 6B 00 03它回显了起始地址和数量但不回显数据值。如果你在取证时发现请求里写的值是 0x0001而设备实际状态却是 0x0002那问题就不在通信层而在设备端的逻辑处理或另一个主站也在写同一个地址。这个时候再继续抓别的报文看看是不是有第二台主站也在往这个寄存器写。同一个 TCP 连接里如果事务标识符出现大量重复且伴随着 TCP 重传不要急着下结论。当连接断开重连后很多主站软件的事务标识符会重新从 0 开始计数。这种重复不代表重放攻击必须结合 TCP 连接的四元组和连接建立时间来判断。3.4 modbus poll / modbus slave 在取证中的正确位置modbus poll 和 modbus slave 是工控圈最常用的两个调试软件一个模拟主站一个模拟从站。它们在开发调试里是主角但在取证里更像验证工具。我遇到过一个场景抓包发现某主站不断地对同一台从站写同一个值频率非常规律每 500 毫秒一次。为了确认这个写入会不会真的改变设备状态我用 modbus slave 在测试环境里模拟一台相同寄存器映射的设备再用同样的频率回放写请求观察外部输出。这个验证不是为了证明攻击存在而是为了弄清写入值在不同初始条件下会驱动什么行为。从站模拟验证的要点测试环境的寄存器映射、字节序、功能码支持必须和现场设备一致否则验证结果没有说服力。官方软件自带免费模式和试用期对单个从站的验证完全够用如果要做批量自动化回放我建议直接用 PyModbus 写脚本比手工点界面高效得多。还有一个纪律必须遵守所有模拟流量都要单独抓包保存不能混入原始的取证 pcap 里。否则将来证据链解释不清哪些是现场真实流量、哪些是复现验证流量会直接影响结论的可信度。4. 网络之外的取证现场固件、内存与 SCADA 日志的时间线流量不是全部。很多关键信息存在于上位机的内存、主机痕迹、设备固件和 SCADA 日志里这些数据和抓包互相印证才能形成完整的证据链。4.1 Windows 内存镜像里的 502 连接痕迹Windows 工控上位机的内存镜像里可以用 Volatility 的 netscan 插件直接扫 TCP 连接表volatility -f mem.raw --profileWin10x64_19041 netscan输出里如果有对 PLC IP 的 502 端口连接后面会带着进程 PID。再用 pstree 和 cmdline 插件查这个 PID基本就知道是哪类程序在维持连接了。需要注意netscan 只能反映内存镜像那一瞬间的连接状态如果事件已经发生一段时间连接可能早已断开。这时候可以在内存字符串里搜 Modbus 相关的特征片段比如读寄存器的功能码 0x03、写多个寄存器的 0x10 等但因为 Modbus 没有魔数直接搜字节模式会产生大量误报。我的做法是先用 netscan 锁定可疑进程再用 memdump 导出该进程内存最后在进程内存里做定向搜索缩小范围。4.2 Windows 主机痕迹从 Prefetch 到操作历史上位机上如果运行过 modbus poll、测试脚本或第三方组态工具会留下不少痕迹Prefetch 文件记录程序首次加载路径和依赖 DLLAmcache 和 ShimCache 记录程序执行时间和路径最近打开的文档、PowerShell 历史、事件日志里也可能有线索之前处理过一台被写入异常值的工作站内存和 pcap 都指向某个自动化脚本在发起写操作。顺着主机痕迹排查时在 PowerShell 历史文件里看到了一段调用 Modbus 库的脚本片段脚本里硬编码了要写入的寄存器地址和值。流量只是在网络侧给出发生了主机痕迹则给出了谁让它发生的两者合起来才算完整。4.3 固件里的寄存器映射和配置线索Modbus 设备取证很容易忽略固件这一层。很多串口服务器、协议网关、智能电表的固件是嵌入式系统备份出来之后用 binwalk 解包可以在文件系统里看到设备配置、寄存器映射表、默认单元标识符等信息。这些配置看起来不起眼但在解释为什么是这些地址被访问时非常有用。比如流量里出现对 0x006B 地址频繁写入设备手册里没写这个地址的含义固件配置里却明确写了它是流量累计值的偏移。这就是把网络行为翻译成业务行为的关键一跳。固件提取要注意版本一致性同型号设备不同固件版本的寄存器映射可能有差异不能想当然拿另一台的配置来套。4.4 SCADA 日志与跨源时间线对齐SCADA 组态软件比如 kingSCADA、WinCC、InTouch有自己的操作记录和报警记录。取证时把这些记录和抓包、主机日志放到同一张时间线表里往往能还原出完整的事件经过。数据源能提供什么时间戳精度最大风险抓包原始请求/响应精确到微秒高抓包主机时钟漂移内存镜像进程与连接关系快照中只反映瞬间状态主机痕迹程序执行路径、脚本内容中可能被清除SCADA 日志操作员动作、报警低秒级时区和本地时间混淆时间线对齐时最容易出问题的就是时钟偏移。有一次我发现抓包里写操作的时间比 SCADA 报警时间晚了 4 秒反复排查后才发现 PLC 的时钟就比抓包主机慢了 4 秒。所以取证开始前先把各主机的时间偏差记录下来最好是用 UTC 统一表述报告里注明每个数据源的时间坐标系。时钟不同步的现场再精准的报文时间戳也没有意义。5. 工具链与时间戳的取舍现场取证的踩坑清单最后一章说点工具链和实际踩坑经验。这些东西教科书很少写但在现场处置时特别容易耽误时间。5.1 采集中常用的三个命令组合除了前面提过的 dumpcap 环形缓冲还有两个命令我每次都会用到。按时间窗口切分大 pcapngeditcap -A 2024-01-01 08:00:00 -B 2024-01-01 09:00:00 fw_capture.pcapng onehour.pcapng把多个接口文件合并mergecap -w all.pcapng eth0.pcapng eth1.pcapng这些操作都建议在副本上进行原始 pcapng 文件存一份带 SHA256 归档就行。取证分析阶段改错了文件不要紧原始证据没被污染才要紧。5.2 Wireshark 时间显示和导出时间戳的注意点Wireshark 默认显示本地时间如果你分析时跨时区或者系统时区设置不对界面上的时间会误导判断。我习惯把显示偏好改为 UTC同时在导出字段时用 frame.time_epoch这样无论谁拿到数据都能知道准确的绝对时间点。导出 CSV 后用 pandas 处理时注意把 epoch 秒和纳秒分开。有些版本的 Wireshark 会把纳秒精度的 epoch 时间戳导出成浮点数直接转字符串再比较大小会出错。稳妥做法是保留两列一列整数秒一列剩余纳秒。5.3 常见误判速查表现场情况容易被误判成实际情况大量 0x83 响应攻击行为从站返回异常码先看后 1 字节的异常码再判断事务 ID 重复重放攻击TCP 重连后事务标识符从 0 重新计数CRC 校验失败设备故障串口参数错误、线路干扰、第三方抢占总线都可能寄存器值看起来乱恶意写入设备用非标准字节序先用已知物理量反推502 端口没有包没有 Modbus 流量设备可能走 RTU 挂网关网络侧看不到 RTU 帧第一条值得多说一句功能码 0x03 的异常响应是 0x83后面还要跟一个异常码。01 表示非法功能02 表示非法数据地址03 表示非法数据值04 表示从站设备故障。如果不看异常码看到 0x83 就紧张很容易把一次正常的越界读请求误判成攻击。反过来如果异常码是 02 且集中在某个地址区间那真的要怀疑是不是有人在扫描寄存器地址空间。5.4 如果是从零写单片机 Modbus 程序这份笔记同样有用热搜里有很多人在搜51 单片机 Modbus 主站程序Qt 串口线程接收 Modbus 帧这说明大量的使用者其实是设备开发者。如果你正处在写从站程序的阶段我强烈建议把 CRC 低字节在前和 3.5 字符时间帧间隔这两个细节刻在脑子里。它们既是设备组帧的正确姿势也是日后出了问题做协议取证时最先要检查的两个点。开发时把每一帧的十六进制日志打出来看起来费时间但将来设备到现场出问题这些日志就是最宝贵的第一手证据。我见过太多现场事故设备厂商远程支持时什么都看不到因为产品没有帧级日志只能靠现场重新抓包而重新抓包往往错过了事故窗口。经历过几次现场取证之后我最大的体会是Modbus 取证不是靠某个神器突然抓出真相而是把协议细节、流量关联、主机痕迹和现场时钟一张一张对齐。平时在关键 SCADA 和 PLC 之间留一份周期抓包归档出事后才有对比的基线每次取证前先把抓包主机的系统时间校到 UTC并且记录现场所有主机的时间偏差。这两个动作几乎不花钱却能在关键时刻省下一个通宵。