Modbus is a protocol used to read or write data to Programmable Logic Controllers (PLC) . It was originally developed in 1979 for use with RS232 connections, but was later adapted to use TCP/IP.
By default, Modbus listens on port 502/TCP.
The protocol exchanges information using a request reply mechanism between a master (the client) and a slave (the server). The slave only transmits information when requested to do so by the master.
Each slave has its own station ID, typically between 0 and 255 for TCP connections.
Coils and Registers
Modbus data is accessed through registers. These are are 16-bit units of data. It is possible to read 32-bit values, but this is done through reading two adjacent 16-bit registers.
In addition to registers, the following terms for data retrieval are also used;
Coils – read/write boolean values
Input registers – read only integers
Discrete inputs – read only boolean values
Modbus lacks modern security features, such as authentication or transport layer security.
Identifying Modbus Nodes
Fingerprinting Modbus slaves allows to us determine the station ID which is required for communication, in addition to providing us with information about the device manufacturer, which may assist with further attacks.
The NMap modbus-discover script can be used to determine the station ID (in this instance 5).
nmap --script modbus-discover -p 502 127.0.0.1 Starting Nmap 7.92 ( https://nmap.org ) at 2022-08-01 19:36 BST Nmap scan report for localhost (127.0.0.1) Host is up (0.000059s latency). PORT STATE SERVICE 502/tcp open modbus | modbus-discover: | sid 0x5: | Slave ID data: FieldTalk |_ Device identification: proconX Pty Ltd FT-MBSV 188.8.131.52
Metasploit can also be used to find this information:
msf6 auxiliary(scanner/scada/modbus_findunitid) > run [*] Running module against 127.0.0.1 [+] 127.0.0.1:502 - Received: correct MODBUS/TCP from stationID 1 [*] Auxiliary module execution completed
In addition, the banner grabbing module can be useful to find more information about the device, such as the vendor and software versions:
msf6 auxiliary(scanner/scada/modbus_banner_grabbing) > run [*] 127.0.0.1:502 - Number of Objects: 8 [+] 127.0.0.1:502 - VendorName: proconX Pty Ltd [+] 127.0.0.1:502 - ProductCode: FT-MBSV [+] 127.0.0.1:502 - Revision: 184.108.40.206 [+] 127.0.0.1:502 - VendorUrl: http://www.modbusdriver.com [+] 127.0.0.1:502 - ProductName: FieldTalk [+] 127.0.0.1:502 - ModelName: Modbus Slave C++ Library [+] 127.0.0.1:502 - UserAppName: diagslave [+] 127.0.0.1:502 - PrivateObjects: Custom data 123 [*] 127.0.0.1:502 - Scanned 1 of 1 hosts (100% complete) [*] Auxiliary module execution completed
Simulating a Modbus Slave
The best Modbus slave simulator I’ve come across is the Java application ModbusMechanic. To start the Slave node, go to Tools > Start Slave Simulator > TCP and add some data:
We can then query the configured values using the Python tool modbus-cli. In the below example the letter before the @ symbol designates the data type (either a coil or integer), and the number after the register slot.
user@MacBook ModbusMechanic % modbus 127.0.0.1 c@1 Parsed 0 registers definitions from 1 files 1: 1 0x1 user@MacBook ModbusMechanic % modbus 127.0.0.1 c@2 Parsed 0 registers definitions from 1 files 2: 0 0x0 user@MacBook ModbusMechanic % modbus 127.0.0.1 c@3 Parsed 0 registers definitions from 1 files 3: 1 0x1 user@MacBook ModbusMechanic % modbus 127.0.0.1 i@4 Parsed 0 registers definitions from 1 files 4: 31337 0x7a69
Being a plaintext protocol, the Modbus traffic can be intercepted and parsed using Wireshark:
Modbus With Python
The Python library PyModBus can be used to programmatically interface with the protocol. The following code demonstrates setting a coil and retrieving the value set:
from pymodbus.client.sync import ModbusTcpClient client = ModbusTcpClient('127.0.0.1') print("Setting coil 1 to false") client.write_coil(1, False) result = client.read_coils(1,1) print("Coil 1 state: " + str(result.bits)) print("Setting coil 1 to true") client.write_coil(1, True) result = client.read_coils(1,1) print("Coil 1 state: " + str(result.bits)) client.close()
We can see the coil state has changed on invoking the code:
python3 read_modbus_data.py Setting coil 1 to false Coil 1 state: False Setting coil 1 to true Coil 1 state: True
The simplity of Modbus has led to it’s prevelance in industry control applications. However, due to it’s lack of modern security features, it should not be deployed on untrusted networks. Work is underway to create a version of Modbus additional features, however this will likely be hampered by interoperability concerns.