├── DISCLAIMER.md ├── HASHES ├── LICENSE.md ├── Lib ├── BlankConsumer.py ├── BufferOutput.py ├── CaptureDataBuffer.py ├── DumpTimeFile.py ├── SerialPortReader.py ├── __init__.py └── util.py ├── Plugins ├── Byte.py ├── DNP3.py ├── ModbusASCII.py ├── ModbusRTU.py ├── PluginCore.py ├── SELFM.py ├── SLIP.py ├── __init__.py └── df1.py ├── README.md ├── USAGE.md ├── VERSION ├── libserial2pcap.py ├── libserial2pcapunittest.py └── serial2pcap.py /DISCLAIMER.md: -------------------------------------------------------------------------------- 1 | ## Disclaimer of Warranty 2 | This Work is provided "as is". Any express or implied warranties, including but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the United States Government be liable for any direct, indirect, incidental, special, exemplary or consequential damages (including, but not limited to, procurement of substitute goods or services, loss of use, data or profits, or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this Work, even if advised of the possibility of such damage. 3 | 4 | The User of this Work agrees to hold harmless and indemnify the United States Government, its agents and employees from every claim or liability (whether in tort or in contract), including attorneys' fees, court costs, and expenses, arising in direct consequence of Recipient's use of the item, including but not limited to, claims or liabilities made for injury to or death of personnel of User or third parties, damage to or destruction of property of User or third parties, infringement or other violations of intellectual property or technical data rights. 5 | 6 | Nothing in this Work is intended to constitute an endorsement, explicit or implied, by the United States Government of any particular manufacturer's product or service. 7 | 8 | ## Disclaimer of Endorsement 9 | Reference herein to any specific commercial product, process, or service by trade name, trademark, manufacturer, or otherwise, in this Work does not constitute an endorsement, recommendation, or favoring by the United States Government and shall not be used for advertising or product endorsement purposes. -------------------------------------------------------------------------------- /HASHES: -------------------------------------------------------------------------------- 1 | MD5 SHA-1 2 | ------------------------------------------------------------------------- 3 | 64ece21dd9b0da617238fcd98c6b344d 24e2f0a0becabb3dfaf8ea88614d898f76dafc2f serial2pcap-release\Lib\BlankConsumer.py 4 | fe3544e4a53fd20bbb0524d9b92abebe 4233c9efec69710bbdbfb7aba300792f28f61fe9 serial2pcap-release\Lib\BufferOutput.py 5 | c14022bc04c8f5a1351f14115a9afc0e 1947a0376c2ab1e29a2a61ab35ba35153d173740 serial2pcap-release\Lib\CaptureDataBuffer.py 6 | 337e4d83c00b2033583205f03086d560 db3778238c777bdebf7beb672a0f9cbe1d80cff2 serial2pcap-release\Lib\DumpTimeFile.py 7 | 213b491b7500e9c8319246468dfbfc40 1f6f6dd57f52d542d96ac4f24168595a99de48df serial2pcap-release\Lib\SerialPortReader.py 8 | 67a6211a8d90e98865e949206b85c2e7 8076566896c949c800e0772de163dc1d86344961 serial2pcap-release\Lib\util.py 9 | d41d8cd98f00b204e9800998ecf8427e da39a3ee5e6b4b0d3255bfef95601890afd80709 serial2pcap-release\Lib\__init__.py 10 | 183e0af194aedc8721d5d3eb8a1c896b 8a980c7c8fae69a5345e5b7c10d42432701436e6 serial2pcap-release\libserial2pcap.py 11 | b90fbc29ace5fdcea1d4b2a1d8807397 c2582cc0d2a5c50db3c132342c088386415f0131 serial2pcap-release\libserial2pcapunittest.py 12 | c994c906fc62bd2bffe342b01935cfc6 48d58a55d5ed3518441667cd0be5c76089d9e284 serial2pcap-release\Plugins\Byte.py 13 | 37ffd33b191ebecb6a9952a4ddf470a4 dc5b0762171a4376b7d641d8e99320d57e507109 serial2pcap-release\Plugins\df1.py 14 | 58f6f0d2ab98e348b01d72a1baae8209 fd82bbf331b9a0a40f2ae97fe002a352e0428ea2 serial2pcap-release\Plugins\DNP3.py 15 | e850bed0ea08debf8a6ef46dd0b23d9c 8250ebe4eabdb5001f657b4c5c929d47cafe2198 serial2pcap-release\Plugins\ModbusASCII.py 16 | d99c88249f2df4150525a37c6c8be03a 481fede6ba631136c4d7631bb44dcd8db2f33591 serial2pcap-release\Plugins\ModbusRTU.py 17 | f13f39d70ae5faaba7eebb9b292defed 6d2f406cd0c6277d13bf19674949a7c05eb4ab56 serial2pcap-release\Plugins\PluginCore.py 18 | a2efadba5e741d2f12481e0fb25ecbcb 33721025d6787a3ea743a8b6501e7640a4200977 serial2pcap-release\Plugins\SELFM.py 19 | 4b53bdbb95102b2240a8bfde16a24c55 ea2f6c02fbad6d493931653c02064b75014b83fe serial2pcap-release\Plugins\SLIP.py 20 | d41d8cd98f00b204e9800998ecf8427e da39a3ee5e6b4b0d3255bfef95601890afd80709 serial2pcap-release\Plugins\__init__.py 21 | 1c136b47f801afbfef6f00c91a4e20a9 5b84aba38967e3b2a9eb8f1e0b870e050628c094 serial2pcap-release\serial2pcap.py 22 | b5fefb987fded860ac6650e59a944143 9e013796c25fae0be2078676c23defcefc893dd6 serial2pcap-release\USAGE 23 | 3cf918272ffa5de195752d73f3da3e5e 7959c969e092f2a5a8604e2287807ac5b1b384ad serial2pcap-release\VERSION -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This Work was prepared by a United States Government employee and, therefore, is excluded from copyright by Section 105 of the Copyright Act of 1976. 2 | 3 | Copyright and Related Rights in the Work worldwide are waived through the [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) [Universal license](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 4 | -------------------------------------------------------------------------------- /Lib/BlankConsumer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import time 4 | import os 5 | import signal 6 | 7 | def BlankConsumer(datastorage, stopevent): 8 | while not stopevent.isSet(): 9 | if len(datastorage.raw) > 1: 10 | datastorage.pop_front(1) 11 | else: 12 | datastorage.newdata.clear() 13 | #let other threads exit 14 | time.sleep(1) 15 | #simulate a ctrl-c event so that the higher level thread will exit 16 | os.kill(os.getpid(), signal.SIGINT) 17 | #need to break so that this thread doesn't try to catch the keyboard interrupt 18 | break 19 | -------------------------------------------------------------------------------- /Lib/BufferOutput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import time 4 | import struct 5 | import logging 6 | import traceback 7 | 8 | from CaptureDataBuffer import FileNameTracking 9 | 10 | def MakePCAPHeader(filehandle, datalinktype): 11 | magic_number = struct.pack("I", int(0xa1b2c3d4)) 12 | version_major = struct.pack("H", 2) 13 | version_minor = struct.pack("H", 4) 14 | thiszone = struct.pack("I", 0) 15 | sigfigs = struct.pack("I", 0) 16 | snaplen = struct.pack("I", 65535) 17 | network = struct.pack("I", datalinktype) 18 | 19 | filehandle.write(magic_number) 20 | filehandle.write(version_major) 21 | filehandle.write(version_minor) 22 | filehandle.write(thiszone) 23 | filehandle.write(sigfigs) 24 | filehandle.write(snaplen) 25 | filehandle.write(network) 26 | 27 | filehandle.flush() 28 | 29 | #This function takes some data and creates a packet entry within a PCAP file. 30 | #filehandle is a file handle to an open PCAP file 31 | #data is the data to be written 32 | #CallBackFunction is a function that can intercept the data and return data to be added to the packet - it can also modify the data if it wants 33 | def MakePCAPEntry(filehandle, data, CallBackFunction): 34 | databuffer = "" 35 | 36 | for byte in data: 37 | databuffer += byte 38 | 39 | #retrieve the capture time from the last byte in the array 40 | ftime = data[len(data)-1].GetTime() 41 | 42 | #do timing calculations to separate seconds from ms 43 | seconds = int(ftime) 44 | mseconds = int((ftime - int(ftime)) * 1000000) 45 | 46 | #run the callback function - this allows programs to intercept the output before its written to disk 47 | callbackdata = CallBackFunction(seconds, mseconds, databuffer, None) 48 | 49 | #Build the packet header within the PCAP file - this includes the time stamp and length 50 | ts_sec = struct.pack("I", seconds) 51 | ts_usec = struct.pack("I", mseconds) 52 | incl_len = struct.pack("I", len(data) + len(callbackdata)) 53 | orig_len = struct.pack("I", len(data) + len(callbackdata)) 54 | 55 | #write the packet header to disk 56 | filehandle.write(ts_sec) 57 | filehandle.write(ts_usec) 58 | filehandle.write(incl_len) 59 | filehandle.write(orig_len) 60 | 61 | #if there is any callback data then write it to disk 62 | if callbackdata is not None: 63 | filehandle.write(callbackdata) 64 | 65 | #finally write the actual data to disk 66 | filehandle.write(databuffer) 67 | 68 | #flush the data to make sure it hits disk 69 | filehandle.flush() 70 | 71 | #this function is used when used with libserial2pcap - it is responsible for taking identified packets from BufferOutput and storing them in a shared PCAP buffer 72 | #pcapbuffer - internal buffer used to store processed packets and header information 73 | #data - actual packet data that will be stored in the pcapbuffer 74 | #CallBackFunction - function pointer from a processing plugin that needs to be called before storing data 75 | #datalinktype - the data link type as provided from the processing plugin 76 | def AddToPCAPBuffer(pcapbuffer, data, CallBackFunction, datalinktype): 77 | databuffer = "" 78 | 79 | for byte in data: 80 | databuffer += byte 81 | 82 | #retrieve the capture time from the last byte in the array 83 | ftime = data[len(data)-1].GetTime() 84 | 85 | #do timing calculations to separate seconds from ms 86 | seconds = int(ftime) 87 | mseconds = int((ftime - int(ftime)) * 1000000) 88 | 89 | #run the callback function - this allows programs to intercept the output before its written to disk 90 | callbackdata = CallBackFunction(seconds, mseconds, databuffer, None) 91 | 92 | #create the packetdata structure 93 | packetdata = {} 94 | 95 | packetdata["timestamp_s"] = seconds 96 | packetdata["timestamp_ms"] = mseconds 97 | packetdata["length"] = len(callbackdata) 98 | packetdata["dlt"] = datalinktype 99 | 100 | pcapbuffer.lock.acquire() 101 | pcapbuffer.raw.append({"pkthdr": packetdata, "packet": callbackdata + databuffer}) 102 | pcapbuffer.lock.release() 103 | 104 | def BufferOutput(datastorage, outfilename, plugin, stopevent, debug, pcapbuffer): 105 | logger = logging.getLogger("serial2pcap").getChild(__name__) 106 | if debug: 107 | logger.setLevel(logging.DEBUG) 108 | 109 | #this should check to make sure that either outfilename or (xor) pcap buffer are used 110 | #logic further down relies on one or the other being set 111 | if outfilename is not None and pcapbuffer is not None: 112 | raise RuntimeError("Can Not Use Output File And PCAP Buffer Together") 113 | elif outfilename is None and pcapbuffer is None: 114 | raise RuntimeError("Must Use Either Output File Or PCAP Buffer") 115 | 116 | fnt = FileNameTracking() 117 | 118 | unknownbuffer = [] 119 | 120 | logger.info("Starting to Process Data") 121 | 122 | #this only needs to be done if using an outfile 123 | if outfilename is not None: 124 | try: 125 | filehandle = open(outfilename, "wb") 126 | except IOError: 127 | logger.error("Could Not Open Output File") 128 | stopevent.set() 129 | return 130 | MakePCAPHeader(filehandle, plugin.PluginDataLinkType) 131 | 132 | while not stopevent.isSet(): 133 | if datastorage.newdata.isSet(): 134 | datastorage.lock.acquire() 135 | 136 | try: 137 | (pluginresult,packetlength) = plugin.Identify(datastorage.raw, datastorage.capture_info) 138 | except Exception as e: 139 | logger.error("Caught Exception in the plugin, Shutting Down") 140 | logger.error("Exception Was: " + traceback.format_exc()) 141 | stopevent.set() 142 | datastorage.lock.release() 143 | return 144 | 145 | if pluginresult == plugin.Status.UNKNOWN: 146 | logger.debug("Got Status.UNKNOWN, data was: " + datastorage.raw[0]) 147 | unknownbuffer.append(datastorage.raw[0]) 148 | datastorage.pop_front(1) 149 | 150 | elif pluginresult == plugin.Status.OK or pluginresult == plugin.Status.INVALID: 151 | #if packet length is 0 then this is probably an error. Treat it the same as an UNKNOWN event otherwise a deadlock may occur 152 | if packetlength <= 0: 153 | logger.debug("Got Status.OK | Status.INVALID, but the packet length was <= 0. Treating as Status.UNKNOWN") 154 | unknownbuffer.append(datastorage.raw[0]) 155 | datastorage.pop_front(1) 156 | 157 | #otherwise process it as normal 158 | else: 159 | if len(unknownbuffer) > 0: 160 | logger.debug("Flushing Unknown Buffer To PCAP:") 161 | logger.debug(unknownbuffer) 162 | 163 | #this if/else checks to see if output needs to go to a file or the pcapbuffer 164 | if outfilename is not None: 165 | #check to see if the output file needs to be split 166 | if datastorage.DoSplit(filehandle) == True: 167 | #if so then close the original file 168 | filehandle.close() 169 | #get a new file handle with the new naming convention 170 | filehandle = fnt.GetNewFileHandle(outfilename) 171 | #make sure it gets the pcap header 172 | MakePCAPHeader(filehandle, plugin.PluginDataLinkType) 173 | 174 | MakePCAPEntry(filehandle, unknownbuffer, plugin.OutputCallback) 175 | 176 | else: 177 | #adds the unknown buffer to the pcap buffer 178 | AddToPCAPBuffer(pcapbuffer, unknownbuffer, plugin.OutputCallback, plugin.PluginDataLinkType) 179 | 180 | datastorage.inc_packet_counter() 181 | 182 | unknownbuffer = [] 183 | 184 | logger.debug("Got Status.OK | Status.INVALID, Packet Length: " + str(packetlength) + " flushing packet to PCAP") 185 | logger.debug(datastorage.raw[:packetlength]) 186 | 187 | #this if/else checks to see if output needs to go to a file or the pcapbuffer 188 | if outfilename is not None: 189 | #check to see if the output file needs to be split 190 | if datastorage.DoSplit(filehandle) == True: 191 | #if so then close the original file 192 | filehandle.close() 193 | #get a new file handle with the new naming convention 194 | filehandle = fnt.GetNewFileHandle(outfilename) 195 | #make sure it gets the pcap header 196 | MakePCAPHeader(filehandle, plugin.PluginDataLinkType) 197 | 198 | MakePCAPEntry(filehandle, datastorage.raw[:packetlength], plugin.OutputCallback) 199 | 200 | else: 201 | #adds the identified packet to the pcap buffer 202 | AddToPCAPBuffer(pcapbuffer, datastorage.raw[:packetlength], plugin.OutputCallback, plugin.PluginDataLinkType) 203 | 204 | datastorage.inc_packet_counter() 205 | 206 | datastorage.pop_front(packetlength) 207 | 208 | elif pluginresult == plugin.Status.TOOSHORT: 209 | logger.debug("Got Status.TOOSHORT") 210 | datastorage.newdata.clear() 211 | 212 | else: 213 | logger.warning("Unknown Response Code From Plugin - This Will Be Ignored") 214 | 215 | if len(datastorage.raw) == 0: 216 | datastorage.newdata.clear() 217 | 218 | datastorage.lock.release() 219 | 220 | else: 221 | time.sleep(.1) 222 | 223 | #end of while loop 224 | 225 | #if there is any data still left after the main while loop then move it over to the unknown buffer 226 | for i in range(0,len(datastorage.raw)): 227 | unknownbuffer.append(datastorage.raw[i]) 228 | 229 | #dump anything in the unknown buffer to a single packet 230 | if len(unknownbuffer) > 0: 231 | logger.debug("Flushing Unknown Buffer To PCAP:") 232 | logger.debug(unknownbuffer) 233 | #this if/else checks to see if output needs to go to a file or the pcapbuffer 234 | if outfilename is not None: 235 | #for now not splitting files at this code since the code writes one big packet and there is not fine grained control over the size 236 | MakePCAPEntry(filehandle, unknownbuffer, plugin.OutputCallback) 237 | else: 238 | #adds the unknown buffer to the pcap buffer 239 | AddToPCAPBuffer(pcapbuffer, unknownbuffer, plugin.OutputCallback, plugin.PluginDataLinkType) 240 | datastorage.inc_packet_counter() 241 | logger.info("Done Processing") 242 | 243 | -------------------------------------------------------------------------------- /Lib/CaptureDataBuffer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import threading 4 | import time 5 | 6 | class FileNameTracking: 7 | def __init__(self): 8 | pass 9 | 10 | filecount = 0 11 | 12 | def GetNewFileHandle(self, OriginalFileName, mode="wb"): 13 | self.filecount += 1 14 | 15 | return open(OriginalFileName + "." + str(self.filecount), mode) 16 | 17 | 18 | 19 | class CaptureInformation: 20 | def __init__(self): 21 | pass 22 | 23 | backing = None 24 | baudrate = None 25 | bytesize = None 26 | parity = None 27 | stopbits = None 28 | 29 | class CaptureDataBuffer: 30 | lock = None 31 | newdata = None 32 | raw = None 33 | 34 | total_packets_captured = 0 35 | total_bytes_captured = 0 36 | start_time = 0.0 37 | file_split_size = 0 38 | 39 | capture_info = None 40 | 41 | def __init__(self): 42 | self.lock = threading.Lock() 43 | self.newdata = threading.Event() 44 | self.raw = [] 45 | 46 | self.capture_info = CaptureInformation() 47 | 48 | self.start_time = time.time() 49 | 50 | def pop_front(self, count): 51 | if count >= len(self.raw): 52 | self.raw = [] 53 | 54 | else: 55 | for i in range(0, count): 56 | self.raw.pop(0) 57 | 58 | def inc_packet_counter(self, count=1): 59 | self.total_packets_captured += count 60 | 61 | def inc_bytes_counter(self, count=1): 62 | self.total_bytes_captured += count 63 | 64 | def DoSplit(self, filehandle): 65 | if self.file_split_size != 0 and filehandle.tell() >= self.file_split_size: 66 | return True 67 | else: 68 | return False 69 | 70 | class CaptureDataBufferSimple: 71 | lock = None 72 | raw = None 73 | 74 | def __init__(self): 75 | self.lock = threading.Lock() 76 | self.raw = [] 77 | 78 | -------------------------------------------------------------------------------- /Lib/DumpTimeFile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import struct 4 | 5 | class DumpTimeFile: 6 | filehandle = None 7 | setup = False 8 | 9 | def __init__(self, filehandle=None, capture_info=None): 10 | if filehandle is not None and capture_info is not None: 11 | self.Init(filehandle, capture_info) 12 | 13 | def Init(self, filehandle, capture_info): 14 | self.filehandle = filehandle 15 | 16 | #raw dump with time stamp file format: 17 | #0x00 header 0x01234567 18 | #0x08 header 0x89987654 19 | #0x10 Baud Rate [0-115200] 20 | #0x14 Byte Size [5,6,7,8] 21 | #0x15 Parity [None - 0, Even - 1, Odd - 2, Mark - 3, Space - 4] 22 | #0x16 StopBits [1 - 1, 1.5 - 3, 2 - 2] 23 | #0x17 Padding (0) 24 | #add the magic value so that the file reader can determine if the file contains time stamp information 25 | self.filehandle.write("0123456789987654") 26 | if capture_info.baudrate is not None: 27 | self.filehandle.write(struct.pack("I", int(capture_info.baudrate))) 28 | else: 29 | self.filehandle.write("\xFF\xFF\xFF\xFF") 30 | if capture_info.bytesize is not None: 31 | self.filehandle.write(struct.pack("B", int(capture_info.bytesize))) 32 | else: 33 | self.filehandle.write("\xFF") 34 | __parity = None 35 | if capture_info.parity == "N": 36 | __parity = 0 37 | elif capture_info.parity == "E": 38 | __parity = 1 39 | elif capture_info.parity == "O": 40 | __parity = 2 41 | elif capture_info.parity == "M": 42 | __parity = 3 43 | elif capture_info.parity == "S": 44 | __parity = 4 45 | 46 | if __parity is not None: 47 | self.filehandle.write(struct.pack("B", __parity)) 48 | else: 49 | self.filehandle.write("\xFF") 50 | 51 | __stopbits = None 52 | if capture_info.stopbits == "1": 53 | __stopbits = 1 54 | elif capture_info.stopbits == "1.5": 55 | __stopbits = 3 56 | elif capture_info.stopbits == "2": 57 | __stopbits = 2 58 | 59 | if __stopbits is not None: 60 | self.filehandle.write(struct.pack("B", __stopbits)) 61 | else: 62 | self.filehandle.write("\xFF") 63 | 64 | self.filehandle.write(struct.pack("B",0)) 65 | 66 | self.filehandle.flush() 67 | 68 | self.setup = True 69 | 70 | def AddTime2File(self, time): 71 | if not self.setup: 72 | raise RuntimeError("The Dump Time File Has Not Been Initialized") 73 | 74 | self.filehandle.write(struct.pack("d", time)) 75 | 76 | def IsReady(self): 77 | return self.setup 78 | 79 | def GetFileHandle(self): 80 | return self.filehandle 81 | 82 | -------------------------------------------------------------------------------- /Lib/SerialPortReader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import time 4 | import serial 5 | import sys 6 | import threading 7 | import struct 8 | import logging 9 | 10 | from CaptureDataBuffer import CaptureDataBuffer 11 | from CaptureDataBuffer import FileNameTracking 12 | from DumpTimeFile import DumpTimeFile 13 | 14 | #class that inherits str and adds embedded timing information. This will be used in the output buffer thread. 15 | class strwtime(str): 16 | __raw_time = None 17 | 18 | def GetTime(self): 19 | return self.__raw_time 20 | def SetTime(self, ltime): 21 | self.__raw_time = ltime 22 | 23 | #this function serves as an intermediate between the SerialPortReader and the BufferOutput thread. This is because of the locking mechanisms that caused a performance coupling between the plugins 24 | #and the performance of the SerialPortReader. E.g. if a plugin is slow it could acquire the lock and prevent the serial port reader from adding new data to the capture data buffer. Since this data 25 | #has timing info added to it this delay could cause a skew in the timing information presented. This function solves this so that the serialportreader only has to compete with an optimized function 26 | #for run time 27 | # Thread1: 28 | # Data - > SerialPortReader -> localdatabuffer 29 | # Thread2: 30 | # localdatabuffer -> CopyHandler -> remotedatabuffer 31 | # Slowdowns that happen in the remote data buffer wont affect the serial port reader 32 | def CopyHandler(stopevent, localdatabuffer, remotedatabuffer, printtoscreen, dumpfilename, addtimestamp): 33 | fnt = FileNameTracking() 34 | 35 | dumpfile = None 36 | dtf = None 37 | if dumpfilename is not None: 38 | dumpfile = open(dumpfilename, "wb") 39 | if addtimestamp == True: 40 | dtf = DumpTimeFile(dumpfile, localdatabuffer.capture_info) 41 | 42 | #temporary byte storage variable 43 | tempbyte = None 44 | 45 | while not stopevent.isSet(): 46 | if localdatabuffer.newdata.isSet(): 47 | #get data from the local data buffer 48 | localdatabuffer.lock.acquire() 49 | if len(localdatabuffer.raw) > 0: 50 | tempbyte = localdatabuffer.raw[0] 51 | localdatabuffer.pop_front(1) 52 | if len(localdatabuffer.raw) == 0: 53 | localdatabuffer.newdata.clear() 54 | else: 55 | tempbyte = None 56 | localdatabuffer.lock.release() 57 | 58 | #move it to the remote data buffer 59 | if tempbyte is not None: 60 | remotedatabuffer.lock.acquire() 61 | #increment the internal counters - this controls things like statistics monitoring and file size splitting 62 | remotedatabuffer.inc_bytes_counter() 63 | remotedatabuffer.raw.append(tempbyte) 64 | remotedatabuffer.newdata.set() 65 | remotedatabuffer.lock.release() 66 | 67 | if printtoscreen: 68 | sys.stdout.write(tempbyte) 69 | sys.stdout.flush() 70 | 71 | if dumpfile is not None: 72 | dumpfile.write(tempbyte) 73 | if addtimestamp == True: 74 | dtf.AddTime2File(tempbyte.GetTime()) 75 | 76 | #check to see if the output file needs to be split - if so then close the old dump file and get a new one 77 | if remotedatabuffer.DoSplit(dumpfile): 78 | dumpfile.close() 79 | dumpfile = fnt.GetNewFileHandle(dumpfilename) 80 | 81 | #if using dump time files then reinitialize the file 82 | if addtimestamp == True: 83 | dtf.Init(dumpfile, remotedatabuffer.capture_info) 84 | 85 | else: 86 | time.sleep(.1) 87 | 88 | if dumpfile is not None: 89 | dumpfile.flush() 90 | dumpfile.close() 91 | 92 | def FileReader(databuffer, printtoscreen, dumpfilename, inputfile, stopevent, fileeof): 93 | count = 1 94 | 95 | logger = logging.getLogger("serial2pcap").getChild(__name__) 96 | 97 | fnt = FileNameTracking() 98 | 99 | dumpfile = None 100 | #don't need to worry about handling time stamp information here because the parent function should not allow this option 101 | #but this will be checked in a few lines 102 | if dumpfilename is not None: 103 | dumpfile = open(dumpfilename, "wb") 104 | 105 | filehandle = open(inputfile, "rb") 106 | 107 | #flag to determine if the raw dump file contains time stamp information 108 | decode_timestamps = False 109 | #read the first 16 bytes of the file to see if has the special marker 110 | magic_data = filehandle.read(16) 111 | 112 | #check to see if the data read is the magic_data 113 | if magic_data is not None and len(magic_data) >= 16 and magic_data == "0123456789987654": 114 | decode_timestamps = True 115 | 116 | #attempt to read the capture information from the dump file 117 | ci = filehandle.read(8) 118 | #make sure that enough data was read - if not the quit 119 | if len(ci) < 8: 120 | logger.error("Dump File With Timestamps Does Not Contain Properly Formatted Capture Information. Can Not Continue") 121 | stopevent.set() 122 | return 123 | else: 124 | #unpack the data structure 125 | __ci = struct.unpack("IBBBB", ci) 126 | __baudrate = __ci[0] 127 | __bytesize = __ci[1] 128 | __parity = __ci[2] 129 | __stopbits = __ci[3] 130 | __zero = __ci[4] 131 | 132 | #need to process parity since its encoded within the dump file 133 | parity = 0 134 | if __parity == 0: 135 | parity = "N" 136 | elif __parity == 1: 137 | parity = "E" 138 | elif __parity == 2: 139 | parity = "O" 140 | elif __parity == 3: 141 | parity = "M" 142 | elif __parity == 4: 143 | parity = "S" 144 | else: 145 | logger.error("Dump File With Timestamps Contains Invalid Data (Parity). Can Not Continue") 146 | stopevent.set() 147 | return 148 | 149 | #need to process stopbits since its encoded within the dump file 150 | stopbits = "1" 151 | if __stopbits == 1: 152 | stopbits = "1" 153 | elif __stopbits == 2: 154 | stopbits = "2" 155 | elif __stopbits == 3: 156 | stopbits = "1.5" 157 | else: 158 | logger.error("Dump File With Timestamps Contains Invalid Data (stopbits). Can Not Continue") 159 | stopevent.set() 160 | return 161 | 162 | #need to convert byte size to string 163 | bytesize = str(__bytesize) 164 | 165 | #set the capture information 166 | databuffer.capture_info.backing = "file_timestamps" 167 | databuffer.capture_info.baudrate = __baudrate 168 | databuffer.capture_info.bytesize = bytesize 169 | databuffer.capture_info.parity = parity 170 | databuffer.capture_info.stopbits = stopbits 171 | 172 | logger.info("Raw Capture File Contains Timestamps - Using Timestamped Raw Data") 173 | 174 | #otherwise (file does not contain timestamped data) reset the file to the beginning (because the beginning contains valid data) 175 | #and update the capture information 176 | else: 177 | databuffer.capture_info.backing = "file" 178 | databuffer.capture_info.baudrate = None 179 | databuffer.capture_info.bytesize = None 180 | databuffer.capture_info.parity = None 181 | databuffer.capture_info.stopbits = None 182 | 183 | filehandle.seek(0) 184 | 185 | 186 | while True: 187 | #create a variable to hold the byte information 188 | byte = "" 189 | 190 | #read in the raw byte 191 | __byte = filehandle.read(1) 192 | #check to make sure that it actually read something (may have gotten to the end of the file) 193 | if __byte is None or __byte == "": 194 | break 195 | 196 | if decode_timestamps == True: 197 | #if the file has embedded timestamps then read the time information from the file 198 | __time = filehandle.read(8) 199 | #check to make sure that it actually read something (may have gotten to the end of the file) 200 | if __time is None or __time == "": 201 | break 202 | #convert to a datatype that can hold the time information 203 | byte = strwtime(__byte) 204 | #add the time information 205 | if len(str(__time)) >= 8: 206 | byte.SetTime(struct.unpack("d", str(__time))[0]) 207 | else: 208 | break 209 | #file does not have embedded time stamps 210 | else: 211 | byte = strwtime(__byte) 212 | byte.SetTime(time.time()) 213 | 214 | databuffer.raw.append(byte) 215 | #increment the internal counters - this controls things like statistics monitoring and file size splitting 216 | databuffer.inc_bytes_counter() 217 | 218 | if printtoscreen == True: 219 | sys.stdout.write(byte) 220 | sys.stdout.flush() 221 | 222 | if dumpfile is not None: 223 | dumpfile.write(byte) 224 | 225 | #check to see if the output file needs to be split - if so then close the old dump file and get a new one 226 | if databuffer.DoSplit(dumpfile): 227 | dumpfile.close() 228 | dumpfile = fnt.GetNewFileHandle(dumpfilename) 229 | #no need to add dump time file header information since you can't do that while reading from a file 230 | 231 | databuffer.newdata.set() 232 | 233 | #signal end of file 234 | fileeof.set() 235 | 236 | while not stopevent.isSet(): 237 | if not databuffer.newdata.isSet(): 238 | stopevent.set() 239 | time.sleep(.5) 240 | 241 | if dumpfile is not None: 242 | dumpfile.flush() 243 | dumpfile.close() 244 | 245 | def SerialPortReader(Device, Baudrate, ByteSize, Parity, StopBits, DataStorage, printtoscreen, dumpfilename, StopEvent, addtimestamp): 246 | logger = logging.getLogger("serial2pcap").getChild(__name__) 247 | 248 | localParity = serial.PARITY_NONE 249 | localByteSize = serial.EIGHTBITS 250 | localStopBits = serial.STOPBITS_ONE 251 | 252 | if ByteSize == '5': 253 | localByteSize = serial.FIVEBITS 254 | elif ByteSize == '6': 255 | localByteSize = serial.SIXBITS 256 | elif ByteSize == '7': 257 | localByteSize = serial.SEVENBITS 258 | elif ByteSize == '8': 259 | localByteSize = serial.EIGHTBITS 260 | 261 | if Parity == 'N': 262 | localParity = serial.PARITY_NONE 263 | elif Parity == 'E': 264 | localParity = serial.PARITY_EVEN 265 | elif Parity == 'O': 266 | localParity = serial.PARITY_ODD 267 | elif Parity == 'M': 268 | localParity = serial.PARITY_MARK 269 | elif Parity == 'S': 270 | localParity = serial.PARITY_SPACE 271 | 272 | if StopBits == '1': 273 | localStopBits = serial.STOPBITS_ONE 274 | elif StopBits == '1.5': 275 | localStopBits = serial.STOPBITS_ONE_POINT_FIVE 276 | elif StopBits == '2': 277 | localStopBits = serial.STOPBITS_TWO 278 | 279 | port = serial.Serial(port=Device, baudrate=Baudrate, bytesize=localByteSize, parity=localParity, stopbits=localStopBits, interCharTimeout=None, xonxoff=False, rtscts=False, dsrdtr=False, timeout=1) 280 | logger.info("Waiting 3 Seconds For Serial Port To Be Ready") 281 | time.sleep(3) 282 | port.flushInput() 283 | logger.info("Starting Capture") 284 | 285 | #Create the local data storage instance of the CpatureDataBuffer - the CopyHandler thread will be responsible for copying data over to the real CaptureDataBuffer instance 286 | localdatastorage = CaptureDataBuffer() 287 | 288 | localdatastorage.capture_info.backing = "device" 289 | localdatastorage.capture_info.baudrate = Baudrate 290 | localdatastorage.capture_info.bytesize = ByteSize 291 | localdatastorage.capture_info.parity = Parity 292 | localdatastorage.capture_info.stopbits = StopBits 293 | 294 | #ensure that things downstream have access to the capture information 295 | DataStorage.capture_info = localdatastorage.capture_info 296 | 297 | #fire up the copy handler thread 298 | copythread = threading.Thread(target=CopyHandler, args=[StopEvent, localdatastorage, DataStorage, printtoscreen, dumpfilename, addtimestamp]) 299 | copythread.start() 300 | 301 | while not StopEvent.isSet(): 302 | 303 | __byte = port.read(1) 304 | 305 | if __byte is not None and __byte != '': 306 | byte = strwtime(__byte) 307 | byte.SetTime(time.time()) 308 | 309 | localdatastorage.lock.acquire() 310 | localdatastorage.raw.append(byte) 311 | #do not need to increment any counters here since this is a local copy of the data - the copy handler thread will handle the statistics 312 | localdatastorage.newdata.set() 313 | localdatastorage.lock.release() 314 | 315 | -------------------------------------------------------------------------------- /Lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsacyber/serial2pcap/6f367df0e7cfe8ef05e25ee810fbed61b4802b99/Lib/__init__.py -------------------------------------------------------------------------------- /Lib/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import logging 4 | import time 5 | 6 | def SetUpLogging(defaultLevel): 7 | logger = logging.getLogger("serial2pcap") 8 | 9 | logging.Formatter.converter = time.gmtime 10 | logging.basicConfig(format='[%(levelname)s] %(message)s', datefmt='%m/%d/%Y %H:%M:%S UTC', level=defaultLevel) 11 | 12 | logger.debug("Set Logging Level To: " + repr(defaultLevel)) 13 | -------------------------------------------------------------------------------- /Plugins/Byte.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | This plugin will identify every byte as a valid packet. It is used to store every byte into PCAP format 4 | """ 5 | 6 | from PluginCore import PluginCore 7 | 8 | class Byte(PluginCore): 9 | ProtocolName = "byte" 10 | ProtocolDescription = "Single Byte - Generates A Packet For Every Byte" 11 | 12 | def Identify(self, data, capture_info): 13 | if len(data) > 0: 14 | return (PluginCore.Status.OK,1) 15 | else: 16 | return (PluginCore.Status.UNKNOWN,0) 17 | -------------------------------------------------------------------------------- /Plugins/DNP3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | This plugin implements identifying the dnp3 protocol for serial2pcap. 4 | """ 5 | 6 | from PluginCore import PluginCore 7 | 8 | class DNP3(PluginCore): 9 | ProtocolName = "dnp3" 10 | ProtocolDescription = "Plugin to detect serial dnp3" 11 | 12 | def Identify(self, data, capture_info): 13 | #first, check to make sure that there are at least 4 bytes in the array, if there is not then return TOOSHORT 14 | if len(data) <= 3: 15 | return (PluginCore.Status.TOOSHORT,0) 16 | 17 | #next check to see if the first two bytes are 0x0564, which is the header for dnp3 18 | if data[0] == "\x05" and data[1] == "\x64": 19 | 20 | #if the first two bytes are 0x0564 then scan the rest of the array to find the next 0x0564. This is how the plugin determines the length of the current dnp3 packet 21 | #calculate how much data to scan 22 | for i in range(2, len(data)-1): 23 | #scan for 0x0564 24 | if data[i] == "\x05" and data[i+1] == "\x64": 25 | #if its found then return OK with the position of the second 0x0564 which is also the length of the first packet 26 | return (PluginCore.Status.OK,i) 27 | 28 | #if another 0x0564 cant be found then there is not enough data 29 | return (PluginCore.Status.TOOSHORT,0) 30 | 31 | #if the first two bytes are not 0x0564 then return UNKNOWN 32 | return (PluginCore.Status.UNKNOWN,0) 33 | -------------------------------------------------------------------------------- /Plugins/ModbusASCII.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | This plugin implements identifying the modbusASCII protocol for serial2pcap. 4 | """ 5 | 6 | from PluginCore import PluginCore 7 | 8 | class ModbusASCII(PluginCore): 9 | ProtocolName = "modbusASCII" 10 | ProtocolDescription = "Plugin to detect serial modbusASCII" 11 | 12 | def Identify(self, data, capture_info): 13 | #first, check to make sure that there are at least 4 bytes in the array, if there is not then return TOOSHORT 14 | if len(data) <= 6: 15 | return (PluginCore.Status.TOOSHORT,0) 16 | 17 | #next check to see if the first byte is ":", which is the header for a Modbus ASCII packet 18 | if data[0] == ":": 19 | 20 | #if the first byte is ":" then scan the rest of the array to find the next ":". This is how the plugin determines the length of the current Modbus ASCII packet 21 | #calculate how much data to scan 22 | for i in range(6, len(data)-1): 23 | #scan for ":" 24 | if data[i] == ":": 25 | #if its found then return OK with the position of the second ":" which is also the length of the first packet 26 | return (PluginCore.Status.OK,i) 27 | 28 | #if another ":" cant be found then there is not enough data 29 | return (PluginCore.Status.TOOSHORT,0) 30 | 31 | #if the first byte is not ":" then return UNKNOWN 32 | return (PluginCore.Status.UNKNOWN,0) 33 | -------------------------------------------------------------------------------- /Plugins/ModbusRTU.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | This plugin implements identifying the modbusRTU protocol for serial2pcap. 4 | 5 | Modbus RTU Frame Format: 6 | Name Length (bits) Function 7 | Start 28 At least 3.5 (28 bits) character times of silence 8 | Address 8 9 | Function 8 10 | Data n*8 11 | CRC 16 12 | End 28 At Least 3.5 (28 bits) character times of silence between frames 13 | 14 | This plugin identifies ModbusRTU frames by matching data to CRC's. The plugin forward slices through received data (up to 256 bytes - max RTU ADU size) and computes the data so far to the next two bytes. If a CRC match is found then the plugin assumes that it has found a valid RTU frame. 15 | """ 16 | 17 | from PluginCore import PluginCore 18 | from ctypes import c_ushort 19 | 20 | class ModbusRTU(PluginCore): 21 | ProtocolName = "modbusRTU" 22 | ProtocolDescription = "Modbus RTU Frame Format Serial Protocol" 23 | 24 | crc16_tab = [] 25 | 26 | crc16_constant = 0xA001 27 | 28 | def __init__(self): 29 | if not len(self.crc16_tab): 30 | self.init_crc16() 31 | 32 | #CRC code derived and modified from PyCRC - Github cristianav/PyCRC - GPLv3 license 33 | #https://github.com/cristianav/PyCRC/blob/master/PyCRC/CRC16.py 34 | def calculate(self, input_data): 35 | is_string = isinstance(input_data, str) 36 | is_bytes = isinstance(input_data, (bytes, bytearray)) 37 | 38 | #if not is_string and not is_bytes: 39 | # raise Exception("input data type is not supported") 40 | 41 | crc_value = 0xFFFF 42 | 43 | for c in input_data: 44 | d = ord(c) 45 | tmp = crc_value ^ d 46 | rotated = crc_value >> 8 47 | crc_value = rotated ^ self.crc16_tab[(tmp & 0x00ff)] 48 | 49 | #added this to rotate the bytes. RTU transmits CRC in a different endian 50 | crc_low = crc_value & 255 51 | crc_high = crc_value >> 8 52 | 53 | return (crc_low << 8) ^ crc_high 54 | 55 | def init_crc16(self): 56 | for i in range(0,256): 57 | crc = c_ushort(i).value 58 | for j in range(0,8): 59 | if crc & 0x0001: 60 | crc = c_ushort(crc >> 1).value ^ self.crc16_constant 61 | else: 62 | crc = c_ushort(crc >> 1).value 63 | self.crc16_tab.append(crc) 64 | #end derived code 65 | 66 | def Identify(self, data, capture_info): 67 | #sizes do not include 2 byte checksum 68 | LOWER_SLICE_LIMIT = 6 #min Modbus RTU Size 8 69 | UPPER_SLICE_LIMIT = 254 #max Modbus RTU Size 256 70 | 71 | #if not enough data then wait 72 | if len(data) <= LOWER_SLICE_LIMIT: 73 | return (PluginCore.Status.TOOSHORT,0) 74 | 75 | sliceat = LOWER_SLICE_LIMIT 76 | while sliceat <= UPPER_SLICE_LIMIT: 77 | #make sure there is enough data 78 | if len(data) < sliceat + 2: 79 | return (PluginCore.Status.TOOSHORT,0) 80 | 81 | #calculate CRC at slice 82 | calc_crc = self.calculate(data[:sliceat]) 83 | #get test CRC from data 84 | recv_crc = (ord(data[sliceat]) << 8) ^ ord(data[sliceat + 1]) 85 | 86 | #check to see if calculated and received CRC match - if so then assume good packet 87 | if calc_crc == recv_crc: 88 | return (PluginCore.Status.OK,sliceat+2) 89 | 90 | sliceat += 1 91 | 92 | #if no packet was found then signal unknown 93 | return (PluginCore.Status.UNKNOWN,0) 94 | -------------------------------------------------------------------------------- /Plugins/PluginCore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | This is the base class for all plugins. The class name that implements PluginCore should be stored in a file named the exact same. This means that if implementing class is TestPlugin, 4 | then it should be in TestPlugin.py. PluginCore defines the minimum functions and variables that any other plugin should implement: 5 | ProtocolName: The short name of the protocol that the plugin parses (ex. testplugin) 6 | ProtocolDescription: A description of the protocol that the plugin parses. (ex. the test protocol is used with test devices) 7 | Identify(self, data): This is the main function that serial2pcap will call in the plugin to identify the protocol. 8 | 9 | The identify function is called whenever serial2pcap needs to determine if a data stream is a valid packet for a specific protocol. The variable data is a python string byte array. 10 | This means that each element in the array is a string representation of a single octet. Ex. data may look like this: data = ["\x01", "\x02", "\x03"]. The identify function should determine 11 | if data[0] + ... + data[n] is a valid packet for the protocl it is parsing. If it is a valid packet then it should return the length of the packet (n) starting from data[0] and the status 12 | code OK. If data[0] + ... + data[n] is not a packet then Identify should return a 0 length and the status code UNKNOWN. If the identify function determines that the data stream might be 13 | a valid packet but the buffer is too short to make a determination then it should return the status code TOOSHORT and a 0 length. 14 | 15 | Status.UNKNOWN - Signals to serial2pcap that data at index 0 does not represent a packet from the implemented protocol 16 | Status.INVALID - Signals to serial2pcap that the data is a packet from the implemented protocol but the packet is invalid. This is treated the same was as signalling OK 17 | Status.OK - Signals to serial2pcap that data at index 0 plus some number of bytes is a packet from the implemented protocol. 18 | Status.TOOSHORT - Signals to serial2pcap that there is not enough data to make a determination. serial2pcap will wait until there is more data before calling identify again. 19 | 20 | The identify function should return status messages and lengths in the following format: 21 | 22 | return(PluginCore.Status.UNKNOWN, 0) 23 | or 24 | return(PluginCore.Status.INVALID, packet_length) 25 | or 26 | return(PluginCore.Status.OK, packet_length) 27 | or 28 | return(PluginCore.Status.TOOSHORT, 0) 29 | 30 | Plugins can override the PluginDataLinkType and the OutputCallBack function. The plugin data link type variable is used to set the data link type within the PCAP header. The default 31 | is to use 250 or LINKTYPE_RTAC_SERIAL. Plugins can override the OutputCallback function to have data added to a pcap entry. The default is to have the RTAC Serial data structure added to the 32 | pcap entry. The function is passed in the seconds and milliseconds of when the packet was captured as well as access to the packet data. There is an unused variable in the function call that 33 | is there for forward compatibility. Anything the function returns is added to the front of the pcap entry, unless it returns None, then nothing is written to disk. 34 | """ 35 | 36 | import struct 37 | 38 | class PluginCore(): 39 | class Status: 40 | UNKNOWN = 0 41 | INVALID = 1 42 | OK = 2 43 | TOOSHORT = 3 44 | 45 | ProtocolName = "" 46 | ProtocolDescription = "" 47 | 48 | def Identify(self, data, capture_info): 49 | return (PluginCore.Status.UNKNOWN,0) 50 | 51 | #This can be overridden 52 | PluginDataLinkType = 250 #LINKTYPE_RTAC_SERIAL 53 | 54 | #This can be overridden 55 | #Defines the RTAC Serial Data Header Needed For RTAC Serial PCAP Packets 56 | def OutputCallback(self, seconds, mseconds, data, Unused): 57 | #build the SERIAL_RTAC Header - This has to be big endian (>) in the pack function 58 | #>IIBBH - seconds, mseconds, Serial Event Type = 4 = DATA_RX_END, UART Control Line State, Footer 59 | padding = struct.pack(">IIBBH", seconds,mseconds,4,0,0) 60 | 61 | return padding 62 | -------------------------------------------------------------------------------- /Plugins/SELFM.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from PluginCore import PluginCore 4 | 5 | import struct 6 | 7 | class SELFM(PluginCore): 8 | 9 | ProtocolName = "selfm" 10 | ProtocolDescription = "SEL Fast Message" 11 | 12 | selfm_codes = [ 13 | "\xA5\x46", #??? Fast SER Block ??? 14 | "\xA5\xC0", 15 | "\xA5\xC1", 16 | "\xA5\xC2", 17 | "\xA5\xC3", #??? 18 | "\xA5\xCE", 19 | "\xA5\xCF", 20 | "\xA5\xDC", #Old Standard Fast Meter 21 | "\xA5\xDA", #Old Extended Fast Meter 22 | "\xA5\xD1", #Regular Fast Meter 23 | "\xA5\xD2", #Demand Fast Meter 24 | "\xA5\xD3", #Peak Demand Fast Meter 25 | "\xA5\xE0", 26 | "\xA5\xE3", 27 | "\xA5\xE5", 28 | "\xA5\xE6", 29 | "\xA5\xE7", 30 | "\xA5\xE8", 31 | "\xA5\xE9", 32 | "\xA5\xB2", 33 | "\xA5\xB5", 34 | "\xA5\xB9", 35 | "\xA5\x60", 36 | #Other codes added in __init__ 37 | ] 38 | 39 | data_stream_msg = [ 40 | "ID", #FID and TRMID 41 | "ENA", #Short Event Packet Data 42 | "DNA", # Digital I\O 43 | "BNA", #Status Bits 44 | "CST", #??? 45 | "SNS", 46 | "ACC", 47 | "2AC", 48 | "CAL", 49 | "BAC", 50 | "MET", 51 | "OPE", 52 | "CLO", 53 | "SET", 54 | "SHO", 55 | "SER", 56 | "HIS", 57 | "PAS", 58 | "PUL", 59 | "TAR", 60 | "PORT", 61 | "VER", 62 | ] 63 | 64 | #init function fills out the rest of the selfm_codes array. 65 | def __init__(self): 66 | #add codes 0xA561 - 0xA56C - Previous Event Reports - 0xA561 is the previous event report 0xA56C is the 12th oldest report 67 | for i in range(97,108+1): 68 | self.selfm_codes.append(struct.pack("BB", 165, i)) 69 | 70 | #add codes 0xA56D - 0xA59F - Previous Event Reports for relays that save up to 64 event reports 71 | for i in range(109,159+1): 72 | self.selfm_codes.append(struct.pack("BB", 165, i)) 73 | 74 | #Merge function is used to take an array of bytes (eg. Data = ["\x00", "\x01", "\x02"]) and turn it into a block of that data (eg. Data = "\x00\x01\x02") 75 | def Merge(self, inbytes): 76 | outbytes = "" 77 | for inbyte in inbytes: 78 | outbytes = outbytes + inbyte 79 | 80 | return outbytes 81 | 82 | #scans through the selfm_codes and determines if the passed in bytes are a recognized code 83 | def IsSelfmCode(self, byte0, byte1): 84 | __bytes = byte0 + byte1 85 | 86 | for code in self.selfm_codes: 87 | if code == __bytes: 88 | return True 89 | return False 90 | 91 | #scans through the data_stream_msg and determines if the bytes passed in are a valid data stream message 92 | def IsDSM(self, inbytes): 93 | if len(inbytes) < 4: 94 | return False 95 | 96 | for msg in self.data_stream_msg: 97 | if self.Merge(inbytes[:3]) == msg: 98 | return True 99 | 100 | return False 101 | 102 | #convert from a sel length byte to an integer representation of that length 103 | def SelfmLength(self, inbyte): 104 | return int(struct.unpack("B", inbyte)[0]) 105 | 106 | def Identify(self, data, capture_info): 107 | if len(data) < 4: 108 | return (PluginCore.Status.TOOSHORT,0) 109 | 110 | #try to identify ASCII Data Stream Messages 111 | if data[3] == "\x0D" and self.IsDSM(data): #\x0D == Carriage Return 112 | return (PluginCore.Status.OK,4) 113 | 114 | if self.IsSelfmCode(data[0], data[1]): 115 | #see if the next set of bytes is also a selfm code, if it is then the first two bytes are a packet 116 | if self.IsSelfmCode(data[2], data[3]) or self.IsDSM(data[2:]): 117 | return (PluginCore.Status.OK,2) 118 | 119 | #try to determine the length 120 | else: 121 | pack_len = self.SelfmLength(data[2]) 122 | 123 | #if there's not enough data then return tooshort 124 | if len(data) < pack_len: 125 | return (PluginCore.Status.TOOSHORT,0) 126 | 127 | #scan to see if there's another selfm status code in the packet 128 | for i in range(3, pack_len-1): 129 | if self.IsSelfmCode(data[i], data[i+1]) or self.IsDSM(data[i:]): 130 | return (PluginCore.Status.INVALID,i) 131 | 132 | #otherwise return the length 133 | if pack_len != 0: 134 | return (PluginCore.Status.OK,pack_len) 135 | 136 | #implied else 137 | return (PluginCore.Status.UNKNOWN,0) 138 | -------------------------------------------------------------------------------- /Plugins/SLIP.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | This plugin will identify the SLIP (Serial Line IP) Protocol 4 | """ 5 | 6 | from PluginCore import PluginCore 7 | import struct 8 | 9 | class SLIP(PluginCore): 10 | ProtocolName = "slip" 11 | ProtocolDescription = "Serial Line IP" 12 | 13 | PluginDataLinkType = 8 #DLT_SLIP 14 | 15 | def OutputCallback(self, seconds, mseconds, data, Unused): 16 | #+----------------------+ 17 | #| Direction | 18 | #| (1 Octet) | 19 | #+----------------------+ 20 | #| Packet Type | 21 | #| (1 Octet) | 22 | #+----------------------+ 23 | #|Compression Info | 24 | #| (14 Octets) | 25 | #+----------------------| 26 | #| Payload | 27 | #. . 28 | #. . 29 | #. . 30 | 31 | return struct.pack("BBHIII", 0,int("40",16),0,0,0,0) #2 CHRS + 14 Octets (96 bits) - \x40 for an unmodified IP Diagram 32 | 33 | def Identify(self, data, capture_info): 34 | ESC = "\xDB" 35 | END = "\xC0" 36 | 37 | if len(data) == 0: 38 | return (PluginCore.Status.UNKNOWN,0) 39 | 40 | #Scan to find the END character 41 | for i in range(0, len(data)-1): 42 | if data[i] == END: 43 | if i > 0 and data[i-1] != ESC: 44 | return (PluginCore.Status.OK,i+1) 45 | 46 | return (PluginCore.Status.UNKNOWN,0) 47 | -------------------------------------------------------------------------------- /Plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsacyber/serial2pcap/6f367df0e7cfe8ef05e25ee810fbed61b4802b99/Plugins/__init__.py -------------------------------------------------------------------------------- /Plugins/df1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | This plugin implements identifying the df1 protocol for serial2pcap. 4 | Reference Document Used: literature.rockwellautomation.com/idc/groups/literature/documents/rm/1770-rm516_-.en-p.pdf 5 | 6 | DF1 Structure: 7 | STX 02 8 | SOH 01 9 | ETX 03 10 | EOT 04 11 | ENQ 05 12 | ACK 06 13 | DLE 10 14 | NAK 0F 15 | 16 | All packets begin with DLE then are followed by another code. E.g: DLE ACK. 17 | Naturally occurring 0x10's are escaped with DLE and look like: DLE DLE 18 | """ 19 | 20 | from PluginCore import PluginCore 21 | 22 | class df1(PluginCore): 23 | ProtocolName = "df1" 24 | ProtocolDescription = "Allen Bradly DF1 Serial Protocol" 25 | 26 | df1_codes = [ 27 | "\x02", #STX 28 | "\x01", #SHO 29 | "\x03", #ETX 30 | "\x04", #EOT 31 | "\x05", #ENQ 32 | "\x06", #ACK 33 | "\x10", #DLE 34 | "\x0F", #NAK 35 | ] 36 | 37 | def Identify(self, data, capture_info): 38 | #Check to see if there is enough data to even process 39 | if len(data) <= 2: 40 | return (PluginCore.Status.TOOSHORT,0) 41 | 42 | #if the data does not start with DLE then it is not the start of a packet so return unknown 43 | if data[0] != "\x10": 44 | return (PluginCore.Status.UNKNOWN,0) 45 | 46 | #if the next bit is also a DLE then it is a DLE DLE so just ignore it 47 | elif data[1] == "\x10": 48 | return (PluginCore.Status.UNKNOWN,0) 49 | 50 | #this assumes that DLE XXX has been found (where XXX is not DLE)- check to see if it a valid DF1 code 51 | elif data[1] in self.df1_codes: 52 | #scan to find the next DLE XXX 53 | for i in range(2,len(data)-1): 54 | if data[i] == "\x10" and data[i+1] != "\x10" and data[i+1] in self.df1_codes: 55 | return (PluginCore.Status.OK,i) 56 | 57 | #if a DLE XXX is not found then return too short 58 | return (PluginCore.Status.TOOSHORT,0) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serial2pcap 2 | Serial2pcap converts serial IP data, typically collected from Industrial Control System devices, to the more commonly used Packet Capture (PCAP) format. 3 | It is designed to support multiple serial protocols and plug-ins can be developed by independent users. 4 | 5 | ### Dependencies 6 | Python 2.7 7 | 8 | Pyserial 9 | 10 | ### Usage 11 | Please see the [usage file](./USAGE.md) 12 | 13 | ## Supported protocols 14 | modbusASCII: Plugin to detect serial modbusASCII 15 | 16 | byte: Single Byte - Generates A Packet For Every Byte 17 | 18 | dnp3: Plugin to detect serial dnp3 19 | 20 | selfm: SEL Fast Message 21 | 22 | df1: Allen Bradly DF1 Serial Protocol 23 | 24 | modbusRTU: Modbus RTU Frame Format Serial Protocol - Currently Only Works Well With 9600 and 19200 Baud Rate On Non Merged Taps 25 | 26 | ## Versioning 27 | Currently on Verison 2.0 28 | 29 | ## License 30 | See [LICENSE](./LICENSE.md). 31 | 32 | ## Disclaimer 33 | See [DISCLAIMER](./DISCLAIMER.md). -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | serial2pcap.py [-h] [-p {modbusASCII,byte,dnp3,selfm,df1,modbusRTU}] 4 | [--list] [-o OUTFILE] [-b BAUDRATE] [-P {N,E,O,M,S}] 5 | [-s {5,6,7,8}] [-x {1,1.5,2}] [-d DEVICE] [--view] 6 | [--dumpfile DUMPFILE] [-t] [-r RAWINPUT] [-w SPLIT] 7 | [--debug] 8 | 9 | optional arguments: 10 | 11 | -h, --help show this help message and exit 12 | 13 | -p {modbusASCII,byte,dnp3,selfm,df1,modbusRTU}, --protocol {modbusASCII,byte,dnp3,selfm,df1,modbusRTU} 14 | to see a more detailed list of protocols use --list 15 | 16 | --list print a detailed list of all available protocols 17 | 18 | -o OUTFILE, --outfile OUTFILE 19 | Location of the output PCAP file to use 20 | 21 | -b BAUDRATE, --baudrate BAUDRATE 22 | Baud rate such as 9600 or 115200 etc. 23 | 24 | -P {N,E,O,M,S}, --parity {N,E,O,M,S} 25 | Enable parity checking. Possible values: [N]one, 26 | [E]ven, [O]dd, [M]ark, [S]pace. Default is None 27 | 28 | -s {5,6,7,8}, --bytesize {5,6,7,8} 29 | Number of data bits. Possible values: 5, 6, 7, 8. 30 | Default is 8 31 | 32 | -x {1,1.5,2}, --stopbits {1,1.5,2} 33 | Number of stop bits. Possible values: 1, 1.5, 2, 34 | Default is 1 35 | 36 | -d DEVICE, --device DEVICE 37 | Device name or port number number 38 | 39 | --view Print the raw capture data to the screen 40 | 41 | --dumpfile DUMPFILE Output raw capture data to this file 42 | 43 | -t, --timestamp Add timing information the the raw dumpfile 44 | 45 | -r RAWINPUT, --rawinput RAWINPUT 46 | raw dump input file 47 | 48 | -w SPLIT, --split SPLIT 49 | Split output files after capturing specified number of 50 | bytes. New files will be appended with .1, .2, etc. 51 | 52 | --debug Display debugging messages 53 | 54 | 55 | At any time while the program is running, press the enter key to display live statistics about the running capture (running time, packets captured, and bytes captured) 56 | 57 | Press ctrl-c to stop the program 58 | 59 | 60 | 61 | ## Supported Protocols 62 | modbusASCII: Plugin to detect serial modbusASCII 63 | 64 | byte: Single Byte - Generates A Packet For Every Byte 65 | 66 | dnp3: Plugin to detect serial dnp3 67 | 68 | selfm: SEL Fast Message 69 | 70 | df1: Allen Bradly DF1 Serial Protocol 71 | 72 | modbusRTU: Modbus RTU Frame Format Serial Protocol - Currently Only Works Well With 9600 and 19200 Baud Rate On Non Merged Taps 73 | 74 | 75 | ## Example Usage 76 | Dump Raw Data To A File: 77 | serial2pcap.py -d [device] --dumpfile [filename] 78 | 79 | Dump Raw Data To A File With Embedded Time Information: 80 | serial2pcap.py -d [device] --dumpfile [filename] -t 81 | 82 | View Raw Data: 83 | serial2pcap.py -d [device] --view 84 | 85 | Dump Raw Data To PCAP When Raw Data Protocol Is DNP3: 86 | serial2pcap.py -d [device] -p dnp3 -o [pcap filename] 87 | 88 | Create A PCAP File and Split The PCAP File Every 1MB Captured: 89 | serial2pcap.py -d [device] -p [protocol] -o [pcap filename] -w 1000000 90 | 91 | Create A Dump File and Split The File Every 1MB Captured: 92 | serial2pcap.py -d [device] --dumpfile [filename] -w 1000000 93 | 94 | Convert A Dump File With Timestamps (-t) To A Raw Dump File: 95 | serial2pcap.py -r [infile] --dumpfile [outfile] 96 | 97 | 98 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.0 2 | -------------------------------------------------------------------------------- /libserial2pcap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import traceback 5 | import threading 6 | import time 7 | import traceback 8 | import pkgutil 9 | 10 | from Lib.CaptureDataBuffer import CaptureDataBuffer 11 | from Lib.CaptureDataBuffer import CaptureDataBufferSimple 12 | from Lib.BufferOutput import BufferOutput 13 | from Lib.SerialPortReader import SerialPortReader 14 | from Lib.SerialPortReader import FileReader 15 | 16 | import logging 17 | 18 | #Definition for INFINITE - used by time out functions with in serial2pcap 19 | INFINITE = 0xFFFFFFFF 20 | 21 | class Serial2PCAP(): 22 | #common buffer shared between serial2pcap and the buffer output thread 23 | pcapbuffer = None 24 | 25 | #holds the status of the running capture - either "Stopped" or "Running" 26 | status = "Stopped" 27 | 28 | #variables that hold information about the available plugins 29 | pluginlist = None 30 | pluginlist_ref = None 31 | pluginlisthelp = None 32 | #holds the configuration settings for the capture 33 | args = {} 34 | 35 | #internal data structure that helps move data between the serialportreader/filereader and the buffer output threads 36 | databuffer = None 37 | #coordination event between all threads to coordinate shut down 38 | StopEvent = None 39 | #file end event to signal the EOF when using a file 40 | FileEOF = None 41 | 42 | #handles to the different threads 43 | DataReaderThread = None 44 | bufferoutputthread = None 45 | 46 | #instance of this class will be created and returned by getStatistics 47 | class Statistics: 48 | def __init__(self): 49 | pass 50 | 51 | total_packets_captured = 0 52 | total_bytes_captured = 0 53 | running_time = 0.0 54 | 55 | def __init__(self): 56 | logger = logging.getLogger("serial2pcap").getChild(__name__) 57 | 58 | #initialize variables 59 | self.pcapbuffer = CaptureDataBufferSimple() 60 | 61 | #temporary variables related to program plugins 62 | pluginlist = [] 63 | pluginlist_ref = {} 64 | pluginlisthelp = "" 65 | 66 | ##This block of code identifies all plugins then dynamically loads them 67 | import Plugins 68 | plugins = [name for _, name, _ in pkgutil.iter_modules([os.path.dirname(Plugins.__file__)])] 69 | 70 | for plugin in plugins: 71 | #load plugins inside of a try block, so that a plugin doesn't crash the program 72 | try: 73 | #plugins need to have a class named the same as the file name for this to work (eg. test.py loads class test) 74 | exec("from Plugins. " + plugin + " import " + plugin) in globals() 75 | tempplugin = eval(plugin + "()") 76 | 77 | #only load the plugin if the ProtocolName has been set - this prevents things like PluginCore from being loaded 78 | if tempplugin.ProtocolName != "": 79 | #make sure that duplicate plugin names are not being loaded 80 | if tempplugin.ProtocolName not in pluginlist_ref.keys(): 81 | #add the plugin name to the list 82 | pluginlist.append(tempplugin.ProtocolName) 83 | #and the LUT 84 | pluginlist_ref[tempplugin.ProtocolName] = plugin 85 | #also build the help string 86 | pluginlisthelp = pluginlisthelp + tempplugin.ProtocolName + ": " + tempplugin.ProtocolDescription + "\n" 87 | 88 | else: 89 | logger.warning("Duplicate Plugin Name \"" + tempplugin.ProtocolName + "\"") 90 | except: 91 | logger.warning("Error Loading Plugin: " + plugin) 92 | logger.warning("Error Message Was: " + traceback.format_exc()) 93 | pass 94 | 95 | #set class variables with the temp variables 96 | self.pluginlist = pluginlist 97 | self.pluginlist_ref = pluginlist_ref 98 | self.pluginlisthelp = pluginlisthelp 99 | 100 | #returns a list of the protocol plugins available 101 | def get_plugins(self): 102 | return self.pluginlist 103 | 104 | #accepts a plugin name and returns the help string associated with the plugin 105 | def get_plugin_description(self, plugin): 106 | logger = logging.getLogger("serial2pcap").getChild(__name__) 107 | 108 | #exception handling so that the plugin does not crash the program 109 | try: 110 | plugin = eval(self.pluginlist_ref[plugin] + "()") 111 | except: 112 | logger.error("Caught Exception in the plugin") 113 | logger.error(traceback.format_exc()) 114 | raise RuntimeError("Caught Exception in the plugin") 115 | 116 | return plugin.ProtocolDescription 117 | 118 | #sets up the configuration for a capture from a device 119 | def setup_device(self, baudrate, parity, bytesize, stopbits, device): 120 | #acceptable inputs 121 | __parity = ['N', 'E', 'O', 'M', 'S'] 122 | __bytesize = ['5', '6', '7', '8'] 123 | __stopbits = ['1', '1.5', '2'] 124 | 125 | if parity not in __parity: 126 | raise RuntimeError("Invalid Parity") 127 | 128 | if bytesize not in __bytesize: 129 | raise RuntimeError("Invlaid bytesize") 130 | 131 | if stopbits not in __stopbits: 132 | raise RuntimeError("Invlaid stopbits") 133 | 134 | if not isinstance(baudrate, int): 135 | raise RuntimeError("Invalid baudrate") 136 | 137 | #Cant do this check on windows 138 | #if not os.path.exists(device): 139 | # raise RuntimeError("Device Does Not Exist") 140 | 141 | self.__setup(baudrate, parity, bytesize, stopbits, device, "device") 142 | 143 | #sets up the configuration for a capture from a file 144 | def setup_file(self, filename): 145 | if not os.path.isfile(filename): 146 | raise IOError("File Not Found") 147 | 148 | self.__setup(None, None, None, None, filename, "file") 149 | 150 | #private call that is used by setup_device and setup_file to implement setting the correct variables 151 | def __setup(self, baudrate, parity, bytesize, stopbits, target, targettype): 152 | self.args['baudrate'] = baudrate 153 | self.args['parity'] = parity 154 | self.args['bytesize'] = bytesize 155 | self.args['stopbits'] = stopbits 156 | self.args['target'] = target 157 | self.args['targettype'] = targettype 158 | 159 | #this function sets up and starts the entire capture pipeline 160 | def start(self, protocol): 161 | logger = logging.getLogger("serial2pcap").getChild(__name__) 162 | 163 | if self.get_status() == "Running": 164 | return False 165 | 166 | if protocol not in self.get_plugins(): 167 | raise RuntimeError("Invalid Protocol") 168 | 169 | self.databuffer = CaptureDataBuffer() 170 | 171 | self.StopEvent = threading.Event() 172 | self.StopEvent.clear() 173 | 174 | self.FileEOF = threading.Event() 175 | self.FileEOF.clear() 176 | 177 | if self.args['targettype'] == "device": 178 | self.DataReaderThread = threading.Thread(target=SerialPortReader, args=[self.args['target'], self.args['baudrate'], self.args['bytesize'], self.args['parity'], self.args['stopbits'], self.databuffer, None, None, self.StopEvent, None]) 179 | 180 | elif self.args['targettype'] == "file": 181 | self.DataReaderThread = threading.Thread(target=FileReader, args=[self.databuffer, None, None, self.args['target'], self.StopEvent, self.FileEOF]) 182 | 183 | else: 184 | raise RuntimeError 185 | 186 | #exception handling so that the plugin does not crash the program 187 | try: 188 | plugin = eval(self.pluginlist_ref[protocol] + "()") 189 | except: 190 | logger.error("Caught Exception in the plugin") 191 | logger.error(traceback.format_exc()) 192 | raise RuntimeError("Caught Exception in the plugin") 193 | 194 | self.bufferoutputthread = threading.Thread(target=BufferOutput, args=[self.databuffer, None, plugin, self.StopEvent, None, self.pcapbuffer]) 195 | 196 | self.DataReaderThread.start() 197 | self.bufferoutputthread.start() 198 | 199 | self.status = "Running" 200 | 201 | return True 202 | 203 | #stops the entire capture pipeline 204 | def stop(self): 205 | self.StopEvent.set() 206 | self.DataReaderThread.join() 207 | self.bufferoutputthread.join() 208 | 209 | self.status = "Stopped" 210 | 211 | return True 212 | 213 | #returns the status of the capture 214 | def get_status(self): 215 | if self.status == "Running" and self.FileEOF.isSet(): 216 | self.status = "Stopped" 217 | 218 | return self.status 219 | 220 | #returns the next available packet 221 | #optional parameter timeout determines how long to wait before returning - default is infinite will not return until more data is received 222 | #either returns the next packet or None if it times out 223 | def get_next_packet(self, timeout=INFINITE): 224 | if self.get_status() == "Stopped" and self.available_packets() == 0: 225 | return None 226 | 227 | sleeptime = 0.25 228 | curtime = 0.0 229 | 230 | packet = None 231 | 232 | while True: 233 | self.pcapbuffer.lock.acquire() 234 | 235 | if len(self.pcapbuffer.raw) > 0: 236 | packet = self.pcapbuffer.raw.pop(0) 237 | 238 | self.pcapbuffer.lock.release() 239 | 240 | if packet is not None: 241 | break 242 | else: 243 | time.sleep(sleeptime) 244 | curtime += sleeptime 245 | 246 | if timeout != INFINITE and curtime >= timeout: 247 | break 248 | 249 | return packet 250 | 251 | #returns the current number of available packets in the buffer 252 | def available_packets(self): 253 | self.pcapbuffer.lock.acquire() 254 | length = len(self.pcapbuffer.raw) 255 | self.pcapbuffer.lock.release() 256 | 257 | return length 258 | 259 | #returns a statistics class with statistical capture information 260 | def get_statistics(self): 261 | stats = self.Statistics() 262 | 263 | stats.total_packets_captured = self.databuffer.total_packets_captured 264 | stats.total_bytes_captured = self.databuffer.total_bytes_captured 265 | stats.running_time = time.time() - self.databuffer.start_time 266 | 267 | return stats 268 | -------------------------------------------------------------------------------- /libserial2pcapunittest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from libserial2pcap import Serial2PCAP 4 | 5 | import unittest 6 | import tempfile 7 | import time 8 | 9 | class TestSerial2PCAP(unittest.TestCase): 10 | 11 | def test_sanity(self): 12 | #check to see if any exceptions are raised 13 | s = Serial2PCAP() 14 | 15 | def test_arguments_file(self): 16 | #get a temp file 17 | t = tempfile.NamedTemporaryFile() 18 | 19 | s = Serial2PCAP() 20 | 21 | #make sure that normal operation is normal 22 | s.setup_file(t.name) 23 | 24 | #close the file to make it disappear from the system 25 | t.close() 26 | 27 | #since the file is gone this should raise and IOError 28 | with self.assertRaises(IOError): 29 | s.setup_file(t.name) 30 | 31 | def test_arguments_device(self): 32 | s = Serial2PCAP() 33 | 34 | #good inputs 35 | baudrate = [9600,19200,115200] 36 | parity = ['N','E','O','M','S'] 37 | bytesize = ['5','6','7','8'] 38 | stopbits = ['1','1.5','2'] 39 | #device = ["/dev/ttyS0"] 40 | device = ["com1"] 41 | 42 | #check every combination of good inputs 43 | for x in baudrate: 44 | for p in parity: 45 | for b in bytesize: 46 | for z in stopbits: 47 | for d in device: 48 | #should not throw exception 49 | s.setup_device(x,p,b,z,d) 50 | 51 | #Check Bad inputs in every field 52 | with self.assertRaises(RuntimeError): 53 | s.setup_device("asdf","N","8","1","/dev/ttyS0") 54 | with self.assertRaises(RuntimeError): 55 | s.setup_device(9600,"X","8","1","/dev/ttyS0") 56 | with self.assertRaises(RuntimeError): 57 | s.setup_device(9600,"N","99","1","/dev/ttyS0") 58 | with self.assertRaises(RuntimeError): 59 | s.setup_device(9600,"N","8","10101","/dev/ttyS0") 60 | #Cant do this check on windows 61 | #with self.assertRaises(RuntimeError): 62 | # s.setup_device(9600,"N","8","1","--blank--") 63 | 64 | def test_getPlugins(self): 65 | s = Serial2PCAP() 66 | 67 | #make sure that getPlugins returns something 68 | self.assertIsNotNone(s.get_plugins()) 69 | #make sure that get plugins does not just return an empty [] 70 | self.assertNotEqual(s.get_plugins(), []) 71 | 72 | #get the list of plugins for the next test 73 | plugins = s.get_plugins() 74 | 75 | #check the help output for every plugin to make sure that it does not return none or "" 76 | for plugin in plugins: 77 | self.assertIsNotNone(s.get_plugin_description(plugin)) 78 | self.assertNotEqual(s.get_plugin_description(plugin), "") 79 | 80 | #this test may error in windows because it cant set wr+b on a named temp file 81 | def test_pipeline_file(self): 82 | test_data = "test_data" 83 | 84 | #create a temp file and write the test data to it 85 | t = tempfile.NamedTemporaryFile(mode="wr+b") 86 | t.write(test_data) 87 | t.flush() 88 | 89 | s = Serial2PCAP() 90 | 91 | #set the file options 92 | s.setup_file(t.name) 93 | 94 | #make sure that s.start checks for valid protocols 95 | with self.assertRaises(RuntimeError): 96 | s.start("asdfasdfasdf") 97 | 98 | #make sure the pipeline registers as shut down 99 | self.assertEqual(s.get_status(), "Stopped") 100 | 101 | #start the pipeline 102 | self.assertEqual(s.start("byte"), True) 103 | 104 | #wait for the pipeline to process the file 105 | time.sleep(3) 106 | 107 | #make sure the pipeline registers as shut down 108 | self.assertEqual(s.get_status(), "Stopped") 109 | 110 | #check to see the available data length 111 | self.assertEqual(s.available_packets(), len(test_data)) 112 | 113 | for i in range(0, s.available_packets()): 114 | data = s.get_next_packet()["packet"] 115 | #make sure the pipeline returns the correct data 116 | self.assertEqual(data[len(data)-1], test_data[i]) 117 | 118 | #make sure available packets is correct 119 | self.assertEqual(s.available_packets(), len(test_data)-i-1) 120 | 121 | #check the statistics output 122 | sat = s.getStatistics() 123 | self.assertEqual(sat.total_packets_captured, len(test_data)) 124 | self.assertEqual(sat.total_bytes_captured, len(test_data)) 125 | self.assertNotEqual(sat.running_time, 0) 126 | 127 | #make sure that the pipeline can be stopped 128 | self.assertEqual(s.stop(), True) 129 | #make sure the pipeline can be stopped twice 130 | self.assertEqual(s.stop(), True) 131 | #make sure the pipeline registers as shut down 132 | self.assertEqual(s.get_status(), "Stopped") 133 | 134 | #try to restart the pipeline 135 | self.assertEqual(s.start("byte"), True) 136 | #wait for the pipeline to process the file 137 | time.sleep(3) 138 | #check to see the available data length 139 | self.assertEqual(s.available_packets(), len(test_data)) 140 | 141 | s.stop() 142 | 143 | def test_pipeline_device(self): 144 | s = Serial2PCAP() 145 | 146 | s.setup_device(9600, "N", "8", "1", "com1") 147 | 148 | #make sure the pipeline registers as shut down 149 | self.assertEqual(s.get_status(), "Stopped") 150 | 151 | #make sure the pipeline starts 152 | self.assertEqual(s.start("byte"), True) 153 | 154 | #give the pipeline time to start 155 | time.sleep(3) 156 | 157 | #make sure the pipeline registers as Running 158 | self.assertEqual(s.get_status(), "Running") 159 | 160 | #make sure the pipeline complains about being started twice 161 | self.assertEqual(s.start("byte"), False) 162 | 163 | #make sure the pipeline registers as Running 164 | self.assertEqual(s.get_status(), "Running") 165 | 166 | #make sure that the pipeline can be stopped 167 | self.assertEqual(s.stop(), True) 168 | #make sure the pipeline can be stopped twice 169 | self.assertEqual(s.stop(), True) 170 | 171 | #make sure the pipeline registers as shut down 172 | self.assertEqual(s.get_status(), "Stopped") 173 | 174 | if s.available_packets() != 0: 175 | raise RuntimeError("Something Went Wrong With The Test - Received Data On Serial Line When No Data Was Supposed To Be Received") 176 | 177 | #make sure that getNextPacket returns none when there is no data in the pipeline and the pipeline is stopped 178 | x1 = time.time() 179 | self.assertIsNone(s.get_next_packet(timeout=5)) 180 | x2 = time.time() 181 | #make sure that it didn't just time out 182 | self.assertLess(x2 - x1, 5) 183 | 184 | if __name__ == "__main__": 185 | unittest.main() 186 | -------------------------------------------------------------------------------- /serial2pcap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function 4 | import sys 5 | if sys.version_info.major != 2 and sys.version_info.minor != 7: 6 | print("serial2pcap only works with python version 2.7") 7 | quit() 8 | import time 9 | import argparse 10 | import threading 11 | import os 12 | import traceback 13 | import pkgutil 14 | 15 | from Lib.CaptureDataBuffer import CaptureDataBuffer 16 | from Lib.BufferOutput import BufferOutput 17 | from Lib.SerialPortReader import SerialPortReader 18 | from Lib.SerialPortReader import FileReader 19 | from Lib.BlankConsumer import BlankConsumer 20 | 21 | import logging 22 | import Lib.util 23 | 24 | if __name__ == "__main__": 25 | Lib.util.SetUpLogging(logging.INFO) 26 | logger = logging.getLogger("serial2pcap").getChild(__name__) 27 | 28 | pluginlist = [] 29 | pluginlist_ref = {} 30 | pluginlisthelp = "" 31 | 32 | ##This block of code identifies all plugins then dynamically loads them 33 | import Plugins 34 | plugins = [name for _, name, _ in pkgutil.iter_modules([os.path.dirname(Plugins.__file__)])] 35 | 36 | for plugin in plugins: 37 | #load plugins inside of a try block, so that a plugin doesn't crash the program 38 | try: 39 | #plug ins need to have a class named the same as the file name for this to work (e.g.. test.py loads class test) 40 | exec("from Plugins. " + plugin + " import " + plugin) in globals() 41 | tempplugin = eval(plugin + "()") 42 | 43 | #only load the plugin if the ProtocolName has been set - this prevents things like PluginCore from being loaded 44 | if tempplugin.ProtocolName != "": 45 | #make sure that duplicate plugin names are not being loaded 46 | if tempplugin.ProtocolName not in pluginlist_ref.keys(): 47 | #add the plugin name to the list 48 | pluginlist.append(tempplugin.ProtocolName) 49 | #and the LUT 50 | pluginlist_ref[tempplugin.ProtocolName] = plugin 51 | #also build the help string 52 | pluginlisthelp = pluginlisthelp + tempplugin.ProtocolName + ": " + tempplugin.ProtocolDescription + "\n" 53 | 54 | else: 55 | logger.warning("Duplicate Plugin Name \"" + tempplugin.ProtocolName + "\"") 56 | except: 57 | logger.warning("Error Loading Plugin: " + plugin) 58 | logger.warning("Error Message Was:") 59 | traceback.print_exc() 60 | print("\n") 61 | pass 62 | 63 | parser = argparse.ArgumentParser() 64 | parser.add_argument("-p", "--protocol", choices=pluginlist, help="to see a more detailed list of protocols use --list") 65 | parser.add_argument("--list", action="store_true", help="print a detailed list of all available protocols") 66 | parser.add_argument("-o", "--outfile", help="Location of the output PCAP file to use") 67 | parser.add_argument("-b", "--baudrate", default='9600', help="Baud rate such as 9600 or 115200 etc.") 68 | parser.add_argument("-P", "--parity", default='N', choices=['N', 'E', 'O', 'M', 'S'], help="Enable parity checking. Possible values: [N]one, [E]ven, [O]dd, [M]ark, [S]pace. Default is None") 69 | parser.add_argument("-s", "--bytesize", default='8', choices=['5', '6', '7', '8'], help="Number of data bits. Possible values: 5, 6, 7, 8. Default is 8") 70 | parser.add_argument("-x", "--stopbits", default='1', choices=['1', '1.5', '2'], help="Number of stop bits. Possible values: 1, 1.5, 2, Default is 1") 71 | parser.add_argument("-d", "--device", help="Device name or port number number") 72 | parser.add_argument("--view", action="store_true", default=False, help="Print the raw capture data to the screen") 73 | parser.add_argument("-D", "--dumpfile", help="Output raw capture data to this file") 74 | parser.add_argument("-t", "--timestamp", action="store_true", help="Add timing information the the raw dump file") 75 | parser.add_argument("-r", "--rawinput", help="raw dump input file") 76 | parser.add_argument("-w", "--split", help="Split output files after capturing specified number of bytes. New files will be appended with .1, .2, etc.") 77 | parser.add_argument("--debug", action="store_true", default=False, help="Display debugging messages") 78 | parser.add_argument("--version", action="store_true", default=False, help="Display Version Information and Exit") 79 | args = parser.parse_args() 80 | 81 | if args.version: 82 | print("2.0") 83 | quit() 84 | 85 | #print the list of available plugins if requested 86 | if args.list: 87 | print("Available Plugins:") 88 | print(pluginlisthelp) 89 | quit() 90 | 91 | #user input needs to either specify an input file or a device to use otherwise there is nothing for the program to do 92 | if args.device is None and args.rawinput is None: 93 | print("Must Specify either --device or --rawinput\n") 94 | parser.print_help() 95 | quit() 96 | 97 | if args.outfile is None and args.dumpfile is None and args.view == False: 98 | print("You Did Not Specify Anything For The Program To Do. This Is Probably Not What You Wanted.\n") 99 | parser.print_help() 100 | quit() 101 | 102 | if args.dumpfile is not None and args.rawinput is not None and args.timestamp == True: 103 | print("You Can Not Add Time Stamp Information To A Dump File When Reading From A Dump File.\n") 104 | parser.print_help() 105 | quit() 106 | 107 | plugin = None 108 | if args.protocol is None or args.outfile is None: 109 | plugin = None 110 | #load the selected plugin 111 | else: 112 | #exception handling so that the plugin does not crash the program 113 | try: 114 | plugin = eval(pluginlist_ref[args.protocol] + "()") 115 | except: 116 | traceback.print_exc() 117 | print("Caught Exception in the plugin, Shutting Down") 118 | quit() 119 | 120 | #output file without a plugin doesn't make sense 121 | if plugin is None and args.outfile is not None: 122 | print("\nYou need to specify a protocol (-p\--protocol) for the output file (-o\--output) to do anything\n") 123 | quit() 124 | 125 | #make sure that the output file does not already exist, if it does then quit and tell the user about it 126 | if (args.outfile is not None and os.path.exists(args.outfile)) or (args.dumpfile is not None and os.path.exists(args.dumpfile)): 127 | print("The Output File Already Exists. Pick Another File Name") 128 | quit() 129 | 130 | #databuffer is the shared memory location that all threads will interact with 131 | databuffer = CaptureDataBuffer() 132 | 133 | if args.split is not None and args.split != 0: 134 | databuffer.file_split_size = int(args.split) 135 | 136 | #define the event that will coordinate shutting down all of the threads 137 | StopEvent = threading.Event() 138 | StopEvent.clear() 139 | 140 | FileEOF = threading.Event() 141 | FileEOF.clear() 142 | 143 | #livecapture variable will be used to determine if live capture information is available 144 | livecapture = False 145 | #select which input thread will be used - either from a serial port or a file 146 | if args.rawinput is None: 147 | DataReaderThread = threading.Thread(target=SerialPortReader, args=[args.device, args.baudrate, args.bytesize, args.parity, args.stopbits, databuffer, args.view, args.dumpfile, StopEvent, args.timestamp]) 148 | livecapture = True 149 | else: 150 | if args.timestamp == True: 151 | print("You Can Not Add Time Stamp Information To A Dump File When Reading From A Dump File.\n") 152 | parser.print_help() 153 | quit() 154 | DataReaderThread = threading.Thread(target=FileReader, args=[databuffer, args.view, args.dumpfile, args.rawinput, StopEvent, FileEOF]) 155 | #start the input thread 156 | DataReaderThread.start() 157 | 158 | bufferoutputthread = None 159 | #if there is a defined output file and a defined plugin then start the output plugin 160 | if args.outfile is not None and plugin is not None: 161 | bufferoutputthread = threading.Thread(target=BufferOutput, args=[databuffer, args.outfile, plugin, StopEvent, args.debug, None]) 162 | bufferoutputthread.start() 163 | #if you're using a dump file and an actual device 164 | elif args.dumpfile is not None and args.device is not None: 165 | pass 166 | elif args.view is not None and args.device is not None: 167 | pass 168 | else: 169 | bufferoutputthread = threading.Thread(target=BlankConsumer, args=[databuffer, StopEvent]) 170 | bufferoutputthread.start() 171 | 172 | #main thread while loop - basically just wait until the stop event is signalled 173 | try: 174 | while not StopEvent.isSet(): 175 | if livecapture == True: 176 | #read(1) will cause the main thread to wait for the user to press enter, then the statistics will be displayed 177 | sys.stdin.read(1) 178 | print("Running Time: " + str(int(time.time() - databuffer.start_time)) + " seconds, Packets Captured: " + str(databuffer.total_packets_captured) + ", Bytes Captured: " + str(databuffer.total_bytes_captured) + "b") 179 | else: 180 | time.sleep(0.5) 181 | except KeyboardInterrupt: 182 | print("Shutting Down") 183 | StopEvent.set() 184 | 185 | #wait for the threads to close and quit 186 | DataReaderThread.join() 187 | 188 | if bufferoutputthread is not None: 189 | bufferoutputthread.join() 190 | 191 | sys.stdout.write("\n") 192 | print("Running Time: " + str(int(time.time() - databuffer.start_time)) + " seconds, Packets Captured: " + str(databuffer.total_packets_captured) + ", Bytes Captured: " + str(databuffer.total_bytes_captured) + "b") 193 | sys.stdout.write("\n") 194 | quit() 195 | --------------------------------------------------------------------------------