ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Modbus协议解析与工控取证实战:从报文到证据链

Modbus协议解析与工控取证实战:从报文到证据链 这段时间整理工控安全相关的学习笔记翻到电脑里一堆pcap文件和Modbus工具截图想起几个月前那次凌晨排查一台PLC的保持寄存器数值被人从640改成了400上位机端没有任何告警最后是靠流量里的功能码和主机内存快照还原出了完整过程。从那之后我就把“Modbus协议”和“取证”放到一起记笔记了——对于工业控制系统来说这个1979年诞生的老协议至今仍是占比极高的现场总线而它最大的特点——明文、无认证、结构固定——恰好也让它在数字取证里成为最容易出成果的协议之一。这篇笔记不是教科书复述而是把我在学习Modbus协议、搭建仿真环境、做流量和主机取证过程中踩过的坑和能直接用的方法写下来。适合三类人看一是刚接触工控安全、想搞明白Modbus到底怎么读的入门者二是做安全事件处置和电子数据取证、需要在工控场景里找线索的分析人员三是运维PLC和上位机、平时只点鼠标不看报文的朋友。内容偏实战读完你至少能自己搭一套Modbus TCP/RTU的调试环境能用Wireshark从pcap里抽出一段操作时间线也能在取证现场知道该先看哪里、怎么把流量和主机痕迹串成证据链。1. Modbus为什么能让取证工作省一半力气1.1 先搞清楚Modbus在工控系统的位置Modbus是Modicon在1979年推出的通信协议后来被施耐德电气继承再后来成为工业自动化领域事实上的开放式总线标准。你去看一个中型工厂的自动化架构上层是ERP、MES中间是SCADA和HMI底层是PLC、RTU、传感器和执行器。底层设备之间、PLC与触摸屏之间、采集器与上位机之间大量通信仍然跑的是Modbus尤其在电力、水务、光伏、楼宇自控这些行业Modbus RTU和Modbus TCP的占比相当高。为什么一个40多年前的协议到现在还这么普及核心原因就是简单。它没有复杂的握手过程没有加密认证就是“主站发请求从站回响应”一问一答的模型。一个功能码加一段寄存器地址就能把设备的开关量、模拟量、设定值全部搬上网络。对设备厂商来说实现成本极低对集成商来说调试门槛也低。但也正是这种“裸奔”特性让它成为取证分析时最容易还原现场通信的协议。我在实际处置中遇到过很多次这种情况企业的工业防火墙只监测OPC UA或者S7协议认为底层Modbus不用管结果异常操作就发生在Modbus这一层。而Modbus流量一旦被完整抓下来几乎不需要解密直接用Wireshark就能看清楚谁在哪个时间改了哪台设备的哪个寄存器。取证工作省力的前提是对协议本身足够熟悉。1.2 协议特性就是取证特性的底层逻辑Modbus最具取证价值的三点我得单独拎出来讲。第一明文传输。PLC和上位机之间的数据包寄存器地址、写入数值、功能码全部以原始二进制或可读的十六进制形式出现在网络上。这意味着一份pcap文件就等同于一卷现场监控录像。对比一下现代Web应用普遍使用TLS加密抓包之后还要面对证书和密钥问题Modbus完全没有这个门槛。你在报文里看到一个写保持寄存器的请求值是多少、写到哪直接就是铁证。第二无身份认证机制。Modbus TCP的MBAP头里有单元标识符Unit ID但它是应用层的一个编号用来区分从站并不是身份认证。也就是说任何能访问到502端口的主机都可以对PLC发起读写操作。在取证里我们通过报文里的IP地址和端口能确认“谁干了什么”但这个“谁”指的是主机不是操作者本人——所以还要结合主机日志、进程信息才能把责任人从“一台机器”推进到“一个用户”。第三结构固定、字段可预测。Modbus协议没有复杂的会话状态请求和响应一一对应Transaction ID可以和圆括号一样配对。你在Wireshark里看到请求帧很快就能找到对应的响应帧超时重发的也能分辨出来。这种结构化的特点让我用程序批量分析pcap时非常顺手一条tshark命令就能把几百兆的流量里所有写操作全部提取出来。2. 协议细节拆解RTU、TCP与功能码怎么读2.1 Modbus RTU帧结构与CRC校验Modbus最初跑在串行链路上典型的就是RS-485总线多台设备挂在一条双绞线上主站轮询从站按地址应答。RTU模式下每一帧的构成非常紧凑从站地址占1字节功能码占1字节数据区长度不定最后是2字节的CRC16校验。举个读保持寄存器的例子。主站想读地址为1的从站寄存器起始地址为0x0000数量为0x0002RTU报文就是这样的十六进制序列01 03 00 00 00 02 C4 0B分解下来0x01是从站地址0x03是功能码0x0000是起始寄存器地址0x0002是寄存器数量0xC40B是CRC16。从站的正常响应则是地址、功能码、字节数、数据、CRC。如果功能码的最高位置1比如返回0x83说明发生了异常后面跟一个异常码比如0x02表示非法数据地址。CRC16的计算方式在写单片机解析程序时是个绕不开的坎。Modbus RTU使用的是CRC-16/MODBUS算法多项式为0xA001即反向的0x8005初值为0xFFFF。实际开发中很少有人手算都是用查表法或者移位循环。我在调试串口程序时最容易踩的坑就是字节序CRC低字节在前高字节在后这跟大多数人的直觉相反。如果你拿到一帧数据怎么校验都不对先试试把CRC两个字节调换顺序。2.2 Modbus TCP的MBAP头与请求-响应模型到以太网阶段Modbus TCP把RTU的地址和CRC去掉了加了一个7字节的MBAP报文头。MBAP四个字段分别是事务处理标识符Transaction ID2字节、协议标识符Protocol ID2字节一般固定为0x0000、长度Length2字节表示后续字节数、单元标识符Unit ID1字节相当于RTU里的从站地址。看一个实际抓包。主站向192.168.1.10:502发送写单个寄存器请求00 01 00 00 00 06 01 06 00 00 01 2CTransaction ID是0x0001Protocol ID是0x0000Length是0x0006Unit ID是0x01功能码0x06表示写单个保持寄存器寄存器地址0x0000写入值0x012C即十进制的300。从站如果写成功会原样返回相同的报文这一点在取证里特别有用——写操作的请求和响应内容是镜像一致的现场很容易核对。Modbus TCP还有一个特点就是它基于TCP连接默认端口502。端口固定为取证带来的好处是你在防火墙日志和流量分析里几乎不用猜直接过滤502端口就能把工控通信从海量背景流量里切出来。当然实际项目里也有改端口的情况但大部分标准设备还是固定502。2.3 功能码、寄存器映射与字节序功能码是Modbus通信里最核心的“动作指令”日常分析里最常用的是以下几个功能码含义对应寄存器区域典型操作0x01读线圈0区输出位读取DO状态0x02读离散输入1区输入位读取DI状态0x03读保持寄存器4区读写寄存器读取设定值、运行参数0x04读输入寄存器3区只读寄存器读取采集量、测量值0x05写单个线圈0区控制单路DO0x06写单个保持寄存器4区修改单个参数0x0F写多个线圈0区批量控制DO0x10写多个保持寄存器4区批量修改参数寄存器地址的“零基”和“一基”问题也是新手容易懵的地方。Modbus协议报文里地址从0开始但很多组态软件和触摸屏上给用户显示的是地址编号比如“40001”表示第1个保持寄存器对应的协议地址其实是0x0000。这两个差1的关系在换算的时候必须注意否则你看到的数值和实际操作的寄存器会错位。数据字节序方面Modbus标准规定16位数据是大端传输也就是高字节在前。但实际设备里32位整数、浮点数怎么拆成两个寄存器各厂商的处理并不统一。有的用低位寄存器在前有的用高位在前有的浮点数按ABCD字节序有的按CDAB。做取证分析遇到32位数据时我建议先用几个已知值反推一下设备的字节排列不要直接套公式。2.4 嵌入式端的接收程序要点不少做单片机或者嵌入式开发的朋友拿到Modbus RTU帧之后要做解析。我在学习过程中也尝试用STM32写了一个简单的Modbus从站接收程序几个要点值得记下来。接收侧最核心的是串口中断里做帧超时判定。一帧结束的标志不是收到固定字节数而是串口空闲时间超过一定阈值一般是3.5个字符时间。打个比方波特率9600时3.5个字符大约是3.6毫秒左右。如果上位机发的两帧之间间隔比较小接收缓冲区没做超时判断很容易把两帧粘在一起。然后是地址判定和功能码分发。从站先看第一个字节是不是自己的地址再看功能码是否支持然后按不同功能码走不同处理分支。CRC校验建议放在完整帧收齐之后做校验失败直接丢掉不要回复异常响应——不回复本身也是一种容错策略避免故障时主站被异常响应淹没。我刚上手时犯过的错误很有意思把CRC校验放在超时判断之前结果半包数据也进了校验函数返回错误率极高。后来调试了很久才想明白串口接收本来就是拼图的思路只有等到超时判定“帧结束”了拼图才算完整才能开始校验和解析。3. 自己搭一套Modbus环境从Poll/Slave到自写报文3.1 Modbus Poll与Modbus Slave的安装与连接想学会读协议光看书不行必须自己动手抓包。我推荐的工具组合是Modbus Poll加上Modbus Slave前者模拟主站后者模拟从站。这两个工具都是商业软件但有30天评估期日常学习和测试完全够用。如果你不想折腾评估授权也可以用开源的QModMaster、Modbus Tester或者直接用Python写脚本效果是一样的。我先说最常见的TCP连接方式。安装好Modbus Slave之后选择“Connection”里的“TCP/IP”监听端口填502然后“Setup”里定义一个从站比如地址从0开始、功能码0x03保持寄存器、寄存器数量10个填一组默认值。接着打开Modbus Poll同样选择TCP/IP模式目标IP填127.0.0.1端口502从站地址Unit ID填1功能码选0x03。两边连上之后Poll窗口会以表格形式不断轮询显示Slave里的寄存器数值。这里有一个常见的坑Windows上502端口可能被某些服务占用连接会失败。可以在Modbus Slave里换一个端口测试但用502端口的好处是Wireshark能直接用“modbus.tcp”过滤器解析所以我建议测试时还是想办法把502让出来。如果系统里没跑其他服务通常不会有问题。3.2 通过虚拟串口包复现RTU生产场景TCP环境很直观但现实中RTU串口才是更常见的现场存在。模拟串口通信需要一对虚拟串口工具比如com0com或者Virtual Serial Port Driver把COM3和COM4“接”在一起。配置好之后Modbus Slave监听COM3Modbus Poll打开串口模式选COM4波特率9600、8位数据位、1位停止位、无校验两边通信就能跑通。串口模式下Modbus Poll发出的是地地道道的RTU报文用串口抓包工具或者一个串口转发小助手就能看到十六进制帧。我当时就是用串口调试助手从中间截了一段正好看到文章前面写的那帧“01 03 00 00 00 02 C4 0B”。有兴趣验证CRC的话可以把这段数据丢到在线CRC计算器里比一下能加深对RTU帧格式的印象。串口模式的另一个价值是让你体会主从轮询的实际节奏。TCP模式下请求频率可以拉得非常高但串口链路带宽有限轮询周期通常要几百毫秒甚至更长。在现场取证时如果你发现某条串口链路上Modbus请求速率异常高排除正常扫描后就要朝异常访问的方向考虑。3.3 用Python手工构造Modbus TCP请求工具能点出报文但还是建议自己写一次数据包构造这样才能真正理解字段。我用Python的socket库写了一个最简版本的Modbus TCP写寄存器请求import socket import struct def build_write_single_register(transaction_id, unit_id, reg_addr, value): mbap struct.pack(HHHB, transaction_id, 0x0000, 0x0006, unit_id) pdu struct.pack(BHH, 0x06, reg_addr, value) return mbap pdu packet build_write_single_register(0x0001, 0x01, 0x0000, 300) sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((127.0.0.1, 502)) sock.send(packet) resp sock.recv(1024) print(resp.hex()) sock.close()这段代码做的事情跟Modbus Poll点一次“写寄存器”按钮完全一样。其中struct.pack(HHHB, ...)里的“HHHB”表示按大端序打包两个双字节整数和一个单字节整数正好对应MBAP头里除了Length之外的四个字段PDU部分BHH对应功能码、寄存器地址和值。用这种方式构造报文的最大好处是你能精确控制每一个字段方便制造“异常访问”样本比如把Unit ID写成一个不存在的从站地址看从站返回什么异常码或者故意写一个超范围的寄存器地址看响应帧功能码最高位是不是变成1。这些样本在练习取证分析时非常有用比干看教程记忆深刻得多。4. 流量取证从pcap到完整操作时间线4.1 抓包位置、Wireshark过滤器与关键字段做Modbus取证流量是最客观的证据来源。抓包位置的选择直接影响证据的完整性。如果条件允许尽量在交换机的镜像端口或者部署了端口镜像的工业交换机上抓包没有镜像条件就只能在受害主机上本机抓包。本机抓包有个弱点一旦主机被人动过手脚数据包的完整性可能存疑所以现场处置时第一反应是拍照、记录主机状态再决定要不要抓包。Wireshark对Modbus TCP有完整的协议解析器直接使用显示过滤器modbus || modbus.tcp就能把Modbus流量单独筛出来。如果只想看写操作用modbus.func_code 0x06 || modbus.func_code 0x10想看单个从站的流量加一个modbus.unit_id 1。在处理VLAN环境中抓下来的pcap时先加vlan过滤器把Wireshark的协议解析链条捋顺否则Modbus解析可能不生效。打开一条写寄存器请求帧Wireshark的“Packet Details”面板会把MBAP和PDU字段一层一层列出来。我最关注的是四个信息帧时间frame.time、源IP和源端口、事务ID、功能码和寄存器地址。这四样信息拼在一起就是一句完整的话某台主机在某个时刻用某个事务ID写了某台设备的某个寄存器。响应帧里如果显示异常码还能判断这次操作是否成功。4.2 用tshark批量提取Modbus会话几十条报文手工看还行但如果抓了一小时的文件里面上千条Modbus请求就得靠命令行工具了。tshark是Wireshark自带的命令行版本我经常用下面这条命令把pcap里的写操作整理成CSVtshark -r capture.pcap -Y modbus.func_code 0x06 || modbus.func_code 0x10 -T fields \ -e frame.number -e frame.time -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport \ -e modbus.unit_id -e modbus.func_code -e modbus.regnum -e modbus.regval16 -E headery -E separator,运行完你会得到一张表每行是一次写操作的时间、来源、目标和寄存器信息。接着用Excel或者pandas透视一下按源IP分组统计就能快速看出哪台主机是写入“大户”。我一次复盘时就是靠这条命令两分钟定位到某个非标准上位机的IP在凌晨时段反复写同一组寄存器后来结合主机调查确认是一段脚本在跑。如果要在Python里做更细的分析可以用scapy库读取pcap并解析Modbus字段。scapy本身对Modbus支持不算特别完善但抓到的每个TCP载荷可以自己按MBAP格式解析。或者更直接一点用pyshark把tshark的解析能力引进来在代码里过滤指定的寄存器地址范围做自动化告警场景。4.3 证据固定与时间线重建取证讲究的是证据可验证光有一堆抓包数据还不够。我在实际工作中固定流量证据遵循三步第一步计算原始pcap文件的SHA-256哈希值记录文件大小和抓包起止时间第二步把tshark提取出的分析结果和原始文件一起归档标注分析工具版本第三步把关键报文截图存证截图里要包含报文时间、TCP会话信息和Wireshark版本号。时间线重建是整个取证过程中讲故事的部分。把抓包时间范围内所有Modbus事件按时间排开再叠加上主机的登录日志、上位机操作日志、设备报警日志就能还原出一个完整的操作链。比如我前面提到的那个凌晨事件从pcap里看到的功能码0x06写寄存器操作发生在03:12:05对应的Windows事件日志里正好有一条4688进程创建记录再加上组态软件的历史数据库里03:12:07有一条设定值变更记录三个来源相互印证时间线就非常硬了。5. 主机、内存与设备侧痕迹流量之外的证据拼图5.1 内存取证与netscan的应用光有网络流量还不够因为被改的值是“哪台机器改的”最终要落到具体机器、具体进程、具体用户上。这时就要做主机内存取证。Windows上位机通常开着组态软件、数据库客户端和一堆进程内存里往往残留着操作上下文。拿到一台开机状态的主机第一反应不是拔电源而是尽量导出内存镜像。Windows上可以用WinPmem、FTK Imager之类工具导出后再用Volatility 3做分析。该用哪个插件取决于你要找什么。重点说一下netscan。这个插件在Volatility里专门扫描内存中的网络连接对象可以列出镜像里当时的TCP、UDP连接状态。If我用它来对比流量里出现的异常源IP确认这台主机在案发时间确实与PLC的502端口建立了TCP连接。这比单纯看日志要有说服力得多因为netscan读的是内存里的实际连接结构不是应用层的日志记录抗抵赖能力更强。配合使用的还有windows.cmdline用来查看进程启动的命令行参数比如某个脚本是python跑的还是计划任务跑的windows.envars可以看环境变量有些带外采工具会在这里留下痕迹filescan可以扫描内存中打开的文件组态工程文件的路径往往就能在这里找到。5.2 Windows事件日志与上位机本地痕迹主机侧另一个重要来源是Windows事件日志。重点关注两类登录事件4624/4625和进程创建事件4688。如果你打开4688的详细日志可以看到进程命令行、运行用户名甚至进程哈希。结合案发时间如果在某个非工作时间段出现一个不常见的进程创建记录而且命令行指向一个上位机脚本这基本就是把“网络流量”和“操作者”连起来的桥梁。组态软件或者SCADA客户端的本地痕迹也不能忽视。很多上位机软件会在安装目录下生成历史报警、趋势曲线和操作日志文件。比如组态王有“历史记录”文件WinCC有SQL Server数据库存储报警和变量归档Wonderware有自己的日志。这些文件的时间戳往往精确到秒可以与pcap里的功能码时间精确对齐。还有一类容易被忽略的痕迹是“最近打开的工程文件”。通过Windows的跳转列表、注册表MRU、软件自身的最近打开列表能看出操作者最近在改动哪些工程在哪台机器上部署过什么配置。这个信息对判断异常操作是否经过“蓄意配置”很有帮助。5.3 设备侧日志与历史数据库交叉验证PLC和RTU侧也会留下痕迹但不同品牌差异很大。有的PLC自带审计日志能记录谁在什么时候改过哪些寄存器有的PLC则完全没有日志只是默默执行。这就导致我在处置中经常要退而求其次用“设备的当前值历史曲线”反推变更行为。具体做法是读取PLC的保持寄存器当前值与上位机历史数据库里记录的设定值对比。如果两者一致但历史记录里存在一次明显的时间断档说明有人绕过上位机直接改了PLC参数或者上位机的历史记录被人为删除过。我在一个项目里就是这么发现问题的历史曲线在凌晨3点12分前后出现一个“跳变”跳到新值后保持稳定而正常设定操作都在白天这才促成了对这个时间段的深度排查。交叉验证的核心原则是网络流量、主机日志、设备侧记录三方来源至少有两个能对上才敢下结论。单独一份数据缺失或者可疑都不能作为完整证据链的终点。6. 实战复盘从异常写寄存器到还原攻击路径6.1 场景设定与异常定位为了把前面的方法串起来我模拟一个经典场景。假设某水处理站一台PLC通过Modbus TCP与上位机通信地址为192.168.10.5:502。某天运维人员发现循环泵的启动频率设定值从640被改成了400导致频率下降、压力异常。上位机没有报警现场PLC这个参数的修改时间无法直接确认。第一步是找流量。如果现场有长期抓包的工业防火墙或者IDS直接从那里导pcap没有的话只能用采集器重新抓一段当前流量分析通信基线。我在这个场景里拿到的是案发时间前后一段tcpdump抓包文件。用那两条tshark命令过滤功能码0x06和0x10很快定位到一条来自192.168.10.88的写保持寄存器请求目标寄存器地址0x000A写入值0x0190十进制400时间正好在凌晨3点12分05秒。6.2 三路证据归并流量、内存、主机日志接着对192.168.10.88做主机取证。先看Windows事件日志3点12分前后有一条由administrator账号创建的进程记录命令行显示运行了一个PowerShell脚本脚本路径在D:\scripts\set_pump.ps1。再导一份内存镜像用Volatility 3的netscan插件找到该主机在3点12分左右与192.168.10.5:502建立过ESTABLISHED状态的TCP连接源端口是52341和pcap里的源端口完全一致。三个证据放一起链条就闭合了网络流量证明某台机器在某个时刻确实向PLC写入了特定值事件日志证明那台机器上有脚本进程在运行内存扫描证明这个TCP连接真实存在于主机当时的连接表中。还差最后一步——确认操作者。这一步需要登录日志查看3点在主机上交互登录的账户以及该账户在D:\scripts下的文件访问记录最后才能把证据指向具体的“人”。6.3 还原操作序列与制作证据链取证报告的叙事顺序我一般是这样组织先描述异常现象设定值变化与业务影响再给流量证据报文时间、源IP、目标寄存器、数值然后给主机侧证据进程创建时间、脚本路径、内存连接记录最后给设备侧佐证当前寄存器值与历史曲线跳变点。四层内容环环相扣每一层都附带哈希值和时间戳。如果这个事件进入司法程序证据固定环节还要额外注意原始pcap所在的移动介质要只读挂载分析过程要记录操作人、分析时间、工具版本必要时全程录屏。宁可过程繁琐一点也不要让证据链在程序上出现瑕疵。7. 常见问题排查与避坑心得7.1 抓不到包先查网卡、VLAN和端口现场处置时最让人着急的就是Wireshark明明开着却一条Modbus包都看不到。按我踩过的坑排查顺序是先确认抓包网卡是不是对应业务流量的那张网卡很多笔记本同时有无线和有线Wireshark默认可能选错再确认交换机端口的镜像配置有没有生效VLAN Trunk口抓到的包带了802.1Q标签Wireshark有时候解析不出来需要把VLAN字段展开后再套Modbus过滤器最后确认源和目的端口如果上位机改过端口号默认的502过滤器自然就白搭。7.2 帧乱码、CRC失败与字节序错位看串口抓包时看到一串数组却解析不出寄存器值先别急着怀疑工具。Modbus RTU的CRC是低字节在前如果你按高字节在前去处理校验永远通不过。寄存器里32位数据更麻烦不同厂商有不同排列。我在分析某型电能表时发现它对32位浮点数的两个寄存器排列是“低位寄存器存高16位”跟常见习惯相反害我整整浪费了一个下午。遇到这种情况最有效的办法是找几个已知水位、温度值反推现场的数据排列。7.3 Poll/Slave连不上别急着删软件连不上时先分清是TCP问题还是配置问题。TCP模式下先确认目标端口有没有被防火墙挡然后确认Unit ID和寄存器地图有没有配错最后确认从站软件的监听地址是不是绑定了某个具体IP而不是0.0.0.0。串口模式下先确认虚拟串口两边是不是同一组再确认波特率、数据位、校验位完全一致。如果两边参数都一致但连不上把轮询间隔调大一点有些从站对过快请求不处理你会看到请求发出去却一直没响应。7.4 取证现场的几个纪律性问题最后说几条纪律性经验。第一拿到目标主机或PLC之后不要急着做在线分析能先做镜像就先做镜像能先抓包就先抓包把现场状态固定下来再动手侦查本身也会留下修改痕迹。第二不要在原始证据介质上运行任何分析软件更不要用Modbus Poll直接连接生产PLC去“试一下”这一步可能改变设备状态导致证据灭失。第三取证工具版本、pcap哈希、分析时间这三样信息在任何报告里都要写全少了任何一样证据的可信度都会被打折。我个人在反复折腾Modbus协议和取证流程后的体会是这个领域没有太多花哨技巧比的就是对协议细节的耐心和对证据来源的较真。一个功能码、一个寄存器地址、一条时间戳看起来枯燥但把它们拼在一起时那种还原现场的感觉是这份工作最有成就感的地方。如果你的环境正好也有Modbus设备不妨先用本文这套方法做一次流量基线记录存好一份干净的pcap真到需要排查异常的那天你会感谢现在的自己。
返回列表