Wednesday, March 6, 2024

Testing the Security of Modbus Services

ICS and Building Management Systems (BMS) support several protocols such as Modbus, Bacnet, Fieldbus and so on. Those protocols were designed to provide read/write control over sensors and actuators from a central point. 

Driven by our past experience with BMS, we decided to release our own methodology and internal tool used for proactive attack surface analysis within systems supporting the Modbus protocol.

The Modbus Protocol

Modbus is a well defined protocol described on It was created in 1979 and has become one of the most used standards for communication between industrial electronic devices in a wide range of buses and network.

It can be used over a variety of communication media, including serial, TCP, UDP, etc..

The application part of the protocol is quite simple. In particular, the part we are interested into is its Protocol Data Unit, which is independent from the lower layer protocols, is defined as follows:


Where FUNCTION CODE is a 1 Byte size 0-127 (0x00-0x7F) value, and DATA is a sequence of bytes that changes according to the function code.

Here is a set of function codes already defined by the protocol specification:

By setting a specific function code together with its expected set of data field values, it will be possibile to read/write the status of coils, inputs and registers, or access information about other interesting aspects such as diagnostic data.

For example the following request, queries about the status of 2 coils starting from address 0x0033 in a remote device:



| \x01 [SlaveId] | \x01 [Function Code] | \x00\x33 [Address] | \x00\x02 [Quantity] |

As it can be noticed, that is quite similar to an API based modern application, the name of the function and its arguments:


Apart from the public function codes, several codes are left as custom implementation and are reserved but not defined in the standard.

In particular 65-72 (0x41-0x48), and 100-110 (0x64-0x6e), for a total of 19 function codes, are left to the vendor/manufacturer for custom implementations.

While some of the vendors make the specification of custom functions, publicly available, with all the expected arguments and formats, in their manuals, others do not release any information.
From a security tester point of view, first questions are:

  • How can we identify if a custom Function Code is implemented but no details are available?
  • How can we find the correct set of expected arguments?
  • How can we fuzz the arguments to find security issues?

Modbus Attack Surface

As defined by the standard, if the client request presents some error the slave response will trigger specific exception codes:

| 0x80 + [Request Function Code] | 0xHH [Exception Code] | ... |

Where exceptions code are the following:

This behavior can help when testing and identifying the exposed services.

In particular, the first three exceptions will help identifying the presence of a custom function code.

  • 0x01 Unimplemented Function: Function does not exist in the present status.
  • 0x02 Function Implemented but address is not correct: Function exists but address is wrong.
  • 0x03 Function Implemented but the arguments are not correct: Function exists but provided arguments are wrong.

As you may already guessed 0x02 and 0x03 responses do actually reveal the presence of a custom function!
On the other hand 0x01 does not mean that there's no custom implementation for that requested function but just that it's not available for the status of the device and it will require some more analysis effort.

The Methodology

Apart from public function codes, where it would be quite easy to check for read/write access to data, we want to identify if there's a set of implemented custom function codes on a black box system.

According to the response, we'll identify if a function code is implemented by analyzing the response for each required function code:

for code in function_codes:

resp = send(code)

if has_exception(resp):


      case 0x01: # UNIMPLEMENTED Function Error

                    #Function does not Exist (maybe)!  


      case 0x02: # Invalid Address Error

                    #Function Exists ! 


      case 0x03: # Invalid Data Error

                    #Function Exists ! 


      default: # other codes..


the previous pseudo code shows the approach we use to identify if a custom function is implemented and where we should fuzz.

The Tool

Here comes M-SAK (the Modbus Swiss Army Knife), a pretty useful command line and library which can help for scanning and identifying custom functions on a Modbus device.
MSAK is a tool written in Python to help discovering and testing exposed standard and custom services of Modbus Servers/Slaves over Serial or TCP/IP connections. 

It also offers a highly customizable payload generator that will help the tester to perform complex scans using a simple but powerful templating format.

MSAK can help in:
- finding undocumented functions 
- fuzzing the arguments in order to find security issues or weird behavior.

For example if we want to scan all function we can just use the Service Scan option, which will scan all functions codes [1-127] using the given payload and then will print a summary grouped by response:

 $ python3 -S -d '0001'
Requested Data \x01\x01\x00\x01\x91\xD8
Requested Data \x01\x02\x00\x01\x91\xD8
Requested Data \x01\x03\x00\x01\x91\xD8
Requested Data \x01\x64\x00\x01\x91\xD8
2 (0x02) Read Discrete Inputs [FUN_ID|ADDRESS|TOTAL NUMBER| >BHH]
3 (0x03) Read Holding Registers [FUN_ID|ADDRESS|TOTAL NUMBER| >BHH]
4 (0x04) Read Input Registers [FUN_ID|ADDRESS|TOTAL NUMBER| >BHH]
16 (0x10) Write Multiple registers [FUN_ID|ADDRESS|TOTAL NUM|BYTE COUNT|VALS >BHHBN*H]
20 (0x14) Read File Record
5 (0x05) Write Single Coil [FUN_ID|ADDRESS|COIL VALUE| >BHH]
6 (0x06) Write Single Register [FUN_ID|ADDRESS|REG VALUE| >BHH]
17 (0x11) Report Server ID (Serial Line only)  [FUN_ID >B]
7 (0x07) Read Exception Status (Serial Line only) [FUN_ID >B]
8 (0x08) Diagnostics (Serial Line only) [|FUN_ID|SUB_FUN|VALUES| >BHN*H]
11 (0x0B) Get Comm Event Counter (Serial Line only) [FUN_ID >B]
12 (0x0C) Get Comm Event Log (Serial Line only)[FUN_ID >B]
21 (0x15) Write File Record

The result shows that a custom function 0x69 (105) was found as the device responded with a 0x03 exception (Illegal data value). We can now try to find the correct set of arguments through fuzzing using the following command: 

$ python3 -C -d '0169{R[0,0xFF,">H"]}'

Which will send 0 to 65535 requests and collect the responses.
In fact:
- 0x01 is the slave ID
- 0x69 is the service function
- R[0,0xFF,">H"] asks to generate 0-65535 sequence of payloads in for a 2 Bytes, little endian format.
- the request will be via serial port and the CRC are automatically computed.

After the whole scan, MSAK will return an output similar to the following:

  [ ... 
  [ b'\x01\x69\x36\xff', 

We have found that the undocumented function responds to arguments values of \x36\xff and \x37\x00! 
Next step is to play with this new function and see if there's some way to abuse it...
What could go wrong if an undocumented function for firmware update is found, right?! :P 

For more information, the fuzzing engine support several template patterns documented on MSAK README file.

N.B.: The fuzzing template engine is available also as a separate python library called Simple Payload Generator.


Although Modbus is a quite old protocol, it's still used on Build Management Systems, Industrial Control Systems and SCADA Systems. 
Apart from dealing using well defined functionalities, to  read/write sensors and controls, which could lead to very interesting security issues, the standard leaves pretty much space to vendors for implementing their own services and that might be even more interesting from a security point of view!
That's where MSAK can give its best by automating the boring part and leave all the fun to the tester!

Feel free to send us your feedback and happy hacking!

Author: Stefano Di Paola 
Twitter: @WisecWisec 

No comments :

Post a Comment