Ouverture du repo avec les fichiers existants du decode
Ajout des deux fichiers principaux:
- decode_modbus.json
> fichier de configuration du script python
- decode_modbus.py
> script python (version 3.9+)
This commit is contained in:
226
decode_modbus.json
Normal file
226
decode_modbus.json
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
{
|
||||||
|
"log_file": "logs\\failover_day_09042025\\test-scada-s3tos1-views3.pcapng",
|
||||||
|
"ip_ats": "10.110.1.142",
|
||||||
|
"ip_srvr": "10.113.23.6",
|
||||||
|
"scada_ene": true,
|
||||||
|
"scada_eqpt": false,
|
||||||
|
"plc": false,
|
||||||
|
"registersNames": {
|
||||||
|
"1000": "TCD_900V_774 (Beekkant )",
|
||||||
|
"1001": "TCD_900V_29 (Etangs Noirs )",
|
||||||
|
"1002": "TCD_900V_28 (Cte de Flandre )",
|
||||||
|
"1003": "TCD_900V_27 (St Catherine )",
|
||||||
|
"1004": "TCD_900V_01 (De Brouckere L1 )",
|
||||||
|
"1005": "TCD_900V_02 (Centrale )",
|
||||||
|
"1006": "TCD_900V_03 (Parc )",
|
||||||
|
"1007": "TCD_900V_04 (Arts-Loi L1 )",
|
||||||
|
"1008": "TCD_900V_05 (Maelbeek )",
|
||||||
|
"1009": "TCD_900V_06A (Schuman A )",
|
||||||
|
"1010": "TCD_900V_07A (Merode A )",
|
||||||
|
"1011": "TCD_900V_08 (Montgomery L1 )",
|
||||||
|
"1012": "TCD_900V_09 (Josephine Charlotte)",
|
||||||
|
"1013": "TCD_900V_10 (Gribaumont )",
|
||||||
|
"1014": "TCD_900V_11 (Tomberg )",
|
||||||
|
"1015": "TCD_900V_12 (Roodebeek )",
|
||||||
|
"1016": "TCD_900V_13 (Vandervelde )",
|
||||||
|
"1017": "TCD_900V_14 (Alma )",
|
||||||
|
"1018": "TCD_900V_15 (Kraainem )",
|
||||||
|
"1019": "TCD_900V_16 (Stockel )",
|
||||||
|
"1020": "TCD_900V_20 (Thieffry )",
|
||||||
|
"1021": "TCD_900V_21 (Petillon )",
|
||||||
|
"1022": "TCD_900V_22 (Hankar )",
|
||||||
|
"1023": "TCD_900V_23 (Delta )",
|
||||||
|
"1024": "TCD_900V_24 (Beaulieu )",
|
||||||
|
"1025": "TCD_900V_25 (Demey )",
|
||||||
|
"1026": "TCD_900V_26 (H.Debroux )",
|
||||||
|
"1027": "TCD_900V_773 (G. Ouest E.O. )",
|
||||||
|
"1028": "TCD_900V_772 (J. Brel )",
|
||||||
|
"1029": "TCD_900V_771 (Aumale )",
|
||||||
|
"1030": "TCD_900V_770 (Saint Guidon )",
|
||||||
|
"1031": "TCD_900V_769 (Veeweyde )",
|
||||||
|
"1032": "TCD_900V_768 (Bizet )",
|
||||||
|
"1033": "TCD_900V_767 (La Roue )",
|
||||||
|
"1034": "TCD_900V_766 (CERIA )",
|
||||||
|
"1035": "TCD_900V_765 (E.Merckx )",
|
||||||
|
"1036": "TCD_900V_764 (Erasme )",
|
||||||
|
"1037": "TCD_900V_775 (L2-6 - Osseghem )",
|
||||||
|
"1038": "TCD_900V_776 (L2-6 - Simonis EO )",
|
||||||
|
"1039": "TCD_900V_777 (L2-6 - Belgica )",
|
||||||
|
"1040": "TCD_900V_778 (L2-6 - Pannenhuis )",
|
||||||
|
"1041": "TCD_900V_779 (L2-6 - Bockstael )",
|
||||||
|
"1042": "TCD_900V_780 (L2-6 - Stuyvenberg )",
|
||||||
|
"1043": "TCD_900V_781 (L2-6 - H. Brugmann )",
|
||||||
|
"1044": "TCD_900V_782 (L2-6 - Heysel )",
|
||||||
|
"1045": "TCD_900V_783 (L2-6 - R. Baudouin )",
|
||||||
|
"2000": "SIMONET (A) ",
|
||||||
|
"2001": "SIMONET (B) ",
|
||||||
|
"2002": "VOGELZANG (A) ",
|
||||||
|
"2003": "VOGELZANG (B) ",
|
||||||
|
"2004": "EDELWEISS (A) ",
|
||||||
|
"2005": "EDELWEISS (B) ",
|
||||||
|
"2006": "GRYSON ",
|
||||||
|
"2007": "DEBUSSY (A) ",
|
||||||
|
"2008": "DEBUSSY (B) ",
|
||||||
|
"2009": "DEBUSSY (C) ",
|
||||||
|
"2010": "DELEERS (A) ",
|
||||||
|
"2011": "DELEERS (B) ",
|
||||||
|
"2012": "DOUVRES (A) ",
|
||||||
|
"2013": "DOUVRES (B) ",
|
||||||
|
"2014": "JANSON (A) ",
|
||||||
|
"2015": "JANSON (B) ",
|
||||||
|
"2016": "SCHEUT (A) ",
|
||||||
|
"2017": "SCHEUT (B) ",
|
||||||
|
"2018": "MALHERBE (A) ",
|
||||||
|
"2019": "MALHERBE (B) ",
|
||||||
|
"2020": "MALHERBE (C) ",
|
||||||
|
"2021": "PIERMEZ (A) ",
|
||||||
|
"2022": "PIERMEZ (B) ",
|
||||||
|
"2023": "MENIN (A) ",
|
||||||
|
"2024": "MENIN (B) ",
|
||||||
|
"2025": "MENIN (C) ",
|
||||||
|
"2026": "MOMAERTS ",
|
||||||
|
"2027": "TAZIEAUX ",
|
||||||
|
"2028": "EVEQUE ",
|
||||||
|
"2029": "Q. BRIQUES ",
|
||||||
|
"2030": "ECUYER ",
|
||||||
|
"2031": "ARENBERG ",
|
||||||
|
"2032": "DUCALE ",
|
||||||
|
"2033": "COLONIES (A) ",
|
||||||
|
"2034": "COLONIES (B) ",
|
||||||
|
"2035": "CHARLEMAGNE (A)",
|
||||||
|
"2036": "CHARLEMAGNE (B)",
|
||||||
|
"2037": "CHARLEMAGNE (C)",
|
||||||
|
"2038": "CHARLEMAGNE (D)",
|
||||||
|
"2039": "TREVES (A) ",
|
||||||
|
"2040": "TREVES (B) ",
|
||||||
|
"2041": "TREVES (C) ",
|
||||||
|
"2042": "P.TERVUERN (A) ",
|
||||||
|
"2043": "P.TERVUERN (B) ",
|
||||||
|
"2044": "50NAIR (A) ",
|
||||||
|
"2045": "50NAIR (B) ",
|
||||||
|
"2046": "MENAPIENS (A) ",
|
||||||
|
"2047": "MENAPIENS (B) ",
|
||||||
|
"2048": "MENAPIENS (C) ",
|
||||||
|
"2049": "BATAVES (A) ",
|
||||||
|
"2050": "BATAVES (B) ",
|
||||||
|
"2051": "BATAVES (C) ",
|
||||||
|
"2052": "BATAVES (D) ",
|
||||||
|
"2053": "ROI CHEVALIER ",
|
||||||
|
"2054": "DE BROQUEVILLE ",
|
||||||
|
"2055": "SOLLEVELD (A) ",
|
||||||
|
"2056": "SOLLEVELD (B) ",
|
||||||
|
"2057": "*Reserved* ",
|
||||||
|
"2058": "VERVLOESEM (A) ",
|
||||||
|
"2059": "VERVLOESEM (B) ",
|
||||||
|
"2060": "*Reserved* ",
|
||||||
|
"2061": "MOUNIER (A) ",
|
||||||
|
"2062": "MOUNIER (B) ",
|
||||||
|
"2063": "ASSOMPTION (A) ",
|
||||||
|
"2064": "ASSOMPTION (B) ",
|
||||||
|
"2065": "BORNIVAL (A) ",
|
||||||
|
"2066": "BORNIVAL (B) ",
|
||||||
|
"2067": "BORNIVAL (C) ",
|
||||||
|
"2068": "AOUT (A) ",
|
||||||
|
"2069": "AOUT (B) ",
|
||||||
|
"2070": "DE DEKEN (A) ",
|
||||||
|
"2071": "DE DEKEN (B) ",
|
||||||
|
"2072": "LEBON (A) ",
|
||||||
|
"2073": "LEBON (B) ",
|
||||||
|
"2074": "LEBON (C) ",
|
||||||
|
"2075": "LEBON (D) ",
|
||||||
|
"2076": "PAEPEDELLE ",
|
||||||
|
"2077": "BELLE-VUE ",
|
||||||
|
"2078": "MEUNIERS (A) ",
|
||||||
|
"2079": "MEUNIERS (B) ",
|
||||||
|
"2080": "POELS (A) ",
|
||||||
|
"2081": "POELS (B) ",
|
||||||
|
"2082": "WYBRAN ",
|
||||||
|
"2083": "BOLET ",
|
||||||
|
"2084": "REMISE 5 ",
|
||||||
|
"2085": "REMISE 6 ",
|
||||||
|
"2500": "REMISE 5 ",
|
||||||
|
"2501": "REMISE 5 TCV 11",
|
||||||
|
"2502": "REMISE 5 TCV 31",
|
||||||
|
"2503": "REMISE 5 TCV 32",
|
||||||
|
"2504": "REMISE 5 TCV 33",
|
||||||
|
"2505": "REMISE 6 ",
|
||||||
|
"2506": "REMISE 6 TCV 41",
|
||||||
|
"2507": "REMISE 6 TCV 42",
|
||||||
|
"2508": "REMISE 6 TCV43A",
|
||||||
|
"2509": "REMISE 6 TCV43B",
|
||||||
|
"2510": "40AINE ",
|
||||||
|
"2511": "40AINE TCV 51 ",
|
||||||
|
"2512": "40AINE TCV 52A ",
|
||||||
|
"2513": "40AINE TCV 52B ",
|
||||||
|
"2514": "40AINE TCV 54 ",
|
||||||
|
"2515": "40AINE TCV 55 ",
|
||||||
|
"2516": "SCHOLLE ",
|
||||||
|
"2517": "TEST TRACK ",
|
||||||
|
"2600": "REMISE 2 TCV 1 ",
|
||||||
|
"2601": "REMISE 2 TCV 2 ",
|
||||||
|
"2602": "REMISE 2 TCV 3 ",
|
||||||
|
"2603": "REMISE 2 TCV 4 ",
|
||||||
|
"2604": "REMISE 2 TCV 5 ",
|
||||||
|
"2605": "REMISE 2 TCV 6 ",
|
||||||
|
"2606": "VOIE D'ESSAIS ",
|
||||||
|
"2607": "REMISE 1 TCV 7 ",
|
||||||
|
"2608": "REMISE 1 TCV 8 ",
|
||||||
|
"2609": "REMISE 1 TCV 9 ",
|
||||||
|
"2610": "REMISE 1 TCV 10",
|
||||||
|
"2611": "REMISE 1 TCV 11",
|
||||||
|
"2612": "REMISE 1 TCV 12",
|
||||||
|
"2613": "PEIGNE A ",
|
||||||
|
"2614": "PEIGNE B ",
|
||||||
|
"2615": "PEIGNE C ",
|
||||||
|
"2616": "PEIGNE D ",
|
||||||
|
"2617": "PEIGNE E TCV 30",
|
||||||
|
"2618": "PEIGNE F ",
|
||||||
|
"2619": "PEIGNE G TCV 13",
|
||||||
|
"2620": "PEIGNE H ",
|
||||||
|
"2621": "PEIGNE I ",
|
||||||
|
"2622": "PEIGNE J ",
|
||||||
|
"2623": "PEIGNE K ",
|
||||||
|
"2624": "*Reserved* ",
|
||||||
|
"2625": "FOSSES TCV 16 ",
|
||||||
|
"2626": "FOSSES TCV 17 ",
|
||||||
|
"2627": "FOSSES TCV 18 ",
|
||||||
|
"2628": "FOSSES TCV 19 ",
|
||||||
|
"2629": "FOSSES TCV 20 ",
|
||||||
|
"2630": "FOSSES TCV 21 ",
|
||||||
|
"2631": "FOSSES TCV 22 ",
|
||||||
|
"2632": "FOSSES TCV 23 ",
|
||||||
|
"2633": "FOSSES TCV 24 ",
|
||||||
|
"2634": "FOSSES TCV 25 ",
|
||||||
|
"2635": "*Reserved* ",
|
||||||
|
"10004": "MERODE_TC ",
|
||||||
|
"10005": "ALMA_TC ",
|
||||||
|
"10006": "MONTGOMERY_TC ",
|
||||||
|
"10007": "THIEFFRY_TC ",
|
||||||
|
"10008": "PETILLON_TC ",
|
||||||
|
"10009": "DELTA_TC ",
|
||||||
|
"10010": "TOMBERG_TC ",
|
||||||
|
"10011": "ST-CATHERINE_TC ",
|
||||||
|
"10012": "NOT_USED ",
|
||||||
|
"10013": "NOT_USED ",
|
||||||
|
"10014": "COMTE_DE_FLANDRE_TC",
|
||||||
|
"10015": "AUMALE_TC ",
|
||||||
|
"10016": "NOT_USED ",
|
||||||
|
"10017": "NOT_USED ",
|
||||||
|
"10018": "BREL_TC ",
|
||||||
|
"11004": "MERODE_TS ",
|
||||||
|
"11005": "ALMA_TS ",
|
||||||
|
"11006": "MONTGOMERY_TS ",
|
||||||
|
"11007": "THIEFFRY_TS ",
|
||||||
|
"11008": "PETILLON_TS ",
|
||||||
|
"11009": "DELTA_TS ",
|
||||||
|
"11010": "TOMBERG_TS ",
|
||||||
|
"11011": "ST-CATHERINE_TS ",
|
||||||
|
"11012": "NOT_USED ",
|
||||||
|
"11013": "NOT_USED ",
|
||||||
|
"11014": "COMTE_DE_FLANDRE_TS",
|
||||||
|
"11015": "AUMALE_TS ",
|
||||||
|
"11016": "NOT_USED ",
|
||||||
|
"11017": "NOT_USED ",
|
||||||
|
"11018": "BREL_TS "
|
||||||
|
}
|
||||||
|
}
|
||||||
303
decode_modbus.py
Normal file
303
decode_modbus.py
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import os
|
||||||
|
from scapy.all import *
|
||||||
|
os.system('cls')
|
||||||
|
from art import *
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
def __init__(self, file):
|
||||||
|
self.file = file
|
||||||
|
with open(self.file) as f:
|
||||||
|
self.data = json.load(f)
|
||||||
|
self.log_file = self.data['log_file']
|
||||||
|
self.output_file = self.data['log_file']+'.out'
|
||||||
|
self.ip_ats = self.data['ip_ats']
|
||||||
|
self.ip_srvr = self.data['ip_srvr']
|
||||||
|
self.scada_ene = self.data['scada_ene']
|
||||||
|
self.scada_eqpt = self.data['scada_eqpt']
|
||||||
|
self.plc = self.data['plc']
|
||||||
|
self.registersNames = self.data['registersNames']
|
||||||
|
|
||||||
|
class ModbusPacket:
|
||||||
|
def __init__(self, isTx, src, dst, src_p, dst_p, infotime, data, registersNames, scada_ene, scada_eqpt, plc):
|
||||||
|
self.isTx = isTx
|
||||||
|
self.src = src
|
||||||
|
self.dst = dst
|
||||||
|
self.src_p = src_p
|
||||||
|
self.dst_p = dst_p
|
||||||
|
self.infotime = infotime
|
||||||
|
self.data = data
|
||||||
|
self.registersNames = registersNames
|
||||||
|
self.scada_ene = scada_ene
|
||||||
|
self.scada_eqpt = scada_eqpt
|
||||||
|
self.plc = plc
|
||||||
|
self.transaction_id = None
|
||||||
|
self.proto_id = None
|
||||||
|
self.length = None
|
||||||
|
self.size = None
|
||||||
|
self.addr_slave = None
|
||||||
|
self.function_id = None
|
||||||
|
self.nb_octets = None
|
||||||
|
self.registers = None
|
||||||
|
self.fst_register = None
|
||||||
|
self.nb_registers = None
|
||||||
|
self.request = None
|
||||||
|
self.extractData()
|
||||||
|
def __str__(self):
|
||||||
|
txt = str(self.infotime)+": "+self.src+"["+self.src_p+"] -> "+self.dst+"["+self.dst_p+"]\n"
|
||||||
|
txt += "PACKET: "+' '.join(self.data[i:i+2] for i in range(0, len(self.data), 2))+"\n"
|
||||||
|
if self.isTx:
|
||||||
|
txt += "Modbus TX:{\n"
|
||||||
|
txt += "\ttransaction_id = "+str(self.transaction_id)+"\n"
|
||||||
|
txt += "\tproto_id = "+str(self.proto_id)+"\n"
|
||||||
|
txt += "\tlength (bytes) = "+str(self.length)+"\n"
|
||||||
|
txt += "\taddr_slave = "+str(self.addr_slave)+"\n"
|
||||||
|
if self.function_id == 1:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Read Coil\n"
|
||||||
|
elif self.function_id == 2:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Read Discrete Input\n"
|
||||||
|
elif self.function_id == 3:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Read Holding Registers\n"
|
||||||
|
txt += "\tfst_r_register = "+str(self.fst_register)+"\n"
|
||||||
|
txt += "\tnb_registers = "+str(self.nb_registers)+"\n"
|
||||||
|
elif self.function_id == 4:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Read Input Registers\n"
|
||||||
|
elif self.function_id == 5:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Write Single Coil\n"
|
||||||
|
elif self.function_id == 6:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Write Single Holding Register\n"
|
||||||
|
elif self.function_id == 16:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Write Multiple Registers\n"
|
||||||
|
if str(self.fst_register) in self.registersNames.keys():
|
||||||
|
txt += "\tfst_w_register = "+str(self.fst_register)+" - "+self.registersNames[str(self.fst_register)]+"\n"
|
||||||
|
else:
|
||||||
|
txt += "\tfst_w_register = "+str(self.fst_register)+" -> NOT LISTED\n"
|
||||||
|
txt += "\tnb_registers = "+str(self.nb_registers)+"\n"
|
||||||
|
txt += "\tnb_octets = "+str(self.nb_octets)+"\n"
|
||||||
|
txt += "\tvalues = 0x"+str(self.registers)+"\n"
|
||||||
|
if self.nb_registers > 0:
|
||||||
|
txt += "\t\t{\n"
|
||||||
|
v = [(self.registers[i:i+4]) for i in range(0, len(self.registers), 4)]
|
||||||
|
for i in range(self.nb_registers):
|
||||||
|
if int(v[i]) == 0:
|
||||||
|
txt+="\t\t\t"+str(self.fst_register+i)+" - "+self.registersNames[str(self.fst_register+i)]+" -> 0x"+v[i]+" (RESET)\n"
|
||||||
|
else:
|
||||||
|
txt+="\t\t\t"+str(self.fst_register+i)+" - "+self.registersNames[str(self.fst_register+i)]+" -> 0x"+v[i]+" (DECLENCHEMENT)\n"
|
||||||
|
txt += "\t\t}\n"
|
||||||
|
elif self.function_id == 15:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Write Multiple Coils\n"
|
||||||
|
txt += "\tfst_w_register = "+str(self.fst_register)+"\n"
|
||||||
|
txt += "\tnb_registers = "+str(self.nb_registers)+"\n"
|
||||||
|
txt += "\tnb_octets = "+str(self.nb_octets)+"\n"
|
||||||
|
txt += "\tvalues = "+str(self.registers)+"\n"
|
||||||
|
elif self.function_id == 129:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Exception\n"
|
||||||
|
elif self.function_id == 130 or self.function_id == 131 or self.function_id == 132 or self.function_id == 133 or self.function_id == 134 or self.function_id == 143 or self.function_id == 144:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Functional error code in response\n"
|
||||||
|
else:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> ILLEGAL_FUNCTION | NON-MODBUS PACKET\n"
|
||||||
|
txt += "}\n"
|
||||||
|
else:
|
||||||
|
txt += "Modbus RX:{\n"
|
||||||
|
txt += "\ttransaction_id = "+str(self.transaction_id)+"\n"
|
||||||
|
txt += "\tproto_id = "+str(self.proto_id)+"\n"
|
||||||
|
txt += "\tlength (bytes) = "+str(self.length)+"\n"
|
||||||
|
txt += "\taddr_slave = "+str(self.addr_slave)+"\n"
|
||||||
|
if self.function_id == 1:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Read Coil\n"
|
||||||
|
elif self.function_id == 2:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Read Discrete Input\n"
|
||||||
|
elif self.function_id == 3:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Read Holding Registers\n"
|
||||||
|
txt += "\tnb_octets = "+str(self.nb_octets)+"\n"
|
||||||
|
txt += "\tvalue(s) = {"
|
||||||
|
if self.request is not None and self.request.fst_register is not None and self.nb_octets is not None and self.request.function_id == self.function_id:
|
||||||
|
dic = self.decodeValuesForRead()
|
||||||
|
for i in range(int(self.request.fst_register), int(self.nb_octets/2)+int(self.request.fst_register)):
|
||||||
|
if i%1==0:
|
||||||
|
txt += '\n'
|
||||||
|
txt += '\t\t\t'+str(i)+' ('+str(self.registersNames[str(i)])+') -> '+dic[i]+" ("+self.computeState(dic[i])+")"
|
||||||
|
txt += "\n\t\t}\n"
|
||||||
|
elif self.function_id == 4:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Read Input Registers\n"
|
||||||
|
elif self.function_id == 5:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Write Single Coil\n"
|
||||||
|
elif self.function_id == 6:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Write Single Holding Register\n"
|
||||||
|
elif self.function_id == 16:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Write Multiple Registers\n"
|
||||||
|
if str(self.fst_register) in self.registersNames.keys():
|
||||||
|
txt += "\tfst_w_register = "+str(self.fst_register)+" - "+self.registersNames[str(self.fst_register)]+"\n"
|
||||||
|
else:
|
||||||
|
txt += "\tfst_w_register = "+str(self.fst_register)+" -> NOT LISTED\n"
|
||||||
|
txt += "\tnb_registers = "+str(self.nb_registers)+"\n"
|
||||||
|
elif self.function_id == 15:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Write Multiple Coils\n"
|
||||||
|
txt += "\tfst_w_register = "+str(self.fst_register)+"\n"
|
||||||
|
txt += "\tnb_registers = "+str(self.nb_registers)+"\n"
|
||||||
|
txt += "\tnb_octets = "+str(self.nb_octets)+"\n"
|
||||||
|
txt += "\tvalues = "+str(self.registers)+"\n"
|
||||||
|
elif self.function_id == 129:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Exception\n"
|
||||||
|
elif self.function_id == 130 or self.function_id == 131 or self.function_id == 132 or self.function_id == 133 or self.function_id == 134 or self.function_id == 143 or self.function_id == 144:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> Functional error code in response\n"
|
||||||
|
else:
|
||||||
|
txt += "\tfunction_id = "+str(self.function_id)+" -> ILLEGAL_FUNCTION | NON-MODBUS PACKET\n"
|
||||||
|
txt += "}\n"
|
||||||
|
return(txt)
|
||||||
|
|
||||||
|
def decodeValuesForRead(self):
|
||||||
|
dicRegisters = {}
|
||||||
|
j=0
|
||||||
|
for i in range(self.request.fst_register, int(self.nb_octets/2)+self.request.fst_register):
|
||||||
|
dicRegisters[i]=self.registers[j:j+4]
|
||||||
|
j+=4
|
||||||
|
return dicRegisters
|
||||||
|
|
||||||
|
def computeState(self, state):
|
||||||
|
s = ""
|
||||||
|
if state[:2] == "00":
|
||||||
|
s += "KNOWN-COMMANDABLE"
|
||||||
|
elif state[:2] == "01":
|
||||||
|
s += "UNKNOWN-COMMANDABLE"
|
||||||
|
elif state[:2] == "02":
|
||||||
|
s += "KNOWN-UNCOMMANDABLE"
|
||||||
|
elif state[:2] == "03":
|
||||||
|
s += "UNKNOWN-UNCOMMANDABLE"
|
||||||
|
s += "-"
|
||||||
|
if state[2:] == "00":
|
||||||
|
if self.scada_ene and self.scada_eqpt:
|
||||||
|
s += "0V | PARC"
|
||||||
|
elif self.scada_ene and not self.scada_eqpt:
|
||||||
|
s += "0V"
|
||||||
|
else:
|
||||||
|
s += "PARC"
|
||||||
|
elif state[2:] == "01":
|
||||||
|
if self.scada_ene and self.scada_eqpt:
|
||||||
|
s += "900V | BREL"
|
||||||
|
elif self.scada_ene and not self.scada_eqpt:
|
||||||
|
s += "900V"
|
||||||
|
else:
|
||||||
|
s += "BREL"
|
||||||
|
return s
|
||||||
|
|
||||||
|
def extractData(self):
|
||||||
|
self.cleanData()
|
||||||
|
try:
|
||||||
|
if len(self.data) > 16:
|
||||||
|
self.transaction_id = int(self.data[:4],16)
|
||||||
|
self.proto_id = int(self.data[4:8],16)
|
||||||
|
self.length = int(self.data[8:12],16)
|
||||||
|
self.addr_slave = int(self.data[12:14],16)
|
||||||
|
self.function_id = int(self.data[14:16],16)
|
||||||
|
if self.isTx:
|
||||||
|
self.fst_register = int(self.data[16:20],16)
|
||||||
|
if self.function_id == 3:
|
||||||
|
self.nb_registers = int(self.data[20:],16)
|
||||||
|
elif self.function_id == 16:
|
||||||
|
self.nb_registers = int(self.data[20:24],16)
|
||||||
|
self.nb_octets = int(self.data[24:26],16)
|
||||||
|
self.registers = self.data[26:]
|
||||||
|
elif self.function_id == 15:
|
||||||
|
self.nb_registers = int(self.data[20:24],16)
|
||||||
|
self.nb_octets = int(self.data[24:26],16)
|
||||||
|
self.registers = self.data[26:]
|
||||||
|
else:
|
||||||
|
if self.function_id == 3:
|
||||||
|
self.nb_octets = int(self.data[16:18],16)
|
||||||
|
self.registers = self.data[18:]
|
||||||
|
elif self.function_id == 16:
|
||||||
|
self.fst_register = int(self.data[16:20],16)
|
||||||
|
self.nb_registers = int(self.data[20:],16)
|
||||||
|
elif self.function_id == 15:
|
||||||
|
self.fst_register = int(self.data[16:20],16)
|
||||||
|
self.nb_registers = int(self.data[20:],16)
|
||||||
|
except Exception as e:
|
||||||
|
print('Wrong data to make a modbus message')
|
||||||
|
print(e)
|
||||||
|
# scapy is crap script .. he's sending Hex string with char when it's detected. E.g. you can have \x00V ... V is 56, then you can miss a byte :)
|
||||||
|
# So, we need to clean that shit
|
||||||
|
def cleanData(self):
|
||||||
|
chars="0123456789abcdef"
|
||||||
|
temp = ""
|
||||||
|
for b in self.data:
|
||||||
|
if b not in chars:
|
||||||
|
temp += str(binascii.hexlify(bytes(b, 'utf-8')))[1:]
|
||||||
|
else:
|
||||||
|
temp += b
|
||||||
|
self.data = temp
|
||||||
|
self.data = str(self.data).replace("'","")
|
||||||
|
def getListOfPacketsFromPcapng(file, ip_ats, ip_srvr):
|
||||||
|
packets = rdpcap(file)
|
||||||
|
listofpackets = []
|
||||||
|
layer = ""
|
||||||
|
for packet in packets:
|
||||||
|
sortedPacket = {}
|
||||||
|
sortedPacket['timestamp']=packet.time
|
||||||
|
raw_modbus =""
|
||||||
|
if 'Raw' in packet.show(dump=True):
|
||||||
|
for b in str(bytes_hex(packet['Raw'].load)).replace('x','').replace('\\','')[2:-1]:
|
||||||
|
raw_modbus+=str(b)
|
||||||
|
sortedPacket['raw_modbus']=raw_modbus
|
||||||
|
try:
|
||||||
|
for line in packet.show2(dump=True).split('\n'):
|
||||||
|
if '###' in line:
|
||||||
|
layer = line.strip('#[] ')
|
||||||
|
sortedPacket[layer] = {}
|
||||||
|
elif '=' in line:
|
||||||
|
key, val = line.split('=', 1)
|
||||||
|
sortedPacket[layer][key.strip()] = val.strip()
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
#Delete packet where IP are not correct and where there is no modbus data
|
||||||
|
if 'IP' in sortedPacket.keys():
|
||||||
|
if (sortedPacket['IP']['src'] == ip_ats and sortedPacket['IP']['dst'] == ip_srvr) or (sortedPacket['IP']['src'] == ip_srvr and sortedPacket['IP']['dst'] == ip_ats):
|
||||||
|
if 'TCP' in sortedPacket.keys() and 'Raw' in sortedPacket.keys():
|
||||||
|
listofpackets.append(sortedPacket)
|
||||||
|
except Exception as e:
|
||||||
|
print('***EXCEPTION***')
|
||||||
|
print(e)
|
||||||
|
return listofpackets
|
||||||
|
|
||||||
|
def buildModbusObjects(packets, settings):
|
||||||
|
ListOfModbusObjects = []
|
||||||
|
# All packets are taken in ATS sys (ATS = source of all)
|
||||||
|
for packet in packets:
|
||||||
|
if packet['IP']['src'] == settings.ip_ats and packet['IP']['dst'] == settings.ip_srvr:
|
||||||
|
tx = ModbusPacket(True, packet['IP']['src'], packet['IP']['dst'], packet['TCP']['sport'], packet['TCP']['dport'], datetime.datetime.fromtimestamp(int(packet['timestamp'])), packet['raw_modbus'], settings.registersNames, settings.scada_ene, settings.scada_eqpt, settings.plc)
|
||||||
|
ListOfModbusObjects.append(tx)
|
||||||
|
elif packet['IP']['src'] == settings.ip_srvr and packet['IP']['dst'] == settings.ip_ats:
|
||||||
|
tx = ModbusPacket(False, packet['IP']['src'], packet['IP']['dst'], packet['TCP']['sport'], packet['TCP']['dport'], datetime.datetime.fromtimestamp(int(packet['timestamp'])), packet['raw_modbus'], settings.registersNames, settings.scada_ene, settings.scada_eqpt, settings.plc)
|
||||||
|
ListOfModbusObjects.append(tx)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
return ListOfModbusObjects
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(text2art("DecodeModbusLog"))
|
||||||
|
print("Getting settings ...")
|
||||||
|
s = Settings('decode_modbus.json')
|
||||||
|
print("Parsing PCAPNG file: "+s.log_file+" ...")
|
||||||
|
packets = getListOfPacketsFromPcapng(s.log_file, s.ip_ats, s.ip_srvr)
|
||||||
|
print("Building Modbus Objetcs ...")
|
||||||
|
objects = buildModbusObjects(packets, s)
|
||||||
|
print("Printing all packets in output file:"+ s.output_file+" ...")
|
||||||
|
output_log = open(s.output_file, 'w')
|
||||||
|
output_log.write(text2art("DecodeModbusLog"))
|
||||||
|
if len(objects) == 0 :
|
||||||
|
output_log.write(str(len(objects))+" MODBUS PACKETS WERE FOUND :( !\n\n")
|
||||||
|
output_log.write("PLEASE CHECK JSON SETTINGS FILE (Are IP correct ?)")
|
||||||
|
else:
|
||||||
|
output_log.write(str(len(objects))+" MODBUS PACKETS WERE FOUND :) !\n\n")
|
||||||
|
for o in objects:
|
||||||
|
if not o.isTx:
|
||||||
|
for i in objects:
|
||||||
|
if i.transaction_id == o.transaction_id:
|
||||||
|
o.request = i
|
||||||
|
break
|
||||||
|
output_log.write(str(o))
|
||||||
|
output_log.close()
|
||||||
|
print("OK.")
|
||||||
|
input()
|
||||||
Reference in New Issue
Block a user