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-01 | 0x03 | 0x0000 | 10 | 正常读响应 | 0x03 + 20字节 | 通过 |
| MB-02 | 0x03 | 0xFFFF | 1 | 异常0x83/0x02 | 0x83/0x02 | 通过 |
| MB-03 | 0x06 | 0x0000 | 0x0000 | 写响应或异常 | 0x06回显 | 未授权写入 |
| MB-04 | 0x08 | 0x0000 | 0x0000 | 异常0x88/0x01 | 0x88/0x01 | 通过 |
记录时建议同时保存PCAP文件、时间戳、源/目的IP、事务ID,便于回溯。对于未授权写入成功的用例,应进一步验证寄存器值是否真实改变(再次读取确认),并评估对工艺的影响。测试报告应包含风险等级、复现步骤、修复建议(如启用写保护、增加认证网关、限制功能码白名单)。
sendp()需指定二层接口;若使用sr1()接收响应,建议设置timeout=2并检查resp是否为None。对于生产环境,务必先获得书面授权,并避免在业务高峰期发送写请求。
综上,Modbus安全测试的完整闭环是:用Scapy构造精确报文 → 发送并捕获响应 → 依据功能码与异常码判读 → 记录用例与PCAP → 评估未授权写入与功能码滥用风险。只有把“构造—发送—判读—记录”四个环节都做扎实,测试结果才具备可复现性和工程参考价值。