Skip to content

Commit 71e9565

Browse files
committed
Release 1.2.0
2 parents 9495720 + de7965d commit 71e9565

36 files changed

+2865
-228
lines changed

README.md

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,14 @@ BOF (Boiboite Opener Framework) is a testing framework for field protocols
55
implementations and devices. It is a Python 3.6+ library that provides means to
66
send, receive, create, parse and manipulate frames from supported protocols.
77

8-
The library currently supports **KNXnet/IP**, which is our focus, but it can be
9-
extended to other types of BMS or industrial network protocols.
10-
11-
There are three ways to use BOF:
12-
13-
* Automated: Use of higher-level interaction functions to discover devices and
14-
start basic exchanges, without requiring to know anything about the protocol.
15-
16-
* Standard: Perform more advanced (legitimate) operations. This requires the end
17-
user to know how the protocol works (how to establish connections, what kind
18-
of messages to send).
19-
20-
* Playful: Modify every single part of exchanged frames and misuse the protocol
21-
instead of using it (we fuzz devices with it). The end user should have
22-
started digging into the protocol's specifications.
8+
The library currently provides discovery and extended testing features for
9+
**KNXnet/IP**, which is our focus, but it can be extended to other types of BMS
10+
or industrial network protocols. It also provides passive discovery functions
11+
for industrial networks relying on KNXnet/IP, LLDP and Profinet DCP.
2312

2413
**Please note that targeting industrial systems can have a severe impact on
25-
buildings and people and that BOF must be used carefully.**
14+
people, industrial operations and buildings and that BOF must be used
15+
carefully.**
2616

2717
[![GitHub license](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://github.com/Orange-Cyberdefense/bof/blob/master/LICENSE)
2818
[![GitHub release](https://img.shields.io/github/release/Orange-Cyberdefense/bof.svg)](https://gitHub.com/Orange-Cyberdefense/bof/releases/)
@@ -55,22 +45,50 @@ Protocol implementations use [Scapy](https://scapy.readthedocs.io/en/latest/)'s
5545
Getting started
5646
---------------
5747

58-
BOF is a Python 3.6+ library that should be imported in scripts. It has no
59-
installer yet so you need to refer to the `bof` subdirectory which contains the
60-
library (inside the repository) in your project or to copy the folder to your
61-
project's folder. Then, inside your code (or interactively), you can import the
62-
library:
48+
BOF is a Python 3.6+ library that should be imported in scripts.
6349

6450
```python
6551
import bof
52+
from bof.layers import profinet, knx
53+
from bof.layers.knx import KnxPacket
6654
```
6755

56+
There are three ways to use BOF, not all of them are available depending on the
57+
layer:
58+
59+
* **Automated**: Import or call directly higher-level functions from layers. No
60+
knowledge about the protocol required.
61+
62+
* **Standard**: Craft packets from layers to interact with remote devices. Basic
63+
knowledge about the protocol requred.
64+
65+
* **Playful**: Play with packets, misuse the protocol (we fuzz devices with it).
66+
The end user should have started digging into the protocol's specifications.
67+
68+
| | Automated | Standard | Playful |
69+
|--------------|-----------|----------|---------|
70+
| KNX | X | X | X |
71+
| LLDP | X | | |
72+
| Modbus | | X | X |
73+
| Profinet DCP | X | | |
74+
75+
6876
Now you can start using BOF!
6977

7078
TL;DR
7179
-----
7280

73-
### Discover devices on a network
81+
### Several ways yo discover devices on a network
82+
83+
* Passive discovery from the discovery module:
84+
85+
```python
86+
from bof.modules.discovery import *
87+
88+
devices = passive_discovery(iface="eth0", verbose=True)
89+
```
90+
91+
* Device discovery using a layer's high-level function
7492

7593
```python
7694
from bof.layers.knx import search
@@ -80,10 +98,15 @@ for device in devices:
8098
print(device)
8199
```
82100

83-
Should output something like:
101+
* Create and send your own discovery packet:
84102

85103
```
86-
Device: "boiboite" @ 192.168.1.242:3671 - KNX address: 15.15.255 - Hardware: 00:00:ff:ff:ff:ff (SN: 0123456789)
104+
from bof.layers.knx import *
105+
106+
pkt = KNXPacket(type="search request")
107+
responses = KNXnet.multicast(pkt, (KNX_MULTICAST_ADDR, KNX_PORT))
108+
for response, _ in responses:
109+
print(KNXPacket(response))
87110
```
88111

89112
### Send and receive packets

bof/__init__.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""
2-
Introduction
1+
"""Introduction
32
============
43
54
Boiboite Opener Framework / Ouvre-Boiboite Framework contains a set of features
@@ -18,19 +17,29 @@
1817
new protocol submodule.
1918
2019
:packet:
21-
Base classe for specialized BOF packets in layers. Such classes link BOF
20+
Base class for specialized BOF packets in layers. Such classes link BOF
2221
content and usage to protocol implementations in Scapy. In other words,
2322
they interface BOF's syntax used by the end user with Scapy Packet and
2423
Field objects used for the packet itself. The base class ``BOFPacket``
2524
is not supposed to be instantiated directly, but whatever.
2625
26+
:device:
27+
Global object for representing industrial devices. All objects in
28+
layers built using data extracted from responses to protocol-specific
29+
discovery requests shall inherit ``BOFDevice``.
30+
2731
:layers:
2832
Protocol implementations to be imported in BOF. Importing ``layers`` gives
29-
acces to BOF protocol implementations inheriting from ``BOFPacket``
33+
access to BOF protocol implementations inheriting from ``BOFPacket``
3034
(interface between BOF and Scapy worlds). The directory
3135
``layers/raw_scapy`` may contain protocol implementations in Scapy which
3236
are not integrated to Scapy's repository (for instance, if you wrote your
3337
own but did not contribute (yet)).
38+
39+
:modules:
40+
Higher level functions gathered around a specific usage that may rely on
41+
several protocols (layers).
42+
3443
"""
3544

3645
###############################################################################
@@ -40,4 +49,6 @@
4049
from .base import *
4150
from .network import *
4251
from .packet import *
52+
from .device import *
4353
from .layers import *
54+
from .modules import *

bof/device.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Global object for representing industrial devices.
2+
3+
All objects in layers built using data extracted from responses to
4+
protocol-specific discovery requests shall inherit ``BOFDevice``.
5+
"""
6+
class BOFDevice(object):
7+
"""Interface class for devices, to inherit in layer-specific device classes.
8+
9+
Device objects are usually built from device description requests in layers.
10+
A device has a set of basic information: a name, a description, a MAC
11+
address and an IP address. All of them are attributes to this base object,
12+
but not all of them may be provided when asking protocols for device
13+
descriptions. On the other hand, most of protocol-specific devices will have
14+
additional attributes.
15+
"""
16+
protocol:str = "BOF"
17+
name:str = None
18+
description:str = None
19+
mac_address:str = None
20+
ip_address:str = None
21+
22+
# Requires unit testing
23+
def __init__(self, name: str=None, description: str=None,
24+
mac_address: str=None, ip_address: str=None):
25+
self.name = name
26+
self.description = description
27+
self.mac_address = mac_address
28+
self.ip_address = ip_address
29+
30+
def __str__(self):
31+
return "[{0}] Device name: {1}\n\tDescription: {2}\n\tMAC address: {3}" \
32+
"\n\tIP address: {4}".format(self.protocol, self.name, self.description,
33+
self.mac_address, self.ip_address)

bof/layers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@
3131

3232
from .knx import *
3333
from .modbus import *
34+
from .lldp import *
35+
from .profinet import *

bof/layers/knx/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@
2727
:knx_packet:
2828
Object representation of a KNX packet. ``KNXPacket`` inherits ``BOFPacket``
2929
and uses Scapy's implementation of KNX (located in ``bof/layers/raw_scapy``
30-
until contribution to Scapy). Contains method to build, read or alter a
30+
or directly in Scapy contrib). Contains method to build, read or alter a
3131
frame or part of it, even if this does not follow KNX's specifications.
3232
33-
:knx_feature:
33+
:knx_messages:
34+
Set of functions that build specific KNX messages with the right values.
35+
36+
:knx_functions:
3437
Higher-level functions to discover and interact with devices via KNXnet/IP.
3538
"""
3639

3740
from .knx_network import *
3841
from .knx_packet import *
39-
from .knx_feature import *
42+
from .knx_messages import *
43+
from .knx_functions import *

bof/layers/knx/knx_constants.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Profinet DCP constants
3+
----------------------
4+
5+
Protocol-dependent constants (network and functions) for PNDCP.
6+
"""
7+
8+
from ... import to_property
9+
from ...layers.raw_scapy import knx as scapy_knx
10+
11+
KNX_MULTICAST_ADDR = MULTICAST_ADDR = "224.0.23.12"
12+
KNX_PORT = PORT = 3671
13+
14+
# Converts Scapy KNX's SERVICE_IDENTIFIER_CODES & CEMI dicts with format
15+
# {byte value: service name} to the opposite, so that the end user can call
16+
# services by their names instead of their values.
17+
SID = type('SID', (object,),
18+
{to_property(v):k.to_bytes(2, byteorder='big') \
19+
for k,v in scapy_knx.SERVICE_IDENTIFIER_CODES.items()})()
20+
CEMI = type('CEMI', (object,),
21+
{to_property(v):k for k,v in scapy_knx.MESSAGE_CODES.items()})()
22+
ACPI = type('ACPI', (object,),
23+
{to_property(v):k for k,v in scapy_knx.KNX_ACPI_CODES.items()})()
24+
25+
CONNECTION_TYPE_CODES = type('CONNECTION_TYPE_CODES', (object,),
26+
{to_property(v):k for k,v in scapy_knx.CONNECTION_TYPE_CODES.items()})()
27+
CEMI_OBJECT_TYPES = type('CEMI_OBJECT_TYPES', (object,),
28+
{to_property(v):k for k,v in scapy_knx.CEMI_OBJECT_TYPES.items()})()
29+
30+
CEMI_PROPERTIES = type('CEMI_PROPERTIES', (object,),
31+
{to_property(v):k for k,v in scapy_knx.CEMI_PROPERTIES.items()})()
32+
33+
TYPE_FIELD = "service_identifier"
34+
CEMI_FIELD = "cemi"
Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
"""
2-
KNX features
3-
------------
2+
KNX functions
3+
-------------
44
5-
This module contains a set of higher-level functions to interact with devices
6-
using KNXnet/IP without prior knowledge about the protocol.
5+
Higher-level functions to interact with devices using KNXnet/IP.
76
87
Contents:
98
109
:KNXDevice:
11-
An object representation of a KNX device with multiple properties. Only
10+
Object representation of a KNX device with multiple properties. Only
1211
supports KNXnet/IP servers so far, but will be extended to KNX devices.
13-
:Features:
12+
:Functions:
1413
High-level functions to interact with a device: search, discover, read,
1514
write, etc.
1615
@@ -19,27 +18,12 @@
1918

2019
from ipaddress import ip_address
2120
# Internal
22-
from ... import BOFNetworkError, BOFProgrammingError
21+
from ... import BOFNetworkError, BOFProgrammingError, BOFDevice, IS_IP
2322
from .knx_network import *
2423
from .knx_packet import *
2524
from .knx_messages import *
2625
from ...layers.raw_scapy import knx as scapy_knx
2726

28-
###############################################################################
29-
# CONSTANTS #
30-
###############################################################################
31-
32-
MULTICAST_ADDR = "224.0.23.12"
33-
KNX_PORT = 3671
34-
35-
def IS_IP(ip: str):
36-
"""Check that ip is a valid IPv4 address."""
37-
try:
38-
ip_address(ip)
39-
except ValueError:
40-
raise BOFProgrammingError("Invalid IP {0}".format(ip)) from None
41-
42-
4327
def INDIV_ADDR(x: int) -> str:
4428
"""Converts an int to KNX individual address."""
4529
return "%d.%d.%d" % ((x >> 12) & 0xf, (x >> 8) & 0xf, (x & 0xff))
@@ -52,7 +36,7 @@ def GROUP_ADDR(x: int) -> str:
5236
# KNX DEVICE REPRESENTATION #
5337
###############################################################################
5438

55-
class KNXDevice(object):
39+
class KNXDevice(BOFDevice):
5640
"""Object representing a KNX device.
5741
5842
Data stored to the object is the one returned by SEARCH RESPONSE and
@@ -66,10 +50,12 @@ class KNXDevice(object):
6650
6751
The information gathered from devices may be completed, improved later.
6852
"""
53+
protocol:str = "KNX"
6954
def __init__(self, name: str, ip_address: str, port: int, knx_address: str,
7055
mac_address: str, multicast_address: str=MULTICAST_ADDR,
7156
serial_number: str=""):
7257
self.name = name
58+
self.description = None
7359
self.ip_address = ip_address
7460
self.port = port
7561
self.knx_address = knx_address
@@ -78,13 +64,10 @@ def __init__(self, name: str, ip_address: str, port: int, knx_address: str,
7864
self.serial_number = serial_number
7965

8066
def __str__(self):
81-
descr = ["Device: \"{0}\" @ {1}:{2}".format(self.name,
82-
self.ip_address,
83-
self.port)]
84-
descr += ["- KNX address: {0}".format(self.knx_address)]
85-
descr += ["- Hardware: {0} (SN: {1})".format(self.mac_address,
86-
self.serial_number)]
87-
return " ".join(descr)
67+
return "{0}\n\tPort: {1}\n\tMulticast address: {2}\n\t" \
68+
"KNX address: {3}\n\tSerial number: {4}".format(
69+
super().__str__(), self.port, self.multicast_address,
70+
self.knx_address, self.serial_number)
8871

8972
@classmethod
9073
def init_from_search_response(cls, response: KNXPacket):
@@ -139,7 +122,7 @@ def init_from_description_response(cls, response: KNXPacket, source: tuple):
139122
return cls(**args)
140123

141124
###############################################################################
142-
# FEATURES #
125+
# FUNCTIONS #
143126
###############################################################################
144127

145128
#-----------------------------------------------------------------------------#

bof/layers/knx/knx_packet.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,7 @@
2727
from bof.layers.raw_scapy import knx as scapy_knx
2828
from bof.packet import BOFPacket
2929
from bof.base import BOFProgrammingError, to_property
30-
31-
###############################################################################
32-
# CONSTANTS #
33-
###############################################################################
34-
35-
# Converts Scapy KNX's SERVICE_IDENTIFIER_CODES & CEMI dicts with format
36-
# {byte value: service name} to the opposite, so that the end user can call
37-
# services by their names instead of their values.
38-
SID = type('SID', (object,),
39-
{to_property(v):k.to_bytes(2, byteorder='big') \
40-
for k,v in scapy_knx.SERVICE_IDENTIFIER_CODES.items()})()
41-
CEMI = type('CEMI', (object,),
42-
{to_property(v):k for k,v in scapy_knx.MESSAGE_CODES.items()})()
43-
ACPI = type('ACPI', (object,),
44-
{to_property(v):k for k,v in scapy_knx.KNX_ACPI_CODES.items()})()
45-
46-
CONNECTION_TYPE_CODES = type('CONNECTION_TYPE_CODES', (object,),
47-
{to_property(v):k for k,v in scapy_knx.CONNECTION_TYPE_CODES.items()})()
48-
CEMI_OBJECT_TYPES = type('CEMI_OBJECT_TYPES', (object,),
49-
{to_property(v):k for k,v in scapy_knx.CEMI_OBJECT_TYPES.items()})()
50-
51-
CEMI_PROPERTIES = type('CEMI_PROPERTIES', (object,),
52-
{to_property(v):k for k,v in scapy_knx.CEMI_PROPERTIES.items()})()
53-
54-
TYPE_FIELD = "service_identifier"
55-
CEMI_FIELD = "cemi"
30+
from .knx_constants import *
5631

5732
###############################################################################
5833
# KNXPacket class #

0 commit comments

Comments
 (0)