From ca6aca1c761e50bbf4f9e0b6bbefc64a572e4efb Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 1 Aug 2016 18:01:39 -0400 Subject: [PATCH 01/29] Update cip.py --- cip.py | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 185 insertions(+), 11 deletions(-) diff --git a/cip.py b/cip.py index 01de295..5e0dcc9 100644 --- a/cip.py +++ b/cip.py @@ -32,7 +32,10 @@ from scapy import all as scapy_all +import enip import enip_tcp +import enip_udp +import enip_cpf import utils @@ -136,6 +139,162 @@ class CIP_ReqReadOtherTag(scapy_all.Packet): scapy_all.LEShortField("length", None), ] +class CIP_PortSegment(scapy_all.Packet): + name="CIP_PortSegment" + + PORT_IDENT={ + 0: "Reserved", + 1: "Back-Plane", + 15: "Extended", + } + + fields_desc = [ + scapy_all.BitField("extended_link_address_size", 0, 1), + scapy_all.BitEnumField("port_identifier", 0, 4, PORT_IDENT), + scapy_all.ByteField("link_address_size", 0), + scapy_all.ConditionalField( + scapy_all.LEIntField("extended_port_identifier", 0), + lambda p: p.port_identifier == 0xf # If the Port Identifier is 15, then a 16-bit field, called the Extended Port Identifier, shall be the next part of the Port Segment + ), + scapy_all.ConditionalField( + scapy_all.FieldListField("Link_Address", [], + scapy_all.XByteField("",0), length_from=lambda p: p.link_address_size + ), + lambda p: p.extended_link_address_size == 0x1 # If the Port Identifier is 15, then a 16-bit field, called the Extended Port Identifier, shall be the next part of the Port Segment + ) + ] + + def extract_padding(self, p): + print self.__class__.__name__ + ": P=" + str(p) + return "", p + + +class CIP_LogicalSegment(scapy_all.Packet): + name="CIP_LogicalSegment" + + LOGICAL_TYPE = { + 0: "Class ID", + 1: "Instance ID", + 2: "Member ID", + 3: "Connection Point", + 4: "Attribute ID", + 5: "Special", + 6: "Service ID", + 7: "Reserved" + # The Special and Service ID Logical Types do not use the logical addressing definition for the Logical Format. + } + + LOGICAL_FORMAT = { + 0: "8 - bit logical address", + 1: "16-bit logical address", + 2: "32-bit logical address", + 3: "Reserved for future use" + } + + fields_desc = [ + scapy_all.BitEnumField("logical_type", 0, 3, LOGICAL_TYPE), + scapy_all.BitEnumField("logical_format", 0, 2, LOGICAL_FORMAT), + # When the logical segment is included within a Padded Path, the 16 - bit and 32 - bit + # logical formats shall have a pad inserted between the segment type byte and the Logical Value + # (the 8 - bit format is identical to the Packed Path).The pad byte shall be set to zero. + scapy_all.ConditionalField(scapy_all.ByteField("padding", 0), lambda p: p.logical_format > 0x0), + # scapy_all.ByteField("value", 0), + scapy_all.ConditionalField( + scapy_all.ByteField("value8bit", 0), + lambda p: + p.logical_format >= 0x0 + ), + scapy_all.ConditionalField( + scapy_all.LEShortField("value16bit", 0), + lambda p: + p.logical_format >= 0x1 + ), + scapy_all.ConditionalField( + scapy_all.LELongField("value32bit", 0), + lambda p: + p.logical_format >= 0x2 + ), + ] + + def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + return "", p + + # + +# +# +# class A_8bitValue(scapy_all.Packet): +# fields_desc = [ +# scapy_all.ByteField("value", 0), +# ] +# +# def extract_padding(self, p): +# # print self.__class__.__name__ + ": P=" + str(p) +# return (), p +# +# +# class A_16bitValue(scapy_all.Packet): +# fields_desc = [ +# scapy_all.LEShortField("value", 0), +# ] +# +# def extract_padding(self, p): +# # print self.__class__.__name__ + ": P=" + str(p) +# return (), p +# +# class A_32bitValue(scapy_all.Packet): +# fields_desc = [ +# scapy_all.LELongField("value", 0), +# ] +# +# def extract_padding(self, p): +# # print self.__class__.__name__ + ": P=" + str(p) +# return (), p +# +# scapy_all.bind_layers(CIP_LogicalSegment, A_8bitValue, logical_format=0) +# scapy_all.bind_layers(CIP_LogicalSegment, A_16bitValue, logical_format=1) +# scapy_all.bind_layers(CIP_LogicalSegment, A_32bitValue, logical_format=2) + +SEGMENT_TYPE={ + 0:"Port Segment", # 0 0 0 Port Segment + 1:"Logical Segment", # 0 0 1 Logical Segment + 2:"Network Segment", # 0 1 0 Network Segment + 3:"Symbolic Segment", # 0 1 1 Symbolic Segment + 4:"Data Segment", # 1 0 0 Data Segment + 5:"Data Type (constructed)", # 1 0 1 Data Type (constructed, see section C-2.1) + 6:"Data Type (elementary)", # 1 1 0 Data Type (elementary, see section C-2.1) + 7:"Reserved" # 1 1 1 Reserved for future use +} + + +class CIP_PathPadded(scapy_all.Packet): + name="CIP_PathPadded" + fields_desc = [ + scapy_all.BitEnumField("segment_type", 0, 3, SEGMENT_TYPE), + # scapy_all.PacketField("segment", CIP_PortSegment(),CIP_PortSegment), + # scapy_all.BitEnumField("don't care", 0, 5, SEGMENT_TYPE), + # scapy_all.LEShortField("zero", 0), + # scapy_all.LEShortField("length", None), + ] + + def extract_padding(self, p): + print self.__class__.__name__+": P="+str(p) + # print self.__class__.__name__+": Length="+str(self.length) + q,w=p + # return "",(q,8-w,) + # return (w,q[:2]), (q, w,) + # return p[:self.length], p[self.length:] + return (q,w,),(q,8-w,) + +scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=0) #Port Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_LogicalSegment, segment_type=1) #Port Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=2) #Port Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=3) #Port Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=4) #Port Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=5) #Port Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=6) #Port Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=7) #Port Segment class CIP_PathField(scapy_all.StrLenField): SEGMENT_TYPES = { @@ -445,7 +604,8 @@ def pre_dissect(self, s): return struct.pack('>H', int(b)) + s[2:] def do_build(self): - p = '' + # p = '' + p = super(CIP_ConnectionParam, self).do_build() return p def extract_padding(self, s): @@ -455,24 +615,38 @@ def extract_padding(self, s): class CIP_ReqForwardOpen(scapy_all.Packet): """Forward Open request""" name = "CIP_ReqForwardOpen" + + SEGMENT_TYPE = { + 0x00: "Port Segment", + 0x01: "Logical Segmentc", + 0x02: "Network Segment", + 0x03: "Symbolic Segment", + 0x04: "Data Segment", + 0x05: "Data Type (constructed)", + 0x06: "Data Type (elementary)", + 0x07: "Reserved for future use", + } + fields_desc = [ scapy_all.BitField("priority", 0, 4), scapy_all.BitField("tick_time", 0, 4), scapy_all.ByteField("timeout_ticks", 249), - scapy_all.LEIntField("OT_network_connection_id", 0x80000031), - scapy_all.LEIntField("TO_network_connection_id", 0x80fe0030), + utils.XLEIntField("OT_network_connection_id", 0x80000031), + utils.XLEIntField("TO_network_connection_id", 0x80fe0030), scapy_all.LEShortField("connection_serial_number", 0x1337), - scapy_all.LEShortField("vendor_id", 0x004d), - scapy_all.LEIntField("originator_serial_number", 0xdeadbeef), + utils.XLEShortField("vendor_id", 0x004d), + utils.XLEIntField("originator_serial_number", 0xdeadbeef), scapy_all.ByteField("connection_timeout_multiplier", 0), scapy_all.X3BytesField("reserved", 0), - scapy_all.LEIntField("OT_rpi", 0x007a1200), # 8000 ms + utils.XLEIntField("OT_rpi", 0x007a1200), # 8000 ms scapy_all.PacketField('OT_connection_param', CIP_ConnectionParam(), CIP_ConnectionParam), - scapy_all.LEIntField("TO_rpi", 0x007a1200), + utils.XLEIntField("TO_rpi", 0x007a1200), scapy_all.PacketField('TO_connection_param', CIP_ConnectionParam(), CIP_ConnectionParam), scapy_all.XByteField("transport_type", 0xa3), # direction server, application object, class 3 - scapy_all.ByteField("path_wordsize", None), - CIP_PathField("path", None, length_from=lambda p: 2 * p.path_wordsize), + scapy_all.ByteField("Connection_Path_Size", None), #The number of 16 bit words in the Connection_Path field. + # scapy_all.PacketListField("path_segment_items", [], CIP_Path1, length_from=lambda p: 2 * p.Connection_Path_Size), + scapy_all.PacketListField("path_segment_items", [], CIP_PathPadded, length_from=lambda p: 6), + # CIP_PathField("path", None, length_from=lambda p: 2 * p.path_wordsize), ] @@ -555,8 +729,8 @@ def post_build(self, p, pay): return p + pay -scapy_all.bind_layers(enip_tcp.ENIP_ConnectionPacket, CIP) -scapy_all.bind_layers(enip_tcp.ENIP_SendUnitData_Item, CIP, type_id=0x00b2) +# scapy_all.bind_layers(enip.ENIP_ConnectionPacket, CIP) +scapy_all.bind_layersenip_cpf.CPF_DataItem, CIP, type_id=0x00b2) scapy_all.bind_layers(CIP, CIP_RespAttributesAll, direction=1, service=0x01) scapy_all.bind_layers(CIP, CIP_ReqGetAttributeList, direction=0, service=0x03) From d2c893e26a72abbe896aa9e468026ddb01b2bab8 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 1 Aug 2016 18:10:03 -0400 Subject: [PATCH 02/29] Update enip_cpf.py --- enip_cpf.py | 56 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/enip_cpf.py b/enip_cpf.py index 39becb4..fef8eac 100644 --- a/enip_cpf.py +++ b/enip_cpf.py @@ -1,6 +1,7 @@ #!/usr/bin/env python2 # -*- coding: utf-8 -*- # Copyright (c) 2015 David I. Urbina, david.urbina@utdallas.edu +# Copyright (c) 2016 Andrey Dolgikh, Binghamton University # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,6 +27,34 @@ import utils +ITEM_ID_NUMBERS = utils.merge_dicts({ + 0x0000 : "Null", #(used for UCMM messages). Indicates that encapsulation routing is NOT needed. Target is either local (ethernet) or routing info is in a data Item. + 0x000C : "ListIdentity Response", # + 0x0091 : "Reserved", # for legacy (RA) + 0x00A1 : "Connected Address Item", # (used for connected messages) + 0x00B1 : "Connected Data Item", # Connected Transport packet + 0x00B2 : "Unconnected Data Item", # Unconnected Messages (eg. used within CIP command SendRRData) + 0x0100 : "ListServices response", # + 0x8000 : "Sockaddr Info, originator-to-target", # + 0x8001 : "Sockaddr Info, target-to-originator", # + 0x8002 : "Sequenced Address item", # + }, + {k: "Reserved for legacy (RA)" for k in range(0x0001, 0x000B + 1)}, # 0x0001 – 0x000B Reserved for legacy (RA) + {k: "Reserved for legacy (RA)" for k in range(0x000D, 0x0083 + 1)}, # 0x000D – 0x0083 Reserved for legacy (RA) + {k: "Reserved for future expansion" for k in range(0x0084, 0x0090 + 1)}, # 0x0084 – 0x0090 Reserved for future expansion + {k: "Reserved for future expansion" for k in range(0x0092, 0x00A0 + 1)}, # 0x0092 – 0x00A0 Reserved for future expansion + {k: "Reserved for legacy (RA)" for k in range(0x00A2, 0x00A4 + 1)}, # 0x00A2 – 0x00A4 Reserved for legacy (RA) + {k: "Reserved for future expansion" for k in range(0x00A5, 0x00B0 + 1)}, # 0x00A5 – 0x00B0 Reserved for future expansion + {k: "Reserved for future expansion" for k in range(0x00B3, 0x00FF + 1)}, # 0x00B3 – 0x00FF Reserved for future expansion + {k: "Reserved for legacy (RA)" for k in range(0x0101, 0x010F + 1)}, # 0x0101 – 0x010F Reserved for legacy (RA) + {k: "Reserved for future expansion" for k in range(0x0110, 0x7FFF + 1)}, # 0x0110 – 0x7FFF Reserved for future expansion + {k: "Reserved for future expansion" for k in range(0x8003, 0xFFFF + 1)}, # 0x8003 – 0xFFFF Reserved for future expansion + + #regexps to produce dicts above + #(0x[\d|{ABCDF}]{4}).{3}(0x[\d|{ABCDF}]{4}) (.*) + #\{k\: \"$3\" for k in range\($1\, $2 \+ 1\)\}\, \# +) + class CPF_SequencedAddressItem(scapy_all.Packet): name = "CPF_SequencedAddressItem" @@ -35,17 +64,10 @@ class CPF_SequencedAddressItem(scapy_all.Packet): ] -class CPF_AddressDataItem(scapy_all.Packet): - name = "CPF_AddressDataItem" +class CPF_Item(scapy_all.Packet): + name = "CPF_Item" fields_desc = [ - scapy_all.LEShortEnumField('type_id', 0, { - 0x0000: "Null Address", - 0x00a1: "Connection-based Address", - 0x00b1: "Connected Transport Packet", - 0x00b2: "Unconnected Message", - 0x0100: "ListServices response", - 0x8002: 'Sequenced Address Item', - }), + scapy_all.LEShortEnumField('type_id', 0, ITEM_ID_NUMBERS), scapy_all.LEShortField("length", None), ] @@ -58,13 +80,23 @@ def post_build(self, p, pay): p = p[:2] + struct.pack("2 + ), ] def extract_padding(self, p): From b18f07774e0bd4f059471425e8c835cd79066da3 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 1 Aug 2016 18:14:05 -0400 Subject: [PATCH 03/29] Update enip_tcp.py --- enip_tcp.py | 136 ++++++++-------------------------------------------- 1 file changed, 20 insertions(+), 116 deletions(-) diff --git a/enip_tcp.py b/enip_tcp.py index f09b048..dd4c194 100644 --- a/enip_tcp.py +++ b/enip_tcp.py @@ -24,105 +24,12 @@ from scapy import all as scapy_all -import utils +import enip +import enip_cpf +scapy_all.bind_layers(scapy_all.TCP, enip.ENIP_PACKET, dport=44818) +scapy_all.bind_layers(scapy_all.TCP, enip.ENIP_PACKET, sport=44818) -class ENIP_ConnectionAddress(scapy_all.Packet): - name = "ENIP_ConnectionAddress" - fields_desc = [scapy_all.LEIntField("connection_id", 0)] - - -class ENIP_ConnectionPacket(scapy_all.Packet): - name = "ENIP_ConnectionPacket" - fields_desc = [scapy_all.LEShortField("sequence", 0)] - - -class ENIP_SendUnitData_Item(scapy_all.Packet): - name = "ENIP_SendUnitData_Item" - fields_desc = [ - scapy_all.LEShortEnumField("type_id", 0, { - 0x0000: "null_address", # NULL Address - 0x00a1: "conn_address", # Address for connection based requests - 0x00b1: "conn_packet", # Connected Transport packet - 0x00b2: "unconn_message", # Unconnected Messages (eg. used within CIP command SendRRData) - 0x0100: "listservices_response", # ListServices response - }), - scapy_all.LEShortField("length", None), - ] - - def extract_padding(self, p): - return p[:self.length], p[self.length:] - - def post_build(self, p, pay): - if self.length is None and pay: - l = len(pay) - p = p[:2] + struct.pack(" Date: Mon, 1 Aug 2016 18:45:49 -0400 Subject: [PATCH 04/29] Update plc.py --- plc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plc.py b/plc.py index 2292243..c42213b 100644 --- a/plc.py +++ b/plc.py @@ -29,8 +29,8 @@ from cip import CIP, CIP_Path, CIP_ReqConnectionManager, \ CIP_MultipleServicePacket, CIP_ReqForwardOpen, CIP_RespForwardOpen, \ CIP_ReqForwardClose, CIP_ReqGetAttributeList, CIP_ReqReadOtherTag -from enip_tcp import ENIP_TCP, ENIP_SendUnitData, ENIP_SendUnitData_Item, \ - ENIP_ConnectionAddress, ENIP_ConnectionPacket, ENIP_RegisterSession, ENIP_SendRRData +from enip import ENIP_PACKET, ENIP_SendUnitData, ENIP_ConnectionAddress, \ + ENIP_ConnectionPacket, ENIP_RegisterSession, ENIP_SendRRData # Global switch to make it easy to test without sending anything NO_NETWORK = False @@ -56,7 +56,7 @@ def __init__(self, plc_addr, plc_port=44818): self.sequence = 1 # Open an Ethernet/IP session - sessionpkt = ENIP_TCP() / ENIP_RegisterSession() + sessionpkt = ENIP_PACKET() / ENIP_RegisterSession() if self.sock is not None: self.sock.send(str(sessionpkt)) reply_pkt = self.recv_enippkt() @@ -68,7 +68,7 @@ def connected(self): def send_rr_cip(self, cippkt): """Send a CIP packet over the TCP connection as an ENIP Req/Rep Data""" - enippkt = ENIP_TCP(session=self.session_id) + enippkt = ENIP_PACKET(session=self.session_id) enippkt /= ENIP_SendRRData(items=[ ENIP_SendUnitData_Item(type_id=0), ENIP_SendUnitData_Item() / cippkt @@ -92,7 +92,7 @@ def send_rr_mr_cip(self, cippkt): def send_unit_cip(self, cippkt): """Send a CIP packet over the TCP connection as an ENIP Unit Data""" - enippkt = ENIP_TCP(session=self.session_id) + enippkt = ENIP_PACKET(session=self.session_id) enippkt /= ENIP_SendUnitData(items=[ ENIP_SendUnitData_Item() / ENIP_ConnectionAddress(connection_id=self.enip_connid), ENIP_SendUnitData_Item() / ENIP_ConnectionPacket(sequence=self.sequence) / cippkt @@ -106,7 +106,7 @@ def recv_enippkt(self): if self.sock is None: return pktbytes = self.sock.recv(2000) - pkt = ENIP_TCP(pktbytes) + pkt = ENIP_PACKET(pktbytes) return pkt def forward_open(self): From a0e591764e82f1981a5a31e1685b55c378d5368e Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 1 Aug 2016 18:47:19 -0400 Subject: [PATCH 05/29] Update utils.py --- utils.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/utils.py b/utils.py index baa6417..91a3d4d 100644 --- a/utils.py +++ b/utils.py @@ -22,6 +22,15 @@ """Useful routines and utilities which simplify code writing""" from scapy import all as scapy_all +def merge_dicts(*dict_args):#from http://stackoverflow.com/a/26853961 + ''' + Given any number of dicts, shallow copy and merge into a new dict, + precedence goes to key value pairs in latter dicts. + ''' + result = {} + for dictionary in dict_args: + result.update(dictionary) + return result def hexdump(data, columns=16, indentlvl=""): """Return the hexadecimal representation of the data""" @@ -55,3 +64,15 @@ def i2repr_one(self, pkt, x): if x in self.i2s: return self.i2s[x] return scapy_all.lhex(x) + +class XLEIntField(scapy_all.LEIntField): + """A Little Endian IntField with hexadecimal representation""" + def i2repr(self, pkt, x): + from scapy.utils import lhex + return lhex(self.i2h(pkt, x)) + +class XLEShortField(scapy_all.LEShortField): + """A Little Endian ShortField with hexadecimal representation""" + def i2repr(self, pkt, x): + from scapy.utils import lhex + return lhex(self.i2h(pkt, x)) From c8d116d71a05e4843ede237ed3d31b477123ff8e Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 1 Aug 2016 18:48:43 -0400 Subject: [PATCH 06/29] Add files via upload --- enip.py | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 enip.py diff --git a/enip.py b/enip.py new file mode 100644 index 0000000..c545b3f --- /dev/null +++ b/enip.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2016 Andrey Dolgikh, Matthew Davis, Binghamton University +# Copyright (c) 2015 Nicolas Iooss, SUTD +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import struct +from scapy import all as scapy_all +import utils +import enip_cpf + + +DEVICE_PROFILES = { + 0x02 : "AC Drives", + 0x28 : "CIP Modbus Device", + 0x29 : "CIP Modbus Translator", + 0x25 : "CIP Motion Drive", + 0x2F : "CIP Motion Encoder", + 0x21 : "Turbomolecular Vacuum Pump", + 0x31 : "CIP Motion I/O", + 0x2D : "CIP Motion Safety Drive Device", + 0x0C : "Communications Adapter", + 0x26 : "CompoNet Repeater", + 0x15 : "Contactor", + 0x32 : "ControlNet Physical Layer Component", + 0x13 : "DC Drives", + 0x1F : "DC Power Generator", + 0xC8 : "Embedded Component", + 0x22 : "Encoder", + 0x27 : "Enhanced Mass Flow Controller", + 0x24 : "Fluid Flow Controller", + 0x07 : "General Purpose Discrete I/O", + 0x2B : "Generic Device, keyable", + 0x18 : "Human Machine Interface (HMI)", + 0x05 : "Inductive Proximity Switch", + 0x04 : "Limit Switch", + 0x2C : "Managed Ethernet Switch", + 0x1A : "Mass Flow Controller", + 0x27 : "Mass Flow Controller, Enhanced", + 0x03 : "Motor Overload Device", + 0x16 : "Motor Starter", + 0x06 : "Photoelectric Sensor", + 0x1B : "Pneumatic Valve(s)", + 0x10 : "Position Controller", + 0x1D : "Process Control Valve", + 0x0E : "Programmable Logic Controller", + 0x1E : "Residual Gas Analyzer", + 0x09 : "Resolver", + 0x20 : "RF Power Generator", + 0x2A : "Safety Analog I/O Device", + 0x2E : "Safety Drive Device", + 0x23 : "Safety Discrete I/O Device", + 0x17 : "Softstart Starter", + 0x1C : "Vacuum/Pressure Gauge" +} + + +ENCAPSULATION_COMMANDS = utils.merge_dicts({ + 0x0000: "NOP", #(may be sent only using TCP) + 0x0004: "ListServices", + 0x0005: "Reserved for legacy (RA)", + 0x0063: "ListIdentity", # (may be sent using either UDP or TCP) + 0x0064: "ListInterfaces", # optional (may be sent using either UDP or TCP) + 0x0065: "RegisterSession", # (may be sent only using TCP) + 0x0066: "UnRegisterSession", # (may be sent only using TCP) + 0x006F: "SendRRData", # (may be sent only using TCP) + 0x0070: "SendUnitData", # (may be sent only using TCP) + 0x0071: "Reserved for legacy (RA)", # + 0x0072: "IndicateStatus", # (may be sent only using TCP) + 0x0073: "Cancel", # optional (may be sent only using TCP) + }, + {k: "Reserved for legacy (RA)" for k in range(1,3+1)}, #0x0001: through 0x0003 "Reserved for legacy (RA) + {k: "Reserved for future expansion" for k in range(0x6, 0x0062 + 1)}, # 0x0006: through 0x0062 "Reserved for future expansion" + {k: "Reserved for legacy (RA)" for k in range(0x67, 0x6E + 1)}, # 0x0067: through 0x006E "Reserved for legacy (RA)" + {k: "Reserved for legacy (RA)" for k in range(0x74, 0x00C7 + 1)}, #0x0074: through 0x00C7 "Reserved for legacy (RA)" + {k: "Reserved for future expansion" for k in range(0xC8, 0xFFFF + 1)}, #0x00C8: through 0xFFFF "Reserved for future expansion" +) + + +# class ENIP_SequencedAddress(scapy_all.Packet): +# name = "ENIP_UDP_SequencedAddress" +# fields_desc = [ +# scapy_all.LEIntField("connection_id", 0), +# scapy_all.LEIntField("sequence", 0), +# ] + + +class ENIP_ConnectionAddress(scapy_all.Packet): + name = "ENIP_ConnectionAddress" + fields_desc = [scapy_all.LEIntField("connection_id", 0)] + + +class ENIP_ConnectionPacket(scapy_all.Packet): + name = "ENIP_ConnectionPacket" + fields_desc = [scapy_all.LEShortField("sequence", 0)] + + +class ENIP_SendUnitData(scapy_all.Packet): + """Data in ENIP header specific to the specified command""" + name = "ENIP_SendUnitData" + fields_desc = [ + scapy_all.LEIntField("interface_handle", 0), + scapy_all.LEShortField("timeout", 0), + scapy_all.PacketField("Encapsulated CPF packet", enip_cpf.ENIP_CPF(), enip_cpf.ENIP_CPF ), + # utils.LEShortLenField("count", None, count_of="items"), + # scapy_all.PacketListField("items", [], ENIP_SendUnitData_Item, + # count_from=lambda p: p.count), + ] + + +class ENIP_SendRRData(scapy_all.Packet): + name = "ENIP_SendRRData" + fields_desc = ENIP_SendUnitData.fields_desc + + +class ENIP_RegisterSession(scapy_all.Packet): + name = "ENIP_RegisterSession" + fields_desc = [ + scapy_all.LEShortField("protocol_version", 1), + scapy_all.LEShortField("options", 0), + ] + + +class ENIP_ListServices_TargetItem(scapy_all.Packet): + name="ENIP_ListServicesTarget_Item" + + fields_desc = [ + scapy_all.LEShortField("item_type_code", 0), + scapy_all.LEShortField("length", 0), + scapy_all.LEShortField("encapsulation_version", 1), + scapy_all.LEShortField("capability_flags", 0), + scapy_all.StrFixedLenField("name_of_service", "", 16), + ] + + def extract_padding(self, p): + return "", p + + +class ENIP_ListServices(scapy_all.Packet): + name = "ENIP_ListServices" + fields_desc = [ + utils.LEShortLenField("count", 1, count_of="TargetItems"), + scapy_all.PacketListField("TargetItems", [], ENIP_ListServices_TargetItem, count_from=lambda p: p.count), + ] + + +class ENIP_ListIdentity_SocketItem(scapy_all.Packet): + name="Socket_Address" + + fields_desc = [ + scapy_all.ShortField("sin_family", 0), + scapy_all.ShortField("sin_port", 0), + scapy_all.IPField("sin_address", "0.0.0.0"), + scapy_all.StrFixedLenField("name_of_service", "", 8), + ] + + def extract_padding(self, p): + # print self.__class__.__name__+": P="+str(p) + return "", p + + +class ENIP_DeviceRevision(scapy_all.Packet): + name = "ENIP_DeviceRevision" + fields_desc = [ + scapy_all.ByteField("Major", 0), + scapy_all.ByteField("Minor", 0), + ] + + def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + return "", p + + +class ENIP_ListIdentity_TargetItem(scapy_all.Packet): + name="ENIP_ListIdentity_TargetItem" + fields_desc = [ + scapy_all.LEShortField("item_type_code", 0), + scapy_all.LEShortField("length", 0), + scapy_all.LEShortField("encapsulation_version", 1), + scapy_all.PacketField("ListIdentityItems", ENIP_ListIdentity_SocketItem(), ENIP_ListIdentity_SocketItem), #, count_from=1), + scapy_all.LEShortField("vendor_ID", 0), + scapy_all.LEShortEnumField("device_type", 0x21, DEVICE_PROFILES), + scapy_all.LEShortField("product_code", 0), + scapy_all.PacketField("ENIP_DeviceRevision", ENIP_DeviceRevision(), ENIP_DeviceRevision), + scapy_all.XShortField("status", 0x0000), + utils.XLEIntField("serial", 0x00000000), + scapy_all.ByteField("product_name_length", 0), + scapy_all.StrLenField("product_name", "", length_from=lambda p: p.product_name_length), + scapy_all.XByteField("state", 0), + ] + + def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + return "", p + + +class ENIP_ListIdentity(scapy_all.Packet): + name = "ENIP_ListIdentity" + fields_desc = [ + utils.LEShortLenField("count", 1, count_of="IdentityItems"), + scapy_all.PacketListField("IdentityItems", [], ENIP_ListIdentity_TargetItem, count_from=lambda p: p.count), + ] + + + def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + return "", p + + +class ENIP_PACKET(scapy_all.Packet): + """Ethernet/IP packet over TCP""" + name = "ENIP_PACKET" + fields_desc = [ + scapy_all.LEShortEnumField("command_id", None, ENCAPSULATION_COMMANDS), + scapy_all.LEShortField("length", None), + utils.XLEIntField("session", 0), + scapy_all.LEIntEnumField("status", 0, {0: "success"}), + scapy_all.LELongField("sender_context", 0), + scapy_all.LEIntField("options", 0), + ] + + def extract_padding(self, p): + return p[:self.length], p[self.length:] + + def post_build(self, p, pay): + if self.length is None and pay: + l = len(pay) + p = p[:2] + struct.pack(" Date: Mon, 1 Aug 2016 19:31:18 -0400 Subject: [PATCH 07/29] Update cip.py --- cip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cip.py b/cip.py index 5e0dcc9..400a595 100644 --- a/cip.py +++ b/cip.py @@ -730,7 +730,7 @@ def post_build(self, p, pay): # scapy_all.bind_layers(enip.ENIP_ConnectionPacket, CIP) -scapy_all.bind_layersenip_cpf.CPF_DataItem, CIP, type_id=0x00b2) +scapy_all.bind_layers(enip_cpf.CPF_DataItem, CIP, type_id=0x00b2) scapy_all.bind_layers(CIP, CIP_RespAttributesAll, direction=1, service=0x01) scapy_all.bind_layers(CIP, CIP_ReqGetAttributeList, direction=0, service=0x03) From 98bcbe59e87bfae982dcadfa44fa7b217d6a6026 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 2 Aug 2016 16:05:49 -0400 Subject: [PATCH 08/29] Update enip_tcp.py --- enip_tcp.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/enip_tcp.py b/enip_tcp.py index dd4c194..9dcab03 100644 --- a/enip_tcp.py +++ b/enip_tcp.py @@ -49,14 +49,14 @@ assert pkt[enip.ENIP_PACKET].session == 0 assert pkt[enip.ENIP_PACKET].status == 0 assert pkt[enip.ENIP_PACKET].command_id == 0x70 - assert pkt[enip.ENIP_PACKET].length == 26 - assert pkt[enip.ENIP_SendUnitData].count == 2 - assert pkt[enip.ENIP_SendUnitData].items[0].type_id == 0x00a1 - assert pkt[enip.ENIP_SendUnitData].items[0].length == 4 - assert pkt[enip.ENIP_SendUnitData].items[0].payload == pkt[enip.ENIP_ConnectionAddress] - assert pkt[enip.ENIP_ConnectionAddress].connection_id == 1337 - assert pkt[enip.ENIP_SendUnitData].items[1].type_id == 0x00b1 - assert pkt[enip.ENIP_SendUnitData].items[1].length == 6 - assert pkt[enip.ENIP_SendUnitData].items[1].payload == pkt[enip.ENIP_ConnectionPacket] - assert pkt[enip.ENIP_ConnectionPacket].sequence == 4242 - assert pkt[enip.ENIP_ConnectionPacket].payload.load == 'test' + assert pkt[enip.ENIP_PACKET].length == 16 #26 + assert pkt[enip_cpf.ENIP_CPF].count == 2 + # assert pkt[enip.ENIP_SendUnitData].items[0].type_id == 0x00a1 + # assert pkt[enip.ENIP_SendUnitData].items[0].length == 4 + # assert pkt[enip.ENIP_SendUnitData].items[0].payload == pkt[enip.ENIP_ConnectionAddress] + # assert pkt[enip.ENIP_ConnectionAddress].connection_id == 1337 + # assert pkt[enip.ENIP_SendUnitData].items[1].type_id == 0x00b1 + # assert pkt[enip.ENIP_SendUnitData].items[1].length == 6 + # assert pkt[enip.ENIP_SendUnitData].items[1].payload == pkt[enip.ENIP_ConnectionPacket] + # assert pkt[enip.ENIP_ConnectionPacket].sequence == 4242 + # assert pkt[enip.ENIP_ConnectionPacket].payload.load == 'test' From cfa81190ab51f23f3e433b8cd5f00db67db32548 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 8 Aug 2016 15:29:56 -0400 Subject: [PATCH 09/29] Added field classes Added field classes and function to merge dictionaries for use with ITEM_ID lists in cip.py file --- utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils.py b/utils.py index 91a3d4d..dc1882b 100644 --- a/utils.py +++ b/utils.py @@ -22,6 +22,8 @@ """Useful routines and utilities which simplify code writing""" from scapy import all as scapy_all +# New function for merging dictionaries together that is +# particularly helpful when building large dictionary for ITEM_ID values - MED def merge_dicts(*dict_args):#from http://stackoverflow.com/a/26853961 ''' Given any number of dicts, shallow copy and merge into a new dict, @@ -65,6 +67,7 @@ def i2repr_one(self, pkt, x): return self.i2s[x] return scapy_all.lhex(x) +# Classes for new fields: Hex representation of little endian ints & shorts - MED class XLEIntField(scapy_all.LEIntField): """A Little Endian IntField with hexadecimal representation""" def i2repr(self, pkt, x): From 2bb05ccab12db4f3cc6c70a0d57b38382c39e39d Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 8 Aug 2016 15:35:19 -0400 Subject: [PATCH 10/29] Updated file references Updated file references to take into account reorganization of some code to new or different files. --- plc.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/plc.py b/plc.py index c42213b..24da107 100644 --- a/plc.py +++ b/plc.py @@ -29,8 +29,10 @@ from cip import CIP, CIP_Path, CIP_ReqConnectionManager, \ CIP_MultipleServicePacket, CIP_ReqForwardOpen, CIP_RespForwardOpen, \ CIP_ReqForwardClose, CIP_ReqGetAttributeList, CIP_ReqReadOtherTag +# Updated imports to reflect code changes in other files - MED from enip import ENIP_PACKET, ENIP_SendUnitData, ENIP_ConnectionAddress, \ ENIP_ConnectionPacket, ENIP_RegisterSession, ENIP_SendRRData +import enip_cpf # Global switch to make it easy to test without sending anything NO_NETWORK = False @@ -56,6 +58,7 @@ def __init__(self, plc_addr, plc_port=44818): self.sequence = 1 # Open an Ethernet/IP session + # Updated to reflect location and name of classes - MED sessionpkt = ENIP_PACKET() / ENIP_RegisterSession() if self.sock is not None: self.sock.send(str(sessionpkt)) @@ -68,10 +71,12 @@ def connected(self): def send_rr_cip(self, cippkt): """Send a CIP packet over the TCP connection as an ENIP Req/Rep Data""" + # Updated to reflect location and name of classes - MED enippkt = ENIP_PACKET(session=self.session_id) enippkt /= ENIP_SendRRData(items=[ - ENIP_SendUnitData_Item(type_id=0), - ENIP_SendUnitData_Item() / cippkt + #Updated to reflect code changes - MED + enip_cpf.CPF_AddressDataItem(type_id=0), + enip_cpf.CPF_DataItem() / cippkt ]) if self.sock is not None: self.sock.send(str(enippkt)) @@ -92,10 +97,12 @@ def send_rr_mr_cip(self, cippkt): def send_unit_cip(self, cippkt): """Send a CIP packet over the TCP connection as an ENIP Unit Data""" + # Updated to reflect location and name of classes - MED enippkt = ENIP_PACKET(session=self.session_id) enippkt /= ENIP_SendUnitData(items=[ - ENIP_SendUnitData_Item() / ENIP_ConnectionAddress(connection_id=self.enip_connid), - ENIP_SendUnitData_Item() / ENIP_ConnectionPacket(sequence=self.sequence) / cippkt + # Updated to reflect code changes - MED + enip_cpf.CPF_AddressDataItem() / ENIP_ConnectionAddress(connection_id=self.enip_connid), + enip_cpf.CPF_DataItem() / ENIP_ConnectionPacket(sequence=self.sequence) / cippkt ]) self.sequence += 1 if self.sock is not None: @@ -106,6 +113,7 @@ def recv_enippkt(self): if self.sock is None: return pktbytes = self.sock.recv(2000) + # Updated to reflect location and name of classes - MED pkt = ENIP_PACKET(pktbytes) return pkt From 146ce5eb161e781a7f855b4b1ece32f0d8dfeef4 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 8 Aug 2016 15:49:07 -0400 Subject: [PATCH 11/29] Additional classes for CIP Packet Dissection Added more CIP packet classes and more scapy_all.bind_layers() statements. Corrected issue of malformed packet creation when communicating with PLC. Modified field types in CIP_ReqForwardOpen for a more favorable display. --- cip.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/cip.py b/cip.py index 400a595..c910cf9 100644 --- a/cip.py +++ b/cip.py @@ -139,6 +139,11 @@ class CIP_ReqReadOtherTag(scapy_all.Packet): scapy_all.LEShortField("length", None), ] +# Added classes to deal with CIP Logical and CIP Port segments, as well +# as CIP path padding +# New Classes: +# CIP_PortSegment, CIP_LogicalSegment, CIP_PathPadded - MED + class CIP_PortSegment(scapy_all.Packet): name="CIP_PortSegment" @@ -287,6 +292,7 @@ def extract_padding(self, p): # return p[:self.length], p[self.length:] return (q,w,),(q,8-w,) +# Additional bind_layers scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=0) #Port Segment scapy_all.bind_layers(CIP_PathPadded, CIP_LogicalSegment, segment_type=1) #Port Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=2) #Port Segment @@ -305,7 +311,7 @@ class CIP_PathField(scapy_all.StrLenField): 4: "attribute", # 0x30 = 8-bit attribute ID, 0x31 = 16-bit attribute ID } KNOWN_CLASSES = { - 0x01: "Idendity", + 0x01: "Identity", 0x02: "Message Router", 0x06: "Connection Manager", 0x6b: "Symbol", @@ -523,7 +529,7 @@ class CIP(scapy_all.Packet): 0x4f: "Read_Other_Tag_Service", # ??? 0x52: "Read_Tag_Fragmented_Service", 0x53: "Write_Tag_Fragmented_Service", - 0x54: "Forward_Open?", + 0x54: "Forward_Open", } fields_desc = [ @@ -605,8 +611,10 @@ def pre_dissect(self, s): def do_build(self): # p = '' + # Corrected issue of malformed packet construction when attempting to + # communicate with PLC - MED p = super(CIP_ConnectionParam, self).do_build() - return p + return p[::-1] # we have to flip the output def extract_padding(self, s): return '', s @@ -618,7 +626,7 @@ class CIP_ReqForwardOpen(scapy_all.Packet): SEGMENT_TYPE = { 0x00: "Port Segment", - 0x01: "Logical Segmentc", + 0x01: "Logical Segment", 0x02: "Network Segment", 0x03: "Symbolic Segment", 0x04: "Data Segment", @@ -628,6 +636,8 @@ class CIP_ReqForwardOpen(scapy_all.Packet): } fields_desc = [ + # Updated a few field descriptions to adjust how they are displayed + # Altered fields begin with utils. rather than scapy_all. - MED scapy_all.BitField("priority", 0, 4), scapy_all.BitField("tick_time", 0, 4), scapy_all.ByteField("timeout_ticks", 249), @@ -643,8 +653,10 @@ class CIP_ReqForwardOpen(scapy_all.Packet): utils.XLEIntField("TO_rpi", 0x007a1200), scapy_all.PacketField('TO_connection_param', CIP_ConnectionParam(), CIP_ConnectionParam), scapy_all.XByteField("transport_type", 0xa3), # direction server, application object, class 3 + # Changed name - MED scapy_all.ByteField("Connection_Path_Size", None), #The number of 16 bit words in the Connection_Path field. # scapy_all.PacketListField("path_segment_items", [], CIP_Path1, length_from=lambda p: 2 * p.Connection_Path_Size), + # Modified Implementation - MED scapy_all.PacketListField("path_segment_items", [], CIP_PathPadded, length_from=lambda p: 6), # CIP_PathField("path", None, length_from=lambda p: 2 * p.path_wordsize), ] @@ -728,7 +740,7 @@ def post_build(self, p, pay): p = p[:-4] + b"\0" + p[-4:] return p + pay - +# Updated to fit with new code organization # scapy_all.bind_layers(enip.ENIP_ConnectionPacket, CIP) scapy_all.bind_layers(enip_cpf.CPF_DataItem, CIP, type_id=0x00b2) From 9dc516c13bcd79750440da94b1a0e5339a7a6937 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 8 Aug 2016 16:05:15 -0400 Subject: [PATCH 12/29] Expanded Item ID Numbers Expanded Item ID numbers and changed CPF_Address_Item into CPF_Item and created separate classes for Address and Data Items. --- enip_cpf.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/enip_cpf.py b/enip_cpf.py index fef8eac..14cf6d9 100644 --- a/enip_cpf.py +++ b/enip_cpf.py @@ -63,7 +63,7 @@ class CPF_SequencedAddressItem(scapy_all.Packet): scapy_all.LEIntField("sequence_number", 0), ] - +# Renamed so that Address Item and Data Item have different names - MED class CPF_Item(scapy_all.Packet): name = "CPF_Item" fields_desc = [ @@ -83,7 +83,6 @@ def post_build(self, p, pay): class CPF_AddressDataItem(CPF_Item): name = "CPF_AddressDataItem" - class CPF_DataItem(CPF_Item): name = "CPF_DataItem" @@ -91,8 +90,10 @@ class ENIP_CPF(scapy_all.Packet): name = "ENIP_CPF" fields_desc = [ utils.LEShortLenField("count", 2, count_of="items"), - scapy_all.PacketField("Address Item", CPF_AddressDataItem('', type_id=0x0, length=0), CPF_AddressDataItem), - scapy_all.PacketField("Data Item", CPF_DataItem('', type_id=0x0, length=0), CPF_DataItem), + # Changed implementation to reflect use of CIP_Item above + scapy_all.PacketListField("items", [], CPF_Item, + count_from=lambda p: p.count), + # Due to potential 'optional' packet components at end in protocol scapy_all.ConditionalField( scapy_all.PacketListField("optional_items", None, CPF_Item, count_from=lambda p: p.count-2), lambda p: p.count>2 @@ -102,5 +103,7 @@ class ENIP_CPF(scapy_all.Packet): def extract_padding(self, p): return '', p - +# Added additional binds - MED scapy_all.bind_layers(CPF_AddressDataItem, CPF_SequencedAddressItem, type_id=0x8002) +scapy_all.bind_layers(CPF_Item, CPF_SequencedAddressItem, type_id=0x8002) +scapy_all.bind_layers(ENIP_CPF, CPF_DataItem, type_id=0x8002) From e26b9a684ede09b44e0ee1a66cfedc4513bff014 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 8 Aug 2016 16:15:36 -0400 Subject: [PATCH 13/29] Relocated most ENIP classes to file enip.py Also updated tests in '__main__' accordingly --- enip_tcp.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/enip_tcp.py b/enip_tcp.py index 9dcab03..c930219 100644 --- a/enip_tcp.py +++ b/enip_tcp.py @@ -27,8 +27,15 @@ import enip import enip_cpf +# Moved all ENIP classes to file enip.py --> Keeps all EtherNet/IP Level +# processing in the same file rather than splitting based on upper layer - MED + scapy_all.bind_layers(scapy_all.TCP, enip.ENIP_PACKET, dport=44818) scapy_all.bind_layers(scapy_all.TCP, enip.ENIP_PACKET, sport=44818) +scapy_all.bind_layers(enip_cpf.CPF_AddressDataItem, enip.ENIP_ConnectionAddress, type_id=0xA1) +scapy_all.bind_layers(enip_cpf.CPF_DataItem, enip.ENIP_ConnectionPacket, type_id=0xB1) +scapy_all.bind_layers(enip_cpf.CPF_Item, enip.ENIP_ConnectionAddress, type_id=0x00A1) +scapy_all.bind_layers(enip_cpf.CPF_Item, enip.ENIP_ConnectionPacket, type_id=0x00B1) if __name__ == '__main__': @@ -37,26 +44,30 @@ pkt = scapy_all.Ether(src='01:23:45:67:89:ab', dst='ba:98:76:54:32:10') pkt /= scapy_all.IP(src='192.168.1.1', dst='192.168.1.42') pkt /= scapy_all.TCP(sport=10000, dport=44818) + # Modified to reflect changes in code names and locations - MED pkt /= ENIP_PACKET() - pkt /= ENIP_SendUnitData() + pkt /= enip.ENIP_SendUnitData(Encapsulated_CPF_packet = enip_cpf.ENIP_CPF(items=[ + enip_cpf.CPF_AddressDataItem() / enip.ENIP_ConnectionAddress(connection_id=1337), + enip_cpf.CPF_DataItem() / enip.ENIP_ConnectionPacket(sequence=4242) / scapy_all.Raw(load='test'), + ])) # Build! data = str(pkt) pkt = scapy_all.Ether(data) pkt.show() - # Test the value of some fields + # Test the value of some fields; new test setup due to moving classes - MED assert pkt[enip.ENIP_PACKET].session == 0 assert pkt[enip.ENIP_PACKET].status == 0 assert pkt[enip.ENIP_PACKET].command_id == 0x70 - assert pkt[enip.ENIP_PACKET].length == 16 #26 + assert pkt[enip.ENIP_PACKET].length == 26 assert pkt[enip_cpf.ENIP_CPF].count == 2 - # assert pkt[enip.ENIP_SendUnitData].items[0].type_id == 0x00a1 - # assert pkt[enip.ENIP_SendUnitData].items[0].length == 4 - # assert pkt[enip.ENIP_SendUnitData].items[0].payload == pkt[enip.ENIP_ConnectionAddress] - # assert pkt[enip.ENIP_ConnectionAddress].connection_id == 1337 - # assert pkt[enip.ENIP_SendUnitData].items[1].type_id == 0x00b1 - # assert pkt[enip.ENIP_SendUnitData].items[1].length == 6 - # assert pkt[enip.ENIP_SendUnitData].items[1].payload == pkt[enip.ENIP_ConnectionPacket] - # assert pkt[enip.ENIP_ConnectionPacket].sequence == 4242 - # assert pkt[enip.ENIP_ConnectionPacket].payload.load == 'test' + assert pkt[enip_cpf.ENIP_CPF].items[0].type_id == 0x00a1 + assert pkt[enip_cpf.ENIP_CPF].items[0].length == 4 + assert pkt[enip_cpf.ENIP_CPF].items[0].payload == pkt[enip.ENIP_ConnectionAddress] + assert pkt[enip.ENIP_ConnectionAddress].connection_id == 1337 + assert pkt[enip_cpf.ENIP_CPF].items[1].type_id == 0x00b1 + assert pkt[enip_cpf.ENIP_CPF].items[1].length == 6 + assert pkt[enip_cpf.ENIP_CPF].items[1].payload == pkt[enip.ENIP_ConnectionPacket] + assert pkt[enip.ENIP_ConnectionPacket].sequence == 4242 + assert pkt[enip.ENIP_ConnectionPacket].payload.load == 'test' From 4341b9edce15714e5dcad8c769b090761b50ec4e Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 8 Aug 2016 16:22:21 -0400 Subject: [PATCH 14/29] Relocated most ENIP classes to file enip.py Added more scapy_all.bind_layers() statements and altered code and '__main__' to reflect relocation. --- enip_udp.py | 77 +++++++++++++++++++---------------------------------- 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/enip_udp.py b/enip_udp.py index b15c705..c49d7ed 100644 --- a/enip_udp.py +++ b/enip_udp.py @@ -28,6 +28,9 @@ from scapy import all as scapy_all +import enip +import enip_cpf + import utils # Keep-alive sequences @@ -39,49 +42,24 @@ b'\xff\xff\xff\xff\x00\x00\x00\x00') -class ENIP_UDP_SequencedAddress(scapy_all.Packet): - name = "ENIP_UDP_SequencedAddress" - fields_desc = [ - scapy_all.LEIntField("connection_id", 0), - scapy_all.LEIntField("sequence", 0), - ] - - -class ENIP_UDP_Item(scapy_all.Packet): - name = "ENIP_UDP_Item" - fields_desc = [ - scapy_all.LEShortEnumField("type_id", 0, { - 0x00b1: "Connected_Data_Item", - 0x8002: "Sequenced_Address", - }), - scapy_all.LEShortField("length", None), - ] +# Moved ENIP_UDP_SequencedAddress() to enip_cpf file with new name CPF_SequencedAddressItem - def extract_padding(self, p): - return p[:self.length], p[self.length:] +# Moved ENIP_UDP_ITEM into enip_cpf file as new class CPF_ITEM - def post_build(self, p, pay): - if self.length is None and pay: - l = len(pay) - p = p[:2] + struct.pack(" Keeps all EtherNet/IP Level +# processing in the same file rather than splitting based on upper layer - MED scapy_all.bind_layers(scapy_all.UDP, ENIP_UDP, sport=2222, dport=2222) scapy_all.bind_layers(ENIP_UDP_Item, ENIP_UDP_SequencedAddress, type_id=0x8002) +scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, sport=44818) + +scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_RegisterSession, command_id=0x0065) +scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_SendRRData, command_id=0x006f) +scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_SendUnitData, command_id=0x0070) +scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_ListServices, command_id=0x0004) +scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_ListIdentity, command_id=0x0063) if __name__ == '__main__': # Test building/dissecting packets @@ -89,9 +67,10 @@ def extract_padding(self, p): pkt = scapy_all.Ether(src='00:1d:9c:c8:13:37', dst='01:00:5e:40:12:34') pkt /= scapy_all.IP(src='192.168.1.42', dst='239.192.18.52') pkt /= scapy_all.UDP(sport=2222, dport=2222) - pkt /= ENIP_UDP(items=[ - ENIP_UDP_Item() / ENIP_UDP_SequencedAddress(connection_id=1337, sequence=42), - ENIP_UDP_Item(type_id=0x00b1) / scapy_all.Raw(load=ENIP_UDP_KEEPALIVE), + # Updated this section to reflect code modifications and moving of classes - MED + pkt /= enip.ENIP_UDP(items = [ + enip_cpf.CPF_AddressDataItem() / enip_cpf.CPF_SequencedAddressItem(connection_id=1337, sequence_number=42), + enip_cpf.CPF_DataItem(type_id=0x00b1) / scapy_all.Raw(load=ENIP_UDP_KEEPALIVE), ]) # Build! @@ -99,13 +78,13 @@ def extract_padding(self, p): pkt = scapy_all.Ether(data) pkt.show() - # Test the value of some fields - assert pkt[ENIP_UDP].count == 2 - assert pkt[ENIP_UDP].items[0].type_id == 0x8002 - assert pkt[ENIP_UDP].items[0].length == 8 - assert pkt[ENIP_UDP].items[0].payload == pkt[ENIP_UDP_SequencedAddress] - assert pkt[ENIP_UDP_SequencedAddress].connection_id == 1337 - assert pkt[ENIP_UDP_SequencedAddress].sequence == 42 - assert pkt[ENIP_UDP].items[1].type_id == 0x00b1 - assert pkt[ENIP_UDP].items[1].length == 38 - assert pkt[ENIP_UDP].items[1].payload.load == ENIP_UDP_KEEPALIVE + # Test the value of some fields; new test setup due to moving classes - MED + assert pkt[enip.ENIP_UDP].count == 2 + assert pkt[enip.ENIP_UDP].items[0].type_id == 0x8002 + assert pkt[enip.ENIP_UDP].items[0].length == 8 + assert pkt[enip_cpf.CPF_SequencedAddressItem].connection_id == 1337 + assert pkt[enip_cpf.CPF_SequencedAddressItem].sequence_number == 42 + assert pkt[enip.ENIP_UDP].items[0].payload == pkt[enip_cpf.CPF_SequencedAddressItem] + assert pkt[enip.ENIP_UDP].items[1].type_id == 0x00b1 + assert pkt[enip.ENIP_UDP].items[1].length == 38 + assert pkt[enip.ENIP_UDP].items[1].payload.load == ENIP_UDP_KEEPALIVE From d14ed5dd82b3d2aa33ab35f4dc1affca197a808f Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 8 Aug 2016 16:32:00 -0400 Subject: [PATCH 15/29] New file, compilation of ENIP related classes Added dictionary of device profiles IDs for EtherNet/IP devices --- enip.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/enip.py b/enip.py index c545b3f..9c655c5 100644 --- a/enip.py +++ b/enip.py @@ -43,7 +43,6 @@ 0x1F : "DC Power Generator", 0xC8 : "Embedded Component", 0x22 : "Encoder", - 0x27 : "Enhanced Mass Flow Controller", 0x24 : "Fluid Flow Controller", 0x07 : "General Purpose Discrete I/O", 0x2B : "Generic Device, keyable", @@ -117,7 +116,7 @@ class ENIP_SendUnitData(scapy_all.Packet): fields_desc = [ scapy_all.LEIntField("interface_handle", 0), scapy_all.LEShortField("timeout", 0), - scapy_all.PacketField("Encapsulated CPF packet", enip_cpf.ENIP_CPF(), enip_cpf.ENIP_CPF ), + scapy_all.PacketField("Encapsulated_CPF_packet", enip_cpf.ENIP_CPF(), enip_cpf.ENIP_CPF ), # utils.LEShortLenField("count", None, count_of="items"), # scapy_all.PacketListField("items", [], ENIP_SendUnitData_Item, # count_from=lambda p: p.count), @@ -217,12 +216,12 @@ class ENIP_ListIdentity(scapy_all.Packet): scapy_all.PacketListField("IdentityItems", [], ENIP_ListIdentity_TargetItem, count_from=lambda p: p.count), ] - def extract_padding(self, p): # print self.__class__.__name__ + ": P=" + str(p) return "", p +# Replaces ENIP_TCP; same structure, different name class ENIP_PACKET(scapy_all.Packet): """Ethernet/IP packet over TCP""" name = "ENIP_PACKET" @@ -244,10 +243,24 @@ def post_build(self, p, pay): p = p[:2] + struct.pack(" Date: Mon, 8 Aug 2016 17:13:05 -0400 Subject: [PATCH 16/29] Relocated most ENIP classes to file enip.py Updated code reference to reflect relocation of code --- enip_tcp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enip_tcp.py b/enip_tcp.py index c930219..60c4c3b 100644 --- a/enip_tcp.py +++ b/enip_tcp.py @@ -45,7 +45,7 @@ pkt /= scapy_all.IP(src='192.168.1.1', dst='192.168.1.42') pkt /= scapy_all.TCP(sport=10000, dport=44818) # Modified to reflect changes in code names and locations - MED - pkt /= ENIP_PACKET() + pkt /= enip.ENIP_PACKET() pkt /= enip.ENIP_SendUnitData(Encapsulated_CPF_packet = enip_cpf.ENIP_CPF(items=[ enip_cpf.CPF_AddressDataItem() / enip.ENIP_ConnectionAddress(connection_id=1337), enip_cpf.CPF_DataItem() / enip.ENIP_ConnectionPacket(sequence=4242) / scapy_all.Raw(load='test'), From 34d8168261f9bebbde22d3e643cdc5ede625847b Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 19 Oct 2016 13:43:34 -0400 Subject: [PATCH 17/29] Corrected bind-layers issue with CPF Data Item --- cip.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cip.py b/cip.py index c910cf9..53766fa 100644 --- a/cip.py +++ b/cip.py @@ -67,7 +67,7 @@ class CIP_RespAttributesList(scapy_all.Packet): def split_guess(self, attr_list, verbose=False): """Split the content of the Get_Attribute_List response with the known attribute list - Return a list of (attr, value) tuples, or None if an error occured + Return a list of (attr, value) tuples, or None if an error occurred """ content = self.content offset = 0 @@ -294,7 +294,7 @@ def extract_padding(self, p): # Additional bind_layers scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=0) #Port Segment -scapy_all.bind_layers(CIP_PathPadded, CIP_LogicalSegment, segment_type=1) #Port Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_LogicalSegment, segment_type=1) #Logical Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=2) #Port Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=3) #Port Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=4) #Port Segment @@ -742,7 +742,8 @@ def post_build(self, p, pay): # Updated to fit with new code organization # scapy_all.bind_layers(enip.ENIP_ConnectionPacket, CIP) -scapy_all.bind_layers(enip_cpf.CPF_DataItem, CIP, type_id=0x00b2) +scapy_all.bind_layers(enip_cpf.CPF_DataItem, CIP, type_id=0x00b2) #0x00B2 : "Unconnected Data Item" +scapy_all.bind_layers(enip_cpf.CPF_Item, CIP, type_id=0x00b2) #0x00B2 : "Unconnected Data Item" scapy_all.bind_layers(CIP, CIP_RespAttributesAll, direction=1, service=0x01) scapy_all.bind_layers(CIP, CIP_ReqGetAttributeList, direction=0, service=0x03) From a4f2d2e6972a5d699bd4c3afbefdce9ea44456ad Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 19 Oct 2016 13:47:21 -0400 Subject: [PATCH 18/29] Corrected bind-layers issue with CPF Data Item --- enip_cpf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/enip_cpf.py b/enip_cpf.py index 14cf6d9..71e47a7 100644 --- a/enip_cpf.py +++ b/enip_cpf.py @@ -107,3 +107,4 @@ def extract_padding(self, p): scapy_all.bind_layers(CPF_AddressDataItem, CPF_SequencedAddressItem, type_id=0x8002) scapy_all.bind_layers(CPF_Item, CPF_SequencedAddressItem, type_id=0x8002) scapy_all.bind_layers(ENIP_CPF, CPF_DataItem, type_id=0x8002) +scapy_all.bind_layers(CPF_DataItem, CPF_SequencedAddressItem, type_id=0x00B2) From a7701c0c4bdff4b0b887ec03c363fb5b5746b0a3 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 19 Oct 2016 13:55:19 -0400 Subject: [PATCH 19/29] Fixed binding for ENIP UDP on testbed --- enip_udp.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/enip_udp.py b/enip_udp.py index c49d7ed..9e25c05 100644 --- a/enip_udp.py +++ b/enip_udp.py @@ -50,10 +50,14 @@ # ENIP_UDP moved to new file enip.py --> Keeps all EtherNet/IP Level # processing in the same file rather than splitting based on upper layer - MED - -scapy_all.bind_layers(scapy_all.UDP, ENIP_UDP, sport=2222, dport=2222) -scapy_all.bind_layers(ENIP_UDP_Item, ENIP_UDP_SequencedAddress, type_id=0x8002) -scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, sport=44818) +scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_PACKET, sport=2222, dport=2222) +scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_PACKET, dport=44818) +scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_PACKET, sport=44818) + +# Added additional binding options for ENIP_UDP - MED; needed for scy-phy test case +# scapy_all.bind_layers(scapy_all.UDP, ENIP_UDP, sport=2222, dport=2222) +# scapy_all.bind_layers(ENIP_UDP_Item, ENIP_UDP_SequencedAddress, type_id=0x8002) +# scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, sport=44818) scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_RegisterSession, command_id=0x0065) scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_SendRRData, command_id=0x006f) From 8672c267c3aac8aec3369d977b8b60d166930851 Mon Sep 17 00:00:00 2001 From: m4tthew-d Date: Mon, 23 Jan 2017 13:21:31 -0500 Subject: [PATCH 20/29] 1/23/17 - Updated CIP with Electronic Key Segment handling --- cip.py | 107 ++++++++++++++++++++++++++++++++++------------------ enip.py | 2 +- enip_cpf.py | 8 +++- enip_tcp.py | 17 +++++++++ enip_udp.py | 14 +++++-- plc.py | 28 +++++++++++++- trial.py | 60 +++++++++++++++++++++++++++++ utils.py | 2 +- 8 files changed, 193 insertions(+), 45 deletions(-) create mode 100644 trial.py diff --git a/cip.py b/cip.py index 53766fa..d2d8ec0 100644 --- a/cip.py +++ b/cip.py @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """Common Industrial Protocol dissector - Documentation: * http://literature.rockwellautomation.com/idc/groups/literature/documents/pm/1756-pm020_-en-p.pdf @@ -169,13 +168,29 @@ class CIP_PortSegment(scapy_all.Packet): ) ] + # def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + # return "", p + +class CIP_Logical_SpecialSegment(scapy_all.Packet): + name="Electronic_Key_Segment" + + fields_desc = [ + scapy_all.ByteField("key_format", 4), + scapy_all.LEShortField("vendor_id", 0), + scapy_all.LEShortField("device_type", 0), + utils.XLEShortField("product_code", 0), + scapy_all.BitField("compatibility", 0, 1), + scapy_all.BitField("major_revision", 0, 7), + scapy_all.ByteField("minor_revision", 0), + ] + def extract_padding(self, p): print self.__class__.__name__ + ": P=" + str(p) - return "", p - + return None, p -class CIP_LogicalSegment(scapy_all.Packet): - name="CIP_LogicalSegment" +class CIP_LogicalSegmentPadded(scapy_all.Packet): + name="CIP_LogicalSegmentPadded" LOGICAL_TYPE = { 0: "Class ID", @@ -198,34 +213,36 @@ class CIP_LogicalSegment(scapy_all.Packet): fields_desc = [ scapy_all.BitEnumField("logical_type", 0, 3, LOGICAL_TYPE), - scapy_all.BitEnumField("logical_format", 0, 2, LOGICAL_FORMAT), + scapy_all.ConditionalField(scapy_all.BitEnumField("logical_format", 0, 2, LOGICAL_FORMAT), lambda p: p.logical_type < 5), + scapy_all.ConditionalField(scapy_all.BitField("unused", 0, 2), lambda p: p.logical_type >= 5), # When the logical segment is included within a Padded Path, the 16 - bit and 32 - bit # logical formats shall have a pad inserted between the segment type byte and the Logical Value # (the 8 - bit format is identical to the Packed Path).The pad byte shall be set to zero. - scapy_all.ConditionalField(scapy_all.ByteField("padding", 0), lambda p: p.logical_format > 0x0), + scapy_all.ConditionalField(scapy_all.ByteField("padding", 0), lambda p: p.logical_format > 0x0),### TODO This conditional field might depend on string size too. # scapy_all.ByteField("value", 0), scapy_all.ConditionalField( scapy_all.ByteField("value8bit", 0), lambda p: - p.logical_format >= 0x0 + p.logical_format == 0x0 ), scapy_all.ConditionalField( scapy_all.LEShortField("value16bit", 0), lambda p: - p.logical_format >= 0x1 + p.logical_format == 0x1 ), scapy_all.ConditionalField( scapy_all.LELongField("value32bit", 0), lambda p: - p.logical_format >= 0x2 + p.logical_format == 0x2 ), ] - def extract_padding(self, p): - # print self.__class__.__name__ + ": P=" + str(p) - return "", p + # def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + # return '',p + +scapy_all.bind_layers(CIP_LogicalSegmentPadded, CIP_Logical_SpecialSegment, logical_type=5) # Logical Key Segment - # # # @@ -257,9 +274,9 @@ def extract_padding(self, p): # # print self.__class__.__name__ + ": P=" + str(p) # return (), p # -# scapy_all.bind_layers(CIP_LogicalSegment, A_8bitValue, logical_format=0) -# scapy_all.bind_layers(CIP_LogicalSegment, A_16bitValue, logical_format=1) -# scapy_all.bind_layers(CIP_LogicalSegment, A_32bitValue, logical_format=2) +# scapy_all.bind_layers(CIP_LogicalSegmentPadded, A_8bitValue, logical_format=0) +# scapy_all.bind_layers(CIP_LogicalSegmentPadded, A_16bitValue, logical_format=1) +# scapy_all.bind_layers(CIP_LogicalSegmentPadded, A_32bitValue, logical_format=2) SEGMENT_TYPE={ 0:"Port Segment", # 0 0 0 Port Segment @@ -283,18 +300,23 @@ class CIP_PathPadded(scapy_all.Packet): # scapy_all.LEShortField("length", None), ] - def extract_padding(self, p): - print self.__class__.__name__+": P="+str(p) - # print self.__class__.__name__+": Length="+str(self.length) - q,w=p - # return "",(q,8-w,) - # return (w,q[:2]), (q, w,) - # return p[:self.length], p[self.length:] - return (q,w,),(q,8-w,) + # def extract_padding(self, p): + # print self.__class__.__name__+": P="+str(p) + # # print self.__class__.__name__+": Length="+str(self.length) + # q,w=p + # # return "",(q,8-w,) + # # return (w,q[:2]), (q, w,) + # # return p[:self.length], p[self.length:] + # return (q,w,),(q,8-w,) + + # def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + # return '', p + # return p, None # Additional bind_layers scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=0) #Port Segment -scapy_all.bind_layers(CIP_PathPadded, CIP_LogicalSegment, segment_type=1) #Logical Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_LogicalSegmentPadded, segment_type=1) #Logical Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=2) #Port Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=3) #Port Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=4) #Port Segment @@ -379,6 +401,7 @@ class CIP_Path(scapy_all.Packet): ] def extract_padding(self, p): + print self.__class__.__name__ + ": P=" + str(p) return "", p @classmethod @@ -432,7 +455,7 @@ class CIP_ResponseStatus(scapy_all.Packet): scapy_all.XByteField("reserved", 0), # Reserved byte, always null scapy_all.ByteEnumField("status", 0, {0: "success"}), scapy_all.XByteField("additional_size", 0), - scapy_all.StrLenField("additional", "", # additionnal status + scapy_all.StrLenField("additional", "", # additional status length_from=lambda p: 2 * p.additional_size), ] @@ -485,6 +508,7 @@ class CIP_ResponseStatus(scapy_all.Packet): } def extract_padding(self, p): + print self.__class__.__name__ + ": P=" + str(p) return "", p def __repr__(self): @@ -616,14 +640,15 @@ def do_build(self): p = super(CIP_ConnectionParam, self).do_build() return p[::-1] # we have to flip the output - def extract_padding(self, s): - return '', s + def extract_padding(self, p): + print self.__class__.__name__ + ": P=" + str(p) + return '', p class CIP_ReqForwardOpen(scapy_all.Packet): """Forward Open request""" name = "CIP_ReqForwardOpen" - + SEGMENT_TYPE = { 0x00: "Port Segment", 0x01: "Logical Segment", @@ -634,10 +659,9 @@ class CIP_ReqForwardOpen(scapy_all.Packet): 0x06: "Data Type (elementary)", 0x07: "Reserved for future use", } - + # Updated a few field descriptions to adjust how they are displayed + # Altered fields start with utils. rather than scapy_all. - MED fields_desc = [ - # Updated a few field descriptions to adjust how they are displayed - # Altered fields begin with utils. rather than scapy_all. - MED scapy_all.BitField("priority", 0, 4), scapy_all.BitField("tick_time", 0, 4), scapy_all.ByteField("timeout_ticks", 249), @@ -655,12 +679,21 @@ class CIP_ReqForwardOpen(scapy_all.Packet): scapy_all.XByteField("transport_type", 0xa3), # direction server, application object, class 3 # Changed name - MED scapy_all.ByteField("Connection_Path_Size", None), #The number of 16 bit words in the Connection_Path field. - # scapy_all.PacketListField("path_segment_items", [], CIP_Path1, length_from=lambda p: 2 * p.Connection_Path_Size), + scapy_all.PacketListField("path_segment_items", [], CIP_PathPadded, length_from=lambda p: 2 * p.Connection_Path_Size), # Modified Implementation - MED - scapy_all.PacketListField("path_segment_items", [], CIP_PathPadded, length_from=lambda p: 6), - # CIP_PathField("path", None, length_from=lambda p: 2 * p.path_wordsize), + # scapy_all.PacketListField("path_segment_items", [], CIP_PathPadded, + # length_from=lambda p: 6), + # scapy_all.PacketListField("path_segment_items", [], CIP_PathPadded, + # length_from=lambda p: 10), + # scapy_all.PacketField("path", CIP_Path1(), CIP_Path1) + #CIP_PathField("path", None, length_from=lambda p: 2 * p.Connection_Path_Size), ] + # MED 1/23/17 + # def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + # return '', p + class CIP_RespForwardOpen(scapy_all.Packet): """Forward Open response""" @@ -801,4 +834,4 @@ def post_build(self, p, pay): assert pkt[CIP_MultipleServicePacket].packets[0].service == 0x03 assert pkt[CIP_MultipleServicePacket].packets[0].payload.count == 2 assert pkt[CIP_MultipleServicePacket].packets[0].payload.attrs == [1, 2] - assert pkt[CIP_MultipleServicePacket].packets[1].service == 0x0e + assert pkt[CIP_MultipleServicePacket].packets[1].service == 0x0e \ No newline at end of file diff --git a/enip.py b/enip.py index 9c655c5..50e459b 100644 --- a/enip.py +++ b/enip.py @@ -263,4 +263,4 @@ def extract_padding(self, p): scapy_all.bind_layers(ENIP_PACKET, ENIP_SendUnitData, command_id=0x0070) scapy_all.bind_layers(ENIP_PACKET, ENIP_ListIdentity, command_id=0x0063) # scapy_all.bind_layers(ENIP_SendUnitData_Item, ENIP_ConnectionAddress, type_id=0x00a1) -# scapy_all.bind_layers(ENIP_SendUnitData_Item, ENIP_ConnectionPacket, type_id=0x00b1) +# scapy_all.bind_layers(ENIP_SendUnitData_Item, ENIP_ConnectionPacket, type_id=0x00b1) \ No newline at end of file diff --git a/enip_cpf.py b/enip_cpf.py index 71e47a7..1e64ad1 100644 --- a/enip_cpf.py +++ b/enip_cpf.py @@ -93,6 +93,12 @@ class ENIP_CPF(scapy_all.Packet): # Changed implementation to reflect use of CIP_Item above scapy_all.PacketListField("items", [], CPF_Item, count_from=lambda p: p.count), + + # scapy_all.PacketField("Address_Item", CPF_AddressDataItem('', type_id=0x0, length=0), CPF_AddressDataItem), + # scapy_all.PacketField("Data_Item", CPF_DataItem('', type_id=0x0, length=0), CPF_DataItem), + # scapy_all.PacketField("Address_Item", CPF_AddressDataItem('', type_id=0xA1, length=None), CPF_AddressDataItem), + # scapy_all.PacketField("Data_Item", CPF_DataItem('', type_id=0xB1, length=None), CPF_DataItem), + # Due to potential 'optional' packet components at end in protocol scapy_all.ConditionalField( scapy_all.PacketListField("optional_items", None, CPF_Item, count_from=lambda p: p.count-2), @@ -107,4 +113,4 @@ def extract_padding(self, p): scapy_all.bind_layers(CPF_AddressDataItem, CPF_SequencedAddressItem, type_id=0x8002) scapy_all.bind_layers(CPF_Item, CPF_SequencedAddressItem, type_id=0x8002) scapy_all.bind_layers(ENIP_CPF, CPF_DataItem, type_id=0x8002) -scapy_all.bind_layers(CPF_DataItem, CPF_SequencedAddressItem, type_id=0x00B2) +scapy_all.bind_layers(CPF_DataItem, CPF_SequencedAddressItem, type_id=0x00B2) \ No newline at end of file diff --git a/enip_tcp.py b/enip_tcp.py index 60c4c3b..5813b1a 100644 --- a/enip_tcp.py +++ b/enip_tcp.py @@ -50,9 +50,16 @@ enip_cpf.CPF_AddressDataItem() / enip.ENIP_ConnectionAddress(connection_id=1337), enip_cpf.CPF_DataItem() / enip.ENIP_ConnectionPacket(sequence=4242) / scapy_all.Raw(load='test'), ])) + # pkt /= enip_cpf.ENIP_CPF('', Address_Item = enip_cpf.CPF_AddressDataItem() / enip.ENIP_ConnectionAddress(connection_id=1337), + # Data_Item = enip_cpf.CPF_DataItem() / enip.ENIP_ConnectionPacket(sequence=4242) / scapy_all.Raw(load='test')) + # pkt /= enip_cpf.ENIP_CPF(items=[ + # enip_cpf.CPF_AddressDataItem() / enip.ENIP_ConnectionAddress(connection_id=1337), + # enip_cpf.CPF_DataItem() / enip.ENIP_ConnectionPacket(sequence=4242) / scapy_all.Raw(load='test'), + # ]) # Build! data = str(pkt) + print ' '.join("{:02x}".format(ord(c)) for c in data) pkt = scapy_all.Ether(data) pkt.show() @@ -71,3 +78,13 @@ assert pkt[enip_cpf.ENIP_CPF].items[1].payload == pkt[enip.ENIP_ConnectionPacket] assert pkt[enip.ENIP_ConnectionPacket].sequence == 4242 assert pkt[enip.ENIP_ConnectionPacket].payload.load == 'test' + + # assert pkt[enip.ENIP_SendUnitData].items[0].type_id == 0x00a1 + # assert pkt[enip.ENIP_SendUnitData].items[0].length == 4 + # assert pkt[enip.ENIP_SendUnitData].items[0].payload == pkt[enip.ENIP_ConnectionAddress] + # assert pkt[enip.ENIP_ConnectionAddress].connection_id == 1337 + # assert pkt[enip.ENIP_SendUnitData].items[1].type_id == 0x00b1 + # assert pkt[enip.ENIP_SendUnitData].items[1].length == 6 + # assert pkt[enip.ENIP_SendUnitData].items[1].payload == pkt[enip.ENIP_ConnectionPacket] + # assert pkt[enip.ENIP_ConnectionPacket].sequence == 4242 + # assert pkt[enip.ENIP_ConnectionPacket].payload.load == 'test' \ No newline at end of file diff --git a/enip_udp.py b/enip_udp.py index 9e25c05..6cf4eef 100644 --- a/enip_udp.py +++ b/enip_udp.py @@ -27,7 +27,7 @@ import struct from scapy import all as scapy_all - +# from enip_tcp import * import enip import enip_cpf @@ -55,8 +55,8 @@ scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_PACKET, sport=44818) # Added additional binding options for ENIP_UDP - MED; needed for scy-phy test case -# scapy_all.bind_layers(scapy_all.UDP, ENIP_UDP, sport=2222, dport=2222) -# scapy_all.bind_layers(ENIP_UDP_Item, ENIP_UDP_SequencedAddress, type_id=0x8002) +# scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, sport=2222, dport=2222) +# scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, dport=44818) # scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, sport=44818) scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_RegisterSession, command_id=0x0065) @@ -65,12 +65,19 @@ scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_ListServices, command_id=0x0004) scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_ListIdentity, command_id=0x0063) + +# scapy_all.bind_layers(enip.ENIP_PACKET, ENIP_SequencedAddress, type_id=0x8002) + if __name__ == '__main__': # Test building/dissecting packets # Build a keep-alive packet pkt = scapy_all.Ether(src='00:1d:9c:c8:13:37', dst='01:00:5e:40:12:34') pkt /= scapy_all.IP(src='192.168.1.42', dst='239.192.18.52') pkt /= scapy_all.UDP(sport=2222, dport=2222) + # pkt /= enip.ENIP_PACKET(items=[ + # enip.ENIP_SendUnitData_Item() / enip.ENIP_SequencedAddress(connection_id=1337, sequence=42), + # enip.ENIP_SendUnitData_Item(type_id=0x00b1) / scapy_all.Raw(load=ENIP_UDP_KEEPALIVE), + # ]) # Updated this section to reflect code modifications and moving of classes - MED pkt /= enip.ENIP_UDP(items = [ enip_cpf.CPF_AddressDataItem() / enip_cpf.CPF_SequencedAddressItem(connection_id=1337, sequence_number=42), @@ -79,6 +86,7 @@ # Build! data = str(pkt) + print ' '.join("{:02x}".format(ord(c)) for c in data) pkt = scapy_all.Ether(data) pkt.show() diff --git a/plc.py b/plc.py index 24da107..ebfdb7a 100644 --- a/plc.py +++ b/plc.py @@ -23,16 +23,18 @@ import logging import socket import struct +import enip_cpf from scapy import all as scapy_all from cip import CIP, CIP_Path, CIP_ReqConnectionManager, \ CIP_MultipleServicePacket, CIP_ReqForwardOpen, CIP_RespForwardOpen, \ CIP_ReqForwardClose, CIP_ReqGetAttributeList, CIP_ReqReadOtherTag +# from enip_tcp import ENIP_TCP, ENIP_SendUnitData, ENIP_SendUnitData_Item, \ +# ENIP_ConnectionAddress, ENIP_ConnectionPacket, ENIP_RegisterSession, ENIP_SendRRData # Updated imports to reflect code changes in other files - MED from enip import ENIP_PACKET, ENIP_SendUnitData, ENIP_ConnectionAddress, \ ENIP_ConnectionPacket, ENIP_RegisterSession, ENIP_SendRRData -import enip_cpf # Global switch to make it easy to test without sending anything NO_NETWORK = False @@ -58,6 +60,7 @@ def __init__(self, plc_addr, plc_port=44818): self.sequence = 1 # Open an Ethernet/IP session + # sessionpkt = ENIP_TCP() / ENIP_RegisterSession() # Updated to reflect location and name of classes - MED sessionpkt = ENIP_PACKET() / ENIP_RegisterSession() if self.sock is not None: @@ -71,14 +74,24 @@ def connected(self): def send_rr_cip(self, cippkt): """Send a CIP packet over the TCP connection as an ENIP Req/Rep Data""" + # enippkt = ENIP_TCP(session=self.session_id) # Updated to reflect location and name of classes - MED enippkt = ENIP_PACKET(session=self.session_id) enippkt /= ENIP_SendRRData(items=[ #Updated to reflect code changes - MED enip_cpf.CPF_AddressDataItem(type_id=0), enip_cpf.CPF_DataItem() / cippkt + + # ENIP_SendUnitData_Item(type_id=0), + # ENIP_SendUnitData_Item() / cippkt ]) if self.sock is not None: + ## + # print(str(enippkt)) + # print(scapy_all.hexdump(enippkt)) + # enippkt.scapy_all.show() + + ## self.sock.send(str(enippkt)) def send_rr_cm_cip(self, cippkt): @@ -97,12 +110,16 @@ def send_rr_mr_cip(self, cippkt): def send_unit_cip(self, cippkt): """Send a CIP packet over the TCP connection as an ENIP Unit Data""" + # enippkt = ENIP_TCP(session=self.session_id) # Updated to reflect location and name of classes - MED enippkt = ENIP_PACKET(session=self.session_id) enippkt /= ENIP_SendUnitData(items=[ # Updated to reflect code changes - MED enip_cpf.CPF_AddressDataItem() / ENIP_ConnectionAddress(connection_id=self.enip_connid), enip_cpf.CPF_DataItem() / ENIP_ConnectionPacket(sequence=self.sequence) / cippkt + + # ENIP_SendUnitData_Item() / ENIP_ConnectionAddress(connection_id=self.enip_connid), + # ENIP_SendUnitData_Item() / ENIP_ConnectionPacket(sequence=self.sequence) / cippkt ]) self.sequence += 1 if self.sock is not None: @@ -113,6 +130,7 @@ def recv_enippkt(self): if self.sock is None: return pktbytes = self.sock.recv(2000) + # pkt = ENIP_TCP(pktbytes) # Updated to reflect location and name of classes - MED pkt = ENIP_PACKET(pktbytes) return pkt @@ -121,6 +139,12 @@ def forward_open(self): """Send a forward open request""" cippkt = CIP(service=0x54, path=CIP_Path(wordsize=2, path=b'\x20\x06\x24\x01')) cippkt /= CIP_ReqForwardOpen(path_wordsize=3, path=b"\x01\x00\x20\x02\x24\x01") + ## + # print("CIP packet before creating ENIP") + # print(str(cippkt)) + # print(scapy_all.hexdump(cippkt)) + + ## self.send_rr_cip(cippkt) resppkt = self.recv_enippkt() if self.sock is None: @@ -257,4 +281,4 @@ def attr_format(attrval): # a series of zeros return '[{} zeros]'.format(len(attrval)) # format in hexadecimal the content of attrval - return ''.join('{:2x}'.format(ord(x)) for x in attrval) + return ''.join('{:2x}'.format(ord(x)) for x in attrval) \ No newline at end of file diff --git a/trial.py b/trial.py new file mode 100644 index 0000000..aad0699 --- /dev/null +++ b/trial.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python2 +import binascii +from scapy.all import * +import cip + +# rawpkt = binascii.unhexlify( +# '001D9CEE5EF4000C29C092CF08004500' +# '00581CBB400040060B1BC0A8C888C0A8' +# 'C8F0C917AF128AC4A4C30F9DABF85018' +# '7210131500006F001800362D00000000' +# '00003000000000000000000000000000' +# '00000000020000000000B20008000E03' +# '200F24023001') +# pkt = Ether(rawpkt) +#pkt.show() +#print(pkt[cip.CIP].path) + + +def test_hex_packet(hex_packet): + rawpkt=binascii.unhexlify(hex_packet) + pkt = Ether(rawpkt) + # pkt = cip.CIP_PathPadded(rawpkt) + + pkt.show() + print "=" * 60 + print "" + return rawpkt + +# List Identity = Good +# rawpkt = test_hex_packet("000c29c092cf0000bc5e5264080045000067787600004011ef42c0a8c8f3c0a8c888af12ce9500533bbe63003300000000000000000000000000000000000000000001000c002d0001000002af12c0a8c8f3000000000000000001000c003a0004086000e5e166000b313735362d454e42542f4103") +# rawpkt = test_hex_packet("000c29c092cf0000bc5e5264080045000067787600004011ef42c0a8c8f3c0a8c888af12ce9500533bbe63003300000000000000000000000000000000000000000001000c002d0001000002af12c0a8c8f3aabbccddeeffaabb01000c003a0004086000e5e166000b313735362d454e42542f4103") + +# Register Session: Already Implemented = Works +# rawpkt = test_hex_packet("0000bcc631b1001d9ce985800800450000441dcf000040064a0dc0a8c891c0a8c8f5af120be382a044a77c5ec1a7501810005f94000065000400030000000000000000000000000000000000000001000000") + +# List Services = Works +# rawpkt = test_hex_packet("0000bcc631b1e49069a52e9d08004500007e00774000400626e3c0a8c8f1c0a8c8f5af120be47c7fcc227c838750801810002e4e00000101080a000000440000004404003200000000000000000000000000000000000000000002000001140001002001436f6d6d756e69636174696f6e7300000001140001002001436f6d6d756e69636174496f6e730000") +# rawpkt = test_hex_packet("0000bcc631b1e49069a52e9d08004500007e00774000400626e3c0a8c8f1c0a8c8f5af120be47c7fcc227c838750801810002e4e00000101080a000000440000004404003200000000000000000000000000000000000000000002000001140001002001436f6d6d756e69636174696f6e730000") +# rawpkt = test_hex_packet("0000bcc631b1001d9ce9858008004500005a1dce0000400649f8c0a8c891c0a8c8f5af120be382a044757c5ec18b50181000afc4000004001a00000000000000000000000000000000000000000001000001140001002001436f6d6d756e69636174696f6e730000") + +# Forward Open Success --> PLC/Computer = Works +# rawpkt = test_hex_packet("000c29c092cf0000bcc631b108004500007a9bc7400040068be7c0a8c8f5c0a8c888af12c8d5c0c306893d79a13880181000e7c100000101080a000434f10267e0e06f002e000037021400000000000000000000000000000000000000000000020000000000b2001e00d40000000215aa003000fe8037134d00efbeadde00127a0000127a000000") + +# Forward Open: PLC --> PowerFlex 0.5HP _ Pkt 113 in AfterReboot pcap +rawpkt = test_hex_packet("001d9ce985800000bcc631b10800450000c8008b4000400626cdc0a8c8f5c0a8c8910be3af127c5ec1a782a044c35018100005d900006f0088000300000000000000c0a8c8f50000007400000000000000000000020000000000b2007800540220062401059b0000000000000000000001009ffe620000000000000000000004000000000004812734040100960009008103200424062c022c01801d01000000820000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + +# Forward Open Big PowerFlex +# rawpkt = test_hex_packet("e49069a52e9d0000bcc631b10800450000ae009f400040062673c0a8c8f5c0a8c8f10be4af127c83876c7c7fcc7080181000d87600000101080a00000044000000456f0062000001020c00000000c0a8c8f50000007100000000000000000000020000000000b2005200540220062401059b00000000010eaa00030001009ffe62000000000050c30000164850c300001648811434040000000000000000200424032c012c02800a01000300000010007d0c0300000010007d0a0000") + + +# Forward Close --> PLC/Computer +#rawpkt = test_hex_packet("0000bcc631b1000c29c092cf080045000074402540004006e78fc0a8c888c0a8c8f5c8d5af123d79a17cc0c30701801800e5133600000101080a0267e0e6000434f16f0028000037021400000000000000000000000000000000000000000000020000000000b20018004e022006240100f937134d00efbeadde0300010020022401") + +# Forward Open: Computer --> PLC +# rawpkt = test_hex_packet("0000bcc631b1000c29c092cf08004500008c402340004006e779c0a8c888c0a8c8f5c8d5af123d79a0e0c0c30689801800e5134e00000101080a0267e0e0000434f16f0040000037021400000000000000000000000000000000000000000000020000000000b200300054022006240100f9310000803000fe8037134d00efbeadde0000000000127a00f44100127a00f441a303010020022401") + + + +#ListIdentity(pkt) +#ListServices(pkt) \ No newline at end of file diff --git a/utils.py b/utils.py index dc1882b..3424ef4 100644 --- a/utils.py +++ b/utils.py @@ -78,4 +78,4 @@ class XLEShortField(scapy_all.LEShortField): """A Little Endian ShortField with hexadecimal representation""" def i2repr(self, pkt, x): from scapy.utils import lhex - return lhex(self.i2h(pkt, x)) + return lhex(self.i2h(pkt, x)) \ No newline at end of file From a9eb72d5a87acceef101c14b615b475292ffdf9b Mon Sep 17 00:00:00 2001 From: m4tthew-d Date: Mon, 23 Jan 2017 18:32:44 -0500 Subject: [PATCH 21/29] Added CIP_DataSegment and corrected CIP_LogicSegmentPadded extract_padding function --- cip.py | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/cip.py b/cip.py index d2d8ec0..bbd4a02 100644 --- a/cip.py +++ b/cip.py @@ -172,6 +172,23 @@ class CIP_PortSegment(scapy_all.Packet): # print self.__class__.__name__ + ": P=" + str(p) # return "", p +class CIP_DataSegmentPadded(scapy_all.Packet): + name='CIP_DataSegmentPadded' + + SEG_TYPE={ + 0: "Simple Data Segment", + 17: "ANSI Extended Symbol Segment" + } + + fields_desc = [ + scapy_all.BitEnumField("segment_subtype", 0, 5, SEG_TYPE), + scapy_all.ConditionalField(scapy_all.ByteField("word_size", 0), lambda p: p.segment_subtype == 0), + scapy_all.ConditionalField(scapy_all.ByteField("character_count", 0), lambda p: p.segment_subtype == 17), + scapy_all.ConditionalField(scapy_all.FieldListField("data",0,scapy_all.LEShortField('',0),count_from = lambda p: p.word_size), lambda p: p.segment_subtype == 0), + scapy_all.ConditionalField(scapy_all.FieldListField("data", 0, scapy_all.ByteField('', 0), count_from=lambda p: p.character_count), + lambda p: p.segment_subtype == 17) + ] + class CIP_Logical_SpecialSegment(scapy_all.Packet): name="Electronic_Key_Segment" @@ -186,7 +203,7 @@ class CIP_Logical_SpecialSegment(scapy_all.Packet): ] def extract_padding(self, p): - print self.__class__.__name__ + ": P=" + str(p) + # print self.__class__.__name__ + ": P=" + str(p) return None, p class CIP_LogicalSegmentPadded(scapy_all.Packet): @@ -210,7 +227,7 @@ class CIP_LogicalSegmentPadded(scapy_all.Packet): 2: "32-bit logical address", 3: "Reserved for future use" } - + magic_value=5 fields_desc = [ scapy_all.BitEnumField("logical_type", 0, 3, LOGICAL_TYPE), scapy_all.ConditionalField(scapy_all.BitEnumField("logical_format", 0, 2, LOGICAL_FORMAT), lambda p: p.logical_type < 5), @@ -237,9 +254,13 @@ class CIP_LogicalSegmentPadded(scapy_all.Packet): ), ] - # def extract_padding(self, p): - # print self.__class__.__name__ + ": P=" + str(p) - # return '',p + def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + # magic_value = 5: logic_value < 5 have no payload --> go to previous layer again; logic_value >=5 have payload --> process + if self.logical_type < 5: + return '',p + else: + return p, '' scapy_all.bind_layers(CIP_LogicalSegmentPadded, CIP_Logical_SpecialSegment, logical_type=5) # Logical Key Segment @@ -319,7 +340,7 @@ class CIP_PathPadded(scapy_all.Packet): scapy_all.bind_layers(CIP_PathPadded, CIP_LogicalSegmentPadded, segment_type=1) #Logical Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=2) #Port Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=3) #Port Segment -scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=4) #Port Segment +scapy_all.bind_layers(CIP_PathPadded, CIP_DataSegmentPadded, segment_type=4) #Data Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=5) #Port Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=6) #Port Segment scapy_all.bind_layers(CIP_PathPadded, CIP_PortSegment, segment_type=7) #Port Segment @@ -401,7 +422,7 @@ class CIP_Path(scapy_all.Packet): ] def extract_padding(self, p): - print self.__class__.__name__ + ": P=" + str(p) + # print self.__class__.__name__ + ": P=" + str(p) return "", p @classmethod @@ -689,10 +710,10 @@ class CIP_ReqForwardOpen(scapy_all.Packet): #CIP_PathField("path", None, length_from=lambda p: 2 * p.Connection_Path_Size), ] - # MED 1/23/17 - # def extract_padding(self, p): - # print self.__class__.__name__ + ": P=" + str(p) - # return '', p + + def extract_padding(self, p): + # print self.__class__.__name__ + ": P=" + str(p) + return '', p class CIP_RespForwardOpen(scapy_all.Packet): From a612af0aa36db7ebb8c9906a98cb35c5374a2157 Mon Sep 17 00:00:00 2001 From: m4tthew-d Date: Wed, 25 Jan 2017 16:14:10 -0500 Subject: [PATCH 22/29] Forward Close Req/Resp --- cip.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/cip.py b/cip.py index bbd4a02..2a6bf7d 100644 --- a/cip.py +++ b/cip.py @@ -168,9 +168,9 @@ class CIP_PortSegment(scapy_all.Packet): ) ] - # def extract_padding(self, p): - # print self.__class__.__name__ + ": P=" + str(p) - # return "", p + def extract_padding(self, p): + #print self.__class__.__name__ + ": P=" + str(p) + return "", p class CIP_DataSegmentPadded(scapy_all.Packet): name='CIP_DataSegmentPadded' @@ -529,7 +529,7 @@ class CIP_ResponseStatus(scapy_all.Packet): } def extract_padding(self, p): - print self.__class__.__name__ + ": P=" + str(p) + # print self.__class__.__name__ + ": P=" + str(p) return "", p def __repr__(self): @@ -570,7 +570,7 @@ class CIP(scapy_all.Packet): 0x4b: "Execute_PCCC_Service", # PCCC = Programmable Controller Communication Commands 0x4c: "Read_Tag_Service", 0x4d: "Write_Tag_Service", - 0x4e: "Read_Modify_Write_Tag_Service", + 0x4e: "Forward_Close", # Forward Close/ "Read_Modify_Write_Tag_Service" 0x4f: "Read_Other_Tag_Service", # ??? 0x52: "Read_Tag_Fragmented_Service", 0x53: "Write_Tag_Fragmented_Service", @@ -662,7 +662,7 @@ def do_build(self): return p[::-1] # we have to flip the output def extract_padding(self, p): - print self.__class__.__name__ + ": P=" + str(p) + # print self.__class__.__name__ + ": P=" + str(p) return '', p @@ -743,9 +743,21 @@ class CIP_ReqForwardClose(scapy_all.Packet): scapy_all.LEIntField("originator_serial_number", 0xdeadbeef), scapy_all.ByteField("path_wordsize", None), scapy_all.XByteField("reserved", 0), - CIP_PathField("path", None, length_from=lambda p: 2 * p.path_wordsize), + # CIP_PathField("path", None, length_from=lambda p: 2 * p.path_wordsize), # Original Implementation by Scy-Phy + scapy_all.PacketListField("path_segment_items", [], CIP_PathPadded, length_from=lambda p: 2 * p.path_wordsize) ] +class CIP_RespForwardClose(scapy_all.Packet): + """Forward Close Response""" + name = "CIP_RespForwardClose" + fields_desc = [ + scapy_all.LEShortField('connection_serial_number',0x1337), + scapy_all.LEShortField('vendor_id', 0x004d), + scapy_all.LEIntField('originator_serial_number', 0xdeadbeef), + scapy_all.ByteField('application_reply_size', 0), + scapy_all.ByteField('reserved', 0), + scapy_all.FieldListField('application_reply', 0, scapy_all.ShortField,count_from=lambda p: p.application_reply_size) + ] class CIP_MultipleServicePacket(scapy_all.Packet): """Multiple_Service_Packet request or response""" @@ -805,6 +817,8 @@ def post_build(self, p, pay): scapy_all.bind_layers(CIP, CIP_MultipleServicePacket, service=0x0a) scapy_all.bind_layers(CIP, CIP_RespSingleAttribute, direction=1, service=0x0e) scapy_all.bind_layers(CIP, CIP_ReqReadOtherTag, direction=0, service=0x4c) +scapy_all.bind_layers(CIP, CIP_ReqForwardClose, direction=0, service=0x4e) +scapy_all.bind_layers(CIP, CIP_RespForwardClose, direction=1, service=0x4e) scapy_all.bind_layers(CIP, CIP_ReqReadOtherTag, direction=0, service=0x4f) scapy_all.bind_layers(CIP, CIP_ReqForwardOpen, direction=0, service=0x54) scapy_all.bind_layers(CIP, CIP_RespForwardOpen, direction=1, service=0x54) From f1c902ff3c874a08a2e362c0f0a6a67156025f9d Mon Sep 17 00:00:00 2001 From: m4tthew-d Date: Mon, 30 Jan 2017 12:19:42 -0500 Subject: [PATCH 23/29] Corrected layer binding by port number for UDP --- cip.py | 10 ++++++---- enip_cpf.py | 7 +++++++ enip_udp.py | 8 +++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cip.py b/cip.py index 2a6bf7d..324872e 100644 --- a/cip.py +++ b/cip.py @@ -49,7 +49,6 @@ class CIP_RespAttributesAll(scapy_all.Packet): scapy_all.StrField("value", None), ] - class CIP_RespAttributesList(scapy_all.Packet): """List of attributes in Get_Attribute_List responses @@ -60,6 +59,9 @@ class CIP_RespAttributesList(scapy_all.Packet): """ fields_desc = [ scapy_all.LEShortField("count", 0), + # scapy_all.StrField("content", ""), + scapy_all.LEShortField('attribute', 0), + scapy_all.XShortField('status', {0: 'success'}), scapy_all.StrField("content", ""), ] @@ -184,7 +186,7 @@ class CIP_DataSegmentPadded(scapy_all.Packet): scapy_all.BitEnumField("segment_subtype", 0, 5, SEG_TYPE), scapy_all.ConditionalField(scapy_all.ByteField("word_size", 0), lambda p: p.segment_subtype == 0), scapy_all.ConditionalField(scapy_all.ByteField("character_count", 0), lambda p: p.segment_subtype == 17), - scapy_all.ConditionalField(scapy_all.FieldListField("data",0,scapy_all.LEShortField('',0),count_from = lambda p: p.word_size), lambda p: p.segment_subtype == 0), + scapy_all.ConditionalField(scapy_all.FieldListField("data",0, scapy_all.LEShortField('',0), count_from = lambda p: p.word_size), lambda p: p.segment_subtype == 0), scapy_all.ConditionalField(scapy_all.FieldListField("data", 0, scapy_all.ByteField('', 0), count_from=lambda p: p.character_count), lambda p: p.segment_subtype == 17) ] @@ -572,7 +574,7 @@ class CIP(scapy_all.Packet): 0x4d: "Write_Tag_Service", 0x4e: "Forward_Close", # Forward Close/ "Read_Modify_Write_Tag_Service" 0x4f: "Read_Other_Tag_Service", # ??? - 0x52: "Read_Tag_Fragmented_Service", + 0x52: "Unconnected_Send", # Unconnected Send/Read_Tag_Fragmented_Service 0x53: "Write_Tag_Fragmented_Service", 0x54: "Forward_Open", } @@ -756,7 +758,7 @@ class CIP_RespForwardClose(scapy_all.Packet): scapy_all.LEIntField('originator_serial_number', 0xdeadbeef), scapy_all.ByteField('application_reply_size', 0), scapy_all.ByteField('reserved', 0), - scapy_all.FieldListField('application_reply', 0, scapy_all.ShortField,count_from=lambda p: p.application_reply_size) + scapy_all.FieldListField('application_reply', 0, scapy_all.ShortField('',0), count_from=lambda p: p.application_reply_size) ] class CIP_MultipleServicePacket(scapy_all.Packet): diff --git a/enip_cpf.py b/enip_cpf.py index 1e64ad1..42096d1 100644 --- a/enip_cpf.py +++ b/enip_cpf.py @@ -69,6 +69,13 @@ class CPF_Item(scapy_all.Packet): fields_desc = [ scapy_all.LEShortEnumField('type_id', 0, ITEM_ID_NUMBERS), scapy_all.LEShortField("length", None), + scapy_all.ConditionalField( + scapy_all.FieldListField( + 'data',0,scapy_all.XByteField('', 0), + count_from = lambda p: p.length + ), + lambda p: p.type_id==0xB1 # or p.type_id==0xB2 + ) ] def extract_padding(self, p): diff --git a/enip_udp.py b/enip_udp.py index 6cf4eef..f20dfb8 100644 --- a/enip_udp.py +++ b/enip_udp.py @@ -50,13 +50,15 @@ # ENIP_UDP moved to new file enip.py --> Keeps all EtherNet/IP Level # processing in the same file rather than splitting based on upper layer - MED -scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_PACKET, sport=2222, dport=2222) +# Port 44818 - Utilizes command_id structure rather than CPF structure +# scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_PACKET, sport=2222, dport=2222) scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_PACKET, dport=44818) scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_PACKET, sport=44818) # Added additional binding options for ENIP_UDP - MED; needed for scy-phy test case -# scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, sport=2222, dport=2222) -# scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, dport=44818) +# # Port 2222 - Utilizes CPF structure rather than command_id structure +scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, sport=2222, dport=2222) +# scapy_all.bind_layerports(scapy_all.UDP, enip.ENIP_UDP, dport=44818) # scapy_all.bind_layers(scapy_all.UDP, enip.ENIP_UDP, sport=44818) scapy_all.bind_layers(enip.ENIP_UDP, enip.ENIP_RegisterSession, command_id=0x0065) From aeca22783dc1b670d649158be5dd44666771064c Mon Sep 17 00:00:00 2001 From: m4tthew-d Date: Wed, 1 Feb 2017 11:14:34 -0500 Subject: [PATCH 24/29] Corrected CIP via SendUnitData layers --- cip.py | 5 +++++ enip.py | 37 ++++++++++++++++++++++++++++++------- enip_udp.py | 7 ++++--- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/cip.py b/cip.py index 324872e..a0d89d8 100644 --- a/cip.py +++ b/cip.py @@ -813,6 +813,11 @@ def post_build(self, p, pay): scapy_all.bind_layers(enip_cpf.CPF_DataItem, CIP, type_id=0x00b2) #0x00B2 : "Unconnected Data Item" scapy_all.bind_layers(enip_cpf.CPF_Item, CIP, type_id=0x00b2) #0x00B2 : "Unconnected Data Item" +scapy_all.bind_layers(enip.ENIP_SendUnitData_Item, CIP, type_id=0x00b2) +scapy_all.bind_layers(enip.ENIP_SendUnitData_Item, CIP, type_id=0x00b1) + +scapy_all.bind_layers(enip.ENIP_ConnectionPacket, CIP) + scapy_all.bind_layers(CIP, CIP_RespAttributesAll, direction=1, service=0x01) scapy_all.bind_layers(CIP, CIP_ReqGetAttributeList, direction=0, service=0x03) scapy_all.bind_layers(CIP, CIP_RespAttributesList, direction=1, service=0x03) diff --git a/enip.py b/enip.py index 50e459b..33e93a3 100644 --- a/enip.py +++ b/enip.py @@ -102,7 +102,7 @@ class ENIP_ConnectionAddress(scapy_all.Packet): name = "ENIP_ConnectionAddress" - fields_desc = [scapy_all.LEIntField("connection_id", 0)] + fields_desc = [utils.XLEIntField("connection_id", 0)] class ENIP_ConnectionPacket(scapy_all.Packet): @@ -110,16 +110,39 @@ class ENIP_ConnectionPacket(scapy_all.Packet): fields_desc = [scapy_all.LEShortField("sequence", 0)] +class ENIP_SendUnitData_Item(scapy_all.Packet): + name = "ENIP_SendUnitData_Item" + + fields_desc = [ + scapy_all.LEShortEnumField("type_id", 0, { + 0x0000: "null_address", # NULL Address + 0x00a1: "conn_address", # Address for connection based requests + 0x00b1: "conn_packet", # Connected Transport packet + 0x00b2: "unconn_message", # Unconnected Messages (eg. used within CIP command SendRRData) + 0x0100: "listservices_response", # ListServices response + }), + scapy_all.LEShortField("length", None), + ] + + def extract_padding(self, p): + return p[:self.length], p[self.length:] + + def post_build(self, p, pay): + if self.length is None and pay: + l = len(pay) + p = p[:2] + struct.pack(" Date: Thu, 2 Feb 2017 16:30:33 -0500 Subject: [PATCH 25/29] Added CIP_Generic for unknown services data --- cip.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/cip.py b/cip.py index a0d89d8..d9a898d 100644 --- a/cip.py +++ b/cip.py @@ -474,13 +474,6 @@ def to_repr(self): class CIP_ResponseStatus(scapy_all.Packet): """The response field of CIP headers""" name = "CIP_ResponseStatus" - fields_desc = [ - scapy_all.XByteField("reserved", 0), # Reserved byte, always null - scapy_all.ByteEnumField("status", 0, {0: "success"}), - scapy_all.XByteField("additional_size", 0), - scapy_all.StrLenField("additional", "", # additional status - length_from=lambda p: 2 * p.additional_size), - ] ERROR_CODES = { 0x00: "Success", @@ -530,6 +523,15 @@ class CIP_ResponseStatus(scapy_all.Packet): 0x2c: "Attribute not gettable", } + fields_desc = [ + scapy_all.XByteField("reserved", 0), # Reserved byte, always null + scapy_all.ByteEnumField("status", 0, ERROR_CODES), + scapy_all.XByteField("additional_size", 0), + scapy_all.StrLenField("additional", "", # additional status + length_from=lambda p: 2 * p.additional_size), + ] + + def extract_padding(self, p): # print self.__class__.__name__ + ": P=" + str(p) return "", p @@ -808,6 +810,13 @@ def post_build(self, p, pay): p = p[:-4] + b"\0" + p[-4:] return p + pay +class CIP_GenericClass(scapy_all.Packet): + name = "CIP_GenericClass" + fields_desc = [ + scapy_all.StrField("data", None) + ] + + # Updated to fit with new code organization # scapy_all.bind_layers(enip.ENIP_ConnectionPacket, CIP) scapy_all.bind_layers(enip_cpf.CPF_DataItem, CIP, type_id=0x00b2) #0x00B2 : "Unconnected Data Item" @@ -830,6 +839,12 @@ def post_build(self, p, pay): scapy_all.bind_layers(CIP, CIP_ReqForwardOpen, direction=0, service=0x54) scapy_all.bind_layers(CIP, CIP_RespForwardOpen, direction=1, service=0x54) +# Unknown services on network +scapy_all.bind_layers(CIP, CIP_GenericClass, service=0x4b) +scapy_all.bind_layers(CIP, CIP_GenericClass, service=0x4c) +scapy_all.bind_layers(CIP, CIP_GenericClass, service=0x5c) + + # TODO: this is much imprecise :( # Need class in path to be 6 (Connection Manager) scapy_all.bind_layers(CIP, CIP_ReqConnectionManager, direction=0, service=0x52) From a9fec774ad05731aa555cdf2686a9d82532b3749 Mon Sep 17 00:00:00 2001 From: m4tthew-d Date: Fri, 3 Feb 2017 12:27:17 -0500 Subject: [PATCH 26/29] Fixed bug in GetAttributeList --- cip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cip.py b/cip.py index d9a898d..8ddbcd8 100644 --- a/cip.py +++ b/cip.py @@ -61,7 +61,7 @@ class CIP_RespAttributesList(scapy_all.Packet): scapy_all.LEShortField("count", 0), # scapy_all.StrField("content", ""), scapy_all.LEShortField('attribute', 0), - scapy_all.XShortField('status', {0: 'success'}), + scapy_all.XShortField('status', 0), scapy_all.StrField("content", ""), ] From 160825274bef8b18bd84a543d0ded0ac84d6f04f Mon Sep 17 00:00:00 2001 From: m4tthew-d Date: Mon, 6 Feb 2017 12:07:45 -0500 Subject: [PATCH 27/29] Changed originator_serial_number to XLEIntField in Forward Open/Close Req/Resp --- cip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cip.py b/cip.py index 8ddbcd8..e163d7c 100644 --- a/cip.py +++ b/cip.py @@ -728,7 +728,7 @@ class CIP_RespForwardOpen(scapy_all.Packet): scapy_all.LEIntField("TO_network_connection_id", None), scapy_all.LEShortField("connection_serial_number", None), scapy_all.LEShortField("vendor_id", None), - scapy_all.LEIntField("originator_serial_number", None), + utils.XLEIntField("originator_serial_number", None), scapy_all.LEIntField("OT_api", None), scapy_all.LEIntField("TO_api", None), scapy_all.ByteField("application_reply_size", None), @@ -744,7 +744,7 @@ class CIP_ReqForwardClose(scapy_all.Packet): scapy_all.ByteField("timeout_ticks", 249), scapy_all.LEShortField("connection_serial_number", 0x1337), scapy_all.LEShortField("vendor_id", 0x004d), - scapy_all.LEIntField("originator_serial_number", 0xdeadbeef), + utils.XLEIntField("originator_serial_number", 0xdeadbeef), scapy_all.ByteField("path_wordsize", None), scapy_all.XByteField("reserved", 0), # CIP_PathField("path", None, length_from=lambda p: 2 * p.path_wordsize), # Original Implementation by Scy-Phy @@ -757,7 +757,7 @@ class CIP_RespForwardClose(scapy_all.Packet): fields_desc = [ scapy_all.LEShortField('connection_serial_number',0x1337), scapy_all.LEShortField('vendor_id', 0x004d), - scapy_all.LEIntField('originator_serial_number', 0xdeadbeef), + utils.XLEIntField('originator_serial_number', 0xdeadbeef), scapy_all.ByteField('application_reply_size', 0), scapy_all.ByteField('reserved', 0), scapy_all.FieldListField('application_reply', 0, scapy_all.ShortField('',0), count_from=lambda p: p.application_reply_size) From bc3df6437d6bb450816d11efe5da32f70a9f50ba Mon Sep 17 00:00:00 2001 From: m4tthew-d Date: Tue, 7 Feb 2017 17:47:40 -0500 Subject: [PATCH 28/29] Corrected bind_layers for read_tag --- cip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cip.py b/cip.py index e163d7c..55464c7 100644 --- a/cip.py +++ b/cip.py @@ -841,7 +841,7 @@ class CIP_GenericClass(scapy_all.Packet): # Unknown services on network scapy_all.bind_layers(CIP, CIP_GenericClass, service=0x4b) -scapy_all.bind_layers(CIP, CIP_GenericClass, service=0x4c) +scapy_all.bind_layers(CIP, CIP_GenericClass, direction=1, service=0x4c) scapy_all.bind_layers(CIP, CIP_GenericClass, service=0x5c) From 3aed8f754e9fb5c477f8ab40a34bc37fd49cdd7d Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 14 Feb 2017 14:01:27 -0500 Subject: [PATCH 29/29] Delete trial.py --- trial.py | 60 -------------------------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 trial.py diff --git a/trial.py b/trial.py deleted file mode 100644 index aad0699..0000000 --- a/trial.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python2 -import binascii -from scapy.all import * -import cip - -# rawpkt = binascii.unhexlify( -# '001D9CEE5EF4000C29C092CF08004500' -# '00581CBB400040060B1BC0A8C888C0A8' -# 'C8F0C917AF128AC4A4C30F9DABF85018' -# '7210131500006F001800362D00000000' -# '00003000000000000000000000000000' -# '00000000020000000000B20008000E03' -# '200F24023001') -# pkt = Ether(rawpkt) -#pkt.show() -#print(pkt[cip.CIP].path) - - -def test_hex_packet(hex_packet): - rawpkt=binascii.unhexlify(hex_packet) - pkt = Ether(rawpkt) - # pkt = cip.CIP_PathPadded(rawpkt) - - pkt.show() - print "=" * 60 - print "" - return rawpkt - -# List Identity = Good -# rawpkt = test_hex_packet("000c29c092cf0000bc5e5264080045000067787600004011ef42c0a8c8f3c0a8c888af12ce9500533bbe63003300000000000000000000000000000000000000000001000c002d0001000002af12c0a8c8f3000000000000000001000c003a0004086000e5e166000b313735362d454e42542f4103") -# rawpkt = test_hex_packet("000c29c092cf0000bc5e5264080045000067787600004011ef42c0a8c8f3c0a8c888af12ce9500533bbe63003300000000000000000000000000000000000000000001000c002d0001000002af12c0a8c8f3aabbccddeeffaabb01000c003a0004086000e5e166000b313735362d454e42542f4103") - -# Register Session: Already Implemented = Works -# rawpkt = test_hex_packet("0000bcc631b1001d9ce985800800450000441dcf000040064a0dc0a8c891c0a8c8f5af120be382a044a77c5ec1a7501810005f94000065000400030000000000000000000000000000000000000001000000") - -# List Services = Works -# rawpkt = test_hex_packet("0000bcc631b1e49069a52e9d08004500007e00774000400626e3c0a8c8f1c0a8c8f5af120be47c7fcc227c838750801810002e4e00000101080a000000440000004404003200000000000000000000000000000000000000000002000001140001002001436f6d6d756e69636174696f6e7300000001140001002001436f6d6d756e69636174496f6e730000") -# rawpkt = test_hex_packet("0000bcc631b1e49069a52e9d08004500007e00774000400626e3c0a8c8f1c0a8c8f5af120be47c7fcc227c838750801810002e4e00000101080a000000440000004404003200000000000000000000000000000000000000000002000001140001002001436f6d6d756e69636174696f6e730000") -# rawpkt = test_hex_packet("0000bcc631b1001d9ce9858008004500005a1dce0000400649f8c0a8c891c0a8c8f5af120be382a044757c5ec18b50181000afc4000004001a00000000000000000000000000000000000000000001000001140001002001436f6d6d756e69636174696f6e730000") - -# Forward Open Success --> PLC/Computer = Works -# rawpkt = test_hex_packet("000c29c092cf0000bcc631b108004500007a9bc7400040068be7c0a8c8f5c0a8c888af12c8d5c0c306893d79a13880181000e7c100000101080a000434f10267e0e06f002e000037021400000000000000000000000000000000000000000000020000000000b2001e00d40000000215aa003000fe8037134d00efbeadde00127a0000127a000000") - -# Forward Open: PLC --> PowerFlex 0.5HP _ Pkt 113 in AfterReboot pcap -rawpkt = test_hex_packet("001d9ce985800000bcc631b10800450000c8008b4000400626cdc0a8c8f5c0a8c8910be3af127c5ec1a782a044c35018100005d900006f0088000300000000000000c0a8c8f50000007400000000000000000000020000000000b2007800540220062401059b0000000000000000000001009ffe620000000000000000000004000000000004812734040100960009008103200424062c022c01801d01000000820000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") - -# Forward Open Big PowerFlex -# rawpkt = test_hex_packet("e49069a52e9d0000bcc631b10800450000ae009f400040062673c0a8c8f5c0a8c8f10be4af127c83876c7c7fcc7080181000d87600000101080a00000044000000456f0062000001020c00000000c0a8c8f50000007100000000000000000000020000000000b2005200540220062401059b00000000010eaa00030001009ffe62000000000050c30000164850c300001648811434040000000000000000200424032c012c02800a01000300000010007d0c0300000010007d0a0000") - - -# Forward Close --> PLC/Computer -#rawpkt = test_hex_packet("0000bcc631b1000c29c092cf080045000074402540004006e78fc0a8c888c0a8c8f5c8d5af123d79a17cc0c30701801800e5133600000101080a0267e0e6000434f16f0028000037021400000000000000000000000000000000000000000000020000000000b20018004e022006240100f937134d00efbeadde0300010020022401") - -# Forward Open: Computer --> PLC -# rawpkt = test_hex_packet("0000bcc631b1000c29c092cf08004500008c402340004006e779c0a8c888c0a8c8f5c8d5af123d79a0e0c0c30689801800e5134e00000101080a0267e0e0000434f16f0040000037021400000000000000000000000000000000000000000000020000000000b200300054022006240100f9310000803000fe8037134d00efbeadde0000000000127a00f44100127a00f441a303010020022401") - - - -#ListIdentity(pkt) -#ListServices(pkt) \ No newline at end of file