Modbus - TCP/502
A protocol for PLCs to store values in coils, inputs, and registers at addresses
Description
Modbus is a communication protocol commonly used in Programmable Logic Controllers (PLCs). There are a few different versions, but the most common are Modbus RTU and TCP:
Modbus RTU (Remote Terminal Unit): Used in slow serial communication, and makes use of a compact, binary representation of the data for protocol communication
Modbus ASCII: Similar to RTU, but only using printable ASCII characters for protocol communication, meaning it is less efficient
Modbus RTU/IP: A variant of Modbus TCP that differs in that a checksum is included in the payload, as with Modbus RTU
You can see a Modbus server as a small piece of memory, being able to store values at addresses in different sizes. The client asks the server for values or tells it to write a value somewhere. The following table shows all the different types of values:
Type | Access | Size | Addresses |
---|---|---|---|
Coil | Read-write | 1 bit (0-1) | 00001 – 09999 |
Discrete input | Read-only | 1 bit (0-1) | 10001 – 19999 |
Input register | Read-only | 16 bits (0-65535) | 30001 – 39999 |
Holding register | Read-write | 16 bits (0-65535) | 40001 – 49999 |
These coils, inputs, and registers can all have arbitrary meanings, like configuration values, status outputs, or ASCII-encoded strings. The addresses are pretty limited, so it is generally useful to dump all the data and then guess what it means locally, and possibly by testing the effects of changing values.
Interaction
Using a tool like modbus-cli
you can interact with a modbus server easily from the CLI:
This writeup is a good reference as an example of reading data, understanding it, and then changing it using the tool above. First, we try reading from the different registers, which we can even do in bulk for large ranges at a time. Be careful with this however because if your requested range falls out of the range the server holds it will give an InvalidAddress
error message. Here we try reading the first 5 values from the Holding registers:
Do dump the entire space, simply keep increasing this amount until you get an error. From here it is a matter of understanding what the data means. The NahamConCTF 2023 - Where's my Water challenge for example contained the following values:
These values are all in the 50-60 and 90-110 range which is likely an ASCII string padded with 17
s. We can decode it using CyberChef to "ow_enabled:false"
. We could try to change this false
to true
to see what happens, and then write it back to the Modbus server at the correct offset (CyberChef):
This was the solution to the challenge, because when the server now reads the Modbus data it finds our altered "ow_enabled:true"
string instead.
Using the tool above you can even write special data types like signed integers or floating point numbers by providing a --int
or --float
parameter:
Type | Size | Formatted Address | Address | Parameter |
---|---|---|---|---|
word (default, unsigned) | 16 bits | %MW100 | 400101 |
|
integer (signed) | 16 bits | %MW100 | 400101 |
|
floating point | 32 bits | %MF100 | 400101 |
|
double word | 32 bits | %MD100 | 400101 |
|
boolean (coils) | 1 bit | %M100 | 101 | N/A |
Last updated