Skip to content

Commit

Permalink
Release 1.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
claire-lex committed Feb 14, 2023
2 parents 71e9565 + 49e7df3 commit 06da12f
Show file tree
Hide file tree
Showing 24 changed files with 709 additions and 218 deletions.
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ send, receive, create, parse and manipulate frames from supported protocols.

The library currently provides discovery and extended testing features for
**KNXnet/IP**, which is our focus, but it can be extended to other types of BMS
or industrial network protocols. It also provides passive discovery functions
for industrial networks relying on KNXnet/IP, LLDP and Profinet DCP.
or industrial network protocols. It also provides multicast and/or end-to-end
discovery functions for industrial networks relying on KNXnet/IP, LLDP,
Profinet DCP and Modbus TCP.

**Please note that targeting industrial systems can have a severe impact on
people, industrial operations and buildings and that BOF must be used
Expand Down Expand Up @@ -57,10 +58,12 @@ There are three ways to use BOF, not all of them are available depending on the
layer:

* **Automated**: Import or call directly higher-level functions from layers. No
knowledge about the protocol required.
knowledge about the protocol required. For instance, the **discovery**
module contains a few functions to discover devices using several industrial
network protocols.

* **Standard**: Craft packets from layers to interact with remote devices. Basic
knowledge about the protocol requred.
knowledge about the protocol required.

* **Playful**: Play with packets, misuse the protocol (we fuzz devices with it).
The end user should have started digging into the protocol's specifications.
Expand All @@ -69,7 +72,7 @@ layer:
|--------------|-----------|----------|---------|
| KNX | X | X | X |
| LLDP | X | | |
| Modbus | | X | X |
| Modbus | X | X | X |
| Profinet DCP | X | | |


Expand All @@ -78,14 +81,15 @@ Now you can start using BOF!
TL;DR
-----

### Several ways yo discover devices on a network
### Several ways to discover devices on a network

* Passive discovery from the discovery module:
* Mutlcast discovery from the discovery module (currently with LLDP, Profinet
DCP, KNX):

```python
from bof.modules.discovery import *

devices = passive_discovery(iface="eth0", verbose=True)
devices = multicast_discovery(iface="eth0", verbose=True)
```

* Device discovery using a layer's high-level function
Expand Down
8 changes: 8 additions & 0 deletions bof/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ class BOFProgrammingError(BOFError):
"""
pass

class BOFDeviceError(BOFError):
"""Exceptions related to errors returned by the device.
Raise when the device responds with an error code, but the network
connection is still fine.
"""
pass

###############################################################################
# BOF LOGGING #
###############################################################################
Expand Down
2 changes: 1 addition & 1 deletion bof/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def __init__(self, name: str=None, description: str=None,
self.description = description
self.mac_address = mac_address
self.ip_address = ip_address

def __str__(self):
return "[{0}] Device name: {1}\n\tDescription: {2}\n\tMAC address: {3}" \
"\n\tIP address: {4}".format(self.protocol, self.name, self.description,
Expand Down
6 changes: 3 additions & 3 deletions bof/layers/knx/knx_constants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""
Profinet DCP constants
----------------------
KNX constants
-------------
Protocol-dependent constants (network and functions) for PNDCP.
Protocol-dependent constants (network and functions) for KNX.
"""

from ... import to_property
Expand Down
28 changes: 19 additions & 9 deletions bof/layers/knx/knx_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ def GROUP_ADDR(x: int) -> str:
"""Converts an int to KNX group address."""
return "%d/%d/%d" % ((x >> 11) & 0x1f, (x >> 8) & 0x7, (x & 0xff))

def ADDR_TO_INT(x, y, z) -> int:
"""Converts a splitted KNX address to an integer"""
if not 0 <= int(x) <= 15 or not 0 <= int(y) <= 15 or not 0 <= int(z) <= 255:
raise ValueError("Invalid interval")
xb = format(int(x), 'b').zfill(4)
yb = format(int(y), 'b').zfill(4)
zb = format(int(z), 'b').zfill(8)
return int(xb + yb + zb, 2)

###############################################################################
# KNX DEVICE REPRESENTATION #
###############################################################################
Expand Down Expand Up @@ -290,14 +299,15 @@ def line_scan(ip: str, line: str="", port: int=3671) -> list:
from 0.0.0 to 15.15.255)
:param port: KNX port, default is 3671.
:returns: A list of existing individual addresses on the KNX bus.
Methods require smart detection of line, so far only line 1.1.X is
supported and it is dirty.
:raises BOFProgrammingError: if KNX address is invalid.
"""
# TODO: decent line parsing and handling
if line.startswith("1.1."):
begin, end = 4352, 4352+255
else:
begin, end = 0, 65635
addr = [INDIV_ADDR(x) for x in range(begin, end)]
try:
line = line.split(".")
begin = ADDR_TO_INT(*line)
end = ADDR_TO_INT(*line[:2] + [255])
if not 0 <= begin <= 65535 or not 0 <= end <= 65535:
raise ValueError
except ValueError as ve:
raise BOFProgrammingError("Invalid KNX address.") from None
addr = [INDIV_ADDR(x) for x in range(begin, end + 1)]
return individual_address_scan(ip, addr, port)
6 changes: 5 additions & 1 deletion bof/layers/lldp/lldp_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
Protocol-dependent constants (network and functions) for LLDP.
"""

from scapy.contrib.lldp import LLDPDUGenericOrganisationSpecific

LLDP_MULTICAST_MAC = MULTICAST_MAC = "01:80:c2:00:00:0e"
LLDP_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT = 20
LLDP_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT = 30
LLDP_DEFAULT_TTL = DEFAULT_TTL = 20

LLDP_DEFAULT_PARAM = DEFAULT_PARAM = {
Expand All @@ -18,3 +20,5 @@
"system_desc": "BOF discovery",
"management_address": "0.0.0.0"
}

LLDP_ORG_CODES = ORG_CODES = LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODES
41 changes: 31 additions & 10 deletions bof/layers/lldp/lldp_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""

from os import geteuid
from time import sleep
from ipaddress import IPv4Address, AddressValueError

from scapy.packet import Packet
Expand All @@ -44,32 +45,52 @@ class LLDPDevice(BOFDevice):
port_id: str = None
port_desc: str = None
capabilities: dict = None
organisation: str = None

def __init__(self, pkt: Packet=None):
if pkt:
self.parse(pkt)

# TODO: Refactoring
def parse(self, pkt: Packet=None) -> None:
"""Parse LLDP response to store device information.
:param pkt: LLDP packet (Scapy), including Ethernet (Ether) layer.
"""
self.name = pkt["LLDPDUSystemName"].system_name.decode('utf-8')
self.description = pkt["LLDPDUSystemDescription"].description.decode('utf-8')
if pkt.haslayer(LLDPDUSystemName):
self.name = pkt["LLDPDUSystemName"].system_name.decode('utf-8')
if pkt.haslayer(LLDPDUSystemDescription):
self.description = pkt["LLDPDUSystemDescription"].description.decode('utf-8')
if "Ether" in pkt:
self.mac_address = pkt["Ether"].src
try: # TODO: Subtypes, we only handle IPv4 so far...
self.ip_address = IPv4Address(pkt["LLDPDUManagementAddress"].management_address)
if pkt.haslayer(LLDPDUManagementAddress):
self.ip_address = IPv4Address(pkt["LLDPDUManagementAddress"].management_address)
# IP address as a property so that we can return it only if subtype==IPv4
except AddressValueError as ave:
raise BOFProgrammingError("Subtypes other than IPv4 not implemented yet.")
self.chassis_id = pkt["LLDPDUChassisID"].id
if not isinstance(self.chassis_id, str):
self.chassis_id = self.chassis_id.decode('utf-8')
self.port_id = pkt["LLDPDUPortID"].id.decode('utf-8')
self.port_desc = pkt["LLDPDUPortDescription"].description.decode('utf-8')
self.capabilities = pkt["LLDPDUSystemCapabilities"] # TODO

if pkt.haslayer(LLDPDUChassisID):
self.chassis_id = pkt["LLDPDUChassisID"].id
if not isinstance(self.chassis_id, str):
self.chassis_id = self.chassis_id.decode('utf-8')
if pkt.haslayer(LLDPDUPortID):
self.port_id = pkt["LLDPDUPortID"].id
if not isinstance(self.port_id, str):
self.port_id = self.port_id.decode('utf-8')
if pkt.haslayer(LLDPDUPortDescription):
self.port_desc = pkt["LLDPDUPortDescription"].description.decode('utf-8')
# if pkt.haslayer(LLDPDUSystemCapabilities):
# self.capabilities = pkt["LLDPDUSystemCapabilities"] # TODO
if pkt.haslayer(LLDPDUGenericOrganisationSpecific):
# We look for the name matching the code
self.organisation = ORG_CODES[pkt["LLDPDUGenericOrganisationSpecific"].org_code]

def __str__(self):
return "{0}\n\tChassis ID: {1}\n\tPort ID: {2}\n\t" \
"Port description: {3}\n\tOrganisation: {4}".format(
super().__str__(), self.chassis_id, self.port_id,
self.port_desc, self.organisation)

#-----------------------------------------------------------------------------#
# Listen to LLDP packets on the network #
#-----------------------------------------------------------------------------#
Expand Down
26 changes: 26 additions & 0 deletions bof/layers/modbus/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,28 @@
"""
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.
"""

from .modbus_network import *
from .modbus_packet import *
from .modbus_functions import *
77 changes: 77 additions & 0 deletions bof/layers/modbus/modbus_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
Modbus TCP constants
--------------------
Protocol-dependent constants (network and functions) for Modbus TCP.
"""

from ... import to_property
from enum import Enum
# We interface some dictionaries here so that this file is the only one
# to update if the Scapy implementation changes.
from scapy.contrib import modbus as scapy_modbus

MODBUS_PORT = 502
MODBUS_TYPES = Enum('MODBUS_TYPES', 'REQUEST RESPONSE')

MODBUS_EXCEPTIONS = scapy_modbus._modbus_exceptions
MODBUS_OBJECT_ID = scapy_modbus._read_device_id_object_id

# User defined function codes from 65 to 72, and from 100 to 110 (decimal)

MODBUS_FUNCTIONS_CODES = FUNCTION_CODES = {
0x01: "Read Coils",
0x02: "Read Discrete Inputs",
0x03: "Read Holding Registers",
0x04: "Read Input Registers",
0x05: "Write Single Coil",
0x06: "Write Single Register",
0x07: "Read ExceptionStatus",
0x08: "Diagnostics",
0x0B: "Get Comm Event Counter",
0x0C: "Get Comm Event Log",
0x0E: "Read Device Identification",
0x0F: "Write Multiple Coils",
0x10: "Write Multiple Registers",
0x11: "Report Slave Id",
0x14: "Read File Record",
0x15: "Write File Record",
0x16: "Mask Write Register",
0x17: "Read Write Multiple Registers",
0x18: "Read FIFO Queue",
0x2B: "Read device identification", # Subcode 14
# Exception codes for functions (== Function code + 0x80)
0x81: "Read Coils Exception",
0x82: "Read Discrete Inputs Exception",
0x83: "Read Holding Registers Exception",
0x84: "Read Input Registers Exception",
0x85: "Write Single Coil Exception",
0x86: "Write Single Register Exception",
0x87: "Read Exception Status Exception",
0x88: "Diagnostics Exception",
0x8B: "Get Comm Event Counter Exception",
0x8C: "Get Comm Event Log Exception",
0x8F: "Write Multiple Coils Exception",
0x90: "Write Multiple Registers Exception",
0x91: "Report Slave Id Exception",
0x94: "Read File Record Exception",
0x95: "Write File Record Exception",
0x96: "Mask Write Register Exception",
0x97: "Read Write Multiple Exception",
0x98: "Read FIFO Queue Exception",
0xAB: "Read Device Identification Exception",
# Function code for Schneider UMAS (90)
0x5a: "Schneider"
}

FUNCTIONS = type('FUNCTIONS', (object,),
{to_property(v):k \
for k,v in MODBUS_FUNCTIONS_CODES.items()})()

MODBUS_EXCEPTION_OFFSET = EXCEPTION_OFFSET = 0x80
MODBUS_MAX_COIL_QUANTITY = MAX_COIL_QUANTITY = 256 # 512
MODBUS_MAX_DISCRETE_QUANTITY = MAX_DISCRETE_QUANTITY = 256 # 512
MODBUS_MAX_REGISTER_QUANTITY = MAX_REGISTER_QUANTITY = 125

MODBUS_MAX_COIL_QUANTITY_SPEC = MAX_COIL_QUANTITY_SPEC = 2000
MODBUS_MAX_DISCRETE_QUANTITY_SPEC = MAX_DISCRETE_QUANTITY_SPEC = 2000
Loading

0 comments on commit 06da12f

Please sign in to comment.