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 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 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\x01\x00\x33\x00\x02
Where:
| \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:
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.
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
| 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.
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):
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
$ python3 msak.py -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...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'\x01\x69\x36\xfe',...],'ACCEPTED_WITH_RESPONSE':[ b'\x01\x69\x36\xff',b'\x01\x69\x37\x00',...]}