Modbus协议及其取证的学习笔记

ModBus协议

ModBus协议是工业自动化领域非常经典的通信协议。虽然诞生于 1979 年,但截至现在仍然大量存在于 PLC、变频器、仪表、传感器、能源设备、楼宇控制和 SCADA 系统中。

Modbus 的基本通信模型

传统 Modbus 通信采用 主从(Master/Slave) 模型,也有client/server的说法。

基本结构如下:

        Master(主站)【负责发起请求】
              │
       ┌──────┼──────┐
       │      │      │
       ▼      ▼      ▼
   Slave 1  Slave 2  Slave 3 【负责响应请求- 每个 Slave 通常具有唯一的设备地址】
    从站      从站      从站

主站负责发起通信请求,从站根据请求返回数据。

例如:

PLC(主站)
   │
   │ 读取温度
   ▼
温度仪表(从站)
   │
   │ 返回 25.6℃
   ▼
PLC

Modbus 常见的通信方式主要有:

  1. Modbus RTU

  2. Modbus ASCII

  3. Modbus TCP

其中工业现场最常见的是:

Modbus RTU 和 Modbus TCP

Modbus RTU和Modbus TCP

Modbus RTU

Modbus RTU 是 Modbus 最常见的串行通信模式之一。

RTU 使用二进制方式传输数据,通常运行在:

  • RS-485

  • RS-232

等串行通信接口上。

工业现场最常见的组合是:

Modbus RTU + RS-485

例如:

PLC
 │
 │ RS-485
 │
 ├───────────────┐
 │               │
 ▼               ▼
变频器1          变频器2
Slave 1          Slave 2
  • RS-485 和 Modbus RTU 的区别

Modbus RTU 是协议通信方式,RS-485 是常用的物理接口

Modbus RTU
   ↑
通信协议 / 数据格式
   │
   ▼
RS-485
   ↑
物理通信接口

例如:

应用层:    Modbus
             │
             ▼
数据格式:   Modbus RTU
             │
             ▼
物理层:     RS-485
             │
             ▼
通信线路:   A / B

Modbus TCP

Modbus TCP 是基于以太网和 TCP/IP 协议进行通信的 Modbus 版本。

它通常使用Ethernet, TCP/IP, RJ45 网口, 工业以太网交换机进行通信。

基本结构:

PLC
 │
 │ Ethernet
 ▼
工业交换机
 │
 ├──────────────┐
 │              │
 ▼              ▼
变频器          远程I/O
IP:             IP:
192.168.1.10    192.168.1.20

Modbus TCP 通常使用502作为默认通信端口。

Modbus RTU 与 Modbus TCP 对比

image.png

Modbus 数据模型

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

Modbus 主要定义了四种数据类型:

image.png

 

总结如下:

                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 对这些寄存器进行读取或者写入。

【----帮助网安学习,以下所有学习资料免费领!加vx:YJ-2021-1,备注 “博客园” 获取!】

 ① 网安学习成长路径思维导图
 ② 60+网安经典常用工具包
 ③ 100+SRC漏洞分析报告
 ④ 150+网安攻防实战技术电子书
 ⑤ 最权威CISSP 认证考试指南+题库
 ⑥ 超1800页CTF实战技巧手册
 ⑦ 最新网安大厂面试题合集(含答案)
 ⑧ APP客户端安全检测指南(安卓+IOS)

Modbus功能码

常见功能码如下:

image.png

工业自动化中最常见的功能码之一是:

03:读取 Holding Register

以及:

06 / 10:写入 Holding Register

Modbus取证题学习

附件是一个pcap文件,使用wireshark打开,一堆Modbus TCP的流量

image-20260906214654249

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

image-20260906215037074

简单来说现在有了一些不同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): 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的相应数据)

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字节隔断

image-20260906222247465

具体打印就是flag:TheModbusProtocolIsFunny!

更多网安技能的在线实操练习,请点击这里>>

 

posted @ 2026-09-09 16:37  蚁景网安实验室  阅读(6)  评论(0)    收藏  举报