ModBus协议是工业自动化领域非常经典的通信协议。虽然诞生于 1979 年,但截至现在仍然大量存在于 PLC、变频器、仪表、传感器、能源设备、楼宇控制和 SCADA 系统中。
传统 Modbus 通信采用 主从(Master/Slave) 模型,也有client/server的说法。
基本结构如下:
Master(主站)【负责发起请求】
│
┌──────┼──────┐
│ │ │
▼ ▼ ▼
Slave 1 Slave 2 Slave 3 【负责响应请求- 每个 Slave 通常具有唯一的设备地址】
从站 从站 从站主站负责发起通信请求,从站根据请求返回数据。
例如:
PLC(主站)
│
│ 读取温度
▼
温度仪表(从站)
│
│ 返回 25.6℃
▼
PLCModbus 常见的通信方式主要有:
Modbus RTU
Modbus ASCII
Modbus TCP
其中工业现场最常见的是:
Modbus RTU 和 Modbus TCP
Modbus RTU 是 Modbus 最常见的串行通信模式之一。
RTU 使用二进制方式传输数据,通常运行在:
RS-485
RS-232
等串行通信接口上。
工业现场最常见的组合是:
Modbus RTU + RS-485
例如:
PLC
│
│ RS-485
│
├───────────────┐
│ │
▼ ▼
变频器1 变频器2
Slave 1 Slave 2RS-485 和 Modbus RTU 的区别
Modbus RTU 是协议通信方式,RS-485 是常用的物理接口
Modbus RTU
↑
通信协议 / 数据格式
│
▼
RS-485
↑
物理通信接口例如:
应用层: Modbus
│
▼
数据格式: Modbus RTU
│
▼
物理层: RS-485
│
▼
通信线路: A / BModbus TCP 是基于以太网和 TCP/IP 协议进行通信的 Modbus 版本。
它通常使用Ethernet, TCP/IP, RJ45 网口, 工业以太网交换机进行通信。
基本结构:
PLC
│
│ Ethernet
▼
工业交换机
│
├──────────────┐
│ │
▼ ▼
变频器 远程I/O
IP: IP:
192.168.1.10 192.168.1.20Modbus TCP 通常使用502作为默认通信端口。

在Modbus通信中,设备数据被组织在称为寄存器的单元中。client指定目标数据区和地址并发送读写请求。然后,server处理该请求并返回读请求的值或写请求的写确认。
Modbus 主要定义了四种数据类型:

总结如下:
Bit Word(16 bit)
│ │
┌────────┴────────┐ ┌───────┴────────┐
│ │ │ │
Coil Discrete Input Input Holding
线圈 离散输入 Register Register
│ │ │ │
读/写 只读 只读 读/写
│ │ │ │
控制 状态 测量值 参数/控制Coil(线圈)
Coil 是一种单 bit 数据。
只能表示:
0 = OFF
1 = ON例如:
Coil 00001 = 电机启动可能定义为:
0 → 电机停止
1 → 电机启动Discrete Input(离散输入)
Discrete Input 也是 bit 类型数据,但通常是只读的。
例如:
Discrete Input = 急停状态可能:
0 → 正常
1 → 急停Input Register(输入寄存器)
Input Register 通常用于存储只读数据。
例如:
30001 = 温度
30002 = 压力
30003 = 电流例如:
30001 = 256实际可能代表:
25.6 ℃具体缩放方式由设备厂家定义。
Holding Register(保持寄存器)
Holding Register 是 Modbus 中非常常用的数据类型。
它通常用于:
参数设置
运行频率
电机转速
温度设定值
启停控制
状态数据
报警代码
例如:
40001 = 运行频率
40002 = 目标频率
40003 = 电机电流
40004 = 电机转速PLC 可以通过 Modbus 对这些寄存器进行读取或者写入。
常见功能码如下:

工业自动化中最常见的功能码之一是:
03:读取 Holding Register
以及:
06 / 10:写入 Holding Register
附件是一个pcap文件,使用wireshark打开,一堆Modbus TCP的流量

Modbus TCP格式如下,它是作为TCP的payload的。因为有tcp的原因因此没有Modbus RTU的CRC校验,

简单来说现在有了一些不同function code的流量,我们先统计所有的function code,使用pyshark库提取
import pyshark
# 获取功能码, 统计出现次数
def get_code():
captures = pyshark.FileCapture(r"xxx\Modbus.pcap", tshark_path=r'D:\Wireshark\tshark.exe')
func_codes = {}
for pkt in captures:
if hasattr(pkt, "modbus"):
func_code = int(pkt.modbus.func_code)
if func_code in func_codes:
func_codes[func_code] += 1
else:
func_codes[func_code] = 1
print(func_codes)
if __name__ == "__main__":
get_code()结果如下
{1: 702, 3: 702, 4: 702, 2: 702, 16: 2}这里出现了非常规的16功能码,而且只有2次,很有可能是解题线索
过滤一下这些功能码为16的packet
import pyshark
# 检查function code的数据
def get_targetcode_data(target_code):
captures = pyshark.FileCapture(r"xxx\Modbus.pcap", tshark_path=r'D:\Wireshark\tshark.exe')
for c in captures:
for pkt in c:
if pkt.layer_name == "modbus":
func_code = int(pkt.func_code)
if func_code == target_code:
print(c)
if __name__ == "__main__":
get_targetcode_data(16)两个packet:
packet1
Packet (Length: 117)
Layer ETH
: Destination: 52:54:00:a4:30:12
Address: 52:54:00:a4:30:12
.... ..1. .... .... .... .... = LG bit: Locally administered address (this
is NOT the factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Source: 52:54:00:f8:5c:21
.... ..1. .... .... .... .... = LG bit: Locally administered address (this
is NOT the factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Type: IPv4 (0x0800)
Address: 52:54:00:f8:5c:21
Layer IP
: 0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
0000 00.. = Differentiated Services Codepoint: Default (0)
.... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0) Total Length: 103
Identification: 0x5f09 (24329)
Flags: 0x40, Don't fragment
0... .... = Reserved bit: Not set
.1.. .... = Don't fragment: Set
..0. .... = More fragments: Not set
...0 0000 0000 0000 = Fragment Offset: 0
Time to Live: 128
Protocol: TCP (6)
Header Checksum: 0x3f2f [validation disabled]
Header checksum status: Unverified
Source Address: 172.16.3.23
Destination Address: 172.16.1.33
Layer TCP
: Source Port: 1073
Destination Port: 502
Stream index: 2195
Conversation completeness: Incomplete, ESTABLISHED (7)
TCP Segment Len: 63
Sequence Number: 3073843007
Next Sequence Number: 3073843070
Acknowledgment Number: 1687629905
0101 .... = Header Length: 20 bytes (5)
Flags: 0x018 (PSH, ACK)
000. .... .... = Reserved: Not set
...0 .... .... = Nonce: Not set
.... 0... .... = Congestion Window Reduced (CWR): Not set
.... .0.. .... = ECN-Echo: Not set
.... ..0. .... = Urgent: Not set
.... ...1 .... = Acknowledgment: Set
.... .... 1... = Push: Set
.... .... .0.. = Reset: Not set
.... .... ..0. = Syn: Not set
.... .... ...0 = Fin: Not set
TCP Flags: ·······AP···
Window: 64240
Calculated window size: 64240
Window size scaling factor: -2 (no window scaling used)
Checksum: 0xe950 [unverified]
Checksum Status: Unverified
Urgent Pointer: 0
Timestamps
Time since first frame in this TCP stream: 0.014549000 seconds
Time since previous frame in this TCP stream: 0.007470000 seconds
SEQ/ACK analysis
iRTT: 0.007079000 seconds
Bytes in flight: 63
Bytes sent since last PSH flag: 63
TCP payload (63 bytes)
PDU Size: 63
Layer MBTCP
: Transaction Identifier: 0
Protocol Identifier: 0
Length: 57
Unit Identifier: 1
Layer MODBUS
: .001 0000 = Function Code: Write Multiple Registers (16)
Reference Number: 1
Word Count: 25
Byte Count: 50
Register 1 (UINT16): 84
Register Number: 1
Register Value (UINT16): 84
Register 2 (UINT16): 104
Register 3 (UINT16): 101
Register 4 (UINT16): 77
Register 5 (UINT16): 111
Register 6 (UINT16): 100
Register 7 (UINT16): 98
Register 8 (UINT16): 117
Register 9 (UINT16): 115
Register 10 (UINT16): 80
Register 11 (UINT16): 114
Register 12 (UINT16): 111
Register 13 (UINT16): 116
Register 14 (UINT16): 111
Register 15 (UINT16): 99
Register 16 (UINT16): 111
Register 17 (UINT16): 108
Register 18 (UINT16): 73
Register 19 (UINT16): 115
Register 20 (UINT16): 70
Register 21 (UINT16): 117
Register 22 (UINT16): 110
Register 23 (UINT16): 110
Register 24 (UINT16): 121
Register 25 (UINT16): 33
Register Number: 2
Register Number: 3
Register Number: 4
Register Number: 5
Register Number: 6
Register Number: 7
Register Number: 8
Register Number: 9
Register Number: 10
Register Number: 11
Register Number: 12
Register Number: 13
Register Number: 14
Register Number: 15
Register Number: 16
Register Number: 17
Register Number: 18
Register Number: 19
Register Number: 20
Register Number: 21
Register Number: 22
Register Number: 23
Register Number: 24
Register Number: 25
Register Value (UINT16): 104
Register Value (UINT16): 101
Register Value (UINT16): 77
Register Value (UINT16): 111
Register Value (UINT16): 100
Register Value (UINT16): 98
Register Value (UINT16): 117
Register Value (UINT16): 115
Register Value (UINT16): 80
Register Value (UINT16): 114
Register Value (UINT16): 111
Register Value (UINT16): 116
Register Value (UINT16): 111
Register Value (UINT16): 99
Register Value (UINT16): 111
Register Value (UINT16): 108
Register Value (UINT16): 73
Register Value (UINT16): 115
Register Value (UINT16): 70
Register Value (UINT16): 117
Register Value (UINT16): 110
Register Value (UINT16): 110
Register Value (UINT16): 121
Register Value (UINT16): 33packet2
Packet (Length: 63)
Layer ETH
: Destination: 52:54:00:f8:5c:21
Address: 52:54:00:f8:5c:21
.... ..1. .... .... .... .... = LG bit: Locally administered address (this
is NOT the factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Source: 52:54:00:a4:30:12
.... ..1. .... .... .... .... = LG bit: Locally administered address (this
is NOT the factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Type: IPv4 (0x0800)
Address: 52:54:00:a4:30:12
Layer IP
: 0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
0000 00.. = Differentiated Services Codepoint: Default (0)
.... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0) Total Length: 49
Identification: 0x1f67 (8039)
Flags: 0x40, Don't fragment
0... .... = Reserved bit: Not set
.1.. .... = Don't fragment: Set
..0. .... = More fragments: Not set
...0 0000 0000 0000 = Fragment Offset: 0
Time to Live: 64
Protocol: TCP (6)
Header Checksum: 0xbf07 [validation disabled]
Header checksum status: Unverified
Source Address: 172.16.1.33
Destination Address: 172.16.3.23
Layer TCP
: Source Port: 502
Destination Port: 1073
Stream index: 2195
Conversation completeness: Incomplete, DATA (15)
TCP Segment Len: 9
Sequence Number: 1687629905
Next Sequence Number: 1687629914
Acknowledgment Number: 3073843070
0101 .... = Header Length: 20 bytes (5)
Flags: 0x018 (PSH, ACK)
000. .... .... = Reserved: Not set
...0 .... .... = Nonce: Not set
.... 0... .... = Congestion Window Reduced (CWR): Not set
.... .0.. .... = ECN-Echo: Not set
.... ..0. .... = Urgent: Not set
.... ...1 .... = Acknowledgment: Set
.... .... 1... = Push: Set
.... .... .0.. = Reset: Not set
.... .... ..0. = Syn: Not set
.... .... ...0 = Fin: Not set
TCP Flags: ·······AP···
Window: 29200
Calculated window size: 29200
Window size scaling factor: -2 (no window scaling used)
Checksum: 0x6c02 [unverified]
Checksum Status: Unverified
Urgent Pointer: 0
Timestamps
Time since first frame in this TCP stream: 0.016131000 seconds
Time since previous frame in this TCP stream: 0.001000000 seconds
SEQ/ACK analysis
iRTT: 0.007079000 seconds
Bytes in flight: 9
Bytes sent since last PSH flag: 9
TCP payload (9 bytes)
PDU Size: 9
Layer MBTCP
: Transaction Identifier: 0
Protocol Identifier: 0
Length: 3
Unit Identifier: 1
Layer MODBUS
: .001 0000 = Function Code: Write Multiple Registers (16)
Exception Code: Illegal data address (2)其中注意到packet2报错Exception Code: Illegal data address (2)
我们把packet1的tcp payload给提取出来(tcp payload其实就是modbus tcp的相应数据)
import pyshark
def get_targetcode_data(target_code):
captures = pyshark.FileCapture(r"xxx\Modbus.pcap", tshark_path=r'D:\Wireshark\tshark.exe')
for c in captures:
for pkt in c:
if pkt.layer_name == "modbus":
func_code = int(pkt.func_code)
if func_code == target_code:
payload = str(c["TCP"].payload)
print(parse_payload(payload))
def parse_payload(payload):
data = payload.split(":")
flag = ""
for i in data:
_ord = ord(bytes.fromhex(i))
if (_ord > 0) and (_ord < 128):
flag += (chr(_ord))
return flag
if __name__ == "__main__":
get_targetcode_data(16)在register之间采用\x00字节隔断

具体打印就是flag:TheModbusProtocolIsFunny!
本课程最终解释权归蚁景网安学院
本页面信息仅供参考,请扫码咨询客服了解本课程最新内容和活动