Layers

Using layers

BOF relies on protocol implementations built using the Scapy syntax, to provide security testing and fuzzing features. In other words, BOF works as follows:

The layers folder contain BOF features for implemented protocols.

Scapy protocol implementations can be imported directly from Scapy or from a KNX implementation not integrated to Scapy that should be located in the layers/raw_scapy folder.

KNX

KNX and KNXnet/IP

KNX is a common field bus protocol in Europe, mostly used in Building Management Systems. KNXnet/IP is the version of the protocol over IP, implementing specific type of frames that either ask information from or send request to a gateway (server) between an IP network and a KNX bus or carry KNX messages that the gateway must relay to KNX devieces on the field bus.

The protocol is a merge a several older ones, the specifications are maintained by the KNX association and can be found on their website (section 3 is the interesting one).

BOF’s knx submodule can be imported with:

from bof.layers import knx
from bof.layers.knx import *

The following files are available in the module:

knx_network:Class for network communication with KNX over UDP. Inherits from BOF’s network UDP class. Implements methods to connect, disconnect and mostly send and receive frames as KNXPacket objects.
knx_packet:Object representation of a KNX packet. KNXPacket inherits BOFPacket and uses Scapy’s implementation of KNX (located in bof/layers/raw_scapy or directly in Scapy contrib). Contains method to build, read or alter a frame or part of it, even if this does not follow KNX’s specifications.
knx_messages:Set of functions that build specific KNX messages with the right values.
knx_functions:Higher-level functions to discover and interact with devices via KNXnet/IP.

Network connection

KNXnet/IP connection features, implementing bof.network’s UDP class.

The KnxNet class translates KNXPacket packet objects and raw Scapy packets to bytes to send them, and received bytes to KNXPacket objects.

KNX usually works over UDP, however KNX specification v2.1 state that TCP can also be used. The communication between BOF and a KNX device still acts like a TCP-based protocol, as (almost) every request expects a response.

Usage:

knxnet = KNXnet()
knxnet.connect("192.168.1.242")
data, addr = knxnet.sr(KNXPacket(type=SID.description_request))
data.show2()
knxnet.disconnect()
class bof.layers.knx.knx_network.KNXnet

Bases: bof.network.UDP

KNXnet/IP communication over UDP with protocol KNX. Relies on bof.network.UDP().

Sent and received datagrams are returned as KNXPacket() objects.

..seealso:: Details on data exchange: KNX Standard v2.1 - 03_03_04.

connect(ip: str, port: int = 3671) → object

Connect to a KNX device (opens socket). Default port is 3671.

Parameters:
  • ip – IPv4 address as a string with format A.B.C.D.
  • port – KNX port. Default is 3671.
Returns:

The KNXnet connection object (this instance).

Raises:

BOFNetworkError – if connection fails.

receive(timeout: float = 1.0) → object

Converts received bytes to a parsed KNXPacket object.

Parameters:timeout – Time to wait to receive a frame (default is 1 sec)
Returns:A KNXPacket object.
send(data: object, address: tuple = None) → int

Converts BOF and Scapy frames to bytes to send. Relies on UDP class to send data.

Parameters:
  • data – Data to send as KNXPacket, Scapy Packet, string or bytes. Will be converted to bytes anyway.
  • address – Address to send data to, with format (ip, port). If address is not specified, uses the address given to `` connect``.
Returns:

The number of bytes sent, as an integer.

sequence_counter = None

KNXPacket

This class inheriting from BOFPacket is the interface between BOF’s usage of KNX by the end user and an actual Scapy packet built using KNX’s implementation in Scapy format.

In BOFPacket and KNXPacket, several builtin methods and attributes are just relayed to the Scapy Packet underneath. We also want to let the user interact directly with the Scapy packet if she wants, using scapy_pkt attribute.

Example:

>>> from bof.layers.knx import *
>>> packet = KNXPacket(type=SID.description_request)
>>> packet
<bof.layers.knx.knx_packet.KNXPacket object at 0x7ff74224add8>
>>> packet.scapy_pkt
<KNX  service_identifier=DESCRIPTION_REQUEST |<KNXDescriptionRequest      control_endpoint=<HPAI  |> |>>
class bof.layers.knx.knx_packet.KNXPacket(_pkt: bytes = None, scapy_pkt: scapy.packet.Packet = None, type: object = None, **kwargs)

Bases: bof.packet.BOFPacket

Builds a KNXPacket packet from a byte array or from attributes.

Parameters:
  • _pkt – KNX frame as byte array to build KNXPacket from.
  • scapy_pkt – Instantiated Scapy Packet to use as a KNXPacket.
  • type – Type of frame to build. Ignored if _pkt set. Should be a value from SID dict imported from KNX Scapy implementation as a dict key, a string or as bytes.
  • kwargs – Any field to initialize when instantiating the frame, with format field_name=value.

Example of initialization:

pkt = KNXPacket(b"[...]") # From frame as a byte array
pkt = KNXPacket(type=SID.description_request) # From service id dict
pkt = KNXPacket(type="DESCRIPTION REQUEST") # From service id name
pkt = KNXPacket(type=b"") # From service id value
pkt = KNXPacket(type=SID.connect_request, communication_channel_id=2)
pkt = KNXPacket(scapy_pkt=KNX()/KNXDescriptionRequest()) # With Scapy Packet
pkt = KNXPacket() # Empty packet (just a KNX header)
set_type(ptype: object, cemi: object = None) → None

Format packet according to the specified type (service identifier).

Parameters:
  • ptype – Type of frame to build. Ignored if _pkt set. Should be a value from SID dict imported from KNX Scapy implementation as a dict key, a string or as bytes.
  • cemi – cEMI field type. Raises error if type does not have have a cEMI field, is ignored if there is no type given.
Raises:

BOFProgrammingError – if type is unknown or invalid or if cEMI is set but there is no cEMI field in packet type.

sid
type

Get information about the packet’s type (protocol-dependent).

Should be overriden in subclasses to match a protocol’s different types of packets. For instance, BOF’s packet for the KNX protocol (KNXPacket) returns the type of packet as a name, relying on its identifier fields. If identifier is 0x0203, pkt.type indicates that the packet is a DESCRIPTION REQUEST.

KNX messages

Module containing a set of functions to build predefined types of KNX messages. Functions in this module do not handle the network exchange, they just create ready-to-send packets.

Contents:

KNXnet/IP requests:
 Direct methods to create initialized requests from the standard.
CEMI:Methods to create specific type of cEMI messages (protocol-independent KNX messages).
bof.layers.knx.knx_messages.cemi_ack(knx_indiv_addr: str, seq_num: int = 0, knx_source: str = '0.0.0') → scapy.packet.Packet

Builds a KNX message (cEMI) to disconnect from an individual address.

Parameters:
  • knx_indiv_addr – KNX individual address of device (with format X.Y.Z)
  • seq_num – Sequence number to use, applies to cEMI when sequence_type is set to “numbered”. So far I haven’t seen seq_num > 0.
  • knx_source – KNX individual address to use as a source for the request. You should usually use the KNXnet/IP server’s individual address, but it works fine with 0.0.0.
Returns:

A raw cEMI object from Scapy’s implementation to be inserted in a KNXPacket object.

Raises:

BOFProgrammingError – if KNX addresses are invalid because the Scapy object does not allow that. You should change the field type if you want to set somethig else.

bof.layers.knx.knx_messages.cemi_connect(knx_indiv_addr: str, knx_source: str = '0.0.0') → scapy.packet.Packet

Builds a KNX message (cEMI) to connect to an individual address.

Parameters:
  • knx_indiv_addr – KNX individual address of device (with format X.Y.Z)
  • knx_source – KNX individual address to use as a source for the request. You should usually use the KNXnet/IP server’s individual address, but it works fine with 0.0.0.
Returns:

A raw cEMI object from Scapy’s implementation to be inserted in a KNXPacket object.

Raises:

BOFProgrammingError – if KNX addresses are invalid because the Scapy object does not allow that. You should change the field type if you want to set somethig else.

bof.layers.knx.knx_messages.cemi_dev_descr_read(knx_indiv_addr: str, seq_num: int = 0, knx_source: str = '0.0.0') → scapy.packet.Packet

Builds a KNX message (cEMI) to write a value to a group address.

Parameters:
  • knx_indiv_addr – KNX individual address of device (with format X.Y.Z)
  • seq_num – Sequence number to use, applies to cEMI when sequence_type is set to “numbered”. So far I haven’t seen seq_num > 0.
  • knx_source – KNX individual address to use as a source for the request. You should usually use the KNXnet/IP server’s individual address, but it works fine with 0.0.0.
Returns:

A raw cEMI object from Scapy’s implementation to be inserted in a KNXPacket object.

Raises:

BOFProgrammingError – if KNX addresses are invalid because the Scapy object does not allow that. You should change the field type if you want to set somethig else.

bof.layers.knx.knx_messages.cemi_disconnect(knx_indiv_addr: str, knx_source: str = '0.0.0') → scapy.packet.Packet

Builds a KNX message (cEMI) to disconnect from an individual address.

Parameters:
  • knx_indiv_addr – KNX individual address of device (with format X.Y.Z)
  • knx_source – KNX individual address to use as a source for the request. You should usually use the KNXnet/IP server’s individual address, but it works fine with 0.0.0.
Returns:

A raw cEMI object from Scapy’s implementation to be inserted in a KNXPacket object.

Raises:

BOFProgrammingError – if KNX addresses are invalid because the Scapy object does not allow that. You should change the field type if you want to set somethig else.

bof.layers.knx.knx_messages.cemi_group_write(knx_group_addr: str, value, knx_source: str = '0.0.0') → scapy.packet.Packet

Builds a KNX message (cEMI) to write a value to a group address.

Parameters:
  • knx_group_addr – KNX group address targeted (with format X/Y/Z) Group addresses are defined in KNX project settings.
  • value – Value to set the group address’ content to.
  • knx_source – KNX individual address to use as a source for the request. You should usually use the KNXnet/IP server’s individual address, but it works fine with 0.0.0.
Returns:

A raw cEMI object from Scapy’s implementation to be inserted in a KNXPacket object.

Raises:

BOFProgrammingError – if KNX addresses are invalid because the Scapy object does not allow that. You should change the field type if you want to set somethig else.

bof.layers.knx.knx_messages.cemi_property_read(object_type: int, property_id: int) → scapy.packet.Packet

Builds a KNX message (cEMI) to write a value to a group address.

Parameters:
  • object_type – Type of object to read, as defined in KNX Standard (and reproduce in Scapy’s KNX implementation).
  • property_id – Property to read, as defined in KNX Standard (and reproduce in Scapy’s KNX implementation).
Returns:

A raw cEMI object from Scapy’s implementation to be inserted in a KNXPacket object.

bof.layers.knx.knx_messages.configuration_ack(channel: int) → bof.layers.knx.knx_packet.KNXPacket

Creates a configuration ack to reply to avoid upsetting KNX servers.

bof.layers.knx.knx_messages.configuration_request(channel: int, cemi: scapy.packet.Packet) → bof.layers.knx.knx_packet.KNXPacket

Creates a configuration request with a specified cEMI message.

Parameters:
  • channel – The communication channel ID for the current KNXnet/IP connection. The channel is set by the server and returned in connect responses.
  • cemi – Protocol-independent KNX message inserted in the request. cEMI are created directly from Scapy’s CEMI object.
Returns:

A configuration request embedding a cEMI packet, as a KNXPacket.

bof.layers.knx.knx_messages.connect_request_management(knxnet: bof.layers.knx.knx_network.KNXnet = None) → bof.layers.knx.knx_packet.KNXPacket

Creates a connect request with device management connection type.

Parameters:knxnet – The KNXnet connection object to use. We only need the source parameter, please create an issue if you think that asking directly for the source instead is a better choice.
Returns:A management connect request as a KNXPacket.
bof.layers.knx.knx_messages.connect_request_tunneling(knxnet: bof.layers.knx.knx_network.KNXnet = None) → bof.layers.knx.knx_packet.KNXPacket

Creates a connect request with tunneling connection type.

Parameters:knxnet – The KNXnet connection object to use. We only need the source parameter, please create an issue if you think that asking directly for the source instead is a better choice.
Returns:A tunneling connect request as a KNXPacket.
bof.layers.knx.knx_messages.description_request(knxnet: bof.layers.knx.knx_network.KNXnet = None) → bof.layers.knx.knx_packet.KNXPacket

Creates a basic description request with appropriate source.

Parameters:knxnet – The KNXnet connection object to use. We only need the source parameter, please create an issue if you think that asking directly for the source instead is a better choice.
Returns:A description request as a KNXPacket.
bof.layers.knx.knx_messages.disconnect_request(knxnet: bof.layers.knx.knx_network.KNXnet = None, channel: int = 1) → bof.layers.knx.knx_packet.KNXPacket

Creates a disconnect request to close connection on given channel.

Parameters:
  • knxnet – The KNXnet connection object to use. We only need the source parameter, please create an issue if you think that asking directly for the source instead is a better choice.
  • channel – The communication channel ID for the current KNXnet/IP connection. The channel is set by the server and returned in connect responses.
Returns:

A disconnect request as a KNXPacket.

bof.layers.knx.knx_messages.search_request(knxnet: bof.layers.knx.knx_network.KNXnet = None) → bof.layers.knx.knx_packet.KNXPacket

Creates a basic search request with appropriate source.

Parameters:knxnet – The KNXnet connection object to use. We only need the source parameter, please create an issue if you think that asking directly for the source instead is a better choice.
Returns:A search request as a KNXPacket.
bof.layers.knx.knx_messages.tunneling_ack(channel: int, sequence_counter: int) → bof.layers.knx.knx_packet.KNXPacket

Creates a tunneling ack to reply to avoid upsetting KNX servers.

bof.layers.knx.knx_messages.tunneling_request(channel: int, sequence_counter: int, cemi: scapy.packet.Packet) → bof.layers.knx.knx_packet.KNXPacket

Creates a tunneling request with a specified cEMI message.

Parameters:
  • channel – The communication channel ID for the current KNXnet/IP connection. The channel is set by the server and returned in connect responses.
  • sequence_counter – Sequence number to use for the request, same principle as TCP’s sequence numbers.
  • cemi – Protocol-independent KNX message inserted in the request. cEMI are created directly from Scapy’s CEMI object.
Returns:

A tunneling request embedding a cEMI packet, as a KNXPacket.

KNX functions

Higher-level functions to interact with devices using KNXnet/IP.

Contents:

KNXDevice:Object representation of a KNX device with multiple properties. Only supports KNXnet/IP servers so far, but will be extended to KNX devices.
Functions:High-level functions to interact with a device: search, discover, read, write, etc.

Relies on KNX Standard v2.1

bof.layers.knx.knx_functions.ADDR_TO_INT(x, y, z) → int

Converts a splitted KNX address to an integer

bof.layers.knx.knx_functions.GROUP_ADDR(x: int) → str

Converts an int to KNX group address.

bof.layers.knx.knx_functions.INDIV_ADDR(x: int) → str

Converts an int to KNX individual address.

class bof.layers.knx.knx_functions.KNXDevice(name: str, ip_address: str, port: int, knx_address: str, mac_address: str, multicast_address: str = '224.0.23.12', serial_number: str = '')

Bases: bof.device.BOFDevice

Object representing a KNX device.

Data stored to the object is the one returned by SEARCH RESPONSE and DESCRIPTION RESPONSE messages, stored to public attributes:

Device name, IPv4 address, KNXnet/IP port, KNX individual address, MAC
address, KNX multicast address used, device serial number.

This class provides two factory class methods to build a KNXDevice object from search responses and description responses.

The information gathered from devices may be completed, improved later.

classmethod init_from_description_response(response: bof.layers.knx.knx_packet.KNXPacket, source: tuple)

Set appropriate values according to the content of description response.

Parameters:
  • response – Description Response provided by a device as a KNXPacket.
  • source – Source of the response, usually provided in KNXnet’s receive() and sr() return values.
Returns:

A KNXDevice object.

Usage example:

response, source = knxnet.sr(description_request(knxnet))
device = KNXDevice.init_from_description_response(response, source)
classmethod init_from_search_response(response: bof.layers.knx.knx_packet.KNXPacket)

Set appropriate values according to the content of search response.

Parameters:response – Search Response provided by a device as a KNXPacket.
Returns:A KNXDevice object.

Uage example:

responses = KNXnet.multicast(search_request(), (ip, port))
for response, source in responses:
  device = KNXDevice.init_from_search_response(KNXPacket(response))
protocol = 'KNX'
bof.layers.knx.knx_functions.discover(ip: str, port: int = 3671) → bof.layers.knx.knx_functions.KNXDevice

Returns discovered information about a device. So far, only sends a DESCRIPTION REQUEST and uses the DESCRIPTION RESPONSE. This function may evolve to gather data on underlying devices.

Parameters:
  • ip – IPv4 address of KNX device.
  • port – KNX port, default is 3671.
Returns:

A KNXDevice object.

Raises:
bof.layers.knx.knx_functions.group_write(ip: str, knx_group_addr: str, value, port: int = 3671) → None

Writes value to KNX group address via the server at address ip. We first need to establish a tunneling connection so that we can reach underlying device groups.

Parameters:
  • ip – IPv4 address of KNX device.
  • knx_group_addr – KNX group address targeted (with format X/Y/Z) Group addresses are defined in KNX project settings.
  • value – Value to set the group address’ content to.
  • port – KNX port, default is 3671.
Returns:

Nothing

Raises:
bof.layers.knx.knx_functions.individual_address_scan(ip: str, addresses: object, port: str = 3671) → bool

Scans KNX gateway to find if individual address exists. We first need to establish a tunneling connection and use cemi connect messages on each address to find out which one responds. As the gateway will answer positively for each address (L_data.con), we also wait for L_data.ind which seems to indicate existing addresses.

Parameters:
  • ip – IPv4 address of KNX device.
  • address – KNx individual addresses as a string or a list.
  • port – KNX port, default is 3671.
Returns:

A list of existing individual addresses.

Raises:

BOFProgrammingError – if IP is invalid.

Does not work (yet) for KNX gateways’ individual addresses. Not reliable: Crashes after 60 addresses… Plz send help ;_; Also requires heavy refactoring after fixing issues.

bof.layers.knx.knx_functions.line_scan(ip: str, line: str = '', port: int = 3671) → list

Scans KNX gateway to find existing individual addresses on a line. We first need to establish a tunneling connection and use cemi connect messages on each address to find out which one responds. As the gateway will answer positively for each address (L_data.con), we also wait for L_data.ind which seems to indicate existing addresses.

Parameters:
  • ip – IPv4 address of KNX device.
  • line – KNX backbone to scan (default == empty == scan all lines from 0.0.0 to 15.15.255)
  • port – KNX port, default is 3671.
Returns:

A list of existing individual addresses on the KNX bus.

Raises:

BOFProgrammingError – if KNX address is invalid.

bof.layers.knx.knx_functions.search(ip: object = '224.0.23.12', port: int = 3671) → list

Search for KNX devices on an network using multicast. Sends a SEARCH REQUEST and expects one SEARCH RESPONSE per device.

Parameters:
  • ip – Multicast IPv4 address. Default value is default KNXnet/IP multicast address 224.0.23.12.
  • port – KNX port, default is 3671.
Returns:

The list of responding KNXnet/IP devices in the network as KNXDevice objects.

Raises:

BOFProgrammingError – if IP is invalid.

KNX constants

Protocol-dependent constants (network and functions) for KNX.

LLDP

LLDP

LLDP (Link Layer Discovery Protocol) is, as its name suggests, used for network discovery directly on the Ethernet link.

BOF uses it for network discovery purposes in higher-level purposes. The implementation is imcomplete, as we only use it as a support protocol (no extended research or fuzzing intended).

Contents:

lldp_functions:LLDP listen, send, create and device representation.
lldp_constants:Protocol-related constants.

Uses Scapy’s LLDP contrib by Thomas Tannhaeuser (hecke@naberius.de).

LLDP functions

Higher-level functions for network discovery using LLDP.

Contents:

LLDPDevice:Object representation of a device discovered via LLDP.
Listen:Sync and async functions to listen on the network for LLDP multicast requests.
Send:Create basic LLDP requests and send them via multicast.

Uses Scapy’s LLDP contrib by Thomas Tannhaeuser (hecke@naberius.de).

class bof.layers.lldp.lldp_functions.LLDPDevice(pkt: scapy.packet.Packet = None)

Bases: bof.device.BOFDevice

Object representation of a device described LLDP requests.

capabilities = None
chassis_id = None
description = None
ip_address = None
mac_address = None
name = None
organisation = None
parse(pkt: scapy.packet.Packet = None) → None

Parse LLDP response to store device information.

Parameters:pkt – LLDP packet (Scapy), including Ethernet (Ether) layer.
port_desc = None
port_id = None
protocol = 'LLDP'
bof.layers.lldp.lldp_functions.create_packet(lldp_param: dict = {'chassis_id': 'BOF', 'management_address': '0.0.0.0', 'port_desc': 'BOF discovery', 'port_id': 'port-BOF', 'system_desc': 'BOF discovery', 'system_name': 'BOF', 'ttl': 20}) → scapy.packet.Packet

Create a LLDP packet for discovery to be sent on Ethernet layer.

Parameters:lldp_param – Dictionary containing LLDP info to set. Optional.
bof.layers.lldp.lldp_functions.listen_sync(iface: str = 'eth0', timeout: int = 30) → list

Search for devices on an network by listening to LLDP requests.

Converts back asynchronous to synchronous with sleep (silly I know). If you want to keep asynchrone, call directly start_listening and stop_listening in your code.

bof.layers.lldp.lldp_functions.send_multicast(pkt: scapy.packet.Packet = None, iface: str = 'eth0', mac_addr: str = '01:80:c2:00:00:0e') → scapy.packet.Packet

Send a LLDP (Link Layer Discovery Protocol) packet on Ethernet layer.

Multicast is used by default. Requires super-user privileges to send on Ethernet link.

Parameters:
  • pkt – LLDP Scapy packet. If not specified, creates a default one.
  • iface – Network interface to use to send the packet.
  • mac_addr – MAC address to send the LLDP packet to (default: multicast)
Returns:

The packet that was sent, mostly for debug and testing purposes.

bof.layers.lldp.lldp_functions.start_listening(iface: str = 'eth0', timeout: int = 30) → scapy.sendrecv.AsyncSniffer

Listen for LLDP requests sent on the network, usually via multicast.

We don’t need to send a request for the others to replies, however we need to wait for devices to talk, so timeout should be high (at least 10s). Requires super-user privileges to receive on Ethernet link.

Parameters:
  • iface – Network interface to use to send the packet.
  • timeout – Sniffing time. We have to wait for LLPD spontaneous multcast.
bof.layers.lldp.lldp_functions.stop_listening(sniffer: scapy.sendrecv.AsyncSniffer) → list

LLDP constants

Protocol-dependent constants (network and functions) for LLDP.

Modbus TCP

Modbus TCP

BOF’s modbus submodule can be imported with:

from bof.layers import modbus
from bof.layers.modbus import *

The following files are available in the module:

modbus_network:Class for network communication with Modbus over TCP. Inherits from BOF’s network TCP class. Implements methods to connect, disconnect and mostly send and receive frames as ModbusPacket objects.
modbus_packet:Object representation of a Modbus packet. ModbusPacket inherits BOFPacket and uses Uses Modbus specification v1.1b3 and Scapy’s Modbus contrib Arthur Gervais, Ken LE PRADO, Sebastien Mainand and Thomas Aurel.
modbus_functions:
 Higher-level functions to discover and interact with devices via Modbus TCP.

Network connection

Modbus TCP/IP connection features, implementing bof.network’s TCP class.

The ModbusNet class translates ModbusPacket packet objects and raw Scapy packets to bytes to send them, and received bytes to ModbusPacket objects.

Usage:

modbus_net = ModbusNet()
modbus_net.connect("192.168.1.242")
modbus_req = ModbusPacket(type=MODBUS_TYPES.REQUEST, function=b'')
modbus_resp = modbus_net.sr(modbus_req)
modbus_resp.show2()
modbus_net.disconnect()
class bof.layers.modbus.modbus_network.ModbusNet

Bases: bof.network.TCP

Modbus TCP/IP communication. Relies on bof.network.TCP().

Sent and received datagrams are returned as ModbusPacket() objects.

..seealso:: Details on TCP exchange: * MODBUS Application Protocol Specification V1.1b3 - 1.1 Introduction * MODBUS Messaging on TCP/IP Implementation Guide V1.0b

connect(ip: str, port: int = 502, timeout: float = 1.0)

Connects to a Modbus Server (opens socket). Default port is 3671.

Parameters:
  • ip – IPv4 address as a string with format A.B.C.D.
  • port – Modbus port. Default is 502.
Returns:

The Modbus connection object (this instance).

Raises:

BOFNetworkError – if connection fails.

receive(timeout: float = 1.0) → object

Converts received bytes to a parsed ModbusPacket object.

Parameters:timeout – Time to wait to receive a frame (default is 1 sec)
Returns:A ModbusPacket object and the sender address.
send(data, address: tuple = None) → int

Converts BOF and Scapy frames to bytes to send. Relies on TCP class to send data.

Parameters:
  • data – Data to send as ModbusPacket, Scapy Packet, string or bytes. Will be converted to bytes anyway.
  • address – Address to send data to, with format (ip, port). If address is not specified, uses the address given to `` connect``.
Returns:

The number of bytes sent, as an integer.

Modbus functions

Higher-level functions to interact with devices using Modbus TCP.

Contents:

ModbusDevice:Object representation of a Modbus device with multiple properties. Only supports basic functions so far.
Functions:High-level functions to interact with a device.

Uses Modbus specification v1.1b3 and Scapy’s Modbus contrib by Arthur Gervais, Ken LE PRADO, Sebastien Mainand and Thomas Aurel.

bof.layers.modbus.modbus_functions.HEX_TO_BIN_DICT(byte_count, hex_table)

Convert hex value table on one or more bytes to binary bit in a dict.

Example: Hex value 0x15 on 2 bytes will be translated to 10101000 00000000 This binary will be stored in a numbered dict starting from 1: { 1: 1, 2: 0, 3: 1, … }

bof.layers.modbus.modbus_functions.HEX_TO_DICT(byte_count, hex_table)

Convert hex value table on one or more bytes to binary bit in a dict.

Example: Hex value 0x15 on 2 bytes will be translated to 10101000 00000000 This binary will be stored in a numbered dict starting from 1: { 1: 1, 2: 0, 3: 1, … }

class bof.layers.modbus.modbus_functions.ModbusDevice

Bases: bof.device.BOFDevice

coils = None
coils_on
description = None
discrete_inputs = None
discrete_inputs_on
holding_registers = None
holding_registers_nonzero
input_registers = None
input_registers_nonzero
name = ''
protocol = 'Modbus TCP'
bof.layers.modbus.modbus_functions.discover(ip: str, port: int = 502) → bof.layers.modbus.modbus_functions.ModbusDevice

Returns discovered information about a device. So far, we only read the different types of data stored on a device.

Parameters:
  • ip_range – IPv4 address of a Modbus device.
  • port – Modbus TCP port, default is 502.
Returns:

A ModbusDevice object.

Raises:
bof.layers.modbus.modbus_functions.full_read_device_identification(modnet: bof.layers.modbus.modbus_network.ModbusNet, device: bof.layers.modbus.modbus_functions.ModbusDevice = None)

Read all information available on the device using read device id requests.

This function sends as many read device identification requests as there are objects to read (6). Returns data as a ModbusDevice object.

Parameters:
  • modnet – Modbus connection object created previously.
  • device – ModbusDevice object. If none: creates a new one.
Returns:

The ModbusDevice object.

Raises:

BOFDeviceError – When the device responds with an exception code.

bof.layers.modbus.modbus_functions.read_coils(modnet: bof.layers.modbus.modbus_network.ModbusNet, start_addr: int = 0, quantity: int = 1, unit_id: int = 0) → dict

Read one or more Modbus coil(s) on device.

Parameters:
  • modnet – Modbus connection object created previously.
  • start_addr – First address to read coils from (default: 0).
  • quantity – Number of coils to read from start_address (default: 1).
Returns:

A dictionary with format {coil_number: value}.

Raises:

BOFDeviceError – When the device responds with an exception code.

Example:

try:
    modnet = ModbusNet().connect(ip)
    coils = read_coils(modnet, quantity=10)
    for x, y in coils.items():
        print("Coil {0: 3}: {1}".format(x, y))
    modnet.disconnect()
except BOFNetworkError as bne:
    print("ERROR:", bne, ip)
bof.layers.modbus.modbus_functions.read_device_identification(modnet: bof.layers.modbus.modbus_network.ModbusNet, read_code: int = 1, object_id: int = 0)

Read device information (if the devices supports function code 43).

Parameters:
  • modnet – Modbus connection object created previously.
  • readCode – Read level to use: 1:basic, 2:regular, 3:extended, 4:specific.
  • objectId – Object to read: 0: VendorName, 1:ProductCode, 2:Revision, 3: VendorUrl, 4: ProductName, 5: ModelName, 6: UserAppName.
Returns:

A tuple (objectId, value) from the response.

Raises:

BOFDeviceError – When the device does not respond or responds with an exception code.

bof.layers.modbus.modbus_functions.read_discrete_inputs(modnet: bof.layers.modbus.modbus_network.ModbusNet, start_addr: int = 0, quantity: int = 1, unit_id: int = 0) → dict

Read one or more Modbus discrete input(s) on device.

Parameters:
  • modnet – Modbus connection object created previously.
  • start_addr – First address to read inputs from (default: 0).
  • quantity – Number of inputs to read from start_address (default: 1).
Returns:

A dictionary with format {input_number: value}.

Raises:

BOFDeviceError – When the device responds with an exception code.

Example: See read_coils()

bof.layers.modbus.modbus_functions.read_holding_registers(modnet: bof.layers.modbus.modbus_network.ModbusNet, start_addr: int = 0, quantity: int = 1, unit_id: int = 0) → dict

Read one or more Modbus holding register(s) on device.

Parameters:
  • modnet – Modbus connection object created previously.
  • start_addr – First address to read registers from (default: 0).
  • quantity – Number of registers to read from start_address (default: 1).
Returns:

A dictionary with format {reg_number: value}.

Raises:

BOFDeviceError – When the device responds with an exception code.

Example: See read_coils()

bof.layers.modbus.modbus_functions.read_input_registers(modnet: bof.layers.modbus.modbus_network.ModbusNet, start_addr: int = 0, quantity: int = 1, unit_id: int = 0) → dict

Read one or more Modbus input register(s) on device.

Parameters:
  • modnet – Modbus connection object created previously.
  • start_addr – First address to read registers from (default: 0).
  • quantity – Number of registers to read from start_address (default: 1).
Returns:

A dictionary with format {reg_number: value}.

Raises:

BOFDeviceError – When the device responds with an exception code.

Example: See read_coils()

Modbus TCP constants

Protocol-dependent constants (network and functions) for Modbus TCP.

class bof.layers.modbus.modbus_constants.MODBUS_TYPES

Bases: enum.Enum

An enumeration.

REQUEST = 1
RESPONSE = 2

Profinet DCP

Profinet DCP

Profinet DCP (Discovery and COnfiguration Protocol) can be, as its name suggests, used for network discovery directly on the Ethernet link.

BOF uses it for network discovery purposes in higher-level purposes. The implementation is imcomplete, as we only use it as a support protocol (no extended research or fuzzing intended so far).

Contents:

profinet_functions:
 Send and receive Profinet DCP identify requests and device representation.
profinet_constants:
 Protocol-related constants.

Uses Scapy’s Profinet IO contrib by Gauthier Sebaux and Profinet DCP contrib by Stefan Mehner (stefan.mehner@b-tu.de).

Profinet DCP functions

Higher-level functions for network discovery using PNDCP.

Contents:

PNDCPDevice:Object representation of a device discovered via PNDCP.
Identify requests:
 Send and receive identify requests and response to discover devices.

Uses Scapy’s Profinet IO contrib by Gauthier Sebaux and Profinet DCP contrib by Stefan Mehner (stefan.mehner@b-tu.de).

class bof.layers.profinet.profinet_functions.ProfinetDevice(pkt: scapy.packet.Packet = None)

Bases: bof.device.BOFDevice

Object representation of a device responding to PN-DCP requests.

description = None
device_id = None
ip_address = None
ip_gateway = None
ip_netmask = None
mac_address = None
name = None
parse(pkt: scapy.packet.Packet = None) → None
protocol = 'ProfinetDCP'
vendor_id = None
bof.layers.profinet.profinet_functions.create_identify_packet() → scapy.packet.Packet

Create a Profinet DCP packet for discovery to be sent on Ethernet layer.

bof.layers.profinet.profinet_functions.send_identify_request(iface: str = 'eth0', mac_addr: str = '01:0e:cf:00:00:00', timeout: int = 10) → list

Send PN-DCP (Profinet Discovery/Config Proto) packets on Ethernet layer.

Some industrial devices such as PLCs respond to them. Responses may be embedded in 802.1Q frames. Multicast is used by default. Requires super-user privileges to send on Ethernet link.

Parameters:
  • iface – Network interface to use to send the packet.
  • mac_addr – MAC address to send the PN-DCP packet to (default: multicast)
  • timeout – Timeout for responses. More than 10s because some devices take time to respond.

Profinet DCP constants

Protocol-dependent constants (network and functions) for Profinet DCP.