Modbus Traffic Analysis and Anomaly Detection in Industrial Control Networks

免责声明:本文为个人技术学习与工程实践笔记,所涉操作仅应在获得授权的环境中进行。因不当使用造成的后果由使用者自行承担。

本文整理自 2022 年的实验记录,主要说明 Modbus 协议的报文结构,以及工业控制网络中基于该协议的流量分析与异常检测方法。

离散量输入:主要用来读取单个位的数据,如IO的状态;
线圈:开关输出信号,主要用来写入单个位的数据,与离散量构成组成对位的操作;
输入寄存器:主要用来读取16位,也就是两个字节的数据;
保持寄存器:主要用来写入16位的数据。
PLC:可编程逻辑控制器,是一种采用一类可编程的存储器,用于其内部存储程序,执行逻辑运算、顺序控制、定时、计数与算术操作等面向用户的指令,并通过数字或模拟式输入/输出控制各种类型的机械或生产过程。

协议原理

Modbus使用一种简单的 Masterand/Slave主从协议(客户机/服务器协议)进行通信。

主从协议是指只有主机(客户机)向从机(服务器)发送请求,从机(服务器)才会对主机(客户机)的请求作出回应,返回报文。

(最多247个,地址范围为1-247,0节点是广播地址)。每个slave设备都具有一个唯一的地址。总线上只能有一个Master节点。
客户机作为主站,向服务器发送请求;服务器(从站)接到请求后,对请求进行分析并作出应答。其中使用的通信帧被称为应用数据单元(Application Data Unit,ADU),它包括通信地址段、功能代码段、数据段和校验段,其中,功能代码段和数据段组合称为协议数据单元(Protocol Data Unit or Protocol Description Unit),PDU。如下图:

image-20260203200251887

协议原理

Modbus传输模式可分为 ASCII和RTU两种模式,同一网络中所有设备必须保持统一,要么是ASCII,要么统一为RTU模式,两者不可共存。相对来说RTU模式传输效率较高,

  • ASCII模式下,消息以冒号字符开始,以回车换行字符结束。
  • RTU模式下,消息发送和接收至少间隔3.5字符时间的停顿间隔作为标志。

寄存器介绍

所有数据都存在寄存器中,寄存器可指物理寄存器,也可是一块内存区域。Modbus根据数据类型及各自读写特性,将寄存器分为了4个部分,分别如下。
image-20260203200347027

Modbus地址、功能码、数据域

Modbus消息帧的地址,在ASCII模式下包含2个字符,在RTU模式下包含1个字符。单个设备地址实际范围是在1-247值之间。0为广播地址、248-255为保留地址。
功能码由1个字节构成,因此取值范围1-255。常用功能码如下:

  • 01:读线圈状态
  • 02:读输入状态
  • 03:读保持寄存器
  • 04:读输入寄存器
  • 05:强制单线圈
  • 06:预制单寄存器
  • 15:强制多线圈
  • 17:报告从设备ID
  • 22:屏蔽写寄存器
  • 23:读/写寄存器

数据内容与功能码紧密相关,存放功能码需要操作的具体数据,数据域以字节为单位,长度可变,对于有些功能码此域可为空。

协议解析

ModbusTCP通常使用端口502作为接收报文端口。
ModbusTCP包含一个应用报文头,该头占用7个字节。Modbus TCP/IP协议最大数据帧长度为260字节。

读取线圈输出状态(示例一)

发送报文

下图为query请求报文,Modbus传输内容为 “05 91 00 00 00 06 ff 01 00 00 00 0a”。

image-20260203200504258

1
2
3
4
5
6
7
8
9
10
05 91(十六进制) 为传输标识,即1425(十进制);
00 00为协议标识;
00 06为字节长度;
ff为单位标识符,即255(十进制);
01为功能码,占1个字节;
00 为Modbus起始地址高位;
00 为Modbus起始地址低位;
00 为寄存器高位;
0a为寄存器低位。
下图为query后的响应报文,Modbus传输内容为 “05 91 00 00 00 05 ff 01 02 01 00”。

响应报文

1
2
3
4
5
6
7
05 91(十六进制) 为传输标识,即1425(十进制);
00 00为协议标识;
00 05为字节长度;
ff为单位标识符,即255(十进制);
01为功能码,占1个字节;
02为数据域字节数;
01 00 为数据字段。响应报文的数据字段中,每个线圈占用1位(bit),01=0000 0001,00=0000 0000。1=ON,0=OFF。如果最后的数据字节不能填满8个线圈的状态(1个字节),则由0填充。

读取线圈输出状态(示例二)

功能码为:01

发送报文

image-20260203200544018

  • 22 F3 - 传输标志(8947)
  • 00 00 - 协议标识
  • 00 06 - 字节长度
  • 01 - 功能码,占一个字节
  • 00 - 为Modbus起始地址高位;
  • 00 - 为Modbus起始地址低位;
  • 00 - 为寄存器高位;
  • 0a - 为寄存器低位。

image-20260203200631020

响应报文

image-20260203200659517

  • 22 F3 - 传输标志(8947)
  • 00 00 - 协议标识
  • 00 04 - 字节长度
  • 01 - 为单位标识符,即255(十进制)
  • 01 - 为功能码,占1个字节;
  • 01 - 为数据域字节数;
  • 01 03 为数据字段。响应报文的数据字段中,每个线圈占用1位(bit),01=0000 0001,00=0000 0000。1=ON,0=OFF。如果最后的数据字节不能填满8个线圈的状态(1个字节),则由0填充。

读取离散量输入值

功能码为:02

发送报文

Modbus流量为:39 f3 00 00 00 06 01 02 00 00 00 03

image-20260203201147610

  • 39 F3 - 传输标志(14835)
  • 00 00 - 协议表示
  • 00 06 - 字节长度
  • 01 - 单位标识符
  • 02 - Modbus起始地址高位
  • 00 - Modbus起始地址地位
  • 00 - 为寄存器高位
  • 03 - 为寄存器地位

响应报文

Modbus流量为:39 f3 00 00 00 04 01 02 01 03

image-20260203201210759

  • 39 F3 - 传输标志
  • 00 00 - 协议标识
  • 00 06 - 字节长度
  • 01 - 单位标识符
  • 02 - 功能码
  • 00 - 字节数
  • 00 00 03 - 数据字段

读取输入寄存器值

功能码为:04

请求报文

下图为query请求报文,Modbus传输内容为 “07 45 00 00 00 06 ff 04 01 8f 00 02”。下图请求报文大体含义为:需读取输入寄存器地址30144-30145,共计2个寄存器内容。即读取Modbus协议地址143至144的内容。

image-20260203201232294

  • 07 - 45(十六进制) 为传输标识,即1861(十进制);
  • 00 - 00为协议标识;
  • 00 - 06为字节长度;
  • FF - 为单位标识符,即255(十进制);
  • 04 - 为功能码;
  • 01 - 为Modbus起始地址高位;
  • 8F - 为Modbus起始地址低位,起始地址为143。
  • 00 - 为寄存器高位;
  • 02 - 为寄存器低位,读取数量为2。

下图为query后的响应报文,Modbus传输内容为 07 45 00 00 00 07 ff 04 04 b6 00 47 7f

image-20260203201315161

  • 07 45 - (十六进制) 为传输标识,即1861(十进制);
  • 00 00 - 为协议标识;
  • 00 07 - 为字节长度;
  • FF - 为单位标识符,即255(十进制);
  • 04 - 为功能码;
  • 04 - 为数据域字节数。
  • B6 00 47 7F为数据字段。响应报文的数据字段中,每个线圈占用1位(bit),b6=1011 0110,00=0000 0000,47=1000 0111,7f=1111111。1=ON,0=OFF。如果最后的数据字节不能填满8个线圈的状态(1个字节),则由0填充。

资产暴露面梳理

在讨论防护之前,需要先明确控制网段中实际存在哪些 Modbus 设备,以及这些设备对谁可见。资产清单是后续所有控制措施的前提:无法枚举的设备既无法加固,也无法在出现异常时被定位。可行的做法是通过网络扫描确认 502 端口的开放情况并提取设备指纹,例如 Nmap 的以下脚本:

  • modbus-discover.nse(nmap 自带)识别并发现 Modbus PLCS 设备及版本
  • modicon-info.nse(需添加)识别并列举 Schneider Electric Modicon PLC
  • modbus-enum.nse (需添加)识别并枚举使用 Modbus 的设备

image-20260203201338545

扫描结果应落入资产台账,记录设备型号、固件版本、所属工艺区域、责任人以及允许访问的来源地址。这份台账同时也是等保与 ISO 27001 资产管理控制项的落地材料,两个用途可以共用一份数据。

Modbus 的固有风险与防护

Modbus 诞生于工业现场总线时代,协议本身没有认证、授权与报文完整性校验机制:任何能够连接到 502 端口的主机都可以发送读写请求,功能码与寄存器地址就是全部权限模型。这一设计前提决定了它不应直接暴露在不可信网络中,防护只能依靠网络层隔离与流程控制。

从流量角度看,需要关注的风险主要有三类:

  • 写操作缺乏约束。05(强制单线圈)、06(预制单寄存器)、15(强制多线圈)、16(预制多寄存器)等功能码可直接改变现场设备状态;23(读/写寄存器)与 22(屏蔽写寄存器)进一步扩大了可写入的范围。
  • 设备信息可被直接读取。设备标识、功能码支持范围与寄存器布局都可由外部枚举,为后续操作提供目标画像。
  • 无状态写入难以区分意图。协议缺少事务确认语义,误操作与恶意写入在报文层面表现一致,事后区分只能依赖操作记录。

对应的防护措施同样是三层:

  1. 网络分区。按区域与管道(zones and conduits)的思路将控制网络与办公网络隔离,使 Modbus 流量只在控制区内流转;跨区访问必须经过防火墙或单向网关,规则按“源设备—目的设备—允许功能码”收窄,而不是仅按端口放行。
  2. 消除直接暴露面。502 端口不应出现在互联网边界的资产清单中;远程维护通过 VPN 加跳板机实施,并保留完整的会话审计记录。
  3. 最小权限与变更管控。工程师站与操作站只允许访问其工艺所需的设备与寄存器区间,只读采集通道与可写控制通道在网络层分离;任何写寄存器操作都应有工单与复核记录,并与工艺参数变更日志关联留存。

流量侧的异常检测

协议缺少认证这一事实无法通过应用层弥补,因此检测能力必须建立在流量可见性之上。以下几类特征可以直接转化为告警规则:

  • 功能码基线。统计每台设备在正常生产中出现的功能码集合,偏离基线的写功能码、或从非工程师站发起的写操作都应告警。
  • 写入行为基线。连续的高频写寄存器请求、短时间内大范围寄存器变更、以及寄存器取值超出工艺允许区间,均是典型的异常信号;关键回路可对写操作设置速率限制。
  • 连接特征。控制区内 502 端口的连接来源数量与连接模式相对稳定,新增来源或扫描式的短连接(对大量寄存器地址依次发起读请求)值得关注。
  • 采集与留存。使用支持 Modbus 解析的流量工具(Wireshark、Zeek 等)对控制区流量做旁路采集并保留足够的回溯周期,使事后分析具备数据基础。下一节的字段提取方法同样适用于真实流量的统计工作。

CTF 场景中的流量分析复盘

下面以一道面向高校竞赛的 Modbus 流量分析题为例,说明从流量包中还原寄存器取值的方法。

在线靶场
https://platform.cyclens.tech/challenge/37

打开流量包并按协议过滤,可以看到寄存器中出现了一组连续且明显异常的数值序列。

image-20260203201733350

将该部分流量导出为独立文件后,用脚本按报文顺序提取寄存器字段即可还原全部取值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python
# coding=utf-8
import pyshark,sys
captures = pyshark.FileCapture('./flag_des.pcapng')
print(captures[1].modbus.regval_uint16)
i = 0
while True:
try:
print(captures[i].modbus.regval_uint16,end=", ")
i += 1
except:
sys.exit(0)
# 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 5, 4, 1, 7, 8, 5, 121, 124, 6, 7, 2, 523, 32, 443, 66, 46, 23, 5, 56, 32, 5, 57, 45, 3, 37, 45, 52, 56, 23, 25, 47, 43, 1, 16, 45, 5, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 54, 54, 54, 99, 54, 49, 54, 55, 55, 98, 51, 49, 51, 52, 51, 50, 51, 52, 51, 48, 51, 49, 51, 53, 51, 57, 50, 100, 51, 55, 54, 54, 51, 51, 51, 54, 50, 100, 51, 52, 54, 51, 51, 51, 51, 54, 50, 100, 54, 50, 51, 52, 51, 54, 54, 49, 50, 100, 51, 55, 51, 52, 54, 49, 51, 54, 54, 49, 51, 54, 54, 52, 54, 53, 54, 52, 51, 56, 51, 52, 54, 49, 55, 100,
flag = [54, 54, 54, 99, 54, 49, 54, 55, 55, 98, 51, 49, 51, 52, 51, 50, 51, 52, 51, 48, 51, 49, 51, 53, 51, 57, 50, 100, 51, 55, 54, 54, 51, 51, 51, 54, 50, 100, 51, 52, 54, 51, 51, 51, 51, 54, 50, 100, 54, 50, 51, 52, 51, 54, 54, 49, 50, 100, 51, 55, 51, 52, 54, 49, 51, 54, 54, 49, 51, 54, 54, 52, 54, 53, 54, 52, 51, 56, 51, 52, 54, 49, 55, 100]
for i in flag:
print(chr(i), end="")

将数值按 ASCII 逐个还原为字符,可得到一个十六进制字符串,再作一次十六进制解码即可得到题目要求的校验值。

image-20260203201810338

1
flag{14240159-7f36-4c36-b46a-74a6a6ded84a}

image-20260203201824123

该题型的考点与防御启示

这类题目的考点集中在三处:识别出承载数据的协议、理解功能码与寄存器语义、以及按报文顺序与字节序还原字段。把它放回真实网络来看,价值在于检测能力:题目中那组异常数值正是生产网络里应当被记录并告警的信号——寄存器取值偏离工艺区间、短时间内被批量写入,本身就可以构成规则。

由此得到的防御启示是,工控环境的可见性建设比单点加固更关键:若控制区流量既未采集也未留存,同样的异常写入在事后不会被发现,也无法回答“哪个寄存器、在什么时间、由谁改的”。流量留存周期、功能码基线与写操作告警,应当与工艺侧的变更记录一并纳入日常运维。

Support via Solana

Solana

Solana

Solana Pay

Solana Pay

WeChat

WeChat