Skip to content

Commit

Permalink
Merge pull request #255 from zeronounours/profinet_rtc
Browse files Browse the repository at this point in the history
Add PROFINET IO real-time layer
  • Loading branch information
guedou authored Sep 15, 2016
2 parents 33420f2 + b69aaee commit cce88a9
Show file tree
Hide file tree
Showing 6 changed files with 963 additions and 0 deletions.
269 changes: 269 additions & 0 deletions doc/scapy/advanced_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -790,3 +790,272 @@ Two methods are hooks to be overloaded:

* The ``master_filter()`` method is called each time a packet is sniffed and decides if it is interesting for the automaton. When working on a specific protocol, this is where you will ensure the packet belongs to the connection you are being part of, so that you do not need to make all the sanity checks in each transition.

PROFINET IO RTC
===============

PROFINET IO is an industrial protocol composed of different layers such as the Real-Time Cyclic (RTC) layer, used to exchange data. However, this RTC layer is stateful and depends on a configuration sent through another layer: the DCE/RPC endpoint of PROFINET. This configuration defines where each exchanged piece of data must be located in the RTC ``data`` buffer, as well as the length of this same buffer. Building such packet is then a bit more complicated than other protocols.

RTC data packet
---------------

The first thing to do when building the RTC ``data`` buffer is to instanciate each Scapy packet which represents a piece of data. Each one of them may require some specific piece of configuration, such as its length. All packets and their configuration are:

* ``PNIORealTimeRawData``: a simple raw data like ``Raw``

* ``length``: defines the length of the data

* ``Profisafe``: the PROFIsafe profile to perform functional safety

* ``length``: defines the length of the whole packet
* ``CRC``: defines the length of the CRC, either ``3`` or ``4``

* ``PNIORealTimeIOxS``: either an IO Consumer or Provider Status byte

* Doesn't require any configuration

To instanciate one of these packets with its configuration, the ``config`` argument must be given. It is a ``dict()`` which contains all the required piece of configuration::

>>> load_contrib('pnio_rtc')
>>> str(PNIORealTimeRawData(load='AAA', config={'length': 4}))
'AAA\x00'
>>> str(Profisafe(load='AAA', Control_Status=0x20, CRC=0x424242, config={'length': 8, 'CRC': 3}))
'AAA\x00 BBB'
>>> hexdump(PNIORealTimeIOxS())
0000 80 .


RTC packet
----------

Now that a data packet can be instanciated, a whole RTC packet may be built. ``PNIORealTime`` contains a field ``data`` which is a list of all data packets to add in the buffer, however, without the configuration, Scapy won't be
able to dissect it::

>>> load_contrib("pnio_rtc")
>>> p=PNIORealTime(cycleCounter=1024, data=[
... PNIORealTimeIOxS(),
... PNIORealTimeRawData(load='AAA', config={'length':4}) / PNIORealTimeIOxS(),
... Profisafe(load='AAA', Control_Status=0x20, CRC=0x424242, config={'length': 8, 'CRC': 3}) / PNIORealTimeIOxS(),
... ])
>>> p.show()
###[ PROFINET Real-Time ]###
len= None
dataLen= None
\data\
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0
| extension= 0
|###[ PNIO RTC Raw data ]###
| load= 'AAA'
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0
| extension= 0
|###[ PROFISafe ]###
| load= 'AAA'
| Control_Status= 0x20
| CRC= 0x424242
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0
| extension= 0
padding= ''
cycleCounter= 1024
dataStatus= primary+validData+run+no_problem
transferStatus= 0
>>> p.show2()
###[ PROFINET Real-Time ]###
len= 44
dataLen= 15
\data\
|###[ PNIO RTC Raw data ]###
| load= '\x80AAA\x00\x80AAA\x00 BBB\x80'
padding= ''
cycleCounter= 1024
dataStatus= primary+validData+run+no_problem
transferStatus= 0

For Scapy to be able to dissect it correctly, one must also configure the layer for it to know the location of each data in the buffer. This configuration is saved in the dictionary ``conf.contribs["PNIO_RTC"]`` which can be updated with the ``pnio_update_config`` method. Each item in the dictionary uses the tuple ``(Ether.src, Ether.dst)`` as key, to be able to separate the configuration of each communication. Each value is then a list of a tuple which describes a data packet. It is composed of the negative index, from the end of the data buffer, of the packet position, the class of the packet as second item and the ``config`` dictionary to provide to the class as last. If we continue the previous example, here is the configuration to set::

>>> load_contrib("pnio")
>>> e=Ether(src='00:01:02:03:04:05', dst='06:07:08:09:0a:0b') / ProfinetIO() / p
>>> e.show2()
###[ Ethernet ]###
dst= 06:07:08:09:0a:0b
src= 00:01:02:03:04:05
type= 0x8892
###[ ProfinetIO ]###
frameID= RT_CLASS_1
###[ PROFINET Real-Time ]###
len= 44
dataLen= 15
\data\
|###[ PNIO RTC Raw data ]###
| load= '\x80AAA\x00\x80AAA\x00 BBB\x80'
padding= ''
cycleCounter= 1024
dataStatus= primary+validData+run+no_problem
transferStatus= 0
>>> pnio_update_config({('00:01:02:03:04:05', '06:07:08:09:0a:0b'): [
... (-9, Profisafe, {'length': 8, 'CRC': 3}),
... (-9 - 5, PNIORealTimeRawData, {'length':4}),
... ]})
>>> e.show2()
###[ Ethernet ]###
dst= 06:07:08:09:0a:0b
src= 00:01:02:03:04:05
type= 0x8892
###[ ProfinetIO ]###
frameID= RT_CLASS_1
###[ PROFINET Real-Time ]###
len= 44
dataLen= 15
\data\
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
|###[ PNIO RTC Raw data ]###
| load= 'AAA'
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
|###[ PROFISafe ]###
| load= 'AAA'
| Control_Status= 0x20
| CRC= 0x424242L
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
padding= ''
cycleCounter= 1024
dataStatus= primary+validData+run+no_problem
transferStatus= 0

If no data packets are configured for a given offset, it defaults to a ``PNIORealTimeIOxS``. However, this method is not very convenient for the user to configure the layer and it only affects the dissection of packets. In such cases, one may have access to several RTC packets, sniffed or retrieved from a PCAP file. Thus, ``PNIORealTime`` provides some methods to analyse a list of ``PNIORealTime`` packets and locate all data in it, based on simple heuristics. All of them take as first argument an iterable which contains the list of packets to analyse.

* ``PNIORealTime.find_data()`` analyses the data buffer and separate real data from IOxS. It returns a dict which can be provided to ``pnio_update_config``.
* ``PNIORealTime.find_profisafe()`` analyses the data buffer and find the PROFIsafe profiles among the real data. It returns a dict which can be provided to ``pnio_update_config``.
* ``PNIORealTime.analyse_data()`` executes both previous methods and update the configuration. **This is usually the method to call.**
* ``PNIORealTime.draw_entropy()`` will draw the entropy of each byte in the data buffer. It can be used to easily visualize PROFIsafe locations as entropy is the base of the decision algorithm of ``find_profisafe``.

::

>>> load_contrib('pnio_rtc')
>>> t=rdpcap('/path/to/trace.pcap', 1024)
>>> PNIORealTime.analyse_data(t)
{('00:01:02:03:04:05', '06:07:08:09:0a:0b'): [(-19, <class 'scapy.contrib.pnio_rtc.PNIORealTimeRawData'>, {'length': 1}), (-15, <class 'scapy.contrib.pnio_rtc.Profisafe'>, {'CRC': 3, 'length': 6}), (-7, <class 'scapy.contrib.pnio_rtc.Profisafe'>, {'CRC': 3, 'length': 5})]}
>>> t[100].show()
###[ Ethernet ]###
dst= 06:07:08:09:0a:0b
src= 00:01:02:03:04:05
type= n_802_1Q
###[ 802.1Q ]###
prio= 6L
id= 0L
vlan= 0L
type= 0x8892
###[ ProfinetIO ]###
frameID= RT_CLASS_1
###[ PROFINET Real-Time ]###
len= 44
dataLen= 22
\data\
|###[ PNIO RTC Raw data ]###
| load= '\x80\x80\x80\x80\x80\x80\x00\x80\x80\x80\x12:\x0e\x12\x80\x80\x00\x12\x8b\x97\xe3\x80'
padding= ''
cycleCounter= 6208
dataStatus= primary+validData+run+no_problem
transferStatus= 0
>>> t[100].show2()
###[ Ethernet ]###
dst= 06:07:08:09:0a:0b
src= 00:01:02:03:04:05
type= n_802_1Q
###[ 802.1Q ]###
prio= 6L
id= 0L
vlan= 0L
type= 0x8892
###[ ProfinetIO ]###
frameID= RT_CLASS_1
###[ PROFINET Real-Time ]###
len= 44
dataLen= 22
\data\
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
[...]
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
|###[ PNIO RTC Raw data ]###
| load= ''
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
[...]
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
|###[ PROFISafe ]###
| load= ''
| Control_Status= 0x12
| CRC= 0x3a0e12L
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
|###[ PROFISafe ]###
| load= ''
| Control_Status= 0x12
| CRC= 0x8b97e3L
|###[ PNIO RTC IOxS ]###
| dataState= good
| instance= subslot
| reserved= 0x0L
| extension= 0L
padding= ''
cycleCounter= 6208
dataStatus= primary+validData+run+no_problem
transferStatus= 0
In addition, one can see, when displaying a ``PNIORealTime`` packet, the field ``len``. This is a computed field which is not added in the final packet build. It is mainly useful for dissection and reconstruction, but it can also be used to modify the behaviour of the packet. In fact, RTC packet must always be long enough for an Ethernet frame and to do so, a padding must be added right after the ``data`` buffer. The default behaviour is to add ``padding`` whose size is computed during the ``build`` process::

>>> str(PNIORealTime(cycleCounter=0x4242, data=[PNIORealTimeIOxS()]))
'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BB5\x00'

However, one can set ``len`` to modify this behaviour. ``len`` controls the length of the whole ``PNIORealTime`` packet. Then, to shorten the length of the padding, ``len`` can be set to a lower value::

>>> str(PNIORealTime(cycleCounter=0x4242, data=[PNIORealTimeIOxS()], len=50))
'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BB5\x00'
>>> str(PNIORealTime(cycleCounter=0x4242, data=[PNIORealTimeIOxS()]))
'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BB5\x00'
>>> str(PNIORealTime(cycleCounter=0x4242, data=[PNIORealTimeIOxS()], len=30))
'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BB5\x00'

2 changes: 2 additions & 0 deletions scapy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ class Conf(ConfClass):
noenum : holds list of enum fields for which conversion to string should NOT be done
AS_resolver: choose the AS resolver class to use
extensions_paths: path or list of paths where extensions are to be looked for
contribs: a dict which can be used by contrib layers to store local configuration
"""
version = VERSION
session = ""
Expand Down Expand Up @@ -385,6 +386,7 @@ class Conf(ConfClass):
"radius", "rip", "rtp", "skinny", "smb", "snmp",
"tftp", "x509", "bluetooth", "dhcp6", "llmnr",
"sctp", "vrrp", "ipsec", "lltd", "vxlan"]
contribs = dict()


if not Conf.ipv6_enabled:
Expand Down
77 changes: 77 additions & 0 deletions scapy/contrib/pnio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# This file is part of Scapy
# Scapy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# any later version.
#
# Scapy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Scapy. If not, see <http://www.gnu.org/licenses/>.

# Copyright (C) 2016 Gauthier Sebaux

# scapy.contrib.description = ProfinetIO base layer
# scapy.contrib.status = loads

"""
A simple and non exhaustive Profinet IO layer for scapy
"""

# Scapy imports
from scapy.all import Packet, bind_layers, Ether, UDP
from scapy.fields import XShortEnumField

# Some constants
PNIO_FRAME_IDS = {
0x0020:"PTCP-RTSyncPDU-followup",
0x0080:"PTCP-RTSyncPDU",
0xFC01:"Alarm High",
0xFE01:"Alarm Low",
0xFEFC:"DCP-Hello-Req",
0xFEFD:"DCP-Get-Set",
0xFEFE:"DCP-Identify-ReqPDU",
0xFEFF:"DCP-Identify-ResPDU",
0xFF00:"PTCP-AnnouncePDU",
0xFF20:"PTCP-FollowUpPDU",
0xFF40:"PTCP-DelayReqPDU",
0xFF41:"PTCP-DelayResPDU-followup",
0xFF42:"PTCP-DelayFuResPDU",
0xFF43:"PTCP-DelayResPDU",
}
for i in xrange(0x0100, 0x1000):
PNIO_FRAME_IDS[i] = "RT_CLASS_3"
for i in xrange(0x8000, 0xC000):
PNIO_FRAME_IDS[i] = "RT_CLASS_1"
for i in xrange(0xC000, 0xFC00):
PNIO_FRAME_IDS[i] = "RT_CLASS_UDP"
for i in xrange(0xFF80, 0xFF90):
PNIO_FRAME_IDS[i] = "FragmentationFrameID"

#################
## PROFINET IO ##
#################

class ProfinetIO(Packet):
"""Basic PROFINET IO dispatcher"""
fields_desc = [XShortEnumField("frameID", 0, PNIO_FRAME_IDS)]
overload_fields = {
Ether: {"type": 0x8892},
UDP: {"dport": 0x8892},
}

def guess_payload_class(self, payload):
# For frameID in the RT_CLASS_* range, use the RTC packet as payload
if (self.frameID >= 0x0100 and self.frameID < 0x1000) or \
(self.frameID >= 0x8000 and self.frameID < 0xFC00):
from scapy.contrib.pnio_rtc import PNIORealTime
return PNIORealTime
else:
return Packet.guess_payload_class(self, payload)

bind_layers(Ether, ProfinetIO, type=0x8892)
bind_layers(UDP, ProfinetIO, dport=0x8892)

32 changes: 32 additions & 0 deletions scapy/contrib/pnio.uts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
% ProfinetIO layer test campaign

+ Syntax check
= Import the ProfinetIO layer
from scapy.contrib.pnio import *


+ Check DCE/RPC layer

= ProfinetIO default values
str(ProfinetIO()) == '\x00\x00'

= ProfinetIO overloads Ethertype
p = Ether() / ProfinetIO()
p.type == 0x8892

= ProfinetIO overloads UDP dport
p = UDP() / ProfinetIO()
p.dport == 0x8892

= Ether guesses ProfinetIO as payload class
p = Ether('ffffffffffff00000000000088920102'.decode('hex'))
p.payload.__class__ == ProfinetIO and p.frameID == 0x0102

= UDP guesses ProfinetIO as payload class
p = UDP('12348892000a00000102'.decode('hex'))
p.payload.__class__ == ProfinetIO and p.frameID == 0x0102

= ProfinetIO guess payload to PNIORealTime
p = UDP('12348892000c000080020102'.decode('hex'))
p.payload.payload.__class__.__name__ == 'PNIORealTime' and p.frameID == 0x8002

Loading

0 comments on commit cce88a9

Please sign in to comment.