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

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 常见的通信方式主要有:

  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

例如:

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!

相关推荐
hengdonghui3 小时前
Writeup 4 2020 - 之江杯 - 异常的工程文件
ctf·工控
hengdonghui3 小时前
Writeup 4 2020 - 之江杯 - 注册表分析
注册表·ctf
yangjj20059 天前
手写 Modbus 上位机我踩过的 7 个工业级坑
modbus·温度监测·上位机开发
dalong1011 天前
WPF:Modbus 状态监控
wpf·modbus
kali-Myon11 天前
分享一个网络安全 AI 工具导航项目 SecSkills
安全·ai·github·ctf
蒲公英eric12 天前
从旧接口泄露到 OAuth 保护:DVWA API 模块完整漏洞分析教程
web安全·ai·ctf·dvwa·ai安全·api模块
白猫不黑13 天前
CTF是什么?从零理解一场攻防竞赛
网络·web安全·计算机·网络安全·信息安全·ctf·红蓝对抗
玫幽倩14 天前
2026第二届湾区杯网络安全大赛决赛(AI专项赛道静态题wp)
pytorch·python·ai·agent·ctf·rag·湾区杯
爱学习的程序媛15 天前
TCP / UDP 协议详解
网络·网络协议·udp·tcp·通信协议
爱学习的程序媛15 天前
以太网协议详解
网络·网络协议·计算机网络·以太网·ethernet·通信协议