一个使用 libmodbus 的多线程 C 语言扫描器,用于检测 Modbus/TCP 服务。
该扫描器不仅仅依赖 TCP 端口 502 是否开放。相反,它会建立 Modbus/TCP 连接并发送应用层 Modbus 请求。有效的 Modbus 响应被用作判断 Modbus 服务存在的主要指标。
Modbus 是一种工业通信协议,通常被 PLC、RTU、HMI、SCADA 系统、传感器、仪表以及其他工业设备所使用。
Modbus/TCP 通过 TCP 传输 Modbus 应用协议。
标准的 Modbus/TCP 端口是:
TCP/502
典型的通信流程如下:
扫描器
|
| TCP 连接 → 502
|
| Modbus/TCP 请求
v
Modbus 设备
|
| Modbus/TCP 响应
v
扫描器
与连接后立即提供横幅(banner)的协议不同,Modbus/TCP 通常要求客户端先发送有效的 Modbus 请求,设备才会产生应用层响应。
扫描器分两个阶段执行检测。
扫描器尝试建立到以下地址的 TCP 连接:
<目标>:502
如果无法建立连接,则该目标被视为不响应 Modbus/TCP。
然而,仅凭 TCP/502 端口开放本身并不足以作为 Modbus 存在的证据。
连接建立后,扫描器使用 libmodbus 发送一个 Modbus 请求。
主要的探测是:
modbus_read_input_registers(ctx, 0, 1, ®);
这会生成一个 Modbus 功能码:
0x04 - 读取输入寄存器
该请求要求目标从地址 0 开始返回一个输入寄存器。
如果目标返回有效的 Modbus 响应,扫描器即认为检测到该服务。
一个 Modbus/TCP 数据包由以下部分组成:
+----------------------+----------------------+
| MBAP 头 | PDU |
+----------------------+----------------------+
MBAP 头:
+------------------+
| 事务标识符 | 2 字节
| 协议标识符 | 2 字节
| 长度 | 2 字节
| 单元标识符 | 1 字节
+------------------+
PDU:
+------------------+
| 功能码 | 1 字节
| 数据 | N 字节
+------------------+
MBAP 头是 Modbus/TCP 特有的。
扫描器的第一个探测使用功能码 0x04。
一个代表性的请求是:
00 01 00 00 00 06 01 04 00 00 00 01
分解如下:
00 01 事务标识符
00 00 协议标识符
00 06 长度
01 单元标识符
04 功能码
00 00 起始地址
00 01 数量
00 01
标识该事务。
该值可以变化,因为事务标识符通常由 Modbus 客户端库管理。
00 00
值为 0 表示 Modbus。
00 06
指定长度字段之后跟随的字节数。
01
标识目标 Modbus 单元。
04
功能码 0x04 表示:
读取输入寄存器
00 00
扫描器从寄存器地址 0 开始。
00 01
扫描器请求一个寄存器。
对该请求的成功响应包含功能码 0x04 以及所请求的寄存器数据。
一个代表性的响应可能如下:
00 01 00 00 00 05 01 04 02 00 00
分解如下:
00 01 事务标识符
00 00 协议标识符
00 05 长度
01 单元标识符
04 功能码
02 字节数
00 00 寄存器值
对于检测而言,重要的是目标成功处理了 Modbus 请求并返回了有效的 Modbus 应用层响应。
实际的寄存器值取决于具体设备。
仅仅检查:
TCP/502 = 开放
并不一定能证明该服务就是 Modbus。
端口号只是约定。其他应用程序也可以监听 TCP/502,而且 Modbus 设备也可能因其配置不同而表现各异。
因此,扫描器使用:
TCP 连通性
+
Modbus 协议响应
=
Modbus 检测
这使得应用层检测比简单的端口扫描更有意义。
某些设备可能因其寄存器配置或支持的功能码而无法响应最初的 0x04 请求。
因此,如果第一次请求失败,扫描器会尝试第二次请求:
modbus_read_bits(ctx, 0, 1, bits);
这使用功能码:
0x01 - 读取线圈
一个代表性的请求是:
00 02 00 00 00 06 01 01 00 00 00 01
分解如下:
00 02 事务标识符
00 00 协议标识符
00 06 长度
01 单元标识符
01 功能码
00 00 起始地址
00 01 数量
当任一 Modbus 操作收到成功响应时,扫描器即认为检测到目标。
目标 IP
|
v
TCP 连接
端口 502
|
+------+------+
| |
失败 成功
| |
v v
忽略 功能码 0x04
|
+------+------+
| |
有效 失败
| |
v v
发现 MODBUS 功能码 0x01
|
+------+------+
| |
有效 失败
| |
v v
发现 MODBUS 未检测到
扫描器使用 libmodbus 来构造和解析 Modbus/TCP 数据包,而不是手动构造协议帧。
主要操作是:
modbus_read_input_registers(ctx, 0, 1, ®);
如果失败:
modbus_read_bits(ctx, 0, 1, bits);
随后关闭连接并释放 libmodbus 上下文。
这样可以将协议处理保留在 Modbus 库内部,而扫描器负责:
扫描器使用较短的连接和响应超时时间:
#define TIMEOUT_SEC 2
这可以防止单个不可达或无响应的主机长时间阻塞工作线程。
工业网络中可能包含响应相对较慢的设备,因此超时值可能需要根据具体环境进行调整。
目标在多个工作线程之间分配。
例如:
线程 1 → 目标 1–64
线程 2 → 目标 65–128
线程 3 → 目标 129–192
线程 4 → 目标 193–254
每个工作线程独立尝试 Modbus/TCP 检测。
这样可以同时测试多个主机,而不是逐个顺序等待每个目标。
阳性结果意味着目标成功响应了扫描器所理解的 Modbus 请求。
它不一定能识别:
这些是单独的指纹识别或评估任务。
该扫描器主要是一个 Modbus/TCP 服务检测工具。
检测方法有意保持保守。
设备可能支持 Modbus,但在以下情况下可能检测失败:
因此:
无响应 ≠ 确定不是 Modbus
这仅表示扫描器使用其尝试的探测未能获得成功响应。
安装所需的依赖项并使用以下命令编译:
make
或直接编译:
gcc -Wall -Wextra -O2 -o modbus modbus.c -lmodbus -lpthread
运行:
./modbus -i 192.168.1.0 -s /24 -t 10 -o results.txt
示例:
[+] 检测到 Modbus:192.168.1.20:502
[+] 检测到 Modbus:192.168.1.42:502
[+] 已扫描:254 | 发现:2 个 Modbus
[+] 结果已保存至:results.txt
该扫描器通过执行实际的协议交互来检测 Modbus/TCP 服务,而不是仅仅依赖 TCP 端口检测。
检测过程如下:
连接到 TCP/502
↓
发送 Modbus 功能码 0x04
↓
收到有效的 Modbus 响应?
↓
是 → 检测到 Modbus
|
否
↓
发送 Modbus 功能码 0x01
↓
收到有效的 Modbus 响应?
↓
是 → 检测到 Modbus
|
否
↓
未检测到
关键原则很简单:
检测协议本身,而不仅仅是端口。