From 072802175cfccaf01aa89b0f4104c1f9e6c7e14d Mon Sep 17 00:00:00 2001 From: Noam Rathaus Date: Mon, 9 Feb 2026 12:40:19 +0200 Subject: [PATCH 1/7] Add DNP3 --- scapy/contrib/scada/dnp3.py | 540 ++++++++++++++++++++++++++++++++++++ 1 file changed, 540 insertions(+) create mode 100644 scapy/contrib/scada/dnp3.py diff --git a/scapy/contrib/scada/dnp3.py b/scapy/contrib/scada/dnp3.py new file mode 100644 index 00000000000..1e489365bcc --- /dev/null +++ b/scapy/contrib/scada/dnp3.py @@ -0,0 +1,540 @@ +__author__ = 'Nicholas Rodofile' +from scapy.all import * +# import crcmod.predefined + +''' +# Copyright 2014-2016 N.R Rodofile + +Licensed under the GPLv3. +This program is free software: you can redistribute it and/or modify it under the terms +of the GNU General Public License as published by the Free Software Foundation, either +version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License for more details. You should have received a copy of +the GNU General Public License along with this program. If not, see +http://www.gnu.org/licenses/. +''' + +bitState = {1: "SET", 0: "UNSET"} +stations = {1: "MASTER", 0: "OUTSTATION"} + +MASTER = 1 +OUTSTATION = 0 +SET = 1 +UNSET = 0 +dnp3_port = 20000 + +Transport_summary = "Seq:%DNP3Transport.SEQUENCE% " +Application_Rsp_summary = "Response %DNP3ApplicationResponse.FUNC_CODE% " +Application_Req_summary = "Request %DNP3ApplicationRequest.FUNC_CODE% " +DNP3_summary = "From %DNP3.SOURCE% to %DNP3.DESTINATION% " + +''' +Initalise a predefined crc object for DNP3 Cyclic Redundancy Check +Info : http://crcmod.sourceforge.net/crcmod.predefined.html +''' + + +def _verifyPoly(poly): + """ + Check the polynomial to make sure that it is acceptable and return the number + of bits in the CRC. + """ + + msg = 'The degree of the polynomial must be 8, 16, 24, 32 or 64' + for n in (8,16,24,32,64): + low = 1<> 1 + + return y + +#----------------------------------------------------------------------------- +# The following functions compute the CRC for a single byte. These are used +# to build up the tables needed in the CRC algorithm. Assumes the high order +# bit of the polynomial has been stripped off. + + +def _bytecrc(crc, poly, n): + crc = crc + poly = poly + mask = 1<<(n-1) + for i in range(8): + if crc & mask: + crc = (crc << 1) ^ poly + else: + crc = crc << 1 + mask = (1<> 1) ^ poly + else: + crc = crc >> 1 + + mask = (1<> 8) + return crc + +def _crc16(data, crc, table): + crc = crc & 0xFFFF + for x in data: + crc = table[ord(x) ^ ((crc>>8) & 0xFF)] ^ ((crc << 8) & 0xFF00) + + return crc + +_sizeMap = { + 16 : [_crc16, _crc16r], +} + + +_sizeToTypeCode = {} + +for typeCode in 'B H I L Q'.split(): + size = {1:8, 2:16, 4:32, 8:64}.get(struct.calcsize(typeCode),None) + if size is not None and size not in _sizeToTypeCode: + _sizeToTypeCode[size] = '256%s' % typeCode + +_sizeToTypeCode[24] = _sizeToTypeCode[32] + +# Use Python3 based implementation for CRC and not third-party +_usingExtension = False + +def _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut): + """ + The following function returns a Python function to compute the CRC. + + It must be passed parameters that are already verified & sanitized by + _verifyParams(). + + The returned function calls a low level function that is written in C if the + extension module could be loaded. Otherwise, a Python implementation is + used. + + In addition to this function, a list containing the CRC table is returned. + """ + if rev: + tableList = _mkTable_r(poly, sizeBits) + _fun = _sizeMap[sizeBits][1] + else: + tableList = _mkTable(poly, sizeBits) + _fun = _sizeMap[sizeBits][0] + + _table = tableList + if _usingExtension: + _table = struct.pack(_sizeToTypeCode[sizeBits], *tableList) + + if xorOut == 0: + def crcfun(data, crc=initCrc, table=_table, fun=_fun): + return fun(data, crc, table) + else: + def crcfun(data, crc=initCrc, table=_table, fun=_fun): + return xorOut ^ fun(data, xorOut ^ crc, table) + + return crcfun, tableList + + +def _verifyParams(poly, initCrc, xorOut): + """ + The following function validates the parameters of the CRC, namely, + poly, and initial/final XOR values. + It returns the size of the CRC (in bits), and "sanitized" initial/final XOR values. + """ + sizeBits = _verifyPoly(poly) + + mask = (1< 18: + chunk = payload[:18] + chunk = update_data_chunk_crc(chunk) + payload = chunk + payload[18:] + + else: + chunk = payload[:-2] + chunk = update_data_chunk_crc(chunk) + payload = chunk + return payload + + +applicationFunctionCode = { + 0: "CONFIRM", + 1: "READ", + 2: "WRITE", + 3: "SELECT", + 4: "OPERATE", + 5: "DIRECT_OPERATE", + 6: "DIRECT_OPERATE_NR", + 7: "IMMED_FREEZE", + 8: "IMMED_FREEZE_NR", + 9: "FREEZE_CLEAR", + 10: "FREEZE_CLEAR_NR", + 11: "FREEZE_AT_TIME", + 12: "FREEZE_AT_TIME_NR", + 13: "COLD_RESTART", + 14: "WARM_RESTART", + 15: "INITIALIZE_DATA", + 16: "INITIALIZE_APPL", + 17: "START_APPL", + 18: "STOP_APPL", + 19: "SAVE_CONFIG", + 20: "ENABLE_UNSOLICITED", + 21: "DISABLE_UNSOLICITED", + 22: "ASSIGN_CLASS", + 23: "DELAY_MEASURE", + 24: "RECORD_CURRENT_TIME", + 25: "OPEN_FILE", + 26: "CLOSE_FILE", + 27: "DELETE_FILE", + 28: "GET_FILE_INFO", + 29: "AUTHENTICATE_FILE", + 30: "ABORT_FILE", + 31: "ACTIVATE_CONFIG", + 32: "AUTHENTICATE_REQ", + 33: "AUTH_REQ_NO_ACK", + 129: "RESPONSE", + 130: "UNSOLICITED_RESPONSE", + 131: "AUTHENTICATE_RESP", +} + + +class DNP3RequestDataObjects(Packet): + fields_desc = [ + BitField("Obj", 1, 4), + BitField("Var", 1, 4), + BitField("IndexPref", 1, 4), + BitEnumField("QualfierCode", 1, 4, bitState), + ] + + def extract_padding(self, p): + return "", p + +class DNP3Application(Packet): + def guess_payload_class(self, payload): + return Packet.guess_payload_class(self, payload) + + +class DNP3ApplicationControl(Packet): + fields_desc = [ + BitEnumField("FIN", 1, 1, bitState), + BitEnumField("FIR", 1, 1, bitState), + BitEnumField("CON", 1, 1, bitState), + BitEnumField("UNS", 1, 1, bitState), + BitField("SEQ", 1, 4), + ] + + def extract_padding(self, p): + return "", p + + +class DNP3ApplicationIIN(Packet): + name = "DNP3_Application_response" + fields_desc = [ + BitEnumField("DEVICE_RESTART", UNSET, 1, bitState), + BitEnumField("DEVICE_TROUBLE", UNSET, 1, bitState), + BitEnumField("LOCAL_CONTROL", UNSET, 1, bitState), + BitEnumField("NEED_TIME", UNSET, 1, bitState), + BitEnumField("CLASS_3_EVENTS", UNSET, 1, bitState), + BitEnumField("CLASS_2_EVENTS", UNSET, 1, bitState), + BitEnumField("CLASS_1_EVENTS", UNSET, 1, bitState), + BitEnumField("BROADCAST", UNSET, 1, bitState), + BitEnumField("RESERVED_1", UNSET, 1, bitState), + BitEnumField("RESERVED_2", UNSET, 1, bitState), + BitEnumField("CONFIG_CORRUPT", UNSET, 1, bitState), + BitEnumField("ALREADY_EXECUTING", UNSET, 1, bitState), + BitEnumField("EVENT_BUFFER_OVERFLOW", UNSET, 1, bitState), + BitEnumField("PARAMETER_ERROR", UNSET, 1, bitState), + BitEnumField("OBJECT_UNKNOWN", UNSET, 1, bitState), + BitEnumField("NO_FUNC_CODE_SUPPORT", UNSET, 1, bitState), + ] + + def extract_padding(self, p): + return "", p + +class DNP3ApplicationResponse(DNP3Application): + name = "DNP3_Application_response" + fields_desc = [ + PacketField("Application_control", DNP3ApplicationControl(), DNP3ApplicationControl), + BitEnumField("FUNC_CODE", 1, 8, applicationFunctionCode), + PacketField("IIN", DNP3ApplicationIIN(), DNP3ApplicationIIN), + ] + + def mysummary(self): + if isinstance(self.underlayer.underlayer, DNP3): + print self.FUNC_CODE.SEQ, "Hello" + return self.underlayer.underlayer.sprintf(DNP3_summary + Transport_summary + Application_Rsp_summary) + if isinstance(self.underlayer, DNP3Transport): + return self.underlayer.sprintf(Transport_summary + Application_Rsp_summary) + else: + return self.sprintf(Application_Req_summary) + +class DNP3ApplicationRequest(DNP3Application): + name = "DNP3_Application_request" + fields_desc = [ + PacketField("Application_control", DNP3ApplicationControl(), DNP3ApplicationControl), + BitEnumField("FUNC_CODE", 1, 8, applicationFunctionCode), + ] + + def mysummary(self): + if isinstance(self.underlayer.underlayer, DNP3): + return self.underlayer.underlayer.sprintf(DNP3_summary + Transport_summary + Application_Req_summary) + if isinstance(self.underlayer, DNP3Transport): + return self.underlayer.sprintf(Transport_summary + Application_Req_summary) + else: + return self.sprintf(Application_Req_summary) + + +class DNP3Transport(Packet): + name = "DNP3_Transport" + fields_desc = [ + BitEnumField("FIN", None, 1, bitState), + BitEnumField("FIR", None, 1, bitState), + BitField("SEQUENCE", None, 6), + ] + + def guess_payload_class(self, payload): + + if isinstance(self.underlayer, DNP3): + DIR = self.underlayer.CONTROL.DIR + + if DIR == MASTER: + return DNP3ApplicationRequest + + if DIR == OUTSTATION: + return DNP3ApplicationResponse + else: + return Packet.guess_payload_class(self, payload) + + +class DNP3HeaderControl(Packet): + name = "DNP3_Header_control" + + controlFunctionCodePri = { + 0: "RESET_LINK_STATES", + 2: "TEST_LINK_STATES", + 3: "CONFIRMED_USER_DATA", + 4: "UNCONFIRMED_USER_DATA", + 9: "REQUEST_LINK_STATUS", + } + + controlFunctionCodeSec = { + 0: "ACK", + 1: "NACK", + 11: "LINK_STATUS", + 15: "NOT_SUPPORTED", + } + + cond_field = [ + BitEnumField("FCB", 0, 1, bitState), + BitEnumField("FCV", 0, 1, bitState), + BitEnumField("FUNC_CODE_PRI", 4, 4, controlFunctionCodePri), + BitEnumField("reserved", 0, 1, bitState), + BitEnumField("DFC", 0, 1, bitState), + BitEnumField("FUNC_CODE_SEC", 4, 4, controlFunctionCodeSec), + ] + + fields_desc = [ + BitEnumField("DIR", MASTER, 1, stations), # 9.2.4.1.3.1 DIR bit field + BitEnumField("PRM", MASTER, 1, stations), # 9.2.4.1.3.2 PRM bit field + ConditionalField(cond_field[0], lambda x:x.PRM == MASTER), + ConditionalField(cond_field[1], lambda x:x.PRM == MASTER), + ConditionalField(cond_field[2], lambda x:x.PRM == MASTER), + ConditionalField(cond_field[3], lambda x:x.PRM == OUTSTATION), + ConditionalField(cond_field[4], lambda x:x.PRM == OUTSTATION), + ConditionalField(cond_field[5], lambda x:x.PRM == OUTSTATION), + ] + + def extract_padding(self, p): + return "", p + + +class DNP3(Packet): + name = "DNP3" + fields_desc = [ + XShortField("START", 0x0564), + ByteField("LENGTH", None), + PacketField("CONTROL", None, DNP3HeaderControl), + LEShortField("DESTINATION", None), + LEShortField("SOURCE", None), + XShortField("CRC", None), + ] + + data_chunks = [] # Data Chunks are 16 octets + data_chunks_crc = [] + chunk_len = 18 + data_chunk_len = 16 + + def show_data_chunks(self): + for i in range(len(self.data_chunks)): + print "\tData Chunk", i, "Len", len(self.data_chunks[i]),\ + "CRC (", hex(struct.unpack(' 0: + chunks += 1 + + if pay_len == 3 and self.CONTROL.DIR == MASTER: + + # No IIN in Application layer and empty Payload + pay = pay + struct.pack('H', crcDNP(pay)) + + if pay_len == 5 and self.CONTROL.DIR == OUTSTATION: + + # IIN in Application layer and empty Payload + pay = pay + struct.pack('H', crcDNP(pay)) + + if self.LENGTH is None: + + # Remove length , crc, start octets as part of length + length = (len(pkt+pay) - ((chunks * 2) + 1 + 2 + 2)) + pkt = pkt[:2] + struct.pack(' 0): + self.add_data_chunk(pay[index:]) + break # should be the last chunk + else: + self.add_data_chunk(pay[index:index + cnk_len]) + remaining_pay -= cnk_len + + payload = '' + for chunk in range(len(self.data_chunks)): + payload = payload + self.data_chunks[chunk] + self.data_chunks_crc[chunk] + # self.show_data_chunks() # --DEBUGGING + return pkt+payload + + def guess_payload_class(self, payload): + if len(payload) > 0: + return DNP3Transport + else: + return Packet.guess_payload_class(self, payload) + + +bind_layers(TCP, DNP3, dport=dnp3_port) +bind_layers(TCP, DNP3, sport=dnp3_port) +bind_layers(UDP, DNP3, dport=dnp3_port) +bind_layers(UDP, DNP3, sport=dnp3_port) From bc43aa9a1f52036515458351acb362d83aeec598 Mon Sep 17 00:00:00 2001 From: Noam Rathaus Date: Mon, 9 Feb 2026 12:54:08 +0200 Subject: [PATCH 2/7] Code cleanup and adjust for phython3 --- scapy/contrib/scada/dnp3.py | 79 +++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/scapy/contrib/scada/dnp3.py b/scapy/contrib/scada/dnp3.py index 1e489365bcc..9d60bde387d 100644 --- a/scapy/contrib/scada/dnp3.py +++ b/scapy/contrib/scada/dnp3.py @@ -1,6 +1,12 @@ +import struct + +from scapy.packet import Packet, bind_layers +from scapy.fields import XShortField, LEShortField, \ + BitField, BitEnumField, PacketField, ConditionalField, \ + ByteField +from scapy.layers.inet import TCP, UDP + __author__ = 'Nicholas Rodofile' -from scapy.all import * -# import crcmod.predefined ''' # Copyright 2014-2016 N.R Rodofile @@ -17,6 +23,7 @@ http://www.gnu.org/licenses/. ''' + bitState = {1: "SET", 0: "UNSET"} stations = {1: "MASTER", 0: "OUTSTATION"} @@ -31,11 +38,6 @@ Application_Req_summary = "Request %DNP3ApplicationRequest.FUNC_CODE% " DNP3_summary = "From %DNP3.SOURCE% to %DNP3.DESTINATION% " -''' -Initalise a predefined crc object for DNP3 Cyclic Redundancy Check -Info : http://crcmod.sourceforge.net/crcmod.predefined.html -''' - def _verifyPoly(poly): """ @@ -57,9 +59,8 @@ def _bitrev(x, n): """ Bit reverse the input value. """ - x = x y = 0 - for i in range(n): + for _ in range(n): y = (y << 1) | (x & 1) x = x >> 1 @@ -72,8 +73,6 @@ def _bitrev(x, n): def _bytecrc(crc, poly, n): - crc = crc - poly = poly mask = 1<<(n-1) for i in range(8): if crc & mask: @@ -86,9 +85,7 @@ def _bytecrc(crc, poly, n): return crc def _bytecrc_r(crc, poly, n): - crc = crc - poly = poly - for i in range(8): + for _ in range(8): if crc & 1: crc = (crc >> 1) ^ poly else: @@ -301,8 +298,8 @@ class DNP3RequestDataObjects(Packet): BitEnumField("QualfierCode", 1, 4, bitState), ] - def extract_padding(self, p): - return "", p + def extract_padding(self, s): + return b"", s class DNP3Application(Packet): def guess_payload_class(self, payload): @@ -318,8 +315,8 @@ class DNP3ApplicationControl(Packet): BitField("SEQ", 1, 4), ] - def extract_padding(self, p): - return "", p + def extract_padding(self, s): + return b"", s class DNP3ApplicationIIN(Packet): @@ -343,8 +340,8 @@ class DNP3ApplicationIIN(Packet): BitEnumField("NO_FUNC_CODE_SUPPORT", UNSET, 1, bitState), ] - def extract_padding(self, p): - return "", p + def extract_padding(self, s): + return b"", s class DNP3ApplicationResponse(DNP3Application): name = "DNP3_Application_response" @@ -355,8 +352,8 @@ class DNP3ApplicationResponse(DNP3Application): ] def mysummary(self): - if isinstance(self.underlayer.underlayer, DNP3): - print self.FUNC_CODE.SEQ, "Hello" + if self.underlayer is not None and isinstance(self.underlayer.underlayer, DNP3): + print(self.FUNC_CODE.SEQ, "Hello") return self.underlayer.underlayer.sprintf(DNP3_summary + Transport_summary + Application_Rsp_summary) if isinstance(self.underlayer, DNP3Transport): return self.underlayer.sprintf(Transport_summary + Application_Rsp_summary) @@ -371,8 +368,10 @@ class DNP3ApplicationRequest(DNP3Application): ] def mysummary(self): - if isinstance(self.underlayer.underlayer, DNP3): - return self.underlayer.underlayer.sprintf(DNP3_summary + Transport_summary + Application_Req_summary) + if self.underlayer is not None and isinstance(self.underlayer.underlayer, DNP3): + return self.underlayer.underlayer.sprintf( + DNP3_summary + Transport_summary + Application_Req_summary + ) if isinstance(self.underlayer, DNP3Transport): return self.underlayer.sprintf(Transport_summary + Application_Req_summary) else: @@ -388,7 +387,6 @@ class DNP3Transport(Packet): ] def guess_payload_class(self, payload): - if isinstance(self.underlayer, DNP3): DIR = self.underlayer.CONTROL.DIR @@ -397,8 +395,8 @@ def guess_payload_class(self, payload): if DIR == OUTSTATION: return DNP3ApplicationResponse - else: - return Packet.guess_payload_class(self, payload) + + return Packet.guess_payload_class(self, payload) class DNP3HeaderControl(Packet): @@ -439,8 +437,8 @@ class DNP3HeaderControl(Packet): ConditionalField(cond_field[5], lambda x:x.PRM == OUTSTATION), ] - def extract_padding(self, p): - return "", p + def extract_padding(self, s): + return b"", s class DNP3(Packet): @@ -460,9 +458,9 @@ class DNP3(Packet): data_chunk_len = 16 def show_data_chunks(self): - for i in range(len(self.data_chunks)): - print "\tData Chunk", i, "Len", len(self.data_chunks[i]),\ - "CRC (", hex(struct.unpack(' 0: - chunks += 1 + chunks += 1 if pay_len == 3 and self.CONTROL.DIR == MASTER: - # No IIN in Application layer and empty Payload pay = pay + struct.pack('H', crcDNP(pay)) if pay_len == 5 and self.CONTROL.DIR == OUTSTATION: - # IIN in Application layer and empty Payload pay = pay + struct.pack('H', crcDNP(pay)) if self.LENGTH is None: - # Remove length , crc, start octets as part of length length = (len(pkt+pay) - ((chunks * 2) + 1 + 2 + 2)) pkt = pkt[:2] + struct.pack(' Date: Mon, 9 Feb 2026 12:54:50 +0200 Subject: [PATCH 3/7] Minor style --- scapy/contrib/scada/dnp3.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scapy/contrib/scada/dnp3.py b/scapy/contrib/scada/dnp3.py index 9d60bde387d..6b5609678fe 100644 --- a/scapy/contrib/scada/dnp3.py +++ b/scapy/contrib/scada/dnp3.py @@ -354,7 +354,9 @@ class DNP3ApplicationResponse(DNP3Application): def mysummary(self): if self.underlayer is not None and isinstance(self.underlayer.underlayer, DNP3): print(self.FUNC_CODE.SEQ, "Hello") - return self.underlayer.underlayer.sprintf(DNP3_summary + Transport_summary + Application_Rsp_summary) + return self.underlayer.underlayer.sprintf( + DNP3_summary + Transport_summary + Application_Rsp_summary + ) if isinstance(self.underlayer, DNP3Transport): return self.underlayer.sprintf(Transport_summary + Application_Rsp_summary) else: @@ -490,7 +492,7 @@ def post_build(self, pkt, pay): if self.LENGTH is None: # Remove length , crc, start octets as part of length - length = (len(pkt+pay) - ((chunks * 2) + 1 + 2 + 2)) + length = len(pkt+pay) - ((chunks * 2) + 1 + 2 + 2) pkt = pkt[:2] + struct.pack(' Date: Mon, 9 Feb 2026 13:20:45 +0200 Subject: [PATCH 4/7] Fix issues found during UTS --- scapy/contrib/scada/dnp3.py | 208 +++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 4 deletions(-) diff --git a/scapy/contrib/scada/dnp3.py b/scapy/contrib/scada/dnp3.py index 6b5609678fe..17c9ac270ae 100644 --- a/scapy/contrib/scada/dnp3.py +++ b/scapy/contrib/scada/dnp3.py @@ -38,6 +38,204 @@ Application_Req_summary = "Request %DNP3ApplicationRequest.FUNC_CODE% " DNP3_summary = "From %DNP3.SOURCE% to %DNP3.DESTINATION% " +_codeTemplate = '''// Automatically generated CRC function +// %(poly)s +%(crcType)s +%(name)s(%(dataType)s *data, int len, %(crcType)s crc) +{ + static const %(crcType)s table[256] = {%(crcTable)s + }; + %(preCondition)s + while (len > 0) + { + crc = %(crcAlgor)s; + data++; + len--; + }%(postCondition)s + return crc; +} +''' + +class Crc: + def __init__(self, poly, initCrc=~0, rev=True, xorOut=0, initialize=True): + if not initialize: + # Don't want to perform the initialization when using new or copy + # to create a new instance. + return + + (sizeBits, initCrc, xorOut) = _verifyParams(poly, initCrc, xorOut) + self.digest_size = sizeBits//8 + self.initCrc = initCrc + self.xorOut = xorOut + + self.poly = poly + self.reverse = rev + + (crcfun, table) = _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut) + self._crc = crcfun + self.table = table + + self.crcValue = self.initCrc + + + def __str__(self): + lst = [] + lst.append('poly = 0x%X' % self.poly) + lst.append('reverse = %s' % self.reverse) + fmt = '0x%%0%dX' % (self.digest_size*2) + lst.append('initCrc = %s' % (fmt % self.initCrc)) + lst.append('xorOut = %s' % (fmt % self.xorOut)) + lst.append('crcValue = %s' % (fmt % self.crcValue)) + return '\n'.join(lst) + + def new(self, arg=None): + '''Create a new instance of the Crc class initialized to the same + values as the original instance. The current CRC is set to the initial + value. If a string is provided in the optional arg parameter, it is + passed to the update method. + ''' + n = Crc(poly=None, initialize=False) + n._crc = self._crc + n.digest_size = self.digest_size + n.initCrc = self.initCrc + n.xorOut = self.xorOut + n.table = self.table + n.crcValue = self.initCrc + n.reverse = self.reverse + n.poly = self.poly + if arg is not None: + n.update(arg) + return n + + def copy(self): + '''Create a new instance of the Crc class initialized to the same + values as the original instance. The current CRC is set to the current + value. This allows multiple CRC calculations using a common initial + string. + ''' + c = self.new() + c.crcValue = self.crcValue + return c + + def update(self, data): + '''Update the current CRC value using the string specified as the data + parameter. + ''' + self.crcValue = self._crc(data, self.crcValue) + + def digest(self): + '''Return the current CRC value as a string of bytes. The length of + this string is specified in the digest_size attribute. + ''' + n = self.digest_size + crc = self.crcValue + lst = [] + while n > 0: + lst.append(chr(crc & 0xFF)) + crc = crc >> 8 + n -= 1 + lst.reverse() + return ''.join(lst) + + def hexdigest(self): + '''Return the current CRC value as a string of hex digits. The length + of this string is twice the digest_size attribute. + ''' + n = self.digest_size + crc = self.crcValue + lst = [] + while n > 0: + lst.append('%02X' % (crc & 0xFF)) + crc = crc >> 8 + n -= 1 + lst.reverse() + return ''.join(lst) + + def generateCode(self, functionName, out, dataType=None, crcType=None): + '''Generate a C/C++ function. + + functionName -- String specifying the name of the function. + + out -- An open file-like object with a write method. This specifies + where the generated code is written. + + dataType -- An optional parameter specifying the data type of the input + data to the function. Defaults to UINT8. + + crcType -- An optional parameter specifying the data type of the CRC + value. Defaults to one of UINT8, UINT16, UINT32, or UINT64 depending + on the size of the CRC value. + ''' + if dataType is None: + dataType = 'UINT8' + + if crcType is None: + size = 8*self.digest_size + if size == 24: + size = 32 + crcType = 'UINT%d' % size + + if self.digest_size == 1: + # Both 8-bit CRC algorithms are the same + crcAlgor = 'table[*data ^ (%s)crc]' + elif self.reverse: + # The bit reverse algorithms are all the same except for the data + # type of the crc variable which is specified elsewhere. + crcAlgor = 'table[*data ^ (%s)crc] ^ (crc >> 8)' + else: + # The forward CRC algorithms larger than 8 bits have an extra shift + # operation to get the high byte. + shift = 8*(self.digest_size - 1) + crcAlgor = 'table[*data ^ (%%s)(crc >> %d)] ^ (crc << 8)' % shift + + fmt = '0x%%0%dX' % (2*self.digest_size) + if self.digest_size <= 4: + fmt = fmt + 'U,' + else: + # Need the long long type identifier to keep gcc from complaining. + fmt = fmt + 'ULL,' + + # Select the number of entries per row in the output code. + n = {1:8, 2:8, 3:4, 4:4, 8:2}[self.digest_size] + + lst = [] + for i, val in enumerate(self.table): + if (i % n) == 0: + lst.append('\n ') + lst.append(fmt % val) + + poly = 'polynomial: 0x%X' % self.poly + if self.reverse: + poly = poly + ', bit reverse algorithm' + + if self.xorOut: + # Need to remove the comma from the format. + preCondition = '\n crc = crc ^ %s;' % (fmt[:-1] % self.xorOut) + postCondition = preCondition + else: + preCondition = '' + postCondition = '' + + if self.digest_size == 3: + # The 24-bit CRC needs to be conditioned so that only 24-bits are + # used from the 32-bit variable. + if self.reverse: + preCondition += '\n crc = crc & 0xFFFFFFU;' + else: + postCondition += '\n crc = crc & 0xFFFFFFU;' + + + parms = { + 'dataType' : dataType, + 'crcType' : crcType, + 'name' : functionName, + 'crcAlgor' : crcAlgor % dataType, + 'crcTable' : ''.join(lst), + 'poly' : poly, + 'preCondition' : preCondition, + 'postCondition' : postCondition, + } + out.write(_codeTemplate % parms) def _verifyPoly(poly): """ @@ -119,14 +317,16 @@ def _mkTable_r(poly, n): def _crc16r(data, crc, table): crc = crc & 0xFFFF + for x in data: - crc = table[ord(x) ^ (crc & 0xFF)] ^ (crc >> 8) + crc = table[x ^ (crc & 0xFF)] ^ (crc >> 8) + return crc def _crc16(data, crc, table): crc = crc & 0xFFFF for x in data: - crc = table[ord(x) ^ ((crc>>8) & 0xFF)] ^ ((crc << 8) & 0xFF00) + crc = table[x ^ ((crc>>8) & 0xFF)] ^ ((crc << 8) & 0xFF00) return crc @@ -217,7 +417,7 @@ def crcfun(data, crc=initCrc): return _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut)[0] def crcDNP(data): - crc16DNP = mkCrcFun('crc-16-dnp') + crc16DNP = mkCrcFun(0x13D65) # 'crc-16-dnp' return crc16DNP(data) @@ -518,7 +718,7 @@ def post_build(self, pkt, pay): self.add_data_chunk(pay[index:index + cnk_len]) remaining_pay -= cnk_len - payload = '' + payload = b'' for chunk, data_chunk in enumerate(self.data_chunks): payload = payload + data_chunk + self.data_chunks_crc[chunk] # self.show_data_chunks() # --DEBUGGING From a2d2ac5a6d430478c6527dda481e678fea0613b0 Mon Sep 17 00:00:00 2001 From: Noam Rathaus Date: Wed, 18 Feb 2026 10:59:47 +0200 Subject: [PATCH 5/7] Reduce CRC calculation complexity --- scapy/contrib/scada/dnp3.py | 534 ++++++------------------------------ 1 file changed, 90 insertions(+), 444 deletions(-) diff --git a/scapy/contrib/scada/dnp3.py b/scapy/contrib/scada/dnp3.py index 17c9ac270ae..66595629014 100644 --- a/scapy/contrib/scada/dnp3.py +++ b/scapy/contrib/scada/dnp3.py @@ -1,28 +1,34 @@ -import struct - -from scapy.packet import Packet, bind_layers -from scapy.fields import XShortField, LEShortField, \ - BitField, BitEnumField, PacketField, ConditionalField, \ - ByteField -from scapy.layers.inet import TCP, UDP - -__author__ = 'Nicholas Rodofile' +""" +DNP3 (Distributed Network Protocol 3). -''' -# Copyright 2014-2016 N.R Rodofile +Original code by: Copyright 2014-2016 N.R Rodofile Licensed under the GPLv3. -This program is free software: you can redistribute it and/or modify it under the terms -of the GNU General Public License as published by the Free Software Foundation, either +This program is free software: you can redistribute it and/or modify it under the terms +of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -See the GNU General Public License for more details. You should have received a copy of -the GNU General Public License along with this program. If not, see +See the GNU General Public License for more details. You should have received a copy of +the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. -''' +""" + +import struct + +from scapy.packet import Packet, bind_layers +from scapy.fields import ( + XShortField, + LEShortField, + BitField, + BitEnumField, + PacketField, + ConditionalField, + ByteField, +) +from scapy.layers.inet import TCP, UDP bitState = {1: "SET", 0: "UNSET"} stations = {1: "MASTER", 0: "OUTSTATION"} @@ -31,408 +37,39 @@ OUTSTATION = 0 SET = 1 UNSET = 0 -dnp3_port = 20000 - -Transport_summary = "Seq:%DNP3Transport.SEQUENCE% " -Application_Rsp_summary = "Response %DNP3ApplicationResponse.FUNC_CODE% " -Application_Req_summary = "Request %DNP3ApplicationRequest.FUNC_CODE% " -DNP3_summary = "From %DNP3.SOURCE% to %DNP3.DESTINATION% " - -_codeTemplate = '''// Automatically generated CRC function -// %(poly)s -%(crcType)s -%(name)s(%(dataType)s *data, int len, %(crcType)s crc) -{ - static const %(crcType)s table[256] = {%(crcTable)s - }; - %(preCondition)s - while (len > 0) - { - crc = %(crcAlgor)s; - data++; - len--; - }%(postCondition)s - return crc; -} -''' - -class Crc: - def __init__(self, poly, initCrc=~0, rev=True, xorOut=0, initialize=True): - if not initialize: - # Don't want to perform the initialization when using new or copy - # to create a new instance. - return - - (sizeBits, initCrc, xorOut) = _verifyParams(poly, initCrc, xorOut) - self.digest_size = sizeBits//8 - self.initCrc = initCrc - self.xorOut = xorOut - - self.poly = poly - self.reverse = rev - - (crcfun, table) = _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut) - self._crc = crcfun - self.table = table - - self.crcValue = self.initCrc - - - def __str__(self): - lst = [] - lst.append('poly = 0x%X' % self.poly) - lst.append('reverse = %s' % self.reverse) - fmt = '0x%%0%dX' % (self.digest_size*2) - lst.append('initCrc = %s' % (fmt % self.initCrc)) - lst.append('xorOut = %s' % (fmt % self.xorOut)) - lst.append('crcValue = %s' % (fmt % self.crcValue)) - return '\n'.join(lst) - - def new(self, arg=None): - '''Create a new instance of the Crc class initialized to the same - values as the original instance. The current CRC is set to the initial - value. If a string is provided in the optional arg parameter, it is - passed to the update method. - ''' - n = Crc(poly=None, initialize=False) - n._crc = self._crc - n.digest_size = self.digest_size - n.initCrc = self.initCrc - n.xorOut = self.xorOut - n.table = self.table - n.crcValue = self.initCrc - n.reverse = self.reverse - n.poly = self.poly - if arg is not None: - n.update(arg) - return n - - def copy(self): - '''Create a new instance of the Crc class initialized to the same - values as the original instance. The current CRC is set to the current - value. This allows multiple CRC calculations using a common initial - string. - ''' - c = self.new() - c.crcValue = self.crcValue - return c - - def update(self, data): - '''Update the current CRC value using the string specified as the data - parameter. - ''' - self.crcValue = self._crc(data, self.crcValue) - - def digest(self): - '''Return the current CRC value as a string of bytes. The length of - this string is specified in the digest_size attribute. - ''' - n = self.digest_size - crc = self.crcValue - lst = [] - while n > 0: - lst.append(chr(crc & 0xFF)) - crc = crc >> 8 - n -= 1 - lst.reverse() - return ''.join(lst) - - def hexdigest(self): - '''Return the current CRC value as a string of hex digits. The length - of this string is twice the digest_size attribute. - ''' - n = self.digest_size - crc = self.crcValue - lst = [] - while n > 0: - lst.append('%02X' % (crc & 0xFF)) - crc = crc >> 8 - n -= 1 - lst.reverse() - return ''.join(lst) - - def generateCode(self, functionName, out, dataType=None, crcType=None): - '''Generate a C/C++ function. - - functionName -- String specifying the name of the function. - - out -- An open file-like object with a write method. This specifies - where the generated code is written. - - dataType -- An optional parameter specifying the data type of the input - data to the function. Defaults to UINT8. - - crcType -- An optional parameter specifying the data type of the CRC - value. Defaults to one of UINT8, UINT16, UINT32, or UINT64 depending - on the size of the CRC value. - ''' - if dataType is None: - dataType = 'UINT8' - - if crcType is None: - size = 8*self.digest_size - if size == 24: - size = 32 - crcType = 'UINT%d' % size - - if self.digest_size == 1: - # Both 8-bit CRC algorithms are the same - crcAlgor = 'table[*data ^ (%s)crc]' - elif self.reverse: - # The bit reverse algorithms are all the same except for the data - # type of the crc variable which is specified elsewhere. - crcAlgor = 'table[*data ^ (%s)crc] ^ (crc >> 8)' - else: - # The forward CRC algorithms larger than 8 bits have an extra shift - # operation to get the high byte. - shift = 8*(self.digest_size - 1) - crcAlgor = 'table[*data ^ (%%s)(crc >> %d)] ^ (crc << 8)' % shift - - fmt = '0x%%0%dX' % (2*self.digest_size) - if self.digest_size <= 4: - fmt = fmt + 'U,' - else: - # Need the long long type identifier to keep gcc from complaining. - fmt = fmt + 'ULL,' - - # Select the number of entries per row in the output code. - n = {1:8, 2:8, 3:4, 4:4, 8:2}[self.digest_size] - - lst = [] - for i, val in enumerate(self.table): - if (i % n) == 0: - lst.append('\n ') - lst.append(fmt % val) - - poly = 'polynomial: 0x%X' % self.poly - if self.reverse: - poly = poly + ', bit reverse algorithm' - - if self.xorOut: - # Need to remove the comma from the format. - preCondition = '\n crc = crc ^ %s;' % (fmt[:-1] % self.xorOut) - postCondition = preCondition - else: - preCondition = '' - postCondition = '' - - if self.digest_size == 3: - # The 24-bit CRC needs to be conditioned so that only 24-bits are - # used from the 32-bit variable. - if self.reverse: - preCondition += '\n crc = crc & 0xFFFFFFU;' +DNP3_PORT = 20000 + +TRANSPORT_SUMMARY = "Seq:%DNP3Transport.SEQUENCE% " +APPLICATION_RSP_SUMMARY = "Response %DNP3ApplicationResponse.FUNC_CODE% " +APPLICATION_REQ_SUMMARY = "Request %DNP3ApplicationRequest.FUNC_CODE% " +DNP3_SUMMARY = "From %DNP3.SOURCE% to %DNP3.DESTINATION% " + + +def crc16_dnp3(data): + """DNP3 CRC-16 calculation""" + crc = 0 + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA6BC else: - postCondition += '\n crc = crc & 0xFFFFFFU;' - - - parms = { - 'dataType' : dataType, - 'crcType' : crcType, - 'name' : functionName, - 'crcAlgor' : crcAlgor % dataType, - 'crcTable' : ''.join(lst), - 'poly' : poly, - 'preCondition' : preCondition, - 'postCondition' : postCondition, - } - out.write(_codeTemplate % parms) - -def _verifyPoly(poly): - """ - Check the polynomial to make sure that it is acceptable and return the number - of bits in the CRC. - """ - - msg = 'The degree of the polynomial must be 8, 16, 24, 32 or 64' - for n in (8,16,24,32,64): - low = 1<> 1 - - return y - -#----------------------------------------------------------------------------- -# The following functions compute the CRC for a single byte. These are used -# to build up the tables needed in the CRC algorithm. Assumes the high order -# bit of the polynomial has been stripped off. - - -def _bytecrc(crc, poly, n): - mask = 1<<(n-1) - for i in range(8): - if crc & mask: - crc = (crc << 1) ^ poly - else: - crc = crc << 1 - mask = (1<> 1) ^ poly - else: - crc = crc >> 1 - - mask = (1<>= 1 return crc -#----------------------------------------------------------------------------- -# The following functions compute the table needed to compute the CRC. The -# table is returned as a list. Note that the array module does not support -# 64-bit integers on a 32-bit architecture as of Python 2.3. -# -# These routines assume that the polynomial and the number of bits in the CRC -# have been checked for validity by the caller. - -def _mkTable(poly, n): - mask = (1<> 8) - - return crc - -def _crc16(data, crc, table): - crc = crc & 0xFFFF - for x in data: - crc = table[x ^ ((crc>>8) & 0xFF)] ^ ((crc << 8) & 0xFF00) - - return crc - -_sizeMap = { - 16 : [_crc16, _crc16r], -} - - -_sizeToTypeCode = {} - -for typeCode in 'B H I L Q'.split(): - size = {1:8, 2:16, 4:32, 8:64}.get(struct.calcsize(typeCode),None) - if size is not None and size not in _sizeToTypeCode: - _sizeToTypeCode[size] = '256%s' % typeCode - -_sizeToTypeCode[24] = _sizeToTypeCode[32] - -# Use Python3 based implementation for CRC and not third-party -_usingExtension = False - -def _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut): - """ - The following function returns a Python function to compute the CRC. - - It must be passed parameters that are already verified & sanitized by - _verifyParams(). - - The returned function calls a low level function that is written in C if the - extension module could be loaded. Otherwise, a Python implementation is - used. - - In addition to this function, a list containing the CRC table is returned. - """ - if rev: - tableList = _mkTable_r(poly, sizeBits) - _fun = _sizeMap[sizeBits][1] - else: - tableList = _mkTable(poly, sizeBits) - _fun = _sizeMap[sizeBits][0] - - _table = tableList - if _usingExtension: - _table = struct.pack(_sizeToTypeCode[sizeBits], *tableList) - - if xorOut == 0: - def crcfun(data, crc=initCrc, table=_table, fun=_fun): - return fun(data, crc, table) - else: - def crcfun(data, crc=initCrc, table=_table, fun=_fun): - return xorOut ^ fun(data, xorOut ^ crc, table) - - return crcfun, tableList - - -def _verifyParams(poly, initCrc, xorOut): - """ - The following function validates the parameters of the CRC, namely, - poly, and initial/final XOR values. - It returns the size of the CRC (in bits), and "sanitized" initial/final XOR values. - """ - sizeBits = _verifyPoly(poly) - - mask = (1< 0: @@ -731,7 +377,7 @@ def guess_payload_class(self, payload): return Packet.guess_payload_class(self, payload) -bind_layers(TCP, DNP3, dport=dnp3_port) -bind_layers(TCP, DNP3, sport=dnp3_port) -bind_layers(UDP, DNP3, dport=dnp3_port) -bind_layers(UDP, DNP3, sport=dnp3_port) +bind_layers(TCP, DNP3, dport=DNP3_PORT) +bind_layers(TCP, DNP3, sport=DNP3_PORT) +bind_layers(UDP, DNP3, dport=DNP3_PORT) +bind_layers(UDP, DNP3, sport=DNP3_PORT) From d44f7a8df0f78432933f9d213825e304694976d0 Mon Sep 17 00:00:00 2001 From: Noam Rathaus Date: Wed, 18 Feb 2026 11:00:22 +0200 Subject: [PATCH 6/7] Comment debug code --- scapy/contrib/scada/dnp3.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/scapy/contrib/scada/dnp3.py b/scapy/contrib/scada/dnp3.py index 66595629014..a2fac54426f 100644 --- a/scapy/contrib/scada/dnp3.py +++ b/scapy/contrib/scada/dnp3.py @@ -303,13 +303,13 @@ class DNP3(Packet): chunk_len = 18 data_chunk_len = 16 - def show_data_chunks(self): - for i, data_chunk in enumerate(self.data_chunks): - print( - f"\tData Chunk {i}, Len {len(data_chunk)}, " "CRC (", - hex(struct.unpack(" Date: Wed, 18 Feb 2026 11:52:44 +0200 Subject: [PATCH 7/7] A test for DNP3 --- test/contrib/dnp3.uts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/contrib/dnp3.uts diff --git a/test/contrib/dnp3.uts b/test/contrib/dnp3.uts new file mode 100644 index 00000000000..86325b1e8d3 --- /dev/null +++ b/test/contrib/dnp3.uts @@ -0,0 +1,30 @@ +% DNP3 regression tests for Scapy + + +############ +############ ++ DNP3 + += DNP3 + +s = b"\x05\x64\x0b\xc4\x04\x00\x03\x00\xe4\x2b\xdb\xcc\x01\xff\x00\x06\xad\xa0" +p = DNP3(s) +assert p.LENGTH == 11 +assert p.CONTROL.DIR == 1 # Direction: Set +assert p.CONTROL.PRM == 1 # Primary: Set +assert p.CONTROL.FCB == 0 # FCB: Not set +assert p.CONTROL.FCV == 0 # FCB: Not set + +assert p.CONTROL.FUNC_CODE_PRI == 4 # Control Function Code: Unconfirmed User Data -> 4 + +assert p.DESTINATION == 4 +assert p.SOURCE == 3 + +assert p.CRC == 0xe42b + +assert p[2].FIN == 1 # Final: Set +assert p[2].FIR == 1 # First: Set +assert p[2].SEQUENCE == 27 # Sequence: 27 + + +