Modbus协议

Modbus是全球第一个真正用于工业现场的总线协议,ModBus采用主/从(Master/Slave)方式通信。最大可支持247个从属控制器,但实际所支持的从属控制器数还得由所用通信设备决定,且每个从属控制器都有一个专属的slave ID。同时Modbus也是一个不经过身份验证的明文协议。

虽然最初是为了进行串行通信才设计出它,但是目前用的最多的还是TCP,其他版本的Modbus(用于串行通信)有Modbus RTU和Modbus ASCII。对于串行通信来说Modbus ASCII与Modbus RTU是不兼容的(也就是说在同一个网络两者不可能同时存在)

每种Modbus协议都必须选择一种帧格式:

Modbus TCP (在低网络层中没有校验和);
Modbus RTU (使用二进制编码和一个CRC错误检测);
Modbus ASCII (使用ASCII字符);

在TCP中客户端通常指的是主控制器,服务端指的是从属控制器。

Modbus TCP

TCP帧格式由以下部分组成:

Transaction identifier : 设备直接进行同步通信
Protocol identifier : 对于Modbus TCP来说其值始终为0
Length field : 确定数据包剩余的长度
Unit identifier : 从属控制器的地址(因为我们已经把TCP/IP地址设置为标识符,所以这里大部分情况为255)
Function code : 要执行的函数
  -大多数函数运行从/到PLC进行读取/写入操作
     3: 读取多个保持寄存器
     1: 读取线圈
     5: 写入单个线圈
       …
  -检测函数
  -其他一些函数
数据字节或者命令

信息存储

这里有两个可用来存储信息的地方:线圈以及寄存器。每个数据存储类型都有两个不同的寄存器:一个是读/写寄存器,一个是只读寄存器,且每个数据存储类型都引用一个内存地址。

简单的说:

线圈用于存储简单的布尔值(1 bit).可读/写,从00001到09999;
离散输入:只读类型的布尔值,从10001到19999;
输入寄存器:只读类型的长值(16 bits),从30001到39999;
保持寄存器:读/写类型的长值(16 bits),从40001到49999;

请注意:由于硬件不同,有些寄存器是从0开始,有些是从1开始。

单元标识符

在大多数情况下,在Modbus单元设备中因为之前已经通过它的IP地址处理了正确单元,所以你不需要一个单元id。但有些时候你可能会遇到多台设备连接到一个IP地址,如果是这样的话,你就必须将单元ID设置为255

单元ID为0你可以将其看作一个广播地址,信息发送到0,所有的从属控制器都可以接收。如果你是设置一个Modbus客户端,记住一定不要将单元ID设置为0

Modbus流量

你可以使用ModbusPal来模拟Modbus从属控制器的行为,这个Java应用允许你同不同的从属控制器(寄存器和线圈)愉快玩耍。你也可以使用MBTGET(纯Perl写的modbus/TCP客户端)来查询Modbus实例。

这儿有几个备选方案,你可以使用它们来玩Modbus:

Modbus poll (主应用在Windows)

CAS Modbus Scanner (主应用在Windows)

ModScan (主应用在Windows)

modbus-tk (Linux上模拟从属控制器)

Conpot (Linux上的一个Modbus -ICS蜜罐)

目前我在Kali VM上部署ModbusPal(Slave),在Linux VM上部署MBTGET(Master)

Modbus Slave : 192.168.171.182
Modbus Master : 192.168.171.139

分析Modbus流量

在OSX上使用vmnet-sniffer获取俩不同虚拟机的流量

sudo "/Applications/VMware Fusion.app/Contents/Library/vmnet-sniffer" -w modbus.pcap vmnet8

过一会儿你就可以使用Wireshark读取pcap文件,Modbus TCP流量运行在tcp/502端口

设置ModusPal

首先我们需要设置ModusPal来模拟Modbus从属控制器,下载ModbusPal之后运行:

java –jar ModbusPal.jar

增加一个从属控制器,编辑从属控制器并添加一些线圈。

理论上来说你也可以更改一些线圈的值,记住他们都是布尔值,所以这些值不是0便是1。单击运行,启动从属控制器。

MBTGET

切换到安装好MBTGET的Linux客户端,MBTGET上手十分容易:

usage : mbtget [-hvdsf] [-2c]
               [-u unit_id] [-a address] [-n number_value]
               [-r[12347]] [-w5 bit_value] [-w6 word_value]
               [-p port] [-t timeout] serveur

你可以使用-r1读取线圈,使用-r3可以读取保持寄存器。

Modbus中查询线圈

第一次流量抓取是在从属控制器中查询线圈,vmnet-sniffer可以完成网络流量抓取的任务,之后将获得的文件导入Wireshark,使用如下Modbus命令:

mbtget -r1 -u 1 -n 8 192.168.171.182

从192.168.171.182(slave)的unit id 1开始读取8寄存器,输出为:

values:
  1 (ad 00000):     0
  2 (ad 00001):     0
  3 (ad 00002):     1
  4 (ad 00003):     0
  5 (ad 00004):     1
  6 (ad 00005):     0
  7 (ad 00006):     0
  8 (ad 00007):     0

在Wireshark中进行筛选过滤:

tcp.port == 502

在TCP3次握手完成之后,紧接着就是Modbus数据包:

让我们看看Modbus数据包,Wireshark有一个Modbus编码器方便观察数据。从抓取到的数据中我们可以看出在unit id 1(-u 1)中从线圈(-r1)中读取8字节(-n 8)的请求:

下面一个是Modbus应答的数据包,在应答数据包中有一个跟上面数据包相同的Transaction Identifier(36710)。仔细研究后发现,这是Modbus同步通信的方式。应答数据包中也包含请求函数(F1 – read coils)和单元标识符(1)。最有趣的还得数payload中的数据。

Data 14是16进制数,线圈值是布尔值或者二进制值。所以我们需要将14转换为二进制:

1 = 0001
4 = 0100

转换为2进制为00010100

这个值与我们之前在ModbusPal设置的线圈一致,第三个和第五个寄存器设置为1。

在Modbus中检索保持寄存器

接下来设置3个保持寄存器的值:

然后查询从属控制器中的值:

mbtget -r3 -u 1 -n 8 192.168.171.182
values:
  1 (ad 00000):     0
  2 (ad 00001):     5
  3 (ad 00002):     0
  4 (ad 00003):    10
  5 (ad 00004):     0
  6 (ad 00005):    20
  7 (ad 00006):     0
  8 (ad 00007):     0

在流量抓取中你可以看到请求的函数是读取保持寄存器(F3):

Modbus应答请求函数function call (3),以及8个(从0开始)寄存器:

在Modbus写保持寄存器

如果写一个保持寄存器,看看会发生什么?

mbtget -w6 333 -u 1 -a 8 192.168.171.182
word write ok

抓取到的数据包显示了一个熟悉的输出,3次握手之后的Modbus数据包中包含Write Single Register请求函数,引用号码(8)以及payload (data, 014d)。

这响应数据包又包含了请求的函数(6)以及提交的payload:

Nmap modbus-discover

你可以使用nmap搜索Modbus设备,而且针对Modbus设备这里还提供了一个脚本:

sudo nmap -p 502 -sV --script modbus-discover.nse 192.168.171.182

如果你看一看脚本的源代码,你可以看到它试图发现可用设备的IDs

for sid = 1, 246 do
  stdnse.debug3("Sending command with sid = %d", sid)
  local rsid = form_rsid(sid, 0x11, "")

注意0×11,十六进制0×11也就是十进制的17。Modbus function code 17是Report Slave ID的诊断函数,如果你打开一个抓取的数据包你可以看到相同的请求。

从ModbusPal返回的应答表示了不支持这个函数请求

之后NSE发现脚本发送了另外请求:

discover_device_id_recursive = function(host, port, sid, start_id, objects_table)
  local rsid = form_rsid(sid, 0x2B, "\x0E\x01" .. bin.pack('C', start_id))
  local status, result = comm.exchange(host, port, rsid)

在注意下payload 0x2B,十六进制0x2B也就是十进制的43。Modbus function code 43同时也是Read Device Identification的诊断函数

结论

读取/阅读Modbus TCP流量并不难,最大的挑战可能就是抓取流量这个过程,特别是这个串行通信。Modbus串行通信和Modbus TCP通信差不多,所以一旦你抓取到数据使用Wireshark就能很简单的进行分析了

*原文地址:vanimpe,编译/ 鸢尾,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

源链接

Hacking more

...