4. Modbus安全测试实战

Modbus是工业现场最常见的串行/以太网应用层协议之一,其安全测试的核心并不在于“扫描端口”,而在于能否精确构造并发送符合协议语义的报文,观察从站如何响应,进而判断是否存在未授权访问、功能码滥用或异常处理缺陷。本节以Scapy为工具,围绕Modbus/TCP(端口502)展开实战讲解,所有示例参数均为教学假设,实际测试须在授权环境中进行。

一、Modbus/TCP报文结构与Scapy构造基础

Modbus/TCP报文由MBAP头(7字节)和PDU组成。MBAP头包含事务标识符(2字节)、协议标识符(2字节,固定0x0000)、长度字段(2字节,表示后续字节数)和单元标识符(1字节)。PDU由功能码(1字节)和数据域组成。Scapy的scapy.contrib.modbus模块提供了ModbusADURequest、ModbusPDU等类,可直接构造读写请求。

from scapy.all import *
from scapy.contrib.modbus import *

# 构造读保持寄存器请求:单元ID=1,起始地址0x0000,读取10个寄存器
pkt = Ether()/IP(dst="192.168.1.10")/TCP(dport=502)/\
      ModbusADURequest(transId=0x0001, unitId=0x01)/\
      ModbusPDU03ReadHoldingRegistersRequest(startAddr=0x0000, quantity=10)
sendp(pkt, iface="eth0")

上述代码中,transId用于匹配请求与响应,unitId对应从站地址。发送后可用sr1()接收响应,检查ModbusPDU03ReadHoldingRegistersResponse中的寄存器值。若从站返回功能码0x83(异常响应),则说明请求被拒绝或地址越界。

二、未授权寄存器篡改与功能码滥用

未授权寄存器篡改的典型场景是:测试者未经过认证直接发送写单个寄存器(功能码0x06)或写多个寄存器(功能码0x10)请求,观察从站是否执行。例如,将保持寄存器40001(地址0x0000)的值改为0x0000,模拟关闭某阀门或复位计数器。

# 写单个保持寄存器:地址0x0000,值0x0000
pkt = Ether()/IP(dst="192.168.1.10")/TCP(dport=502)/\
      ModbusADURequest(transId=0x0002, unitId=0x01)/\
      ModbusPDU06WriteSingleRegisterRequest(registerAddr=0x0000, registerValue=0x0000)
resp = sr1(pkt, timeout=2)
if resp and resp.haslayer(ModbusPDU06WriteSingleRegisterResponse):
    print("写入成功,寄存器已被篡改")

功能码滥用则包括使用保留功能码(如0x08诊断、0x2B读设备标识)或对只读寄存器发起写操作。部分从站实现不严格,可能对非法功能码返回正常响应,或对越界地址不校验。测试时应记录功能码、地址范围、响应码,判断是否存在信息泄露或拒绝服务风险。

三、异常响应触发与结果判读

Modbus异常响应格式为:功能码最高位置1(如0x03变为0x83),后跟1字节异常码。常见异常码:0x01非法功能、0x02非法数据地址、0x03非法数据值、0x04从站设备故障。触发异常响应的方式包括:请求不存在的寄存器地址、请求数量超过上限(如读寄存器数量>125)、写入只读寄存器等。

# 触发非法数据地址异常:读取地址0xFFFF,数量1
pkt = Ether()/IP(dst="192.168.1.10")/TCP(dport=502)/\
      ModbusADURequest(transId=0x0003, unitId=0x01)/\
      ModbusPDU03ReadHoldingRegistersRequest(startAddr=0xFFFF, quantity=1)
resp = sr1(pkt, timeout=2)
if resp and resp.haslayer(ModbusPDU03ReadHoldingRegistersResponse):
    print("异常码:", hex(resp.funcCode))  # 期望0x83
    print("异常码值:", hex(resp.exceptionCode))  # 期望0x02

结果判读要点:若返回0x83且异常码为0x02,说明从站正确校验了地址;若返回正常响应但数据全为0,则可能存在地址映射缺陷;若超时无响应,可能是从站崩溃或网络阻断,需结合重试与抓包确认。

四、测试用例设计与结果记录

测试用例应覆盖正常读写、边界地址、越界地址、非法功能码、异常数量、并发请求等维度。下表给出一个可执行的用例模板,字段包括用例编号、功能码、起始地址、数量/值、预期响应、实际响应、判定结果。

用例编号功能码起始地址数量/值预期响应实际响应判定
MB-010x030x000010正常读响应0x03 + 20字节通过
MB-020x030xFFFF1异常0x83/0x020x83/0x02通过
MB-030x060x00000x0000写响应或异常0x06回显未授权写入
MB-040x080x00000x0000异常0x88/0x010x88/0x01通过

记录时建议同时保存PCAP文件、时间戳、源/目的IP、事务ID,便于回溯。对于未授权写入成功的用例,应进一步验证寄存器值是否真实改变(再次读取确认),并评估对工艺的影响。测试报告应包含风险等级、复现步骤、修复建议(如启用写保护、增加认证网关、限制功能码白名单)。

Modbus/TCP 安全测试报文交互与判读模型 测试端 (Scapy) 构造 MBAP + PDU transId / unitId / funcCode Modbus 从站 寄存器区 / 线圈区 功能码处理逻辑 请求 PDU(功能码 0x03/0x06/0x10/0x08) 正常响应(功能码回显 + 数据) 异常响应(功能码 | 0x80 + 异常码) 响应判读与安全结论 正常响应 功能码回显,数据有效 → 检查是否越权写入 异常响应 0x83/0x02 等异常码 → 校验逻辑正常 超时/无响应 从站崩溃或网络阻断 → 结合抓包与重试 测试记录要素 用例编号 | 功能码 | 地址/数量 | 预期响应 | 实际响应 | PCAP时间戳 | 风险等级 | 修复建议
提示:Scapy发送Modbus/TCP报文时,若使用sendp()需指定二层接口;若使用sr1()接收响应,建议设置timeout=2并检查resp是否为None。对于生产环境,务必先获得书面授权,并避免在业务高峰期发送写请求。
注意:未授权寄存器篡改可能直接改变PLC输出,导致阀门动作、电机启停等物理后果。测试前应确认从站处于安全状态或已隔离,必要时使用仿真从站(如Modbus Simulator)进行验证。

综上,Modbus安全测试的完整闭环是:用Scapy构造精确报文 → 发送并捕获响应 → 依据功能码与异常码判读 → 记录用例与PCAP → 评估未授权写入与功能码滥用风险。只有把“构造—发送—判读—记录”四个环节都做扎实,测试结果才具备可复现性和工程参考价值。