M

Fuzzing ICS protocols

ICS stands for Industrial Control Systems, the term is very generic and is commonly used to describe control systems and their instrumentation, usually for controlling and monitoring industrial processes. ICS basically integrates hardware, software and their network connectivity for running and supporting critical infrastructure. ICS systems get data from remote sensors and send commands to the machinery for the appropriate actions to take.

In this post we are going to focus in BACnet and modbus protocols.

Typical ICS components

  • Supervisory Control and Data Acquisition (SCADA)
  • Industrial Automation and Control Systems (IACS)
  • Human Machine Interface (HMI)
  • Distributed Control Systems (DCS)
  • Control Servers
  • Programmable Automation Controllers (PAC)
  • Programmable Logic Controllers (PLC)
  • Intelligent Electronic Devices (IED)
  • Sensors
  • Remote Terminal Units (RTU)

Typical ICS protocols used

  • The Common Industrial Protocol (CIP) is a protocol created by the ODVA company for automating industrial processes. CIP comprises a set of services and messages for control, security, synchronization, configuration, information, and so forth which can be integrated into Ethernet networks and into the Internet.

  • Modbus is one of the oldest industrial control protocols. It was introduced in 1979 using serial communications to interact with PLCs. It is open-source and freely distributed and can be built by anyone into their equipment. Modbus work in the application layer, which permits different physical uses for transport layer. It provides communication in client-server mode among differing sorts of equipment connected through different technologies on lower layers, which include but not limited to, the TCP/IP protocol layer. Modbus has several security concerns: Lack of authentication, lack of encryption, lack of message checksum and lack of broadcast suppression.

  • DNP3 stands for Distributed Network Protocol. It was developed in 1993 and is widely used in the USA and Canada. It is only sparsely used in Europe, because there are alternatives such as IEC-60870-5-101 or IEC-60870-5-104. It operates at the application, data link and transport layers; thus, it is a three-layer protocol.

  • HART (Highway Addressable Remote Transducer) is a hybrid analog+digital industrial automation open protocol. HART is widely used in process and instrumentation systems ranging from small automation applications up to highly sophisticated industrial applications.

  • TASE (Telecontrol Application Service Element) 2.0 is also known as ICCP (Inter Control Center Protocol) or International Electrotechnical Commission (IEC) 60870-6, but they are more commonly referred to as ICCP. Since different vendors had their own custom and proprietary protocols, there was a need for a common protocol for communication and data exchange between different control centers. Keeping this in mind, ICCP/TASE 2.0 was designed. Unlike Modbus, which was designed for serial communication, ICCP has been designed specifically for communication over LAN (Local Area Network) and WAN (Wide Area Network). ICCP is used in communication between different control centers, power pools, sub-stations, other utilities and non-utility generators.

  • Profibus (PROcessFieldBUS acronym) is a standard for communication through Fieldbus promoted in 1989 by the German Department of Education and Researchand used by Siemens. It is based on serial communications by cable (RS-485, MBP) or optical fibercable. Has two variants, Profibus DP (for decentralized peripherals) and Profibus PA (for process automation).

  • Profinet is a standard based on Profibus that adopts Ethernet as its physical interface for connections rather than RS485. It offers the complete TCP/IP functionality for data transmission. Profinet is oriented toward reliability and real-time communications, together with usability.

  • BACnet was designed to allow communication of building automation and control systems for applications such as heating, ventilating, and air-conditioning control (HVAC), lighting control, access control, and fire detection systems and their associated equipment. The BACnet protocol provides mechanisms for computerized building automation devices to exchange information, regardless of the particular building service they perform.

  • OPC (OLE for process control) is not an industrial communications protocol, but rather an operational framework for communications in process control systems based on Windows that use object linking and embedding (OLE).

Standard Protocol Ports

Protocol Ports
BACnet/IP UDP/47808
DNP3 TCP/20000, UDP/20000
EtherCAT UDP/34980
Ethernet/IP TCP/44818, UDP/2222, UDP/44818
FL-net UDP/55000 to 55003
Foundation Fieldbus HSE TCP/1089 to 1091, UDP/1089 to 1091
ICCP TCP/102
Modbus TCP TCP/502
OPC UA Binary Vendor Application Specific
OPC UA Discovery Server TCP/4840
OPC UA XML TCP/80, TCP/443
PROFINET TCP/34962 to 34964, UDP/34962 to 34964
ROC PLus TCP/UDP 4000

Vendor Specific Ports

Vendor Product or Protocol Ports
ABB Ranger 2003 TCP/10307, TCP/10311, TCP/10364 to 10365, TCP/10407, TCP/10409 to 10410, TCP/10412, TCP/10414 to 10415, TCP/10428, TCP/10431 to 10432, TCP/10447, TCP/10449 to 10450, TCP/12316, TCP/12645, TCP/12647 to 12648, TCP/13722, TCP/13724, TCP/13782 to 13783, TCP/38589, TCP/38593, TCP/38600, TCP/38971, TCP/39129, TCP/39278
Emerson / Fisher ROC Plus TCP/UDP/4000
Foxboro/Invensys Foxboro DCS FoxApi TCP/UDP/55555
Foxboro/Invensys Foxboro DCS AIMAPI TCP/UDP/45678
Foxboro/Invensys Foxboro DCS Informix TCP/UDP/1541
Iconics Genesis32 GenBroker (TCP) TCP/18000
Johnson Controls Metasys N1 TCP/UDP/11001
Johnson Controls Metasys BACNet UDP/47808
OSIsoft PI Server TCP/5450
Siemens Spectrum Power TG TCP/50001 to 50016, TCP/50018 to 50020, UDP/50020 to 50021, TCP/50025 to 50028, TCP/50110 to 50111
SNC GENe TCP/38000 to 38001, TCP/38011 to 38012, TCP/38014 to 38015, TCP/38200, TCP/38210, TCP/38301, TCP/38400, TCP/38700, TCP/62900, TCP/62911, TCP/62924, TCP/62930, TCP/62938, TCP/62956 to 62957, TCP/62963, TCP/62981 to 62982, TCP/62985, TCP/62992, TCP/63012, TCP/63027 to 63036, TCP/63041, TCP/63075, TCP/63079, TCP/63082, TCP/63088, TCP/63094, TCP/65443
Telvent OASyS DNA UDP/5050 to 5051, TCP/5052, TCP/5065, TCP/12135 to 12137, TCP/56001 to 56099

Fuzzing BACnet

BACnet

BACnet was designed to allow communication of building automation and control systems for applications such as heating, ventilating, and air-conditioning control (HVAC), lighting control, access control, and fire detection systems and their associated equipment. The BACnet protocol provides mechanisms for computerized building automation devices to exchange information, regardless of the particular building service they perform.

BACnet is a communications protocol for Building Automation and Control (BAC) networks UDP based and contains 3 main headers the BVLC, NPDU and APDU.

A request to BACnet passes down through the lower layers of the protocol stack in the local device, this process can be observed in the next image Source.

BACnet_protocol_stack_data_flow

The 3 headers can be examined in the BACnet Wiki

BACnet_packet

The firts byte defines the type, in this case bacnet/ip 0x81, the second one defines the function 0x0a, and the last 2 bytes defines the length of the whole packet BACnet Wiki:

  • The firts byte defines the type: bacnet/ip 0x81
  • The second byte defines the function 0x0a (ORIGINAL_UNICAST_NPDU = 10)
  • The last 2 bytes defines the length of the whole packet

bvlc

  • NPDU Function:

bvlc

As we can see in the BACnet packet the UDP Port number used by BACnet communications over IP is 47808:

bvlc

NPDU (Network layer protocol data unit): 2 bytes

The NPDU consists of a NPCI followed by a NSDU. BACnet Wiki

According bacnetwiki this is the representation, but be careful here, in practice the headers is just 2 bytes, Version (Always 0x01) and NPCI Control Octet:

npdu

NPDU Layer:

npdu

APDU (Network layer protocol data unit): 2 bytes

BACnet APDUs carry the Application Layer parameters. The maximum size of an APDU is specified by a device’s Max_APDU_Length_Accepted, be careful with that, otherwise you will face malformed packet issues. BACnet APDU

apdu

In fuzzing we trust

First of all we need to understand and known how the protocol that we want to fuzz works. With this in mind in the next lines will be provided some examples on how to fuzz certain operations that are used in this case by BACnet. The examples provided below are written to be used with Fuzzowski a Network Protocol Fuzzer forked from BooFuzz (which is a fork of Sulley), so the next examples can be translated easily to boofuzz.

In BACnet the devices have a Device ID (Device Object Identifier) which is used in a BACnet network as the unique identifier of a specific device. The Device ID for each device must be unique on the entire BACnet network. Device Instance can be in the range of 0 to 4194304. So first of all we need to modify and set the BACnet Device ID in the BACnet fuzzer that we can found in the Fuzzowski repository

DeviceID = 12345 # BACnet Device ID

Also some Codes, Functions and Services or Types have been defined in the BACnet fuzzer to be used by the methods defined:

BVLC_Function_Code = [
    '\x00',  # BVLC Result
    '\x01',  # Write Broadcast Distribution Table 
    '\x02',  # Read Broadcast Distribution Table 
    '\x03',  # Read Broadcast Distribution Table ACK 
    '\x04',  # Forwarded-NPDU
    '\x05',  # Register Foreign Device
    '\x0a',  # Original-Unicast-NPDU
    '\x0b',  # Original-Broadcast-NPDU
    '\x0c'   # Secure-BVLL 
]

Confirmed_Service_Choices = [
    '\x05',  # Subscribe COV
    '\x0c',  # Read Property
    '\x0e',  # Read Property Multiple
    '\x0f',  # Write Property 
    '\x10',  # Write Property Multiple
    '\x11',  # Device Communication Control
    '\x14'   # Reinitialize Device
]

Network_Layer_Message_Type = [
    '\x00',  # Who-Is-Router-To-Network
    '\x01',  # I-Am-Router-To-Network
    '\x02',  # I-Could-Be-Router-To-Network
    '\x03',  # Reject-Message-To-Network
    '\x04',  # Router-Busy-To-Network
    '\x05',  # Router-Available-To-Network 
    '\x06',  # Initialize-Routing-Table
    '\x07',  # Initialize-Routing-Table-ACK
    '\x08',  # Establish-Connection-To-Network
    '\x09',  # Disconnect-Connection-To-Network
    '\x0a',  # Challenge-Request
    '\x0b',  # Security-Payload
    '\x0c',  # Security-Response
    '\x0d',  # Request-Key-Update
    '\x0e',  # Update-Key-Set
    '\x0f',  # Update-Distribution-Key
    '\x10',  # Request-Master-Key
    '\x11'  # Set-Master-Key
    # 0x12 to 0x7F Reserved for use by ASHRAE
    # 0x80 to 0xFF Available for Vendor Proprietary Messages
]

Here we can found the class where was defined the fuzzer and the methods that define all the operations that could be used to fuzz a BACnet device or server. In the example has been defined methods to fuzz DeviceCommunicationControl, who_is, i_Am, Initialize_Routing_Table, Who_Is_Router_To_Network, readProperty, atomicReadFile and atomicWriteFile. More can be defined, just adding the definition of the packet or operation.

class BACnet(IFuzzer):
    """
    BACnet Fuzzing Module example
    virtualenv venv -p python3
    source venv/bin/activate
    pip install -r requirements.txt
    python -m fuzzowski 127.0.0.1 47808 -p udp -f bacnet -rt 0.5 -m BACnetMon
    python -m fuzzowski 127.0.0.1 47808 -p udp -f bacnet -rt 0.5 -r who_is -m BACnetMon
    python -m fuzzowski 127.0.0.1 47808 -p udp -f bacnet -rt 0.5 -r DeviceCommunicationControl -m BACnetMon
    """

    name = 'bacnet'

    @staticmethod
    def get_requests() -> List[callable]:
        return [BACnet.DeviceCommunicationControl, BACnet.who_is, BACnet.i_Am, BACnet.Initialize_Routing_Table, BACnet.Who_Is_Router_To_Network, BACnet.readProperty, BACnet.atomicReadFile, BACnet.atomicWriteFile]

    @staticmethod
    def define_nodes(*args, **kwargs) -> None:
        
        # ---------------- DeviceCommunicationControl ------------------- #
        # Used in CVE-2019-12480
        # Start DeviceCommunicationControl bacnet request packet
        s_initialize("DeviceCommunicationControl")
        with s_block("bacnet_virtual_link_control"):
            s_byte(0x81, name='type_bvlc', fuzzable=False)
            s_byte(0x0a, name='function_bvlc', fuzzable=False)
            s_word(0x0017, name='length_bvlc', endian='>', fuzzable=True)
        with s_block("bacnet_npdu"):
            s_byte(0x01, name='version_bacnet', fuzzable=False)
            s_byte(0x04, name='control_bacnet', fuzzable=False)
        with s_block("bacnet_apdu"):
            s_byte(0x02, name='type_bacapp', fuzzable=True)
            s_byte(0x44, name='max_adpu_size_bacapp', fuzzable=True)
            s_byte(0x08, name='invoke_id_bacapp', fuzzable=True)
            s_byte(0x11, name='confirmed_service_bacapp', fuzzable=True)
            s_byte(0x0d, name='context_tag', fuzzable=True)
            s_byte(0xff, name='tag_class', fuzzable=True)
            s_byte(0x80, name='tag_number', fuzzable=True)
            s_word(0x0000, name='enable', endian='>', fuzzable=True)
            s_word(0x0000, name='passwd_length', endian='>', fuzzable=True)
            s_byte(0x00, name='lvt', fuzzable=True)
            s_dword(0x0a1a0300, name='lenght_value_type', endian='>', fuzzable=True)
            s_word(0x1900, name='enable-disable', endian='>', fuzzable=True)
            s_byte(0x2a, name='lvt_passwd', fuzzable=True)
            s_byte(0x00, name='string_char_set', fuzzable=True)
            s_string('A', name='passwd', fuzzable=True)
        # end bacnet DeviceCommunicationControl
        # ---------------- DeviceCommunicationControl ------------------- #
        
        # ------------------------- Who-Is ------------------------------ #
        # Start Who-Is bacnet request packet
        s_initialize("who_is")
        with s_block("bacnet_virtual_link_control"):
            s_byte(0x81, name='type_bvlc', fuzzable=False)
            s_byte(0x0b, name='function_bvlc', fuzzable=False)
            s_word(0x000c, name='length_bvlc', endian='>', fuzzable=True)
        with s_block("bacnet_npdu"):
            s_byte(0x01, name='version_bacnet', fuzzable=True)
            s_byte(0x20, name='control_bacnet', fuzzable=True)
            s_word(0xffff, name='detination', endian='>', fuzzable=True)
            s_byte(0x00, name='mac', fuzzable=True)
            s_byte(0xff, name='hop', fuzzable=True)
        with s_block("bacnet_apdu"):
            s_byte(0x10, name='type_bacapp', fuzzable=True)
            s_byte(0x08, name='confirmed_service_bacapp', fuzzable=True)
        # end
        # ------------------------- Who-Is ------------------------------ #

        # ------------------- Initialize_Routing_Table ------------------ #
        # Start Initialize_Routing_Table bacnet request packet
        s_initialize("Initialize_Routing_Table")
        with s_block("bacnet_virtual_link_control"):
            s_byte(0x81, name='type_bvlc', fuzzable=False)
            s_byte(0x0b, name='function_bvlc', fuzzable=False)
            s_word(0x0008, name='length_bvlc', endian='>', fuzzable=True)
        with s_block("bacnet_npdu"):
            s_byte(0x01, name='version_bacnet', fuzzable=True)
            s_byte(0x80, name='control_bacnet', fuzzable=True)
            s_byte(0x06, name='message_type', fuzzable=True)
            s_byte(0x00, name='rpot_number', fuzzable=True)
        # end
        # ------------------- Initialize_Routing_Table ------------------ #


        # ------------------ Who_Is_Router_To_Network ------------------- #
        # Start Who_Is_Router_To_Network bacnet request packet 
        s_initialize("Who_Is_Router_To_Network")
        with s_block("bacnet_virtual_link_control"):
            s_byte(0x81, name='type_bvlc', fuzzable=False)
            s_byte(0x0b, name='function_bvlc', fuzzable=False)
            s_word(0x0007, name='length_bvlc', endian='>', fuzzable=True)
        with s_block("bacnet_npdu"):
            s_byte(0x01, name='version_bacnet', fuzzable=True)
            s_byte(0x80, name='control_bacnet', fuzzable=True)
            s_byte(0x00, name='message_type', fuzzable=True)
        # end
        # ------------------ Who_Is_Router_To_Network ------------------- #
        
        # ---------------------------- i-Am ----------------------------- #
        # Start i-Am bacnet request packet 
        s_initialize("i_Am")
        with s_block("bacnet_virtual_link_control"):
            s_byte(0x81, name='type_bvlc', fuzzable=False)
            s_byte(0x0b, name='function_bvlc', fuzzable=False)
            s_word(0x0018, name='length_bvlc', endian='>', fuzzable=True)
        with s_block("bacnet_npdu"):
            s_byte(0x01, name='version_bacnet', fuzzable=True)
            s_byte(0x20, name='control_bacnet', fuzzable=True)
            s_word(0xffff, name='destination', endian='>', fuzzable=True)
            s_byte(0x00, name='destination_mac', fuzzable=True)
            s_byte(0xff, name='hop_count', fuzzable=True)
        with s_block("bacnet_apdu"):
            s_byte(0x10, name='type_bacapp', fuzzable=True)
            s_byte(0x00, name='confirmed_service_bacapp', fuzzable=True)
            # deviceID
            s_byte(0xc4, name='ObjectIdentifier_device', fuzzable=True)
            s_byte(0x02, name='ObjectIdentifier_instance_number', fuzzable=True)
            s_byte(DeviceID_byte1, name='ObjectIdentifier_deviceID_byte1', fuzzable=True)
            s_byte(DeviceID_byte2, name='ObjectIdentifier_deviceID_byte2', fuzzable=True)
            s_byte(DeviceID_byte3, name='ObjectIdentifier_deviceID_byte3', fuzzable=True)
            # deviceID
            s_byte(0x22, name='lvt', fuzzable=True)
            s_word(0x0400, name='max_adpu_size_bacapp', endian='>', fuzzable=True)
            s_word(0x9100, name='segmented_both', endian='>', fuzzable=True)
            s_word(0x2105, name='vendor_id', endian='>', fuzzable=True)
        # end
        # ---------------------------- i-Am ----------------------------- #
        
        # ------------------------- readProperty ------------------------ #
        # Start readProperty bacnet request packet
        s_initialize("readProperty")
        with s_block("bacnet_virtual_link_control"):
            s_byte(0x81, name='type_bvlc', fuzzable=False)
            s_byte(0x0b, name='function_bvlc', fuzzable=False)
            s_word(0x0011, name='length_bvlc', endian='>', fuzzable=True)
        with s_block("bacnet_npdu"):
            s_byte(0x01, name='version', fuzzable=False)
            s_byte(0x04, name='control', fuzzable=False)
        with s_block("bacnet_apdu"):
            s_byte(0x02, name='type_bacapp', fuzzable=False)
            s_byte(0x44, name='max_adpu_size_bacapp', fuzzable=True)
            s_byte(0x03, name='invoke_id_bacapp', fuzzable=True)
            s_byte(0x0c, name='confirmed_service_bacapp', fuzzable=True)
            # deviceID
            s_byte(0x0c, name='ObjectIdentifier_deviceID', fuzzable=True)
            s_byte(0x02, name='ObjectIdentifier_instance_number', fuzzable=True)
            s_byte(DeviceID_byte1, name='ObjectIdentifier_deviceID_byte1', fuzzable=True)
            s_byte(DeviceID_byte2, name='ObjectIdentifier_deviceID_byte2', fuzzable=True)
            s_byte(DeviceID_byte3, name='ObjectIdentifier_deviceID_byte3', fuzzable=True)
            s_word(0x194b, name='property_identifier_bacapp', endian='>', fuzzable=True)
        # end
        # ------------------------- readProperty ------------------------ #

        # ------------------------- atomicReadFile ----------------------- #
        # Start atomicReadFile bacnet request packet
        s_initialize("atomicReadFile")
        with s_block("bacnet_virtual_link_control"):
            s_byte(0x81, name='type_bvlc', fuzzable=False)
            s_byte(0x0a, name='function_bvlc', fuzzable=False)
            s_word(0x001b, name='length_bvlc', endian='>', fuzzable=True)
        with s_block("bacnet_npdu"):
            s_byte(0x01, name='version', fuzzable=False)
            s_byte(0x04, name='control', fuzzable=False)
        with s_block("bacnet_apdu"):
            s_byte(0x00, name='type_bacapp', fuzzable=False)
            s_byte(0x05, name='max_adpu_size_bacapp', fuzzable=True)
            s_byte(0x01, name='invoke_id_bacapp', fuzzable=True)
            s_byte(0x06, name='confirmed_service_bacapp', fuzzable=True)
            # file
            s_dword(0xc4028000, name='file', endian='>', fuzzable=True)
            s_byte(0x00, name='ObjectIdentifier', fuzzable=True)
            # stream
            s_word(0x0e35, name='named_tag', endian='>', fuzzable=True)
            s_dword(0xffdf62ee, name='lvt', endian='>', fuzzable=True)
            s_byte(0x00, name='ObjectIdentifier_2', fuzzable=True)
            s_dword(0x00220584, name='text', endian='>', fuzzable=True)
            s_byte(0x0f, name='named_tag_2', fuzzable=True)
        # end
        # ------------------------- atomicReadFile ----------------------- #

        # ------------------------- atomicWriteFile ---------------------- #
        # Start atomicWriteFile bacnet request packet
        s_initialize("atomicWriteFile")
        with s_block("bacnet_virtual_link_control"):
            s_byte(0x81, name='type_bvlc', fuzzable=False)
            s_byte(0x0a, name='function_bvlc', fuzzable=False)
            s_word(0x001b, name='length_bvlc', endian='>', fuzzable=True)
        with s_block("bacnet_npdu"):
            s_byte(0x01, name='version', fuzzable=False)
            s_byte(0x04, name='control', fuzzable=False)
        with s_block("bacnet_apdu"):
            s_byte(0x00, name='type_bacapp', fuzzable=False)
            s_byte(0x05, name='max_adpu_size_bacapp', fuzzable=True)
            s_byte(0x02, name='invoke_id_bacapp', fuzzable=True)
            s_byte(0x07, name='confirmed_service_bacapp', fuzzable=True)
            # file
            s_dword(0xc4028000, name='file', endian='>', fuzzable=True)
            s_byte(0x00, name='ObjectIdentifier', fuzzable=True)
            # stream
            s_word(0x0e35, name='named_tag', endian='>', fuzzable=True)
            s_dword(0xff5ed5c0, name='lvt', endian='>', fuzzable=True)
            s_byte(0x85, name='ObjectIdentifier_2', fuzzable=True)
            s_dword(0x0a62640a, name='text', endian='>', fuzzable=True)
            s_byte(0x0f, name='named_tag_2', fuzzable=True)
        # end
        # ------------------------- atomicWriteFile ---------------------- #

So we can start launching the fuzzer: python -m fuzzowski 127.0.0.1 47808 -p udp -f bacnet -rt 0.5 -m BACnetMon

While Fuzzing we can found errors like observed in the image below that could bring to us some information of the issue like a segmentation fault or error in the server that could help in a further attack. In the next image can be observed that the monitor defined for this fuzzer has been detected no response from the server and launched a fail message:

BACnet-fuzz-fault

If an error is detected, fuzzowski offers a cool functionallity that help us to generates a proof of concept of any fault identified.

BACnet-fuzz-poc

BACnet Monitor

Fuzzing is not the magic thing that provide us a way to found vulnerabilities, one of the most important things apart from define the fuzzer is to develop the monitor, and implement how we want to identify faults in the fuzzed system. In this case we have defined a monitor for the BACnet fuzzer that sends a query for Property Identifier ID to the target in order to get the BACnet device information and check the response. In our case if the BACnet device does not respond to this query a fault is generated or identified, and indeed all the faults are needed to be double checked to verify if exists or not an issue, crash or other type of output that could us help in a further attack.

class BACnetMonitor(IMonitor):
    """
    BACnet Monitor Module interface
    @Author: https://github.com/1modm
    Based on https://svn.nmap.org/nmap/scripts/bacnet-info.nse
    """

    get_bacnet_property_identifier_id = (b"\x81"  # Type: BACnet/IP (Annex J)
                                   b"\x0a"  # Function: Original-Unicast-NPDU
                                   b"\x00\x11"  # BVLC-Length: 4 of 17 bytes
                                   # BACnet NPDU
                                   b"\x01"  # Version: 0x01 (ASHRAE 135-1995)
                                   b"\x04"  # Control (expecting reply)
                                   # BACnet APDU
                                   b"\x00"  # APDU Type: Confirmed-REQ, PDU flags: 0x0
                                   b"\x05"  # Max response segments unspecified, Max APDU size: 1476 octets
                                   b"\x01"  # Invoke ID: 1
                                   b"\x0c"  # Service Choice: readProperty
                                   b"\x0c"  # Context-specific tag, number 0, Length Value Type 4
                                   b"\x02\x3f\xff\xff" # Object Type: device; instance number 4194303
                                   b"\x19\x4b" # Context-specific tag, number 1, Length Value Type 1
                                   # TODO, send DeviceID
                                   )

    @staticmethod
    def name() -> str:
        return "BACnetMon"

    @staticmethod
    def help():
        return "Sends a query for Property Identifier id to the target in order to get the BACnet device information and check the response"

    def test(self):
        conn = self.get_connection_copy()
        result = self._get_bacnet_info(conn)
        return result

    def _get_bacnet_info(self, conn: ITargetConnection):
        try:
            conn.open()
            conn.send(self.get_bacnet_property_identifier_id)
            data = conn.recv_all(10000)
            if len(data) == 0:
                self.logger.log_error("BACnet error response, getting BACnet device information Failed!!")
                result = False
            else:
                # validate valid BACNet Packet and verify that the response APDU was not an error packet
                if hex(data[0]) == '0x81' and hex(data[1]) == '0xa' and hex(data[6]) != '0x50':
                  self.logger.log_info(f"Getting BACnet device information succeeded")
                else:
                  self.logger.log_warn(f"Getting BACnet error response in the APDU")
                result = True
        except Exception as e:
            self.logger.log_error(f"BACnet error response, getting BACnet device information Failed!! Exception while receiving: {type(e).__name__}. {str(e)}")
            result = False
        finally:
            conn.close()

        return result

In the next image can be found the request sent to check if the BACnet device is still working:

BACnet-monitor-request

And the response in which can be observed that the device send the Instance Number (12345) of our target server tested:

BACnet-monitor-response

Fuzzing Modbus

In the same way than our previous protocol fuzzer, a modbus fuzzer has been uploaded as an example. And the way we could launch the fuzzer using the monitor implemented could be: python -m fuzzowski 127.0.0.1 502 -p tcp -f modbus -rt 1 -m modbusMon

The modbus class has the next methods implemented that could be used to fuzz any modbus/TCP server; read_coil, read_input, read_holding, read_discrete, single_coil, single_register, multiple_coil, multiple_register

class MODBUS(IFuzzer):
    """
    MODBUS Fuzzing Module
    Use at your own risk, and please do not use in a production environment
    @Author: https://github.com/1modm
    Based on https://github.com/youngcraft/boofuzz-modbus
    and https://github.com/riptideio/pymodbus
    virtualenv venv -p python3
    source venv/bin/activate
    pip install -r requirements.txt
    python -m fuzzowski 127.0.0.1 502 -p tcp -f modbus -rt 0.5 -r read_coil
    python -m fuzzowski 127.0.0.1 502 -p tcp -f modbus 
    python -m fuzzowski 127.0.0.1 502 -p tcp -f modbus -rt 1 -m modbusMon
    """

    # --------------------------------------------------------------- #

    name = 'modbus'

    @staticmethod
    def get_requests() -> List[callable]:
        return [MODBUS.read_coil, MODBUS.read_input, MODBUS.read_holding, MODBUS.read_discrete, MODBUS.single_coil, MODBUS.single_register, MODBUS.multiple_coil, MODBUS.multiple_register, MODBUS.other_operations]

    # --------------------------------------------------------------- #

    @staticmethod
    def define_nodes(*args, **kwargs) -> None:
        
        # ------------------ Read Coil Status (FC=01) ------------------- #

        s_initialize("modbus_read_coil")
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0000,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('pdu'):
                s_byte(0x01,name='funcCode read coil memory',fuzzable=False)
                s_word(0x0000,name='start address')
                s_word(0x0000,name='quantity')
        

        s_initialize('read_holding_registers')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('read_holding_registers_block'):
                s_byte(0x01,name='read_holding_registers')
                s_word(0x0000,name='start address')
                s_word(0x0000,name='quantity')
        # --------------------------------------------------------------- #


        # ------------------ Read Input Status (FC=02) ------------------ #
        s_initialize('ReadDiscreteInputs')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('ReadDiscreteInputsRequest'):
                s_byte(0x02,name='funcCode',fuzzable=False)
                s_word(0x0000,name='start_address')
                s_word(0x0000,name='quantity')
        # --------------------------------------------------------------- #

        # ---------------- Read Holding Registers (FC=03) --------------- #
        s_initialize('ReadHoldingRegisters')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('ReadHoldingRegistersRequest'):
                s_byte(0x03,name='funcCode',fuzzable=False)
                s_word(0x0000,name='start_address')
                s_word(0x0000,name='quantity')
        # --------------------------------------------------------------- #

        # ---------------- Read Input Registers (FC=04) ----------------- #
        s_initialize('ReadInputRegisters')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('ReadInputRegistersRequest'):
                s_byte(0x04,name='funcCode',fuzzable=False)
                s_word(0x0000,name='start_address')
                s_word(0x0000,name='quantity')
        # --------------------------------------------------------------- #


        # ------------------ Force Single Coil (FC=05) ------------------ #
        s_initialize('WriteSingleCoil')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('WriteSingleCoilRequest'):
                s_byte(0x05,name='funcCode',fuzzable=False)
                s_word(0x0000,name='start_address')
                s_word(0x0000,name='quantity')
        # --------------------------------------------------------------- #

        # ---------------- Preset Single Register (FC=06) --------------- #

        s_initialize('WriteSingleRegister')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('WriteSingleRegisterRequest'):
                s_byte(0x06,name='funcCode',fuzzable=False)
                s_word(0x0000,name='output_address')
                s_word(0x0000,name='output_value')
        # --------------------------------------------------------------- #

        # ---------------- Force Multiple Coils (FC=15) ----------------- #
        s_initialize('WriteMultipleCoils')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('WriteMultipleCoilsRequest'):
                s_byte(0x0f,name='func_code',fuzzable=False)
                s_word(0x0000,name='starting_address')
                s_dword(0x0000,name='byte_count')
                s_size("outputsValue", length=8)
                with s_block("outputs"):
                    s_word(0x00,name='outputsValue')
        # --------------------------------------------------------------- #

        # --------------- Preset Multiple Registers (FC=16) ------------- #
        s_initialize('WriteMultipleRegisters')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('WriteMultipleRegistersRequest'):
                s_byte(0x10,name='func_code',fuzzable=False)
                s_word(0x0000,name='starting_address')
                s_dword(0x0000,name='byte_count')
                s_size("outputsValue",length=16)
                s_size("outputsValue2", length=8)
                with s_block("outputs"):
                    s_dword(0x0000,name='outputsValue3')
        # --------------------------------------------------------------- #


        # ---------------------------- Other ---------------------------- #
        s_initialize('ReadExceptionStatus')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('ReadExceptionStatusRequest'):
                s_byte(0x07,name='funcCode',fuzzable=False)
    

        s_initialize('ReadExceptionStatusError')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('ReadExceptionStatusErrorRequest'):
                s_byte(0x87,name='funcCode',fuzzable=False)

        s_initialize('ReportSlaveId')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('ReportSlaveIdRequest'):
                s_byte(0x11,name='func_code',fuzzable=False)

        s_initialize('ReadFileSub')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('ReadFileSubRequest'):
                s_byte(0x06,name='refType',fuzzable=False)
                s_word(0x0001,name='fileNumber')
                s_word(0x0000,name='recordNumber')
                s_word(0x0000,name='recordLength')

        s_initialize('ReadFileRecord')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('ReadFileRecordRequest'):
                s_byte(0x14,name='funcCode',fuzzable=False)
                s_byte(0x0001,name='byteCount')

        s_initialize('WriteFileSub')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('WriteFileSubRequest'):
                s_byte(0x06,name='refType',fuzzable=False)
                s_word(0x0001,name='fileNumber')
                s_word(0x0000,name='recordNumber')
                # ---------------------------------
                # s_size is record
                s_size('recordData',length=16,name='recordLength')
                with s_block("recordData"):
                    s_word(0x0000,name='recordData_value')
                s_word(0x0000,name='recordLength_value')

        s_initialize('WriteFileRecord')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('WriteFileRecordRequest'):
                s_byte(0x15,name='funcCode',fuzzable=False)
                s_byte(0x00,name='datalength')

        s_initialize('MaskWriteRegister')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('MaskWriteRegisterRequest'):
                s_byte(0x96,name='funcCode',fuzzable=False)
                s_word(0x0000,name='refAddr')
                s_word(0xffff,name='andMask')
                s_word(0x0000,name='orMask')

        s_initialize('ReadWriteMultipleRegisters')
        with s_block("modbus_head"):
            s_word(0x0001,name='transId',fuzzable=True)
            s_word(0x0002,name='protoId',fuzzable=False)
            s_word(0x06,name='length')
            s_byte(0xff,name='unit Identifier',fuzzable=False)
            with s_block('ReadWriteMultipleRegistersRequest'):
                s_byte(0x17,name='funcCode',fuzzable=False)
                s_word(0x0000,name='readStartingAddr')
                s_word(0x0001,name='readQuantityRegisters')
                s_word(0x0000,name='writeStartingAddr')
                s_size('writeQuantityRegisters',length=16,endian='>',name="writeQuantityRegisters")
                s_size('writeQuantityRegisters', length=8, endian='>',name="byteCount",math=lambda x:2*x)
                with s_block('writeQuantityRegisters_block'):
                    s_size('modbus_head',length=2)

The monitor piece define a way to send a query for MODBUS device id to the target and check the response.

# --------------------------------------------------------------- #
SlaveID = 1 # Modbus TCP Unit Identifier: 1..247
# --------------------------------------------------------------- #

# ----------------- Device ID to bytes -------------------------- #
def bitDeviceID(DeviceID):
    bytes_id = (DeviceID).to_bytes((DeviceID.bit_length() + 7) // 8, byteorder='big')
    return bytes_id

HexSlaveID = bitDeviceID(SlaveID)

class modbusMonitor(IMonitor):
    """
    MODBUS Monitor Module interface
    @Author: https://github.com/1modm
    """

    # Based on https://svn.nmap.org/nmap/scripts/modbus-discover.nse
    # Send Read Device Identification 
    get_modbus_device_id_nse = (b"\x00\x00"  # Modbus TCP Transaction Identifier
                               b"\x00\x00"  # Modbus TCP Protocol Identifier
                               b"\x00\x05"  # Modbus TCP Length
                               # Modbus TCP Unit Identifier
                               + HexSlaveID +
                               # Discover device ID
                               b"\x2b"  # Modbus Function Code: Encapsulated Interface Transport
                               b"\x0e"  # Modbus MEI type: Read Device Identification
                               b"\x01"  # Modbus Read Device ID: Basic Device Identification
                               b"\x00"  # Modbus Object ID: VendorName
                               )

    # Send Report Slave ID request
    get_modbus_slave_id = (b"\x00\x00"  # Modbus TCP Transaction Identifier
                           b"\x00\x00"  # Modbus TCP Protocol Identifier
                           b"\x00\x02"  # Modbus TCP Length
                           # Modbus TCP Unit Identifier
                           + HexSlaveID +  
                           b"\x11"  # Report Slave ID
                           #b"\x04", # Send read input register instead previous?
                           )

    @staticmethod
    def name() -> str:
        return "modbusMon"

    @staticmethod
    def help():
        return "Sends a query for MODBUS device id to the target and check the response"

    def test(self):
        conn = self.get_connection_copy()
        result = self._get_modbus_info(conn)
        return result


    def _get_modbus_info(self, conn: ITargetConnection):
        try:
            conn.open()
            conn.send(self.get_modbus_device_id_nse) # or get_modbus_slave_id
            data = conn.recv_all(10000)
            if len(data) == 0:
                self.logger.log_error("MODBUS error response, getting MODBUS device information Failed!!")
                result = False
            else:
                Unit_ID = data[6].to_bytes((data[6].bit_length() + 7) // 8, byteorder='big')
                Func_code = data[7]
                Exception_code = data[8]

                if data[5] > 0 and Unit_ID == HexSlaveID:
                    if hex(Func_code) == '0x11':
                      self.logger.log_info(f"Getting MODBUS device information succeeded")
                    elif hex(Exception_code) == '0xb': # more details needed? and (hex(Func_code) == '0x91' or hex(Func_code) == '0x84')
                      self.logger.log_warn(f"Getting MODBUS device information: Gateway target device failed to respond")
                    elif hex(Exception_code) == '0x1':
                      self.logger.log_warn(f"Getting MODBUS device information: Illegal function")
                    else:
                      self.logger.log_warn(f"Getting MODBUS device information warning")
                else:
                  self.logger.log_warn(f"Getting MODBUS data error")

                result = True
        except Exception as e:
            self.logger.log_error(f"MODBUS response error, getting MODBUS device information Failed!! Exception while receiving: {type(e).__name__}. {str(e)}")
            result = False
        finally:
            conn.close()

        return result

References