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!

相关推荐
2601_962381864 小时前
Modbus地址40001就是0x0000?我干了8年,发现80%工程师都搞错
错误·modbus·工程师·地址·工业协议
努力的lpp16 小时前
php反序列化一(大白话版)
开发语言·web安全·php·ctf·反序列化
蒲公英eric19 小时前
从直接重定向到白名单验证:DVWA 开放重定向模块完整漏洞分析教程
web安全·ai·ctf·dvwa·ai安全·开放重定向模块
蒲公英eric3 天前
从页面检查到功能验证:DVWA 授权绕过模块完整漏洞分析教程
web安全·ai·ctf·dvwa·ai安全·授权绕过模块
Sagittarius_A*4 天前
[LitCTF 2026] lit_elgamal_handshake
密码学·ctf·crypto·elgamal
0xBADCODE7 天前
CTF Writeup 合集
安全·web安全·网络安全·系统安全·密码学·php·ctf
仰科网关7 天前
采集opc da 服务器数据 转 EthernetIP项目案例
网关·modbus·协议转换·规约转换器
@嵌入式扫地僧7 天前
RK2108 实现 Modbus 异常流量 100μs 级检测全流程
tcp·modbus·嵌入式ai·边缘ai·tcn-tiny
玫幽倩8 天前
2025MoeCTF(Pwn全)
网络·安全·pwn·ctf·新生赛·二进程·moectf