Be part of the Intuity Army
Send your application to careers@intuity.it.
By submitting your application, you confirm that you have read the privacy policy regarding the processing of your personal data for recruitment purposes.
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 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
The Protocol
Modbus is a well-defined protocol described on modbus.org. 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:
| FUNCTION CODE | DATA |
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 possible 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:
x01x01x00x33x00x02
Where:
| x01 [SlaveId] | x01 [Function Code] | x00x33 [Address] | x00x02
[Quantity] |
As it can be noticed, that is quite similar to an API based modern application, the name of the function and its arguments:
Protocol://URL/EndPoint?Parameters=Values..
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 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
| 0x80 + [Request Function Code] | 0xHH [Exception Code] | … |
Where exceptions code are the following:
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.
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 ofimplemented 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):
switch(exception(resp)):
case 0x01: # UNIMPLEMENTED Function Error
#Function does not Exist (maybe)!
break;
case 0x02: # Invalid Address Error
#Function Exists !
break;
case 0x03: # Invalid Data Error
#Function Exists !
break;
default: # other codes..
break;
the previous pseudo code shows the approach we use to identify if a custom function is implemented and where we should fuzz.
The Tool
- finding undocumented functions
- fuzzing the arguments in order to find security issues or weird behavior.
$ python3 msak.py -S -d ‘0001’Requested Data x01x01x00x01x91xD8..Requested Data x01x02x00x01x91xD8..Requested Data x01x03x00x01x91xD8…Requested Data x01x64x00x01x91xD8…ILLEGAL DATA VALUE1 (0x01) Read Coils [FUN_ID|ADDRESS|TOTAL NUMBER| >BHH]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]15 (0x0F) Write Multiple Coils [FUN_ID|ADDRESS|TOTAL NUM|BYTE COUNT|BYTE VALS >BHHBN*B]16 (0x10) Write Multiple registers [FUN_ID|ADDRESS|TOTAL NUM|BYTE COUNT|VALS >BHHBN*H]20 (0x14) Read File RecordACCEPTED_WITH_RESPONSE5 (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]105 CUSTOMILLEGAL FUNCTION7 (0x07) Read Exception Status (Serial Line only) [FUN_ID >B]8 (0x08) Diagnostics (Serial Line only) [|FUN_ID|SUB_FUN|VALUES| >BHN*H]9 CUSTOM10 CUSTOM11 (0x0B) Get Comm Event Counter (Serial Line only) [FUN_ID >B]12 (0x0C) Get Comm Event Log (Serial Line only)[FUN_ID >B]13 CUSTOM….ILLEGAL DATA ADDRESS21 (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 msak.py -C -d ‘0169{R[0,0xFF,”>H”]}’
{‘NO_RESPONSE’:[ …b’x01x69x36xfe’,…],‘ACCEPTED_WITH_RESPONSE’:[ b’x01x69x36xff’,b’x01x69x37x00′,…]}