ModBus协议
ModBus协议是工业自动化领域非常经典的通信协议。虽然诞生于 1979 年,但截至现在仍然大量存在于 PLC、变频器、仪表、传感器、能源设备、楼宇控制和 SCADA 系统中。
Modbus 的基本通信模型
传统 Modbus 通信采用 主从(Master/Slave) 模型,也有client/server的说法。
基本结构如下:
text
Master(主站)【负责发起请求】
│
┌──────┼──────┐
│ │ │
▼ ▼ ▼
Slave 1 Slave 2 Slave 3 【负责响应请求- 每个 Slave 通常具有唯一的设备地址】
从站 从站 从站
主站负责发起通信请求,从站根据请求返回数据。
例如:
text
PLC(主站)
│
│ 读取温度
▼
温度仪表(从站)
│
│ 返回 25.6℃
▼
PLC
Modbus 常见的通信方式主要有:
- Modbus RTU
- Modbus ASCII
- Modbus TCP
其中工业现场最常见的是:
Modbus RTU 和 Modbus TCP
Modbus RTU和Modbus TCP
Modbus RTU
Modbus RTU 是 Modbus 最常见的串行通信模式之一。
RTU 使用二进制方式传输数据,通常运行在:
- RS-485
- RS-232
等串行通信接口上。
工业现场最常见的组合是:
Modbus RTU + RS-485
例如:
text
PLC
│
│ RS-485
│
├───────────────┐
│ │
▼ ▼
变频器1 变频器2
Slave 1 Slave 2
- RS-485 和 Modbus RTU 的区别
Modbus RTU 是协议通信方式,RS-485 是常用的物理接口
text
Modbus RTU
↑
通信协议 / 数据格式
│
▼
RS-485
↑
物理通信接口
例如:
text
应用层: Modbus
│
▼
数据格式: Modbus RTU
│
▼
物理层: RS-485
│
▼
通信线路: A / B
Modbus TCP
Modbus TCP 是基于以太网和 TCP/IP 协议进行通信的 Modbus 版本。
它通常使用Ethernet, TCP/IP, RJ45 网口, 工业以太网交换机进行通信。
基本结构:
text
PLC
│
│ Ethernet
▼
工业交换机
│
├──────────────┐
│ │
▼ ▼
变频器 远程I/O
IP: IP:
192.168.1.10 192.168.1.20
Modbus TCP 通常使用502作为默认通信端口。
Modbus RTU 与 Modbus TCP 对比
| 项目 | Modbus RTU | Modbus TCP |
|---|---|---|
| 通信类型 | 串行通信 | 以太网通信 |
| 常用接口 | RS-485 | Ethernet |
| 常用线缆 | 双绞线 | 网线 |
| 地址方式 | 从站地址 | IP 地址 |
| 默认端口 | 无 | TCP 502 |
| 数据传输 | 二进制 | TCP/IP |
| 通信速度 | 相对较低 | 通常较高 |
| 网络结构 | 总线型较常见 | 星型/交换式网络常见 |
| 工业现场使用 | 非常广泛 | 非常广泛 |
Modbus 数据模型
在Modbus通信中,设备数据被组织在称为寄存器的单元中。client指定目标数据区和地址并发送读写请求。然后,server处理该请求并返回读请求的值或写请求的写确认。
Modbus 主要定义了四种数据类型:
| 类型 | 名称 | 读/写 | 常见用途 |
|---|---|---|---|
| 0xxxx | Coils | 读/写 | 开关量输出 |
| 1xxxx | Discrete Inputs | 只读 | 开关量输入 |
| 3xxxx | Input Registers | 只读 | 模拟量输入 |
| 4xxxx | Holding Registers | 读/写 | 参数、数据、控制 |
总结如下:
Bit Word(16 bit)
│ │
┌────────┴────────┐ ┌───────┴────────┐
│ │ │ │
Coil Discrete Input Input Holding
线圈 离散输入 Register Register
│ │ │ │
读/写 只读 只读 读/写
│ │ │ │
控制 状态 测量值 参数/控制
- Coil(线圈)
Coil 是一种单 bit 数据。
只能表示:
text
0 = OFF
1 = ON
例如:
text
Coil 00001 = 电机启动
可能定义为:
text
0 → 电机停止
1 → 电机启动
- Discrete Input(离散输入)
Discrete Input 也是 bit 类型数据,但通常是只读的。
例如:
text
Discrete Input = 急停状态
可能:
text
0 → 正常
1 → 急停
- Input Register(输入寄存器)
Input Register 通常用于存储只读数据。
例如:
text
30001 = 温度
30002 = 压力
30003 = 电流
例如:
text
30001 = 256
实际可能代表:
text
25.6 ℃
具体缩放方式由设备厂家定义。
- Holding Register(保持寄存器)
Holding Register 是 Modbus 中非常常用的数据类型。
它通常用于:
- 参数设置
- 运行频率
- 电机转速
- 温度设定值
- 启停控制
- 状态数据
- 报警代码
例如:
text
40001 = 运行频率
40002 = 目标频率
40003 = 电机电流
40004 = 电机转速
PLC 可以通过 Modbus 对这些寄存器进行读取或者写入。
帮助网安学习,全套资料S信领取:
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC 分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF 实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)
Modbus功能码
常见功能码如下:
| 功能码 | 名称 | 功能 |
|---|---|---|
| 01 | Read Coils | 读取线圈 |
| 02 | Read Discrete Inputs | 读取离散输入 |
| 03 | Read Holding Registers | 读取保持寄存器 |
| 04 | Read Input Registers | 读取输入寄存器 |
| 05 | Write Single Coil | 写单个线圈 |
| 06 | Write Single Register | 写单个寄存器 |
| 0F | Write Multiple Coils | 写多个线圈 |
| 10 | Write Multiple Registers | 写多个寄存器 |
工业自动化中最常见的功能码之一是:
03:读取 Holding Register
以及:
06 / 10:写入 Holding Register
Modbus取证题学习
附件是一个pcap文件,使用wireshark打开,一堆Modbus TCP的流量

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

简单来说现在有了一些不同function code的流量,我们先统计所有的function code,使用pyshark库提取
python
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
python
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): 33 -
packet2
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的相应数据)
python
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!