#!/usr/bin/env python #------------------------------------------------------------------ # November 2014, created within ASIG # Author James Spadaro (jaspadar) # Co-Author Lilith Wyatt (liwyatt) #------------------------------------------------------------------ # Copyright (c) 2014-2017 by Cisco Systems, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the Cisco Systems, Inc. nor the # names of its contributors may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND 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 COPYRIGHT HOLDERS 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 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #------------------------------------------------------------------ # Type definitions for the fuzzer # # This script defines the various message and data types used in # the fuzzer, and utility functions used by them. #------------------------------------------------------------------ class MessageSubComponent(object): def __init__(self, message, isFuzzed): self.message = message self.isFuzzed = isFuzzed # This includes both fuzzed messages and messages the user # has altered with messageprocessor callbacks self._altered = message def setAlteredByteArray(self, byteArray): self._altered = byteArray def getAlteredByteArray(self): return self._altered def getOriginalByteArray(self): return self.message # Contains all data of a given packet of the session class Message(object): class Direction: Outbound = "outbound" Inbound = "inbound" class Format: CommaSeparatedHex = 0 # 00,01,02,20,2a,30,31 Ascii = 1 # asdf\x00\x01\x02 Raw = 2 # a raw byte array from a pcap def __init__(self): self.direction = -1 # Whether any subcomponent is fuzzed - might not be entire message # Default to False, set to True as message subcomponents are set below self.isFuzzed = False # This will be populated with message subcomponents # IE, specified as message 0 11,22,33 # 44,55,66 # Then 11,22,33 will be subcomponent 0, 44,55,66 will be subcomponent 1 # If it's a traditional message, it will only have one element (entire message) self.subcomponents = [] def getOriginalSubcomponents(self): return [subcomponent.message for subcomponent in self.subcomponents] # May or may not have actually been changed # Version of subcomponents that includes fuzzing and messageprocessor changes from user # Is transient and reverted to original every iteration def getAlteredSubcomponents(self): return [subcomponent.getAlteredByteArray() for subcomponent in self.subcomponents] def getOriginalMessage(self): return bytearray().join([subcomponent.message for subcomponent in self.subcomponents]) # May or may not have actually been changed # Version of message that includes fuzzing and messageprocessor changes from user # Is transient and reverted to original every iteration def getAlteredMessage(self): return bytearray().join([subcomponent.getAlteredByteArray() for subcomponent in self.subcomponents]) def resetAlteredMessage(self): for subcomponent in self.subcomponents: subcomponent.setAlteredByteArray(subcomponent.message) # Set the message on the Message # sourceType - Format.CommaSeparatedHex, Ascii, or Raw # message - Message in above format # isFuzzed - whether this message should have its subcomponent # flag isFuzzed set def setMessageFrom(self, sourceType, message, isFuzzed): if sourceType == self.Format.CommaSeparatedHex: message = bytearray([x.decode("hex") for x in message.split(",")]) elif sourceType == self.Format.Ascii: message = self.deserializeByteArray(message) elif sourceType == self.Format.Raw: message = message else: raise RuntimeError("Invalid sourceType") self.subcomponents = [MessageSubComponent(message, isFuzzed)] if isFuzzed: self.isFuzzed = True # Same arguments as above, but adds to .message as well as # adding a new subcomponent # createNewSubcomponent - If false, don't create another subcomponent, # instead, append new message data to last subcomponent in message def appendMessageFrom(self, sourceType, message, isFuzzed, createNewSubcomponent=True): if sourceType == self.Format.CommaSeparatedHex: newMessage = bytearray([x.decode("hex") for x in message.split(",")]) elif sourceType == self.Format.Ascii: newMessage = self.deserializeByteArray(message) elif sourceType == self.Format.Raw: newMessage = message else: raise RuntimeError("Invalid sourceType") if createNewSubcomponent: self.subcomponents.append(MessageSubComponent(newMessage, isFuzzed)) else: self.subcomponents[-1].message += newMessage if isFuzzed: # Make sure message is set to fuzz as well self.isFuzzed = True def isOutbound(self): return self.direction == self.Direction.Outbound def __eq__(self, other): # bytearray (for message) implements __eq__() return self.direction == other.direction and self.message == other.message @classmethod def serializeByteArray(cls, byteArray): return repr(str(byteArray)) @classmethod def deserializeByteArray(cls, string): # This appears to properly reverse repr() without the risks of eval return bytearray(string[1:-1].encode('utf8').decode('unicode-escape').encode('utf8')) def getAlteredSerialized(self): if len(self.subcomponents) < 1: return "{0} {1}\n".format(self.direction, "ERROR: No data in message.") else: serializedMessage = "{0}{1} {2}\n".format("fuzz " if self.subcomponents[0].isFuzzed else "", self.direction, self.serializeByteArray(self.subcomponents[0].getAlteredByteArray())) for subcomponent in self.subcomponents[1:]: serializedMessage += "sub {0}{1}\n".format("fuzz " if subcomponent.isFuzzed else "", self.serializeByteArray(subcomponent.getAlteredByteArray())) return serializedMessage def getSerialized(self): if len(self.subcomponents) < 1: return "{0} {1}\n".format(self.direction, "ERROR: No data in message.") else: serializedMessage = "{0} {1}{2}\n".format(self.direction, "fuzz " if self.subcomponents[0].isFuzzed else "", self.serializeByteArray(self.subcomponents[0].message)) for subcomponent in self.subcomponents[1:]: serializedMessage += "sub {0}{1}\n".format("fuzz " if subcomponent.isFuzzed else "", self.serializeByteArray(subcomponent.message)) return serializedMessage # Utility function for setFromSerialized and appendFromSerialized below def _extractMessageComponents(self, serializedData): firstQuoteSingle = serializedData.find('\'') lastQuoteSingle = serializedData.rfind('\'') firstQuoteDouble = serializedData.find('"') lastQuoteDouble = serializedData.rfind('"') firstQuote = -1 lastQuote = -1 if firstQuoteSingle == -1 or firstQuoteSingle == lastQuoteSingle: # If no valid single quotes, go double quote firstQuote = firstQuoteDouble lastQuote = lastQuoteDouble elif firstQuoteDouble == -1 or firstQuoteDouble == lastQuoteDouble: # If no valid double quotes, go single quote firstQuote = firstQuoteSingle lastQuote = lastQuoteSingle elif firstQuoteSingle < firstQuoteDouble: # If both are valid, go single if further out firstQuote = firstQuoteSingle lastQuote = lastQuoteSingle else: # Both are valid but double is further out firstQuote = firstQuoteDouble lastQuote = lastQuoteDouble if firstQuote == -1 or lastQuote == -1 or firstQuote == lastQuote: raise RuntimeError("Invalid message data, no message found") # Pull out everything, quotes and all, and deserialize it messageData = serializedData[firstQuote:lastQuote+1] # Process the args serializedData = serializedData[:firstQuote].split(" ") return (serializedData, messageData) # Handles _one line_ of data, either "inbound" or "outbound" # Lines following this should be passed to appendFromSerialized() below def setFromSerialized(self, serializedData): serializedData = serializedData.replace("\n", "") (serializedData, messageData) = self._extractMessageComponents(serializedData) if len(messageData) == 0 or len(serializedData) < 1: raise RuntimeError("Invalid message data") direction = serializedData[0] args = serializedData[1:-1] if direction != "inbound" and direction != "outbound": raise RuntimeError("Invalid message data, unknown direction {0}".format(direction)) isFuzzed = False if "fuzz" in args: isFuzzed = True if len(serializedData) < 3: raise RuntimeError("Invalid message data") self.direction = direction self.setMessageFrom(self.Format.Ascii, messageData, isFuzzed) # Add another line, used for multiline messages def appendFromSerialized(self, serializedData, createNewSubcomponent=True): serializedData = serializedData.replace("\n", "") (serializedData, messageData) = self._extractMessageComponents(serializedData) if createNewSubcomponent: if len(messageData) == 0 or len(serializedData) < 1 or serializedData[0] != "sub": raise RuntimeError("Invalid message data") else: # If not creating a subcomponent, we won't have "sub", "fuzz", and the other fun stuff if len(messageData) == 0: raise RuntimeError("Invalid message data") args = serializedData[1:-1] # Put either "fuzz" or nothing before actual message # Can tell the difference even with ascii because ascii messages have '' quotes # IOW, even a message subcomponent 'fuzz' will have the 's around it, not be fuzz without quotes isFuzzed = False if "fuzz" in args: isFuzzed = True self.appendMessageFrom(self.Format.Ascii, messageData, isFuzzed, createNewSubcomponent=createNewSubcomponent) class MessageCollection(object): def __init__(self): self.messages = [] def addMessage(self, message): self.messages.append(message) def doClientMessagesMatch(self, otherMessageCollection): for i in range(0, len(self.messages)): # Skip server messages if not self.messages[i].isOutbound(): continue try: # Message implements __eq__() if self.messages[i] != otherMessageCollection.messages[i]: return False except IndexError: return False # All messages passed return True import os import os.path from copy import deepcopy # Handles all the logging of the fuzzing session # Log messages can be found at sample_apps/<app>/<app>_logs/<date>/ class Logger(object): def __init__(self, folderPath): self._folderPath = folderPath if os.path.exists(folderPath): print("Data output directory already exists: %s" % (folderPath)) exit() else: try: os.makedirs(folderPath) except: print("Unable to create logging directory: %s" % (folderPath)) exit() self.resetForNewRun() # Store just the data, forget trying to make a Message object # With the subcomponents and everything, it just gets weird, # and we don't need it def setReceivedMessageData(self, messageNumber, data): self.receivedMessageData[messageNumber] = data def setHighestMessageNumber(self, messageNumber): # The highest message # this fuzz session made it to self._highestMessageNumber = messageNumber def outputLastLog(self, runNumber, messageCollection, errorMessage): return self._outputLog(runNumber, messageCollection, errorMessage, self._lastReceivedMessageData, self._lastHighestMessageNumber) def outputLog(self, runNumber, messageCollection, errorMessage): return self._outputLog(runNumber, messageCollection, errorMessage, self.receivedMessageData, self._highestMessageNumber) def _outputLog(self, runNumber, messageCollection, errorMessage, receivedMessageData, highestMessageNumber): with open(os.path.join(self._folderPath, str(runNumber)), "w") as outputFile: print("Logging run number %d" % (runNumber)) outputFile.write("Log from run with seed %d\n" % (runNumber)) outputFile.write("Error message: %s\n" % (errorMessage)) if highestMessageNumber == -1 or runNumber == 0: outputFile.write("Failed to connect on this run.\n") outputFile.write("\n") i = 0 for message in messageCollection.messages: outputFile.write("Packet %d: %s" % (i, message.getSerialized())) if message.isFuzzed: outputFile.write("Fuzzed Packet %d: %s\n" % (i, message.getAlteredSerialized())) if i in receivedMessageData: # Compare what was actually sent to what we expected, log if they differ if receivedMessageData[i] != message.getOriginalMessage(): outputFile.write("Actual data received for packet %d: %s" % (i, Message.serializeByteArray(receivedMessageData[i]))) else: outputFile.write("Received expected data\n") if highestMessageNumber == i: if message.isOutbound(): outputFile.write("This is the last message sent\n") else: outputFile.write("This is the last message received\n") outputFile.write("\n") i += 1 def resetForNewRun(self): try: self._lastReceivedMessageData = deepcopy(self.receivedMessageData) self._lastHighestMessageNumber = self._highestMessageNumber except AttributeError: self._lastReceivedMessageData = {} self._lastHighestMessageNumber = -1 self.receivedMessageData = {} self.setHighestMessageNumber(-1)