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:

TypeAccessSizeAddresses

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:

$ modbus read 10.10.10.10 400001 5
1
1
1
1
1

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:

...
111
119
95
101
110
97
98
108
101
100
58
102
97
108
115
101
17
17
17
...

These values are all in the 50-60 and 90-110 range which is likely an ASCII string padded with 17s. 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):

$ modbus write 10.10.10.10 400020 116 114 117 101 17

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:

TypeSizeFormatted AddressAddressParameter

word (default, unsigned)

16 bits

%MW100

400101

--word

integer (signed)

16 bits

%MW100

400101

--int

floating point

32 bits

%MF100

400101

--float

double word

32 bits

%MD100

400101

--dword

boolean (coils)

1 bit

%M100

101

N/A

Last updated