# coding: utf8 # 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 . # Copyright (C) 2019 Stefan Mehner (stefan.mehner@b-tu.de) # scapy.contrib.description = Profinet DCP layer # scapy.contrib.status = loads from scapy.compat import orb from scapy.all import Packet, bind_layers, Padding from scapy.fields import ByteEnumField, ShortField, XShortField, \ ShortEnumField, FieldLenField, XByteField, XIntField, MultiEnumField, \ IPField, MACField, StrLenField, PacketListField, PadField, \ ConditionalField, LenField # minimum packet is 60 bytes.. 14 bytes are Ether() MIN_PACKET_LENGTH = 44 ##################################################### # Constants # ##################################################### DCP_GET_SET_FRAME_ID = 0xFEFD DCP_IDENTIFY_REQUEST_FRAME_ID = 0xFEFE DCP_IDENTIFY_RESPONSE_FRAME_ID = 0xFEFF DCP_REQUEST = 0x00 DCP_RESPONSE = 0x01 DCP_SERVICE_ID_GET = 0x03 DCP_SERVICE_ID_SET = 0x04 DCP_SERVICE_ID_IDENTIFY = 0x05 DCP_SERVICE_ID = { 0x00: "reserved", 0x01: "Manufacturer specific", 0x02: "Manufacturer specific", 0x03: "Get", 0x04: "Set", 0x05: "Identify", 0x06: "Hello", } DCP_SERVICE_TYPE = { 0x00: "Request", 0x01: "Response Success", 0x05: "Response - Request not supported", } DCP_DEVICE_ROLES = { 0x00: "IO Supervisor", 0x01: "IO Device", 0x02: "IO Controller", } DCP_OPTIONS = { 0x00: "reserved", 0x01: "IP", 0x02: "Device properties", 0x03: "DHCP", 0x04: "Reserved", 0x05: "Control", 0x06: "Device Initiative", 0xff: "All Selector" } DCP_OPTIONS.update({i: "reserved" for i in range(0x07, 0x7f)}) DCP_OPTIONS.update({i: "Manufacturer specific" for i in range(0x80, 0xfe)}) DCP_SUBOPTIONS = { # ip 0x01: { 0x00: "Reserved", 0x01: "MAC Address", 0x02: "IP Parameter" }, # device properties 0x02: { 0x00: "Reserved", 0x01: "Manufacturer specific (Type of Station)", 0x02: "Name of Station", 0x03: "Device ID", 0x04: "Device Role", 0x05: "Device Options", 0x06: "Alias Name", 0x07: "Device Instance", 0x08: "OEM Device ID", }, # dhcp 0x03: { 0x0c: "Host name", 0x2b: "Vendor specific", 0x36: "Server identifier", 0x37: "Parameter request list", 0x3c: "Class identifier", 0x3d: "DHCP client identifier", 0x51: "FQDN, Fully Qualified Domain Name", 0x61: "UUID/GUID-based Client", 0xff: "Control DHCP for address resolution" }, # control 0x05: { 0x00: "Reserved", 0x01: "Start Transaction", 0x02: "End Transaction", 0x03: "Signal", 0x04: "Response", 0x05: "Reset Factory Settings", 0x06: "Reset to Factory" }, # device initiative 0x06: { 0x00: "Reserved", 0x01: "Device Initiative" }, 0xff: { 0xff: "ALL Selector" } } BLOCK_INFOS = { 0x00: "Reserved", } BLOCK_INFOS.update({i: "reserved" for i in range(0x01, 0xff)}) IP_BLOCK_INFOS = { 0x0000: "IP not set", 0x0001: "IP set", 0x0002: "IP set by DHCP", 0x0080: "IP not set (address conflict detected)", 0x0081: "IP set (address conflict detected)", 0x0082: "IP set by DHCP (address conflict detected)", } IP_BLOCK_INFOS.update({i: "reserved" for i in range(0x0003, 0x007f)}) BLOCK_ERRORS = { 0x00: "Ok", 0x01: "Option unsupp.", 0x02: "Suboption unsupp. or no DataSet avail.", 0x03: "Suboption not set", 0x04: "Resource Error", 0x05: "SET not possible by local reasons", 0x06: "In operation, SET not possible", } BLOCK_QUALIFIERS = { 0x0000: "Use the value temporary", 0x0001: "Save the value permanent", } BLOCK_QUALIFIERS.update({i: "reserved" for i in range(0x0002, 0x00ff)}) ##################################################### # DCP Blocks # ##################################################### # GENERIC DCP BLOCK # DCP RESPONSE BLOCKS class DCPBaseBlock(Packet): """ base class for all DCP Blocks """ fields_desc = [ ByteEnumField("option", 1, DCP_OPTIONS), MultiEnumField("sub_option", 2, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), FieldLenField("dcp_block_length", None, length_of="data"), ShortEnumField("block_info", 0, BLOCK_INFOS), StrLenField("data", "", length_from=lambda x: x.dcp_block_length), ] def extract_padding(self, s): return '', s # OPTION: IP class DCPIPBlock(Packet): fields_desc = [ ByteEnumField("option", 1, DCP_OPTIONS), MultiEnumField("sub_option", 2, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), LenField("dcp_block_length", None), ShortEnumField("block_info", 1, IP_BLOCK_INFOS), IPField("ip", "192.168.0.2"), IPField("netmask", "255.255.255.0"), IPField("gateway", "192.168.0.1"), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s class DCPMACBlock(Packet): fields_desc = [ ByteEnumField("option", 1, DCP_OPTIONS), MultiEnumField("sub_option", 1, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), FieldLenField("dcp_block_length", None), ShortEnumField("block_info", 0, BLOCK_INFOS), MACField("mac", "00:00:00:00:00:00"), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s # OPTION: Device Properties class DCPManufacturerSpecificBlock(Packet): fields_desc = [ ByteEnumField("option", 2, DCP_OPTIONS), MultiEnumField("sub_option", 1, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), FieldLenField("dcp_block_length", None), ShortEnumField("block_info", 0, BLOCK_INFOS), StrLenField("device_vendor_value", "et200sp", length_from=lambda x: x.dcp_block_length - 2), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s class DCPNameOfStationBlock(Packet): fields_desc = [ ByteEnumField("option", 2, DCP_OPTIONS), MultiEnumField("sub_option", 2, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), FieldLenField("dcp_block_length", None, length_of="name_of_station", adjust=lambda p, x: x + 2), ShortEnumField("block_info", 0, BLOCK_INFOS), StrLenField("name_of_station", "et200sp", length_from=lambda x: x.dcp_block_length - 2), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s class DCPDeviceIDBlock(Packet): fields_desc = [ ByteEnumField("option", 2, DCP_OPTIONS), MultiEnumField("sub_option", 3, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), LenField("dcp_block_length", None), ShortEnumField("block_info", 0, BLOCK_INFOS), XShortField("vendor_id", 0x002a), XShortField("device_id", 0x0313), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s class DCPDeviceRoleBlock(Packet): fields_desc = [ ByteEnumField("option", 2, DCP_OPTIONS), MultiEnumField("sub_option", 4, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), LenField("dcp_block_length", 4), ShortEnumField("block_info", 0, BLOCK_INFOS), ByteEnumField("device_role_details", 1, DCP_DEVICE_ROLES), XByteField("reserved", 0x00), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s # one DeviceOptionsBlock can contain 1..n different options class DeviceOption(Packet): fields_desc = [ ByteEnumField("option", 2, DCP_OPTIONS), MultiEnumField("sub_option", 5, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), ] def extract_padding(self, s): return '', s class DCPDeviceOptionsBlock(Packet): fields_desc = [ ByteEnumField("option", 2, DCP_OPTIONS), MultiEnumField("sub_option", 5, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), LenField("dcp_block_length", None), ShortEnumField("block_info", 0, BLOCK_INFOS), PacketListField("device_options", [], DeviceOption, length_from=lambda p: p.dcp_block_length - 2), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s class DCPAliasNameBlock(Packet): fields_desc = [ ByteEnumField("option", 2, DCP_OPTIONS), MultiEnumField("sub_option", 6, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), FieldLenField("dcp_block_length", None, length_of="alias_name", adjust=lambda p, x: x + 2), ShortEnumField("block_info", 0, BLOCK_INFOS), StrLenField("alias_name", "et200sp", length_from=lambda x: x.dcp_block_length - 2), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s class DCPDeviceInstanceBlock(Packet): fields_desc = [ ByteEnumField("option", 2, DCP_OPTIONS), MultiEnumField("sub_option", 7, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), LenField("dcp_block_length", 4), ShortEnumField("block_info", 0, BLOCK_INFOS), XByteField("device_instance_high", 0x00), XByteField("device_instance_low", 0x01), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s class DCPControlBlock(Packet): fields_desc = [ ByteEnumField("option", 5, DCP_OPTIONS), MultiEnumField("sub_option", 4, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), LenField("dcp_block_length", 3), ByteEnumField("response", 2, DCP_OPTIONS), MultiEnumField("response_sub_option", 2, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), ByteEnumField("block_error", 0, BLOCK_ERRORS), PadField(StrLenField("padding", b"\x00", length_from=lambda p: p.dcp_block_length % 2), 1, padwith=b"\x00") ] def extract_padding(self, s): return '', s def guess_dcp_block_class(packet, **kargs): """ returns the correct dcp block class needed to dissect the current tag if nothing can be found -> dcp base block will be used :param packet: the current packet :return: dcp block class """ # packet = unicode(packet, "utf-8") option = orb(packet[0]) suboption = orb(packet[1]) # NOTE implement the other functions if needed class_switch_case = { # IP 0x01: { 0x01: "DCPMACBlock", 0x02: "DCPIPBlock" }, # Device Properties 0x02: { 0x01: "DCPManufacturerSpecificBlock", 0x02: "DCPNameOfStationBlock", 0x03: "DCPDeviceIDBlock", 0x04: "DCPDeviceRoleBlock", 0x05: "DCPDeviceOptionsBlock", 0x06: "DCPAliasNameBlock", 0x07: "DCPDeviceInstanceBlock", 0x08: "OEM Device ID" }, # DHCP 0x03: { 0x0c: "Host name", 0x2b: "Vendor specific", 0x36: "Server identifier", 0x37: "Parameter request list", 0x3c: "Class identifier", 0x3d: "DHCP client identifier", 0x51: "FQDN, Fully Qualified Domain Name", 0x61: "UUID/GUID-based Client", 0xff: "Control DHCP for address resolution" }, # Control 0x05: { 0x00: "Reserved (0x00)", 0x01: "Start Transaction (0x01)", 0x02: "End Transaction (0x02)", 0x03: "Signal (0x03)", 0x04: "DCPControlBlock", 0x05: "Reset Factory Settings (0x05)", 0x06: "Reset to Factory (0x06)" }, # Device Inactive 0x06: { 0x00: "Reserved (0x00)", 0x01: "Device Initiative (0x01)" }, # ALL Selector 0xff: { 0xff: "ALL Selector (0xff)" } } try: c = class_switch_case[option][suboption] except KeyError: c = "DCPBaseBlock" cls = globals()[c] return cls(packet, **kargs) # GENERIC DCP PACKET class ProfinetDCP(Packet): """ Profinet DCP Packet Requests are handled via ConditionalField because here only 1 Block is used every time. Response can contain 1..n Blocks, for that you have to use one ProfinetDCP Layer with one or multiple DCP*Block Layers:: ProfinetDCP / DCPNameOfStationBlock / DCPDeviceIDBlock ... Example for a DCP Identify All Request:: Ether(dst="01:0e:cf:00:00:00") / ProfinetIO(frameID=DCP_IDENTIFY_REQUEST_FRAME_ID) / ProfinetDCP(service_id=DCP_SERVICE_ID_IDENTIFY, service_type=DCP_REQUEST, option=255, sub_option=255, dcp_data_length=4) Example for a DCP Identify Response:: Ether(dst=dst_mac) / ProfinetIO(frameID=DCP_IDENTIFY_RESPONSE_FRAME_ID) / ProfinetDCP( service_id=DCP_SERVICE_ID_IDENTIFY, service_type=DCP_RESPONSE) / DCPNameOfStationBlock(name_of_station="device1") Example for a DCP Set Request:: Ether(dst=mac) / ProfinetIO(frameID=DCP_GET_SET_FRAME_ID) / ProfinetDCP(service_id=DCP_SERVICE_ID_SET, service_type=DCP_REQUEST, option=2, sub_option=2, dcp_data_length=14, dcp_block_length=10, name_of_station=name, reserved=0) """ name = "Profinet DCP" # a DCP PDU consists of some fields and 1..n DCP Blocks fields_desc = [ ByteEnumField("service_id", 5, DCP_SERVICE_ID), ByteEnumField("service_type", 0, DCP_SERVICE_TYPE), XIntField("xid", 0x01000001), # XShortField('reserved', 0), ShortField('reserved', 0), LenField("dcp_data_length", None), # DCP REQUEST specific ConditionalField(ByteEnumField("option", 2, DCP_OPTIONS), lambda pkt: pkt.service_type == 0), ConditionalField( MultiEnumField("sub_option", 3, DCP_SUBOPTIONS, fmt='B', depends_on=lambda p: p.option), lambda pkt: pkt.service_type == 0), # calculate the len fields - workaround ConditionalField(LenField("dcp_block_length", 0), lambda pkt: pkt.service_type == 0), # DCP SET REQUEST # ConditionalField(ShortEnumField("block_qualifier", 1, BLOCK_QUALIFIERS), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0), # Name Of Station ConditionalField(StrLenField("name_of_station", "et200sp", length_from=lambda x: x.dcp_block_length - 2), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0 and pkt.option == 2 and pkt.sub_option == 2), # MAC ConditionalField(MACField("mac", "00:00:00:00:00:00"), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0 and pkt.option == 1 and pkt.sub_option == 1), # IP ConditionalField(IPField("ip", "192.168.0.2"), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0 and pkt.option == 1 and pkt.sub_option == 2), ConditionalField(IPField("netmask", "255.255.255.0"), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0 and pkt.option == 1 and pkt.sub_option == 2), ConditionalField(IPField("gateway", "192.168.0.1"), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0 and pkt.option == 1 and pkt.sub_option == 2), # DCP IDENTIFY REQUEST # # Name of station ConditionalField(StrLenField("name_of_station", "et200sp", length_from=lambda x: x.dcp_block_length), lambda pkt: pkt.service_id == 5 and pkt.service_type == 0 and pkt.option == 2 and pkt.sub_option == 2), # Alias name ConditionalField(StrLenField("alias_name", "et200sp", length_from=lambda x: x.dcp_block_length), lambda pkt: pkt.service_id == 5 and pkt.service_type == 0 and pkt.option == 2 and pkt.sub_option == 6), # implement further REQUEST fields if needed .... # DCP RESPONSE BLOCKS # ConditionalField( PacketListField("dcp_blocks", [], guess_dcp_block_class, length_from=lambda p: p.dcp_data_length), lambda pkt: pkt.service_type == 1), ] def post_build(self, pkt, pay): # add padding to ensure min packet length padding = MIN_PACKET_LENGTH - (len(pkt + pay)) pay += b"\0" * padding return Packet.post_build(self, pkt, pay) bind_layers(ProfinetDCP, Padding)