├── .gitignore ├── .gitmodules ├── LICENSE ├── backend ├── __init__.py ├── fuzzer_types.py ├── fuzzerdata.py ├── menu_functions.py ├── packets.py └── proc_director.py ├── mutiny.py ├── mutiny_classes ├── __init__.py ├── exception_processor.py ├── message_processor.py ├── monitor.py └── mutiny_exceptions.py ├── mutiny_prep.py ├── radamsa-v0.6.tar.gz ├── readme.md ├── sample_apps ├── pidlisten │ ├── data │ │ ├── pid_listen.fuzzer │ │ └── pidlisten.fuzzer │ └── source │ │ ├── pid_listener.py │ │ └── test_client.py ├── server │ ├── data │ │ ├── message_processor.py │ │ └── server-0.fuzzer │ └── source │ │ └── server.py ├── session_server │ ├── data │ │ ├── message_processor.py │ │ └── session_server-3.fuzzer │ └── source │ │ └── server.py ├── subcomponent_server │ ├── data │ │ ├── message_processor.py │ │ └── subcomponent-0.fuzzer │ └── source │ │ └── server.py └── vnc │ └── data │ ├── vnc-1.fuzzer │ └── vnc.c_arrays ├── tests ├── mutator │ └── mutator_test.py └── serialization │ ├── __init__.py │ └── serialization_test.py └── util ├── bsd_denull.py ├── fuzzer_converter.py └── pcap_dump.py /.gitignore: -------------------------------------------------------------------------------- 1 | radamsa-*/* 2 | *.swp 3 | *.pyc 4 | *_logs* 5 | sadsox 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cisco-Talos/mutiny-fuzzer/3f347d292ebf7e4dff596f54757663346222aeb1/.gitmodules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------ 2 | # November 2014, created within ASIG 3 | # Author James Spadaro (jaspadar) 4 | # Co-Author Lilith Wyatt (liwyatt) 5 | #------------------------------------------------------------------ 6 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 1. Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # 2. Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # 3. Neither the name of the Cisco Systems, Inc. nor the 17 | # names of its contributors may be used to endorse or promote products 18 | # derived from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 21 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | #------------------------------------------------------------------ 31 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /backend/fuzzer_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | #------------------------------------------------------------------ 32 | # Type definitions for the fuzzer 33 | # 34 | # This script defines the various message and data types used in 35 | # the fuzzer, and utility functions used by them. 36 | #------------------------------------------------------------------ 37 | import ast 38 | 39 | class MessageSubComponent(object): 40 | def __init__(self, message, isFuzzed): 41 | self.message = message 42 | self.isFuzzed = isFuzzed 43 | # This includes both fuzzed messages and messages the user 44 | # has altered with messageprocessor callbacks 45 | self._altered = message 46 | 47 | def setAlteredByteArray(self, byteArray): 48 | self._altered = byteArray 49 | 50 | def getAlteredByteArray(self): 51 | return self._altered 52 | 53 | def getOriginalByteArray(self): 54 | return self.message 55 | 56 | # Contains all data of a given packet of the session 57 | class Message(object): 58 | class Direction: 59 | Outbound = "outbound" 60 | Inbound = "inbound" 61 | 62 | class Format: 63 | CommaSeparatedHex = 0 # 00,01,02,20,2a,30,31 64 | Ascii = 1 # asdf\x00\x01\x02 65 | Raw = 2 # a raw byte array from a pcap 66 | 67 | def __init__(self): 68 | self.direction = -1 69 | # Whether any subcomponent is fuzzed - might not be entire message 70 | # Default to False, set to True as message subcomponents are set below 71 | self.isFuzzed = False 72 | # This will be populated with message subcomponents 73 | # IE, specified as message 0 11,22,33 74 | # 44,55,66 75 | # Then 11,22,33 will be subcomponent 0, 44,55,66 will be subcomponent 1 76 | # If it's a traditional message, it will only have one element (entire message) 77 | self.subcomponents = [] 78 | 79 | def getOriginalSubcomponents(self): 80 | return [subcomponent.message for subcomponent in self.subcomponents] 81 | 82 | # May or may not have actually been changed 83 | # Version of subcomponents that includes fuzzing and messageprocessor changes from user 84 | # Is transient and reverted to original every iteration 85 | def getAlteredSubcomponents(self): 86 | return [subcomponent.getAlteredByteArray() for subcomponent in self.subcomponents] 87 | 88 | def getOriginalMessage(self): 89 | return bytearray().join([subcomponent.message for subcomponent in self.subcomponents]) 90 | 91 | # May or may not have actually been changed 92 | # Version of message that includes fuzzing and messageprocessor changes from user 93 | # Is transient and reverted to original every iteration 94 | def getAlteredMessage(self): 95 | return bytearray().join([subcomponent.getAlteredByteArray() for subcomponent in self.subcomponents]) 96 | 97 | def resetAlteredMessage(self): 98 | for subcomponent in self.subcomponents: 99 | subcomponent.setAlteredByteArray(subcomponent.message) 100 | 101 | # Set the message on the Message 102 | # sourceType - Format.CommaSeparatedHex, Ascii, or Raw 103 | # message - Message in above format 104 | # isFuzzed - whether this message should have its subcomponent 105 | # flag isFuzzed set 106 | def setMessageFrom(self, sourceType, message, isFuzzed): 107 | if sourceType == self.Format.CommaSeparatedHex: 108 | message = bytearray([x.decode("hex") for x in message.split(",")]) 109 | elif sourceType == self.Format.Ascii: 110 | message = self.deserializeByteArray(message) 111 | elif sourceType == self.Format.Raw: 112 | message = message 113 | else: 114 | raise RuntimeError("Invalid sourceType") 115 | 116 | self.subcomponents = [MessageSubComponent(message, isFuzzed)] 117 | 118 | if isFuzzed: 119 | self.isFuzzed = True 120 | 121 | # Same arguments as above, but adds to .message as well as 122 | # adding a new subcomponent 123 | # createNewSubcomponent - If false, don't create another subcomponent, 124 | # instead, append new message data to last subcomponent in message 125 | def appendMessageFrom(self, sourceType, message, isFuzzed, createNewSubcomponent=True): 126 | if sourceType == self.Format.CommaSeparatedHex: 127 | newMessage = bytearray([x.decode("hex") for x in message.split(",")]) 128 | elif sourceType == self.Format.Ascii: 129 | newMessage = self.deserializeByteArray(message) 130 | elif sourceType == self.Format.Raw: 131 | newMessage = message 132 | else: 133 | raise RuntimeError("Invalid sourceType") 134 | 135 | if createNewSubcomponent: 136 | self.subcomponents.append(MessageSubComponent(newMessage, isFuzzed)) 137 | else: 138 | self.subcomponents[-1].message += newMessage 139 | 140 | if isFuzzed: 141 | # Make sure message is set to fuzz as well 142 | self.isFuzzed = True 143 | 144 | def isOutbound(self): 145 | return self.direction == self.Direction.Outbound 146 | 147 | def __eq__(self, other): 148 | # bytearray (for message) implements __eq__() 149 | return self.direction == other.direction and self.message == other.message 150 | 151 | @classmethod 152 | def serializeByteArray(cls, byteArray): 153 | if type(byteArray) != bytearray: 154 | raise Exception(f'Argument to serializeByteArray isn\'t a byte array: {byteArray}') 155 | return repr(bytes(byteArray))[1:] # Don't include leading 'b', clearer/easier in .fuzzer file 156 | 157 | @classmethod 158 | def deserializeByteArray(cls, string): 159 | return bytearray(ast.literal_eval(f'b{string}')) 160 | 161 | def getAlteredSerialized(self): 162 | if len(self.subcomponents) < 1: 163 | return "{0} {1}\n".format(self.direction, "ERROR: No data in message.") 164 | else: 165 | serializedMessage = "{0}{1} {2}\n".format("fuzz " if self.subcomponents[0].isFuzzed else "", self.direction, self.serializeByteArray(self.subcomponents[0].getAlteredByteArray())) 166 | 167 | for subcomponent in self.subcomponents[1:]: 168 | serializedMessage += "sub {0}{1}\n".format("fuzz " if subcomponent.isFuzzed else "", self.serializeByteArray(subcomponent.getAlteredByteArray())) 169 | 170 | return serializedMessage 171 | 172 | def getSerialized(self): 173 | if len(self.subcomponents) < 1: 174 | return "{0} {1}\n".format(self.direction, "ERROR: No data in message.") 175 | else: 176 | serializedMessage = "{0} {1}{2}\n".format(self.direction, "fuzz " if self.subcomponents[0].isFuzzed else "", self.serializeByteArray(self.subcomponents[0].message)) 177 | 178 | for subcomponent in self.subcomponents[1:]: 179 | serializedMessage += "sub {0}{1}\n".format("fuzz " if subcomponent.isFuzzed else "", self.serializeByteArray(subcomponent.message)) 180 | 181 | return serializedMessage 182 | 183 | # Utility function for setFromSerialized and appendFromSerialized below 184 | def _extractMessageComponents(self, serializedData): 185 | firstQuoteSingle = serializedData.find('\'') 186 | lastQuoteSingle = serializedData.rfind('\'') 187 | firstQuoteDouble = serializedData.find('"') 188 | lastQuoteDouble = serializedData.rfind('"') 189 | firstQuote = -1 190 | lastQuote = -1 191 | 192 | if firstQuoteSingle == -1 or firstQuoteSingle == lastQuoteSingle: 193 | # If no valid single quotes, go double quote 194 | firstQuote = firstQuoteDouble 195 | lastQuote = lastQuoteDouble 196 | elif firstQuoteDouble == -1 or firstQuoteDouble == lastQuoteDouble: 197 | # If no valid double quotes, go single quote 198 | firstQuote = firstQuoteSingle 199 | lastQuote = lastQuoteSingle 200 | elif firstQuoteSingle < firstQuoteDouble: 201 | # If both are valid, go single if further out 202 | firstQuote = firstQuoteSingle 203 | lastQuote = lastQuoteSingle 204 | else: 205 | # Both are valid but double is further out 206 | firstQuote = firstQuoteDouble 207 | lastQuote = lastQuoteDouble 208 | 209 | if firstQuote == -1 or lastQuote == -1 or firstQuote == lastQuote: 210 | raise RuntimeError("Invalid message data, no message found") 211 | 212 | # Pull out everything, quotes and all, and deserialize it 213 | messageData = serializedData[firstQuote:lastQuote+1] 214 | # Process the args 215 | serializedData = serializedData[:firstQuote].split(" ") 216 | 217 | return (serializedData, messageData) 218 | 219 | # Handles _one line_ of data, either "inbound" or "outbound" 220 | # Lines following this should be passed to appendFromSerialized() below 221 | def setFromSerialized(self, serializedData): 222 | serializedData = serializedData.replace("\n", "") 223 | (serializedData, messageData) = self._extractMessageComponents(serializedData) 224 | 225 | if len(messageData) == 0 or len(serializedData) < 1: 226 | raise RuntimeError("Invalid message data") 227 | 228 | direction = serializedData[0] 229 | args = serializedData[1:-1] 230 | 231 | if direction != "inbound" and direction != "outbound": 232 | raise RuntimeError("Invalid message data, unknown direction {0}".format(direction)) 233 | 234 | isFuzzed = False 235 | if "fuzz" in args: 236 | isFuzzed = True 237 | if len(serializedData) < 3: 238 | raise RuntimeError("Invalid message data") 239 | 240 | self.direction = direction 241 | self.setMessageFrom(self.Format.Ascii, messageData, isFuzzed) 242 | 243 | # Add another line, used for multiline messages 244 | def appendFromSerialized(self, serializedData, createNewSubcomponent=True): 245 | serializedData = serializedData.replace("\n", "") 246 | (serializedData, messageData) = self._extractMessageComponents(serializedData) 247 | 248 | if createNewSubcomponent: 249 | if len(messageData) == 0 or len(serializedData) < 1 or serializedData[0] != "sub": 250 | raise RuntimeError("Invalid message data") 251 | else: 252 | # If not creating a subcomponent, we won't have "sub", "fuzz", and the other fun stuff 253 | if len(messageData) == 0: 254 | raise RuntimeError("Invalid message data") 255 | 256 | args = serializedData[1:-1] 257 | # Put either "fuzz" or nothing before actual message 258 | # Can tell the difference even with ascii because ascii messages have '' quotes 259 | # IOW, even a message subcomponent 'fuzz' will have the 's around it, not be fuzz without quotes 260 | isFuzzed = False 261 | if "fuzz" in args: 262 | isFuzzed = True 263 | 264 | self.appendMessageFrom(self.Format.Ascii, messageData, isFuzzed, createNewSubcomponent=createNewSubcomponent) 265 | 266 | class MessageCollection(object): 267 | def __init__(self): 268 | self.messages = [] 269 | 270 | def addMessage(self, message): 271 | self.messages.append(message) 272 | 273 | def doClientMessagesMatch(self, otherMessageCollection): 274 | for i in range(0, len(self.messages)): 275 | # Skip server messages 276 | if not self.messages[i].isOutbound(): 277 | continue 278 | try: 279 | # Message implements __eq__() 280 | if self.messages[i] != otherMessageCollection.messages[i]: 281 | return False 282 | except IndexError: 283 | return False 284 | 285 | # All messages passed 286 | return True 287 | 288 | import os 289 | import os.path 290 | from copy import deepcopy 291 | 292 | # Handles all the logging of the fuzzing session 293 | # Log messages can be found at sample_apps//_logs// 294 | class Logger(object): 295 | def __init__(self, folderPath): 296 | self._folderPath = folderPath 297 | if os.path.exists(folderPath): 298 | print("Data output directory already exists: %s" % (folderPath)) 299 | exit() 300 | else: 301 | try: 302 | os.makedirs(folderPath) 303 | except: 304 | print("Unable to create logging directory: %s" % (folderPath)) 305 | exit() 306 | 307 | self.resetForNewRun() 308 | 309 | # Store just the data, forget trying to make a Message object 310 | # With the subcomponents and everything, it just gets weird, 311 | # and we don't need it 312 | def setReceivedMessageData(self, messageNumber, data): 313 | self.receivedMessageData[messageNumber] = data 314 | 315 | def setHighestMessageNumber(self, messageNumber): 316 | # The highest message # this fuzz session made it to 317 | self._highestMessageNumber = messageNumber 318 | 319 | def outputLastLog(self, runNumber, messageCollection, errorMessage): 320 | return self._outputLog(runNumber, messageCollection, errorMessage, self._lastReceivedMessageData, self._lastHighestMessageNumber) 321 | 322 | def outputLog(self, runNumber, messageCollection, errorMessage): 323 | return self._outputLog(runNumber, messageCollection, errorMessage, self.receivedMessageData, self._highestMessageNumber) 324 | 325 | def _outputLog(self, runNumber, messageCollection, errorMessage, receivedMessageData, highestMessageNumber): 326 | with open(os.path.join(self._folderPath, str(runNumber)), "w") as outputFile: 327 | print("Logging run number %d" % (runNumber)) 328 | outputFile.write("Log from run with seed %d\n" % (runNumber)) 329 | outputFile.write("Error message: %s\n" % (errorMessage)) 330 | 331 | if highestMessageNumber == -1 or runNumber == 0: 332 | outputFile.write("Failed to connect on this run.\n") 333 | 334 | outputFile.write("\n") 335 | 336 | i = 0 337 | for message in messageCollection.messages: 338 | outputFile.write("Packet %d: %s" % (i, message.getSerialized())) 339 | 340 | if message.isFuzzed: 341 | outputFile.write("Fuzzed Packet %d: %s\n" % (i, message.getAlteredSerialized())) 342 | 343 | if i in receivedMessageData: 344 | # Compare what was actually sent to what we expected, log if they differ 345 | if receivedMessageData[i] != message.getOriginalMessage(): 346 | outputFile.write("Actual data received for packet %d: %s" % (i, Message.serializeByteArray(receivedMessageData[i]))) 347 | else: 348 | outputFile.write("Received expected data\n") 349 | 350 | if highestMessageNumber == i: 351 | if message.isOutbound(): 352 | outputFile.write("This is the last message sent\n") 353 | else: 354 | outputFile.write("This is the last message received\n") 355 | 356 | outputFile.write("\n") 357 | i += 1 358 | 359 | def resetForNewRun(self): 360 | try: 361 | self._lastReceivedMessageData = deepcopy(self.receivedMessageData) 362 | self._lastHighestMessageNumber = self._highestMessageNumber 363 | except AttributeError: 364 | self._lastReceivedMessageData = {} 365 | self._lastHighestMessageNumber = -1 366 | 367 | self.receivedMessageData = {} 368 | self.setHighestMessageNumber(-1) 369 | -------------------------------------------------------------------------------- /backend/fuzzerdata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | #------------------------------------------------------------------ 32 | # 33 | # Class to hold fuzzer data (.fuzzer file info) 34 | # Can read/write .fuzzer files from an instantiation 35 | # 36 | #------------------------------------------------------------------ 37 | 38 | from backend.fuzzer_types import MessageCollection, Message 39 | from backend.menu_functions import validateNumberRange 40 | import os.path 41 | import sys 42 | 43 | class FuzzerData(object): 44 | # Init creates fuzzer data and populates with defaults 45 | # readFromFile to load a .fuzzer file 46 | def __init__(self): 47 | # All messages in the conversation 48 | self.messageCollection = MessageCollection() 49 | # Directory containing custom processors (Exception, Message, Monitor) 50 | # or "default" 51 | self.processorDirectory = "default" 52 | # Number of times a test case causing a crash should be repeated 53 | self.failureThreshold = 3 54 | # How long to wait between retests 55 | self.failureTimeout = 5 56 | # Protocol (TCP, UDP) 57 | self.proto = "tcp" 58 | # Port to use 59 | self.port = 0 60 | # Source port to use, -1 = auto 61 | self.sourcePort = -1 62 | # Source IP to use, 0.0.0.0 or "" is default/automatic 63 | self.sourceIP = "0.0.0.0" 64 | # Whether to perform a test run 65 | self.shouldPerformTestRun = True 66 | # How long to time out on receive() (seconds) 67 | self.receiveTimeout = 1.0 68 | # Dictionary to save comments made to a .fuzzer file. Only really does anything if 69 | # using readFromFile and then writeToFile in the same program 70 | # (For example, fuzzerconverter) 71 | self.comments = {} 72 | # Kind of kludgy string for use in readFromFD, made global to not have to pass around 73 | # Details in readFromFD() 74 | self._readComments = "" 75 | # Update for compatibilty with new Decept 76 | self.messagesToFuzz = [] 77 | 78 | 79 | # Read in the FuzzerData from the specified .fuzzer file 80 | def readFromFile(self, filePath, quiet=False): 81 | with open(filePath, 'r') as inputFile: 82 | self.readFromFD(inputFile, quiet=quiet) 83 | 84 | # Utility function to fix up self.comments and self._readComments within readFromFD() 85 | # as data is read in 86 | def _pushComments(self, commentSectionName): 87 | self.comments[commentSectionName] = self._readComments 88 | self._readComments = "" 89 | 90 | # Same as above, but appends to existing comment section if possible 91 | def _appendComments(self, commentSectionName): 92 | if commentSectionName in self.comments: 93 | self.comments[commentSectionName] += self._readComments 94 | else: 95 | self.comments[commentSectionName] = self._readComments 96 | self._readComments = "" 97 | 98 | # Update for compatibilty with newer versions of Decept. 99 | 100 | 101 | # Read in the FuzzerData from a specific file descriptor 102 | # Most usefully can be used to read from stdout by passing 103 | # sys.stdin 104 | def readFromFD(self, fileDescriptor, quiet=False): 105 | messageNum = 0 106 | 107 | # This is used to track multiline messages 108 | lastMessage = None 109 | # Build up comments in this string until we're ready to push them out to the dictionary 110 | # Basically, we build lines and lines of comments, then when a command is encountered, 111 | # push them into the dictionary using that command as a key 112 | # Thus, when we go to write them back out, we can print them all before a given key 113 | self._readComments = "" 114 | 115 | for line in fileDescriptor: 116 | # Record comments on read so we can play them back on write if applicable 117 | if line.startswith("#") or line == "\n": 118 | self._readComments += line 119 | # Skip all further processing for this line 120 | continue 121 | 122 | line = line.replace("\n", "") 123 | 124 | # Skip comments and whitespace 125 | if not line.startswith("#") and not line == "" and not line.isspace(): 126 | args = line.split(" ") 127 | 128 | # Populate FuzzerData obj with any settings we can parse out 129 | try: 130 | if args[0] == "processor_dir": 131 | self.processorDirectory = args[1] 132 | self._pushComments("processor_dir") 133 | elif args[0] == "failureThreshold": 134 | self.failureThreshold = int(args[1]) 135 | self._pushComments("failureThreshold") 136 | elif args[0] == "failureTimeout": 137 | self.failureTimeout = int(args[1]) 138 | self._pushComments("failureTimeout") 139 | elif args[0] == "proto": 140 | self.proto = args[1] 141 | self._pushComments("proto") 142 | elif args[0] == "port": 143 | self.port = int(args[1]) 144 | self._pushComments("port") 145 | elif args[0] == "sourcePort": 146 | self.sourcePort = int(args[1]) 147 | self._pushComments("sourcePort") 148 | elif args[0] == "sourceIP": 149 | self.sourceIP = args[1] 150 | self._pushComments("sourceIP") 151 | elif args[0] == "shouldPerformTestRun": 152 | # Use 0 or 1 for setting 153 | if args[1] == "0": 154 | self.shouldPerformTestRun = False 155 | elif args[1] == "1": 156 | self.shouldPerformTestRun = True 157 | else: 158 | raise RuntimeError("shouldPerformTestRun must be 0 or 1") 159 | self._pushComments("shouldPerformTestRun") 160 | elif args[0] == "receiveTimeout": 161 | self.receiveTimeout = float(args[1]) 162 | self._pushComments("receiveTimeout") 163 | elif args[0] == "messagesToFuzz": 164 | print("WARNING: It looks like you're using a legacy .fuzzer file with messagesToFuzz set. This is now deprecated, so please update to the new format") 165 | self.messagesToFuzz = validateNumberRange(args[1], flattenList=True) 166 | # Slight kludge: store comments above messagesToFuzz with the first message. *shrug* 167 | # Comment saving is best effort anyway, right? 168 | self._pushComments("message0") 169 | elif args[0] == "unfuzzedBytes": 170 | print("ERROR: It looks like you're using a legacy .fuzzer file with unfuzzedBytes set. This has been replaced by the new multi-line format. Please update your .fuzzer file.") 171 | sys.exit(-1) 172 | elif args[0] == "inbound" or args[0] == "outbound": 173 | message = Message() 174 | message.setFromSerialized(line) 175 | self.messageCollection.addMessage(message) 176 | # Legacy code to handle old messagesToFuzz format 177 | if messageNum in self.messagesToFuzz: 178 | message.isFuzzed = True 179 | if not quiet: 180 | print("\tMessage #{0}: {1} bytes {2}".format(messageNum, len(message.getOriginalMessage()), message.direction)) 181 | self._pushComments("message{0}".format(messageNum)) 182 | messageNum += 1 183 | lastMessage = message 184 | # "sub" means this is a subcomponent 185 | elif args[0] == "sub": 186 | if not 'message' in locals(): 187 | print("\tERROR: 'sub' line declared before any 'message' lines, throwing subcomponent out: {0}".format(line)) 188 | else: 189 | message.appendFromSerialized(line) 190 | if not quiet: 191 | print("\t\tSubcomponent: {1} additional bytes".format(messageNum, len(message.subcomponents[-1].message))) 192 | elif line.lstrip()[0] == "'" and 'message' in locals(): 193 | # If the line begins with ' and a message line has been found, 194 | # assume that this is additional message data 195 | # (Different from a subcomponent because it can't have additional data 196 | # tacked on) 197 | message.appendFromSerialized(line.lstrip(), createNewSubcomponent=False) 198 | else: 199 | if not quiet: 200 | print("Unknown setting in .fuzzer file: {0}".format(args[0])) 201 | # Slap any messages between "message" and "sub", etc (ascii same way) above message 202 | # It's way too annoying to print these out properly, as they get 203 | # automagically outserialized by the Message object 204 | # Plus they may change... eh, forget it, user can fix up themselves if they want 205 | self._appendComments("message{0}".format(messageNum-1)) 206 | except Exception as e: 207 | print("Invalid line: {0}".format(line)) 208 | raise e 209 | # Catch any comments below the last line 210 | self._pushComments("endcomments") 211 | 212 | # Utility function to get comments for a section after checking if they exist 213 | # If not, returns "" 214 | def _getComments(self, commentSectionName): 215 | if commentSectionName in self.comments: 216 | return self.comments[commentSectionName] 217 | else: 218 | return "" 219 | 220 | # Set messagesToFuzz from string (such as "1,3-4") 221 | def setMessagesToFuzzFromString(self, messagesToFuzzStr): 222 | self.messagesToFuzz = validateNumberRange(messagesToFuzzStr, flattenList=True) 223 | #print self._messagesToFuzz 224 | 225 | 226 | # Write out the FuzzerData to the specified .fuzzer file 227 | def writeToFile(self, filePath, defaultComments=False, finalMessageNum=-1): 228 | origFilePath = filePath 229 | tail = 0 230 | while os.path.isfile(filePath): 231 | tail += 1 232 | filePath = "{0}-{1}".format(origFilePath, tail) 233 | # print "File %s already exists" % (filePath,) 234 | 235 | if origFilePath != filePath: 236 | print(("File {0} already exists, using {1} instead".format(origFilePath, filePath))) 237 | 238 | with open(filePath, 'w') as outputFile: 239 | self.writeToFD(outputFile, defaultComments=defaultComments, finalMessageNum=finalMessageNum) 240 | 241 | return filePath 242 | 243 | # Write out the FuzzerData to a specific file descriptor 244 | # Most usefully can be used to write to stdout by passing 245 | # sys.stdout 246 | def writeToFD(self, fileDescriptor, defaultComments=False, finalMessageNum=-1): 247 | if not defaultComments and "start" in self.comments: 248 | fileDescriptor.write(self.comments["start"]) 249 | 250 | # Processor Directory 251 | if defaultComments: 252 | comment = "# Directory containing any custom exception/message/monitor processors\n" 253 | comment += "# This should be either an absolute path or relative to the .fuzzer file\n" 254 | comment += "# If set to \"default\", Mutiny will use any processors in the same\n" 255 | comment += "# folder as the .fuzzer file\n" 256 | fileDescriptor.write(comment) 257 | else: 258 | fileDescriptor.write(self._getComments("processor_dir")) 259 | fileDescriptor.write("processor_dir {0}\n".format(self.processorDirectory)) 260 | 261 | # Failure Threshold 262 | if defaultComments: 263 | fileDescriptor.write("# Number of times to retry a test case causing a crash\n") 264 | else: 265 | fileDescriptor.write(self._getComments("failure_threshold")) 266 | fileDescriptor.write("failureThreshold {0}\n".format(self.failureThreshold)) 267 | 268 | # Failure Timeout 269 | if defaultComments: 270 | fileDescriptor.write("# How long to wait between retrying test cases causing a crash\n") 271 | else: 272 | fileDescriptor.write(self._getComments("failureTimeout")) 273 | fileDescriptor.write("failureTimeout {0}\n".format(self.failureTimeout)) 274 | 275 | # Receive Timeout 276 | if defaultComments: 277 | fileDescriptor.write("# How long for recv() to block when waiting on data from server\n") 278 | else: 279 | fileDescriptor.write(self._getComments("receiveTimeout")) 280 | fileDescriptor.write("receiveTimeout {0}\n".format(self.receiveTimeout)) 281 | 282 | # Should Perform Test Run 283 | if defaultComments: 284 | fileDescriptor.write("# Whether to perform an unfuzzed test run before fuzzing\n") 285 | else: 286 | fileDescriptor.write(self._getComments("shouldPerformTestRun")) 287 | sPTR = 1 if self.shouldPerformTestRun else 0 288 | fileDescriptor.write("shouldPerformTestRun {0}\n".format(sPTR)) 289 | 290 | # Protocol 291 | if defaultComments: 292 | fileDescriptor.write("# Protocol (udp or tcp)\n") 293 | else: 294 | fileDescriptor.write(self._getComments("proto")) 295 | fileDescriptor.write("proto {0}\n".format(self.proto)) 296 | 297 | # Port 298 | if defaultComments: 299 | fileDescriptor.write("# Port number to connect to\n") 300 | else: 301 | fileDescriptor.write(self._getComments("port")) 302 | fileDescriptor.write("port {0}\n".format(self.port)) 303 | 304 | # Source Port 305 | if defaultComments: 306 | fileDescriptor.write("# Port number to connect from\n") 307 | else: 308 | fileDescriptor.write(self._getComments("sourcePort")) 309 | fileDescriptor.write("sourcePort {0}\n".format(self.sourcePort)) 310 | 311 | # Source IP 312 | if defaultComments: 313 | fileDescriptor.write("# Source IP to connect from\n") 314 | else: 315 | fileDescriptor.write(self._getComments("sourceIP")) 316 | fileDescriptor.write("sourceIP {0}\n\n".format(self.sourceIP)) 317 | 318 | # Messages 319 | if finalMessageNum == -1: 320 | finalMessageNum = len(self.messageCollection.messages)-1 321 | if defaultComments: 322 | fileDescriptor.write("# The actual messages in the conversation\n# Each contains a message to be sent to or from the server, printably-formatted\n") 323 | for i in range(0, finalMessageNum+1): 324 | message = self.messageCollection.messages[i] 325 | if not defaultComments: 326 | fileDescriptor.write(self._getComments("message{0}".format(i))) 327 | fileDescriptor.write(message.getSerialized()) 328 | 329 | 330 | if not defaultComments: 331 | fileDescriptor.write(self._getComments("endcomments")) 332 | -------------------------------------------------------------------------------- /backend/menu_functions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | #------------------------------------------------------------------ 32 | # 33 | # Utility functions for interactive scripts 34 | # 35 | #------------------------------------------------------------------ 36 | 37 | # used during the mutiny_prep.py .fuzzer generation 38 | # asks for and returns a boolean 39 | def prompt(question, answers=["y", "n"], defaultIndex=None): 40 | answer = "" 41 | while answer not in answers: 42 | print("%s (%s)" % (question, "/".join(answers))) 43 | if defaultIndex != None: 44 | answer = input("Default %s: " % (answers[defaultIndex])) 45 | else: 46 | answer = input("No default: ") 47 | # Pretty up responses with a newline after 48 | print("") 49 | 50 | if defaultIndex != None and answer == "": 51 | answer = answers[defaultIndex] 52 | break 53 | 54 | if len(answers) == 2 and answers[0] == "y" and answers[1] == "n": 55 | if answer == "y": 56 | return True 57 | else: 58 | return False 59 | else: 60 | return answer 61 | 62 | # used during the mutiny_prep.py .fuzzer generation 63 | # asks for and returns an integer 64 | def promptInt(question, defaultResponse=None, allowNo=False): 65 | answer = None 66 | 67 | while answer == None: 68 | print("%s" % (question)) 69 | try: 70 | if defaultResponse: 71 | answer = input("Default {0}: ".format(defaultResponse)).strip() 72 | else: 73 | answer = input("No default: ") 74 | # Pretty up responses with a newline after 75 | print("") 76 | 77 | if allowNo and (answer == "n" or answer == ""): 78 | return None 79 | else: 80 | answer = int(answer) 81 | 82 | except ValueError: 83 | answer = None 84 | 85 | if answer == None and defaultResponse: 86 | answer = defaultResponse 87 | 88 | return answer 89 | 90 | # Return input given as string if it passes the validationFunction test, else return None. 91 | # If there is not validationFunc given, only return string. 92 | # Return default repsonse if empty, the defaultResponse or Ctrl-C are given. 93 | def promptString(question, defaultResponse="n", validateFunc=None): 94 | retStr = "" 95 | while not retStr or not len(retStr): 96 | if defaultResponse: 97 | inputStr = input("%s\nDefault %s: " % (question, defaultResponse)) 98 | else: 99 | inputStr = input("%s\nNo default: " % (question)) 100 | 101 | # Pretty up responses with a newline after 102 | print("") 103 | if defaultResponse and (inputStr == defaultResponse or not len(inputStr)): 104 | return defaultResponse 105 | # If we're looking for a specific format, validate 106 | # Validate functions must return None on failure of validation, 107 | # and != None on success 108 | if validateFunc: 109 | if validateFunc(inputStr): 110 | retStr = inputStr 111 | 112 | return retStr 113 | 114 | # Takes a string of numbers, seperated via commas 115 | # or by hyphens, and generates an appropriate list of 116 | # numbers from it. 117 | # e.g. str("1,2,3-6") => list([1,2,xrange(3,7)]) 118 | # 119 | # If flattenList=True, will return a list of distinct elements 120 | # 121 | # If given an invalid number string, returns None 122 | def validateNumberRange(inputStr, flattenList=False): 123 | retList = [] 124 | tmpList = [_f for _f in inputStr.split(',') if _f] 125 | 126 | # Print msg if invalid chars/typo detected 127 | for num in tmpList: 128 | try: 129 | retList.append(int(num)) 130 | except ValueError: 131 | if '-' in num: 132 | intRange = num.split('-') 133 | # Invalid x-y-z 134 | if len(intRange) > 2: 135 | print("Invalid range given") 136 | return None 137 | try: 138 | if not flattenList: 139 | # Append iterator with bounds = intRange 140 | retList.append(range(int(intRange[0]),int(intRange[1])+1)) 141 | else: 142 | # Append individual elements 143 | retList.extend(list(range(int(intRange[0]),int(intRange[1])+1))) 144 | except TypeError: 145 | print("Invalid range given") 146 | return None 147 | else: 148 | print("Invalid number given") 149 | return None 150 | # All elements in the range are valid integers or integer ranges 151 | if flattenList: 152 | # If list is flattened, every element is an integer 153 | retList = sorted(list(set(retList))) 154 | return retList 155 | 156 | -------------------------------------------------------------------------------- /backend/packets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | #------------------------------------------------------------------ 32 | # 33 | # Definitions for l2/l3 packet handling 34 | # 35 | #------------------------------------------------------------------ 36 | 37 | 38 | from ctypes import * 39 | ### L2 ### 40 | class ETH(Structure): 41 | _pack_=1 42 | _fields_ = [ 43 | ("ethDstU", c_uint32), 44 | ("ethDstL", c_uint16), 45 | ("ethSrcU", c_uint32), 46 | ("ethSrcL", c_uint16), 47 | ("type", c_ushort,8) 48 | ] 49 | 50 | ### L3 ### 51 | # http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml 52 | # Just took the most used ones, if there is one missing that you need, 53 | # take a look at above link 54 | PROTO = { 55 | "icmp":1, 56 | "igmp":2, 57 | "ipv4":4, 58 | "tcp":6, 59 | "igp":9, 60 | "udp":17, 61 | "ipv6":41, 62 | "ipv6-route":43, 63 | "ipv6-frag":44, 64 | "gre":47, 65 | "dsr":48, 66 | "esp":50, 67 | "ipv6-icmp":58, 68 | "ipv6-nonxt":59, 69 | "ipv6-opts":60, 70 | "eigrp":88, 71 | "ospf":89, 72 | "mtp":92, 73 | "l2tp":116, 74 | "sctp":132 75 | } 76 | 77 | class IP(Structure): 78 | _pack_=1 79 | _fields_ = [ 80 | ("version", c_ubyte,4), 81 | ("ihl", c_ubyte,4), 82 | ("tos", c_ubyte), 83 | ("length", c_ushort), 84 | ("id", c_ushort), 85 | ("flags", c_ubyte,3), 86 | ("fragOffset", c_ushort,13), 87 | ("ttl", c_ubyte), 88 | ("proto", c_ubyte), 89 | ("checksum", c_ushort), 90 | ("ipSrc", c_uint), 91 | ("ipDst", c_uint), 92 | #("options", c_uint), 93 | #("padding", c_ubyte * 2) 94 | ] 95 | 96 | ### L4 ### 97 | class TCP(Structure): 98 | _fields_ = [ 99 | ("test", c_ubyte ) 100 | ] 101 | 102 | class UDP(Structure): 103 | _fields_ = [ 104 | ("test", c_ubyte ) 105 | ] 106 | -------------------------------------------------------------------------------- /backend/proc_director.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | #------------------------------------------------------------------ 32 | # 33 | # This file finds and imports any custom exception_processor.py, 34 | # message_processor.py, or monitor.py files specified by the 35 | # processor_dir parameter passed in the .fuzzer file generated 36 | # by the mutiny_prep.py file. 37 | # It also spawns any Monitors in a parallel thread 38 | # 39 | #------------------------------------------------------------------ 40 | 41 | import imp 42 | import sys 43 | import os.path 44 | import threading 45 | import socket 46 | 47 | from os import listdir 48 | from threading import Event 49 | from mutiny_classes.mutiny_exceptions import MessageProcessorExceptions 50 | 51 | class ProcDirector(object): 52 | def __init__(self, processDir): 53 | self.messageProcessor = None 54 | self.exceptionProcessor = None 55 | self.exceptionList = None 56 | self.monitor = None 57 | mod_name = "" 58 | self.classDir = "mutiny_classes" 59 | 60 | defaultDir = os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir),self.classDir) 61 | filelist = [ "exception_processor","message_processor","monitor" ] 62 | 63 | # Load all processors, attempting to do custom first then default 64 | for filename in filelist: 65 | try: 66 | # Attempt to load custom processor 67 | filepath = os.path.join(processDir, "{0}.py".format(filename)) 68 | imp.load_source(filename, filepath) 69 | print(("Loaded custom processor: {0}".format(filepath))) 70 | except IOError: 71 | # On failure, load default 72 | filepath = os.path.join(defaultDir, "{0}.py".format(filename)) 73 | imp.load_source(filename, filepath) 74 | print(("Loaded default processor: {0}".format(filepath))) 75 | 76 | # Set all the appropriate classes to the appropriate modules 77 | self.messageProcessor = sys.modules['message_processor'].MessageProcessor 78 | self.exceptionProcessor = sys.modules['exception_processor'].ExceptionProcessor 79 | self.monitor = sys.modules['monitor'].Monitor 80 | self.crashQueue = Event() 81 | 82 | class MonitorWrapper(object): 83 | def __init__(self, targetIP, targetPort, monitor): 84 | # crashDetectedEvent signals main thread on a detected crash, 85 | # interrupt_main() and CTRL+C, otherwise raise the same signal 86 | # monitor is the actual user custom monitor that implements monitorTarget 87 | self.monitor = monitor 88 | self.crashEvent = threading.Event() 89 | self.task = threading.Thread(target=self.monitor.monitorTarget,args=(targetIP,targetPort,self.signalCrashDetectedOnMain)) 90 | self.task.daemon = True 91 | self.task.start() 92 | 93 | # Don't override this function 94 | def signalCrashDetectedOnMain(self): 95 | # Raises a KeyboardInterrupt exception on main thread 96 | self.crashEvent.set() 97 | # Ugly but have to import here for this to work in monitorTarget on a custom processor 98 | import _thread 99 | _thread.interrupt_main() 100 | 101 | def startMonitor(self, host, port): 102 | self.monitorWrapper = self.MonitorWrapper(host, port, self.monitor()) 103 | return self.monitorWrapper 104 | 105 | -------------------------------------------------------------------------------- /mutiny.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | #------------------------------------------------------------------ 32 | # 33 | # This is the main fuzzing script. It takes a .fuzzer file and performs the 34 | # actual fuzzing 35 | # 36 | #------------------------------------------------------------------ 37 | 38 | import datetime 39 | import errno 40 | import importlib 41 | import os.path 42 | import os 43 | import signal 44 | import socket 45 | import subprocess 46 | import sys 47 | import threading 48 | import time 49 | import argparse 50 | import ssl 51 | from copy import deepcopy 52 | from backend.proc_director import ProcDirector 53 | from backend.fuzzer_types import Message, MessageCollection, Logger 54 | from backend.packets import PROTO,IP 55 | from mutiny_classes.mutiny_exceptions import * 56 | from mutiny_classes.message_processor import MessageProcessorExtraParams 57 | from backend.fuzzerdata import FuzzerData 58 | from backend.menu_functions import validateNumberRange 59 | 60 | # Path to Radamsa binary 61 | RADAMSA=os.path.abspath( os.path.join(__file__, "../radamsa-0.6/bin/radamsa") ) 62 | # Whether to print debug info 63 | DEBUG_MODE=False 64 | # Test number to start from, 0 default 65 | MIN_RUN_NUMBER=0 66 | # Test number to go to, -1 is unlimited 67 | MAX_RUN_NUMBER=-1 68 | # For seed loop, finite range to repeat 69 | SEED_LOOP = [] 70 | # For dumpraw option, dump into log directory by default, else 'dumpraw' 71 | DUMPDIR = "" 72 | 73 | # Takes a socket and outbound data packet (byteArray), sends it out. 74 | # If debug mode is enabled, we print out the raw bytes 75 | def sendPacket(connection, addr, outPacketData): 76 | connection.settimeout(fuzzerData.receiveTimeout) 77 | if connection.type == socket.SOCK_STREAM: 78 | connection.send(outPacketData) 79 | else: 80 | connection.sendto(outPacketData,addr) 81 | 82 | print("\tSent %d byte packet" % (len(outPacketData))) 83 | if DEBUG_MODE: 84 | print("\tSent: %s" % (outPacketData)) 85 | print("\tRaw Bytes: %s" % (Message.serializeByteArray(outPacketData))) 86 | 87 | 88 | def receivePacket(connection, addr, bytesToRead): 89 | readBufSize = 4096 90 | connection.settimeout(fuzzerData.receiveTimeout) 91 | 92 | if connection.type == socket.SOCK_STREAM or connection.type == socket.SOCK_DGRAM: 93 | response = bytearray(connection.recv(readBufSize)) 94 | else: 95 | response = bytearray(connection.recvfrom(readBufSize,addr)) 96 | 97 | 98 | if len(response) == 0: 99 | # If 0 bytes are recv'd, the server has closed the connection 100 | # per python documentation 101 | raise ConnectionClosedException("Server has closed the connection") 102 | if bytesToRead > readBufSize: 103 | # If we're trying to read > 4096, don't actually bother trying to guarantee we'll read 4096 104 | # Just keep reading in 4096 chunks until we should have read enough, and then return 105 | # whether or not it's as much data as expected 106 | i = readBufSize 107 | while i < bytesToRead: 108 | response += bytearray(connection.recv(readBufSize)) 109 | i += readBufSize 110 | 111 | print("\tReceived %d bytes" % (len(response))) 112 | if DEBUG_MODE: 113 | print("\tReceived: %s" % (response)) 114 | return response 115 | 116 | # Perform a fuzz run. 117 | # If seed is -1, don't perform fuzzing (test run) 118 | def performRun(fuzzerData, host, logger, messageProcessor, seed=-1): 119 | # Before doing anything, set up logger 120 | # Otherwise, if connection is refused, we'll log last, but it will be wrong 121 | if logger != None: 122 | logger.resetForNewRun() 123 | 124 | addrs = socket.getaddrinfo(host,fuzzerData.port) 125 | host = addrs[0][4][0] 126 | if host == "::1": 127 | host = "127.0.0.1" 128 | 129 | # cheap testing for ipv6/ipv4/unix 130 | # don't think it's worth using regex for this, since the user 131 | # will have to actively go out of their way to subvert this. 132 | if "." in host: 133 | socket_family = socket.AF_INET 134 | addr = (host,fuzzerData.port) 135 | elif ":" in host: 136 | socket_family = socket.AF_INET6 137 | addr = (host,fuzzerData.port) 138 | else: 139 | socket_family = socket.AF_UNIX 140 | addr = (host) 141 | 142 | #just in case filename is like "./asdf" !=> AF_INET 143 | if "/" in host: 144 | socket_family = socket.AF_UNIX 145 | addr = (host) 146 | 147 | # Call messageprocessor preconnect callback if it exists 148 | try: 149 | messageProcessor.preConnect(seed, host, fuzzerData.port) 150 | except AttributeError: 151 | pass 152 | 153 | # for TCP/UDP/RAW support 154 | if fuzzerData.proto == "tcp": 155 | connection = socket.socket(socket_family,socket.SOCK_STREAM) 156 | # Don't connect yet, until after we do any binding below 157 | elif fuzzerData.proto == "tls": 158 | try: 159 | _create_unverified_https_context = ssl._create_unverified_context 160 | except AttributeError: 161 | # Legacy Python that doesn't verify HTTPS certificates by default 162 | pass 163 | else: 164 | # Handle target environment that doesn't support HTTPS verification 165 | ssl._create_default_https_context = _create_unverified_https_context 166 | tcpConnection = socket.socket(socket_family,socket.SOCK_STREAM) 167 | connection = ssl.wrap_socket(tcpConnection) 168 | # Don't connect yet, until after we do any binding below 169 | elif fuzzerData.proto == "udp": 170 | connection = socket.socket(socket_family,socket.SOCK_DGRAM) 171 | # PROTO = dictionary of assorted L3 proto => proto number 172 | # e.g. "icmp" => 1 173 | elif fuzzerData.proto in PROTO: 174 | connection = socket.socket(socket_family,socket.SOCK_RAW,PROTO[fuzzerData.proto]) 175 | if fuzzerData.proto != "raw": 176 | connection.setsockopt(socket.IPPROTO_IP,socket.IP_HDRINCL,0) 177 | addr = (host,0) 178 | try: 179 | connection = socket.socket(socket_family,socket.SOCK_RAW,PROTO[fuzzerData.proto]) 180 | except Exception as e: 181 | print(e) 182 | print("Unable to create raw socket, please verify that you have sudo access") 183 | sys.exit(0) 184 | elif fuzzerData.proto == "L2raw": 185 | connection = socket.socket(socket.AF_PACKET,socket.SOCK_RAW,0x0300) 186 | else: 187 | addr = (host,0) 188 | try: 189 | #test if it's a valid number 190 | connection = socket.socket(socket_family,socket.SOCK_RAW,int(fuzzerData.proto)) 191 | connection.setsockopt(socket.IPPROTO_IP,socket.IP_HDRINCL,0) 192 | except Exception as e: 193 | print(e) 194 | print("Unable to create raw socket, please verify that you have sudo access") 195 | sys.exit(0) 196 | 197 | if fuzzerData.proto == "tcp" or fuzzerData.proto == "udp" or fuzzerData.proto == "tls": 198 | # Specifying source port or address is only supported for tcp and udp currently 199 | if fuzzerData.sourcePort != -1: 200 | # Only support right now for tcp or udp, but bind source port address to something 201 | # specific if requested 202 | if fuzzerData.sourceIP != "" or fuzzerData.sourceIP != "0.0.0.0": 203 | connection.bind((fuzzerData.sourceIP, fuzzerData.sourcePort)) 204 | else: 205 | # User only specified a port, not an IP 206 | connection.bind(('0.0.0.0', fuzzerData.sourcePort)) 207 | elif fuzzerData.sourceIP != "" and fuzzerData.sourceIP != "0.0.0.0": 208 | # No port was specified, so 0 should auto-select 209 | connection.bind((fuzzerData.sourceIP, 0)) 210 | if fuzzerData.proto == "tcp" or fuzzerData.proto == "tls": 211 | # Now that we've had a chance to bind as necessary, connect 212 | connection.connect(addr) 213 | 214 | i = 0 215 | for i in range(0, len(fuzzerData.messageCollection.messages)): 216 | message = fuzzerData.messageCollection.messages[i] 217 | 218 | # Go ahead and revert any fuzzing or messageprocessor changes before proceeding 219 | message.resetAlteredMessage() 220 | 221 | if message.isOutbound(): 222 | # Primarily used for deciding how to handle preFuzz/preSend callbacks 223 | doesMessageHaveSubcomponents = len(message.subcomponents) > 1 224 | 225 | # Get original subcomponents for outbound callback only once 226 | originalSubcomponents = [subcomponent.getOriginalByteArray() for subcomponent in message.subcomponents] 227 | 228 | if doesMessageHaveSubcomponents: 229 | # For message with subcomponents, call prefuzz on fuzzed subcomponents 230 | for j in range(0, len(message.subcomponents)): 231 | subcomponent = message.subcomponents[j] 232 | # Note: we WANT to fetch subcomponents every time on purpose 233 | # This way, if user alters subcomponent[0], it's reflected when 234 | # we call the function for subcomponent[1], etc 235 | actualSubcomponents = [subcomponent.getAlteredByteArray() for subcomponent in message.subcomponents] 236 | prefuzz = messageProcessor.preFuzzSubcomponentProcess(subcomponent.getAlteredByteArray(), MessageProcessorExtraParams(i, j, subcomponent.isFuzzed, originalSubcomponents, actualSubcomponents)) 237 | subcomponent.setAlteredByteArray(prefuzz) 238 | else: 239 | # If no subcomponents, call prefuzz on ENTIRE message 240 | actualSubcomponents = [subcomponent.getAlteredByteArray() for subcomponent in message.subcomponents] 241 | prefuzz = messageProcessor.preFuzzProcess(actualSubcomponents[0], MessageProcessorExtraParams(i, -1, message.isFuzzed, originalSubcomponents, actualSubcomponents)) 242 | message.subcomponents[0].setAlteredByteArray(prefuzz) 243 | 244 | # Skip fuzzing for seed == -1 245 | if seed > -1: 246 | # Now run the fuzzer for each fuzzed subcomponent 247 | for subcomponent in message.subcomponents: 248 | if subcomponent.isFuzzed: 249 | radamsa = subprocess.Popen([RADAMSA, "--seed", str(seed)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 250 | byteArray = subcomponent.getAlteredByteArray() 251 | (fuzzedByteArray, error_output) = radamsa.communicate(input=byteArray) 252 | fuzzedByteArray = bytearray(fuzzedByteArray) 253 | subcomponent.setAlteredByteArray(fuzzedByteArray) 254 | 255 | # Fuzzing has now been done if this message is fuzzed 256 | # Always call preSend() regardless for subcomponents if there are any 257 | if doesMessageHaveSubcomponents: 258 | for j in range(0, len(message.subcomponents)): 259 | subcomponent = message.subcomponents[j] 260 | # See preFuzz above - we ALWAYS regather this to catch any updates between 261 | # callbacks from the user 262 | actualSubcomponents = [subcomponent.getAlteredByteArray() for subcomponent in message.subcomponents] 263 | presend = messageProcessor.preSendSubcomponentProcess(subcomponent.getAlteredByteArray(), MessageProcessorExtraParams(i, j, subcomponent.isFuzzed, originalSubcomponents, actualSubcomponents)) 264 | subcomponent.setAlteredByteArray(presend) 265 | 266 | # Always let the user make any final modifications pre-send, fuzzed or not 267 | actualSubcomponents = [subcomponent.getAlteredByteArray() for subcomponent in message.subcomponents] 268 | byteArrayToSend = messageProcessor.preSendProcess(message.getAlteredMessage(), MessageProcessorExtraParams(i, -1, message.isFuzzed, originalSubcomponents, actualSubcomponents)) 269 | 270 | if args.dumpraw: 271 | loc = os.path.join(DUMPDIR,"%d-outbound-seed-%d"%(i,args.dumpraw)) 272 | if message.isFuzzed: 273 | loc+="-fuzzed" 274 | with open(loc,"wb") as f: 275 | f.write(repr(str(byteArrayToSend))[1:-1]) 276 | 277 | sendPacket(connection, addr, byteArrayToSend) 278 | else: 279 | # Receiving packet from server 280 | messageByteArray = message.getAlteredMessage() 281 | data = receivePacket(connection,addr,len(messageByteArray)) 282 | if data == messageByteArray: 283 | print("\tReceived expected response") 284 | if logger != None: 285 | logger.setReceivedMessageData(i, data) 286 | 287 | messageProcessor.postReceiveProcess(data, MessageProcessorExtraParams(i, -1, False, [messageByteArray], [data])) 288 | 289 | if args.dumpraw: 290 | loc = os.path.join(DUMPDIR,"%d-inbound-seed-%d"%(i,args.dumpraw)) 291 | with open(loc,"wb") as f: 292 | f.write(repr(str(data))[1:-1]) 293 | 294 | if logger != None: 295 | logger.setHighestMessageNumber(i) 296 | 297 | 298 | i += 1 299 | 300 | connection.close() 301 | 302 | # Usage case 303 | if len(sys.argv) < 3: 304 | sys.argv.append('-h') 305 | 306 | #TODO: add description/license/ascii art print out?? 307 | desc = "======== The Mutiny Fuzzing Framework ==========" 308 | epi = "==" * 24 + '\n' 309 | 310 | parser = argparse.ArgumentParser(description=desc,epilog=epi) 311 | parser.add_argument("prepped_fuzz", help="Path to file.fuzzer") 312 | parser.add_argument("target_host", help="Target to fuzz") 313 | parser.add_argument("-s","--sleeptime",help="Time to sleep between fuzz cases (float)",type=float,default=0) 314 | seed_constraint = parser.add_mutually_exclusive_group() 315 | seed_constraint.add_argument("-r", "--range", help="Run only the specified cases. Acceptable arg formats: [ X | X- | X-Y ], for integers X,Y") 316 | seed_constraint.add_argument("-l", "--loop", help="Loop/repeat the given finite number range. Acceptible arg format: [ X | X-Y | X,Y,Z-Q,R | ...]") 317 | seed_constraint.add_argument("-d", "--dumpraw", help="Test single seed, dump to 'dumpraw' folder",type=int) 318 | 319 | verbosity = parser.add_mutually_exclusive_group() 320 | verbosity.add_argument("-q", "--quiet", help="Don't log the outputs",action="store_true") 321 | verbosity.add_argument("--logAll", help="Log all the outputs",action="store_true") 322 | 323 | args = parser.parse_args() 324 | 325 | #---------------------------------------------------- 326 | # Set MIN_RUN_NUMBER and MAX_RUN_NUMBER when provided 327 | # by the user below 328 | def getRunNumbersFromArgs(strArgs): 329 | if "-" in strArgs: 330 | testNumbers = strArgs.split("-") 331 | if len(testNumbers) == 2: 332 | if len(testNumbers[1]): #e.g. strArgs="1-50" 333 | return (int(testNumbers[0]), int(testNumbers[1])) 334 | else: #e.g. strArgs="3-" (equiv. of --skip-to) 335 | return (int(testNumbers[0]),-1) 336 | else: #e.g. strArgs="1-2-3-5.." 337 | sys.exit("Invalid test range given: %s" % args) 338 | else: 339 | # If they pass a non-int, allow this to bomb out 340 | return (int(strArgs),int(strArgs)) 341 | #---------------------------------------------------- 342 | 343 | #Populate global arguments from parseargs 344 | fuzzerFilePath = args.prepped_fuzz 345 | host = args.target_host 346 | #Assign Lower/Upper bounds on test cases as needed 347 | if args.range: 348 | (MIN_RUN_NUMBER, MAX_RUN_NUMBER) = getRunNumbersFromArgs(args.range) 349 | elif args.loop: 350 | SEED_LOOP = validateNumberRange(args.loop,True) 351 | 352 | #Check for dependency binaries 353 | if not os.path.exists(RADAMSA): 354 | sys.exit("Could not find radamsa in %s... did you build it?" % RADAMSA) 355 | 356 | #Logging options 357 | isReproduce = False 358 | logAll = False 359 | 360 | if args.quiet: 361 | isReproduce = True 362 | elif args.logAll: 363 | logAll = True 364 | 365 | 366 | outputDataFolderPath = os.path.join("%s_%s" % (os.path.splitext(fuzzerFilePath)[0], "logs"), datetime.datetime.now().strftime("%Y-%m-%d,%H%M%S")) 367 | fuzzerFolder = os.path.abspath(os.path.dirname(fuzzerFilePath)) 368 | 369 | ########## Declare variables for scoping, "None"s will be assigned below 370 | messageProcessor = None 371 | monitor = None 372 | 373 | ###Here we read in the fuzzer file into a dictionary for easier variable propagation 374 | optionDict = {"unfuzzedBytes":{}, "message":[]} 375 | 376 | fuzzerData = FuzzerData() 377 | print("Reading in fuzzer data from %s..." % (fuzzerFilePath)) 378 | fuzzerData.readFromFile(fuzzerFilePath) 379 | 380 | ######## Processor Setup ################ 381 | # The processor just acts as a container # 382 | # class that will import custom versions # 383 | # messageProcessor/exceptionProessor/ # 384 | # monitor, if they are found in the # 385 | # process_dir specified in the .fuzzer # 386 | # file generated by fuzz_prep.py # 387 | ########################################## 388 | 389 | # Assign options to variables, error on anything that's missing/invalid 390 | processorDirectory = fuzzerData.processorDirectory 391 | if processorDirectory == "default": 392 | # Default to fuzzer file folder 393 | processorDirectory = fuzzerFolder 394 | else: 395 | # Make sure fuzzer file path is prepended 396 | processorDirectory = os.path.join(fuzzerFolder, processorDirectory) 397 | 398 | #Create class director, which import/overrides processors as appropriate 399 | procDirector = ProcDirector(processorDirectory) 400 | 401 | ########## Launch child monitor thread 402 | ### monitor.task = spawned thread 403 | ### monitor.crashEvent = threading.Event() 404 | monitor = procDirector.startMonitor(host,fuzzerData.port) 405 | 406 | #! make it so logging message does not appear if reproducing (i.e. -r x-y cmdline arg is set) 407 | logger = None 408 | 409 | if not isReproduce: 410 | print("Logging to %s" % (outputDataFolderPath)) 411 | logger = Logger(outputDataFolderPath) 412 | 413 | if args.dumpraw: 414 | if not isReproduce: 415 | DUMPDIR = outputDataFolderPath 416 | else: 417 | DUMPDIR = "dumpraw" 418 | try: 419 | os.mkdir("dumpraw") 420 | except: 421 | print("Unable to create dumpraw dir") 422 | pass 423 | 424 | 425 | exceptionProcessor = procDirector.exceptionProcessor() 426 | messageProcessor = procDirector.messageProcessor() 427 | 428 | # Set up signal handler for CTRL+C and signals from child monitor thread 429 | # since this is the same signal, we use the monitor.crashEvent flag() 430 | # to differentiate between a CTRL+C and a interrupt_main() call from child 431 | def sigint_handler(signal, frame): 432 | if not monitor.crashEvent.isSet(): 433 | # No event = quit 434 | # Quit on ctrl-c 435 | print("\nSIGINT received, stopping\n") 436 | sys.exit(0) 437 | 438 | signal.signal(signal.SIGINT, sigint_handler) 439 | 440 | ########## Begin fuzzing 441 | i = MIN_RUN_NUMBER-1 if fuzzerData.shouldPerformTestRun else MIN_RUN_NUMBER 442 | failureCount = 0 443 | loop_len = len(SEED_LOOP) # if --loop 444 | 445 | while True: 446 | lastMessageCollection = deepcopy(fuzzerData.messageCollection) 447 | wasCrashDetected = False 448 | print("\n** Sleeping for %.3f seconds **" % args.sleeptime) 449 | time.sleep(args.sleeptime) 450 | 451 | try: 452 | try: 453 | if args.dumpraw: 454 | print("\n\nPerforming single raw dump case: %d" % args.dumpraw) 455 | performRun(fuzzerData, host, logger, messageProcessor, seed=args.dumpraw) 456 | elif i == MIN_RUN_NUMBER-1: 457 | print("\n\nPerforming test run without fuzzing...") 458 | performRun(fuzzerData, host, logger, messageProcessor, seed=-1) 459 | elif loop_len: 460 | print("\n\nFuzzing with seed %d" % (SEED_LOOP[i%loop_len])) 461 | performRun(fuzzerData, host, logger, messageProcessor, seed=SEED_LOOP[i%loop_len]) 462 | else: 463 | print("\n\nFuzzing with seed %d" % (i)) 464 | performRun(fuzzerData, host, logger, messageProcessor, seed=i) 465 | #if --quiet, (logger==None) => AttributeError 466 | if logAll: 467 | try: 468 | logger.outputLog(i, fuzzerData.messageCollection, "LogAll ") 469 | except AttributeError: 470 | pass 471 | 472 | except Exception as e: 473 | if monitor.crashEvent.isSet(): 474 | print("Crash event detected") 475 | try: 476 | logger.outputLog(i, fuzzerData.messageCollection, "Crash event detected") 477 | #exit() 478 | except AttributeError: 479 | pass 480 | monitor.crashEvent.clear() 481 | 482 | elif logAll: 483 | try: 484 | logger.outputLog(i, fuzzerData.messageCollection, "LogAll ") 485 | except AttributeError: 486 | pass 487 | 488 | if e.__class__ in MessageProcessorExceptions.all: 489 | # If it's a MessageProcessorException, assume the MP raised it during the run 490 | # Otherwise, let the MP know about the exception 491 | raise e 492 | else: 493 | exceptionProcessor.processException(e) 494 | # Will not get here if processException raises another exception 495 | print("Exception ignored: %s" % (str(e))) 496 | 497 | except LogCrashException as e: 498 | if failureCount == 0: 499 | try: 500 | print("MessageProcessor detected a crash") 501 | logger.outputLog(i, fuzzerData.messageCollection, str(e)) 502 | except AttributeError: 503 | pass 504 | 505 | if logAll: 506 | try: 507 | logger.outputLog(i, fuzzerData.messageCollection, "LogAll ") 508 | except AttributeError: 509 | pass 510 | 511 | failureCount = failureCount + 1 512 | wasCrashDetected = True 513 | 514 | except AbortCurrentRunException as e: 515 | # Give up on the run early, but continue to the next test 516 | # This means the run didn't produce anything meaningful according to the processor 517 | print("Run aborted: %s" % (str(e))) 518 | 519 | except RetryCurrentRunException as e: 520 | # Same as AbortCurrentRun but retry the current test rather than skipping to next 521 | print("Retrying current run: %s" % (str(e))) 522 | # Slightly sketchy - a continue *should* just go to the top of the while without changing i 523 | continue 524 | 525 | except LogAndHaltException as e: 526 | if logger: 527 | logger.outputLog(i, fuzzerData.messageCollection, str(e)) 528 | print("Received LogAndHaltException, logging and halting") 529 | else: 530 | print("Received LogAndHaltException, halting but not logging (quiet mode)") 531 | exit() 532 | 533 | except LogLastAndHaltException as e: 534 | if logger: 535 | if i > MIN_RUN_NUMBER: 536 | print("Received LogLastAndHaltException, logging last run and halting") 537 | if MIN_RUN_NUMBER == MAX_RUN_NUMBER: 538 | #in case only 1 case is run 539 | logger.outputLastLog(i, lastMessageCollection, str(e)) 540 | print("Logged case %d" % i) 541 | else: 542 | logger.outputLastLog(i-1, lastMessageCollection, str(e)) 543 | else: 544 | print("Received LogLastAndHaltException, skipping logging (due to last run being a test run) and halting") 545 | else: 546 | print("Received LogLastAndHaltException, halting but not logging (quiet mode)") 547 | exit() 548 | 549 | except HaltException as e: 550 | print("Received HaltException halting") 551 | exit() 552 | 553 | if wasCrashDetected: 554 | if failureCount < fuzzerData.failureThreshold: 555 | print("Failure %d of %d allowed for seed %d" % (failureCount, fuzzerData.failureThreshold, i)) 556 | print("The test run didn't complete, continuing after %d seconds..." % (fuzzerData.failureTimeout)) 557 | time.sleep(fuzzerData.failureTimeout) 558 | else: 559 | print("Failed %d times, moving to next test." % (failureCount)) 560 | failureCount = 0 561 | i += 1 562 | else: 563 | i += 1 564 | 565 | # Stop if we have a maximum and have hit it 566 | if MAX_RUN_NUMBER >= 0 and i > MAX_RUN_NUMBER: 567 | exit() 568 | 569 | if args.dumpraw: 570 | exit() 571 | 572 | -------------------------------------------------------------------------------- /mutiny_classes/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /mutiny_classes/exception_processor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | #------------------------------------------------------------------ 32 | # 33 | # This file handles all custom errors that are raised 34 | # Copy this file to your project's mutiny classes directory to 35 | # change exception handling 36 | # This is useful for telling Mutiny how to interpret the server 37 | # closing a connection, and so on 38 | # 39 | #------------------------------------------------------------------ 40 | 41 | import errno 42 | import socket 43 | from mutiny_classes.mutiny_exceptions import * 44 | 45 | class ExceptionProcessor(object): 46 | 47 | def __init__(self): 48 | pass 49 | 50 | # Determine how to handle a given exception 51 | # Raise the exceptions defined in mutiny_exceptions to cause Mutiny 52 | # to do different things based on what has occurred 53 | def processException(self, exception): 54 | print(str(exception)) 55 | if isinstance(exception, socket.error): 56 | if exception.errno == errno.ECONNREFUSED: 57 | # Default to assuming this means server is crashed so we're done 58 | raise LogLastAndHaltException("Connection refused: Assuming we crashed the server, logging previous run and halting") 59 | elif "timed out" in str(exception): 60 | raise AbortCurrentRunException("Server closed the connection") 61 | else: 62 | if exception.errno: 63 | raise AbortCurrentRunException("Unknown socket error: %d" % (exception.errno)) 64 | else: 65 | raise AbortCurrentRunException("Unknown socket error: %s" % (str(exception))) 66 | elif isinstance(exception, ConnectionClosedException): 67 | raise AbortCurrentRunException("Server closed connection: %s" % (str(exception))) 68 | elif exception.__class__ not in MessageProcessorExceptions.all: 69 | # Default to logging a crash if we don't recognize the error 70 | raise LogCrashException(str(exception)) 71 | -------------------------------------------------------------------------------- /mutiny_classes/message_processor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | #------------------------------------------------------------------ 32 | # 33 | # Processor for a fuzzing session 34 | # 35 | # Copy this file to your project's mutiny classes directory to 36 | # change message processing 37 | # This is useful to alter fuzzed messages before transmission, 38 | # such as updating outbound messages based on the server's responses 39 | # 40 | #------------------------------------------------------------------ 41 | 42 | import errno 43 | import socket 44 | import _thread 45 | from mutiny_classes.mutiny_exceptions import * 46 | 47 | # This class is used to provide extra parameters beyond only the message 48 | # contents to the MessageProcessor callbacks 49 | # Do not bother this here, as only the base mutiny_classes version will get 50 | # imported by design 51 | class MessageProcessorExtraParams(object): 52 | def __init__(self, messageNumber, subcomponentNumber, isFuzzed, originalSubcomponents, actualSubcomponents): 53 | # Which message number this is in the .fuzzer file list, 0-indexed 54 | self.messageNumber = messageNumber 55 | 56 | # Which subcomponent is being called within this specific callback 57 | # Is -1 if it doesn't apply (examples: preFuzzProcess/preSendProcess/postReceiveProcess) 58 | self.subcomponentNumber = subcomponentNumber 59 | 60 | # Will message / subcomponent be fuzzed? 61 | self.isFuzzed = isFuzzed 62 | 63 | # List of subcomponent data as they are recorded in the .fuzzer file 64 | self.originalSubcomponents = originalSubcomponents 65 | 66 | # List of subcomponent data as it was actually received or will be 67 | # transmitted after fuzzing 68 | self.actualSubcomponents = actualSubcomponents 69 | 70 | # Convenience variable that is literally just all the originalSubcomponents combined 71 | self.originalMessage = bytearray().join(self.originalSubcomponents) 72 | 73 | # Convenience variable that is literally just all the actualSubcomponents combined 74 | self.actualMessage = bytearray().join(self.actualSubcomponents) 75 | 76 | class MessageProcessor(object): 77 | def __init__(self): 78 | self.postReceiveStore = {} 79 | 80 | # runNumber = number of current run 81 | # targetIP = address to connect to 82 | # targetPort = port being connected to 83 | # Called when the fuzzer is about to connect for runNumber 84 | def preConnect(self, runNumber, targetIP, targetPort): 85 | pass 86 | 87 | # subcomponent = subcomponent of message 88 | # Called whether or not subcomponent will be fuzzed 89 | # Will not be called if message has no subcomponents 90 | # extraParams contains MessageProcessorExtraParams based on the Message this 91 | # is a subcomponent for 92 | # Return subcomponent with any required modifications made 93 | def preFuzzSubcomponentProcess(self, subcomponent, extraParams): 94 | return subcomponent 95 | 96 | # message = full message, called whether or not message will be fuzzed 97 | # ONLY called if message has no subcomponents 98 | # If you use subcomponents, handle in preFuzzSubcomponentProcess() 99 | # extraParams contains MessageProcessorExtraParams based on this Message 100 | # Return message with any required modifications made 101 | def preFuzzProcess(self, message, extraParams): 102 | return message 103 | 104 | # subcomponent = subcomponent of message about to be sent 105 | # Will not be called if message has no subcomponents 106 | # extraParams contains MessageProcessorExtraParams based on the Message this 107 | # is a subcomponent for 108 | # Return subcomponent with any required modifications made 109 | # If subcomponent was fuzzed, this is the post-fuzzing subcomponent 110 | def preSendSubcomponentProcess(self, subcomponent, extraParams): 111 | return subcomponent 112 | 113 | # message = full message about to be sent 114 | # Any fuzzing on this message has been performed by this point 115 | # Called after preSendSubcomponentProcess() is called for every subcomponent, 116 | # if applicable 117 | # extraParams contains MessageProcessorExtraParams based on this Message 118 | # Return message with any required modifications made 119 | def preSendProcess(self, message, extraParams): 120 | return message 121 | 122 | # message = message that was actually received 123 | # extraParams contains MessageProcessorExtraParams based on this Message 124 | # Does not return anything 125 | # Can store messages for later use in the class as shown 126 | def postReceiveProcess(self, message, extraParams): 127 | self.postReceiveStore[int(extraParams.messageNumber)] = message 128 | -------------------------------------------------------------------------------- /mutiny_classes/monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # November 2014, created within ASIG 4 | # Author James Spadaro (jaspadar) 5 | # Co-Author Lilith Wyatt (liwyatt) 6 | #------------------------------------------------------------------ 7 | # Copyright (c) 2014-2017 by Cisco Systems, Inc. 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 1. Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 3. Neither the name of the Cisco Systems, Inc. nor the 18 | # names of its contributors may be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | #------------------------------------------------------------------ 32 | # 33 | # Copy this file to your project's mutiny classes directory to 34 | # implement a long-running thread to monitor your target 35 | # This is useful for watching files, logs, remote connections, 36 | # PIDs, etc in parallel while mutiny is operating 37 | # This parallel thread can signal Mutiny when it detects a crash 38 | # 39 | #------------------------------------------------------------------ 40 | 41 | class Monitor(object): 42 | # This function will run asynchronously in a different thread to monitor the host 43 | def monitorTarget(self, targetIP, targetPort, signalMain): 44 | # Can do something like: 45 | # while True: 46 | # read file, etc 47 | # if errorConditionHasOccurred: 48 | # signalMain() 49 | # 50 | # Calling signalMain() at any time will indicate to Mutiny 51 | # that the target has crashed and a crash should be logged 52 | pass 53 | -------------------------------------------------------------------------------- /mutiny_classes/mutiny_exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # 4 | # Cisco Confidential 5 | # November 2014, created within ASIG 6 | # Author James Spadaro (jaspadar) 7 | # Contributor Lilith Wyatt (liwyatt) 8 | # 9 | # Copyright (c) 2014-2015 by Cisco Systems, Inc. 10 | # All rights reserved. 11 | # 12 | # This file has the custom exceptions that can be raised during fuzzing 13 | #------------------------------------------------------------------ 14 | 15 | # Raise this to log and continue on 16 | class LogCrashException(Exception): 17 | pass 18 | 19 | # Raise this to indicate the current test shouldn't continue, skip to next 20 | class AbortCurrentRunException(Exception): 21 | pass 22 | 23 | # Raise this to indicate that the current test should be re-run 24 | # (Same as AbortCurrentRun, but will re-try current test) 25 | class RetryCurrentRunException(Exception): 26 | pass 27 | 28 | # Raise this to log, just like LogCrashException, except 29 | # stop testing entirely afterwards 30 | class LogAndHaltException(Exception): 31 | pass 32 | 33 | # Raise this to log the previous run and stop testing completely 34 | # Primarily used if daemon gives connection refused 35 | # Assumes that previous run caused a crash 36 | class LogLastAndHaltException(Exception): 37 | pass 38 | 39 | # Raise this to simply abort testing altogether 40 | class HaltException(Exception): 41 | pass 42 | 43 | # List of exceptions that can be thrown by a MessageProcessor 44 | class MessageProcessorExceptions(object): 45 | all = [LogCrashException, AbortCurrentRunException, RetryCurrentRunException, LogAndHaltException, LogLastAndHaltException, HaltException] 46 | 47 | # This is raised by the fuzzer when the server has closed the connection gracefully 48 | class ConnectionClosedException(Exception): 49 | pass 50 | 51 | -------------------------------------------------------------------------------- /mutiny_prep.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # Prep traffic log for fuzzing 4 | # 5 | # Cisco Confidential 6 | # November 2014, created within ASIG 7 | # Author James Spadaro (jaspadar) 8 | # Contributor Lilith Wyatt (liwyatt) 9 | # 10 | # Copyright (c) 2014-2015 by Cisco Systems, Inc. 11 | # All rights reserved. 12 | # 13 | # This script takes pcap or c_arrays output from Wireshark and 14 | # processes it into a .fuzzer file for use with mutiny.py 15 | #------------------------------------------------------------------ 16 | import os 17 | import sys 18 | import argparse 19 | 20 | from backend.fuzzer_types import Message 21 | from backend.menu_functions import prompt, promptInt, promptString, validateNumberRange 22 | from backend.fuzzerdata import FuzzerData 23 | import scapy.all 24 | 25 | GREEN = "\033[92m" 26 | CLEAR = "\033[00m" 27 | 28 | #So argparse prints help if no args are given 29 | if len(sys.argv) == 1: 30 | sys.argv.append("-h") 31 | 32 | parser = argparse.ArgumentParser() 33 | parser.add_argument("pcap_file", 34 | help="Pcap/c_array output from wireshark") 35 | 36 | parser.add_argument("-d","--processor_dir", 37 | help = "Location of custom pcap Message/exception/log/monitor processors if any, see appropriate *processor.py source in ./mutiny_classes/ for implimentation details", 38 | nargs=1, 39 | default=["default"]) 40 | 41 | 42 | parser.add_argument("-a", "--dump_ascii", 43 | help="Dump the ascii output from packets ", 44 | action="store_true", 45 | default=False) 46 | 47 | parser.add_argument("-f", "--force", 48 | help="Take all default options", 49 | action = "store_true", 50 | default=False) 51 | 52 | args = parser.parse_args() 53 | inputFilePath = args.pcap_file 54 | 55 | # This stores all the fuzzer data and will eventually write it to the .fuzzer file with comments 56 | fuzzerData = FuzzerData() 57 | fuzzerData.processorDirectory = args.processor_dir[0] 58 | 59 | ############# Process input files 60 | if not os.path.isfile(inputFilePath): 61 | print("Cannot read input %s" % (inputFilePath)) 62 | exit() 63 | 64 | STATE_BETWEEN_MESSAGES = 0 65 | STATE_READING_MESSAGE = 2 66 | STATE_COMBINING_PACKETS = 3 67 | data = [] 68 | defaultPort = None 69 | 70 | with open(inputFilePath, 'r') as inputFile: 71 | # This is a little naive, but it works 72 | # These two get recreated frequently 73 | message = Message() 74 | tempMessageData = "" 75 | # Track what we're looking for 76 | state = STATE_BETWEEN_MESSAGES 77 | 78 | # Allow combining packets in same direction back-to-back into one message 79 | askedToCombinePackets = False 80 | isCombiningPackets = False 81 | lastMessageDirection = -1 82 | 83 | print("Processing %s..." % (inputFilePath)) 84 | 85 | try: 86 | # Process as Pcap preferentially 87 | clientPort = None 88 | serverPort = None 89 | 90 | data = scapy.all.rdpcap(inputFilePath) 91 | 92 | j = -1 93 | 94 | for i in range(0, len(data)): 95 | try: 96 | if not clientPort: 97 | # First packet will usually but not always come from client 98 | # Use port instead of ip/MAC in case we're fuzzing on the same machine as the daemon 99 | # Guess at right port based, confirm to user 100 | port1 = data[i].sport 101 | port2 = data[i].dport 102 | 103 | # IF port1 == port2, then it can't be the same ip/MAC, so go based on that 104 | useMacs = False 105 | if port1 == port2: 106 | print("Source and destination ports are the same, using MAC addresses to differentiate server and client.") 107 | useMacs = True 108 | mac1 = data[i].src 109 | mac2 = data[i].dst 110 | 111 | serverPort = port2 112 | if useMacs: 113 | serverMac = mac2 114 | if not args.force: 115 | if not useMacs: 116 | serverPort = int(prompt("Which port is the server listening on?", [str(port2), str(port1)], defaultIndex=0 if port1 > port2 else 1)) 117 | else: 118 | serverMac = prompt("Which mac corresponds to the server?", [str(mac1), str(mac2)], defaultIndex=1) 119 | 120 | clientPort = port1 if serverPort == port2 else port2 121 | if useMacs: 122 | clientMac = mac1 if serverMac == mac2 else mac2 123 | defaultPort = serverPort 124 | elif data[i].sport not in [clientPort, serverPort]: 125 | print("Error: unknown source port %d - is the capture filtered to a single stream?" % (data[i].sport)) 126 | elif data[i].dport not in [clientPort, serverPort]: 127 | print("Error: unknown destination port %d - is the capture filtered to a single stream?" % (data[i].dport)) 128 | 129 | if not useMacs: 130 | newMessageDirection = Message.Direction.Outbound if data[i].sport == clientPort else Message.Direction.Inbound 131 | else: 132 | newMessageDirection = Message.Direction.Outbound if data[i].src == clientMac else Message.Direction.Inbound 133 | 134 | try: 135 | # This appear to work for UDP. Go figure, thanks scapy. 136 | tempMessageData = bytes(data[i].payload.payload.payload) 137 | except AttributeError: 138 | tempMessageData = "" 139 | if len(tempMessageData) == 0: 140 | # This appears to work for TCP 141 | tempMessageData = data[i].load 142 | 143 | if newMessageDirection == lastMessageDirection: 144 | if args.force: 145 | isCombiningPackets = True 146 | askedToCombinePackets = True 147 | if not askedToCombinePackets: 148 | if prompt("There are multiple packets from client to server or server to client back-to-back - combine payloads into single messages?"): 149 | isCombiningPackets = True 150 | askedToCombinePackets = True 151 | if isCombiningPackets: 152 | message.appendMessageFrom(Message.Format.Raw, bytearray(tempMessageData), False) 153 | print("\tMessage #%d - Added %d new bytes %s" % (j, len(tempMessageData), message.direction)) 154 | continue 155 | # Either direction isn't the same or we're not combining packets 156 | message = Message() 157 | message.direction = newMessageDirection 158 | lastMessageDirection = newMessageDirection 159 | message.setMessageFrom(Message.Format.Raw, bytearray(tempMessageData), False) 160 | fuzzerData.messageCollection.addMessage(message) 161 | j += 1 162 | print("\tMessage #%d - Processed %d bytes %s" % (j, len(message.getOriginalMessage()), message.direction)) 163 | except AttributeError: 164 | # No payload, keep going (different from empty payload) 165 | continue 166 | except Exception as rdpcap_e: 167 | print(str(rdpcap_e)) 168 | print("Processing as c_array...") 169 | try: 170 | # Process c_arrays 171 | # This is processing the wireshark syntax looking like: 172 | # char peer0_0[] = { 173 | # 0x66, 0x64, 0x73, 0x61, 0x0a }; 174 | # char peer1_0[] = { 175 | # 0x61, 0x73, 0x64, 0x66, 0x0a }; 176 | # First is message from client to server, second is server to client 177 | # Format is peer0/1_messagenum 178 | # 0 = client, 1 = server 179 | i = 0 180 | for line in inputFile: 181 | 182 | #remove comments 183 | com_start,com_end = line.find('/*'),line.rfind('*/') 184 | if com_start > -1 and com_end > -1: 185 | line = line[:com_start] + line[com_end+2:] 186 | 187 | if state == STATE_BETWEEN_MESSAGES: 188 | # On a new message, seek data 189 | message = Message() 190 | tempMessageData = "" 191 | 192 | peerPos = line.find("peer") 193 | if peerPos == -1: 194 | continue 195 | elif line[peerPos+4] == str(0): 196 | message.direction = Message.Direction.Outbound 197 | elif line[peerPos+4] == str(1): 198 | message.direction = Message.Direction.Inbound 199 | else: 200 | continue 201 | 202 | bracePos = line.find("{") 203 | if bracePos == -1: 204 | continue 205 | tempMessageData += line[bracePos+1:] 206 | state = STATE_READING_MESSAGE 207 | 208 | # Sometimes HTTP requests, etc, get separated into multiple packets but they should 209 | # really be treated as one message. Allow the user to decide to do this automatically 210 | if message.direction == lastMessageDirection: 211 | if args.force: 212 | askedToCombinePackets=True 213 | isCombiningPackets=True 214 | if not askedToCombinePackets: 215 | if prompt("There are multiple packets from client to server or server to client back-to-back - combine payloads into single messages?"): 216 | isCombiningPackets = True 217 | askedToCombinePackets = True 218 | if isCombiningPackets: 219 | message = fuzzerData.messageCollection.messages[-1] 220 | state = STATE_COMBINING_PACKETS 221 | elif state == STATE_READING_MESSAGE or state == STATE_COMBINING_PACKETS: 222 | bracePos = line.find("}") 223 | if bracePos == -1: 224 | # No close brace means keep reading 225 | tempMessageData += line 226 | else: 227 | # Close brace means save the message 228 | tempMessageData += line[:bracePos] 229 | # Turn list of comma&space-separated bytes into a string of 0x hex bytes 230 | messageArray = tempMessageData.replace(",", "").replace("0x", "").split() 231 | 232 | if state == STATE_READING_MESSAGE: 233 | message.setMessageFrom(Message.Format.CommaSeparatedHex, ",".join(messageArray), False) 234 | fuzzerData.messageCollection.addMessage(message) 235 | print("\tMessage #%d - Processed %d bytes %s" % (i, len(messageArray), message.direction)) 236 | elif state == STATE_COMBINING_PACKETS: 237 | # Append new data to last message 238 | i -= 1 239 | message.appendMessageFrom(Message.Format.CommaSeparatedHex, ",".join(messageArray), False, createNewSubcomponent=False) 240 | print("\tMessage #%d - Added %d new bytes %s" % (i, len(messageArray), message.direction)) 241 | if args.dump_ascii: 242 | print("\tAscii: %s" % (str(message.getOriginalMessage()))) 243 | i += 1 244 | state = STATE_BETWEEN_MESSAGES 245 | lastMessageDirection = message.direction 246 | except Exception as e: 247 | print("Unable to parse as pcap: %s" % (str(rdpcap_e))) 248 | print("Unable to parse as c_arrays: %s" % (str(e))) 249 | 250 | if len(fuzzerData.messageCollection.messages) == 0: 251 | print("\nCouldn't process input file - are you sure you gave a file containing a tcpdump pcap or wireshark c_arrays?") 252 | exit() 253 | print("Processed input file %s" % (inputFilePath)) 254 | 255 | ############# Get fuzzing details 256 | # Ask how many times we should repeat a failed test, as in one causing a crash 257 | fuzzerData.failureThreshold = promptInt("\nHow many times should a test case causing a crash or error be repeated?", defaultResponse=3) if not args.force else 3 258 | # Timeout between failure retries 259 | fuzzerData.failureTimeout = promptInt("When the test case is repeated above, how many seconds should it wait between tests?", defaultResponse=5) if not args.force else 5 260 | # Ask if tcp or udp 261 | fuzzerData.proto = prompt("Which protocol?", answers=["tcp", "udp", "layer3" ], defaultIndex=0) if not args.force else "tcp" 262 | 263 | # for finding out which L3 protocol 264 | if fuzzerData.proto == "layer3": 265 | fuzzerData.proto = prompt("Which layer3 protocol?", answers=["icmp","igmp","ipv4","tcp","igp","udp","ipv6","ipv6-route","ipv6-frag","gre", \ 266 | "dsr","esp","ipv6-icmp","ipv6-nonxt","ipv6-opts","eigrp","ospf","mtp","l2tp","sctp","manual"],defaultIndex=0) 267 | # in the case that it's not in the above list 268 | if fuzzerData.proto == "manual": 269 | fuzzerData.proto = promptInt("What is the L3 protocol number?", defaultResponse=0) 270 | 271 | # Port number to connect on 272 | fuzzerData.port = promptInt("What port should the fuzzer %s?" % ("connect to"), defaultResponse=defaultPort) if not args.force else defaultPort 273 | 274 | # How many of the messages to output to the .fuzzer 275 | default = len(fuzzerData.messageCollection.messages)-1 276 | 277 | ###################################################### 278 | 279 | ############# Helper function to get next message from either client or server 280 | # Inclusive (if startMessage is fromClient and so is direction, 281 | # will return startMessage) 282 | # Returns message number or None if no messages remain 283 | def getNextMessage(startMessage, messageDirection): 284 | i = startMessage 285 | 286 | while i < len(fuzzerData.messageCollection.messages): 287 | if fuzzerData.messageCollection.messages[i].direction == messageDirection: 288 | return i 289 | i += 1 290 | 291 | return None 292 | 293 | ############# Prompt for .fuzzer-specific questions and write file (calls above function) 294 | # Allows us to let the user crank out a bunch of .fuzzer files quickly 295 | # Param is the highest message output last time, if they're creating multiple .fuzzer files 296 | # autogenerateAllClient will make a .fuzzer file per client automatically 297 | def promptAndOutput(outputMessageNum, autogenerateAllClient=False): 298 | # How many of the messages to output to the .fuzzer 299 | if args.force or autogenerateAllClient: 300 | finalMessageNum = len(fuzzerData.messageCollection.messages)-1 301 | else: 302 | finalMessageNum = promptInt("What is the last message number you want output?", defaultResponse=len(fuzzerData.messageCollection.messages)-1) 303 | 304 | # Any messages previously marked for fuzzing, unmark first 305 | # Inefficient as can be, but who cares 306 | for message in fuzzerData.messageCollection.messages: 307 | if message.isFuzzed: 308 | message.isFuzzed = False 309 | for subcomponent in message.subcomponents: 310 | subcomponent.isFuzzed = False 311 | 312 | if not autogenerateAllClient: 313 | while True: 314 | tmp = promptString("Which message numbers should be fuzzed? Valid: 0-%d" % (finalMessageNum),defaultResponse=str(outputMessageNum),validateFunc=validateNumberRange) 315 | if len(tmp) > 0: 316 | outputFilenameEnd = tmp 317 | for messageIndex in validateNumberRange(tmp, flattenList=True): 318 | fuzzerData.messageCollection.messages[messageIndex].isFuzzed = True 319 | for subcomponent in fuzzerData.messageCollection.messages[messageIndex].subcomponents: 320 | subcomponent.isFuzzed = True 321 | break 322 | else: 323 | outputFilenameEnd = str(outputMessageNum) 324 | fuzzerData.messageCollection.messages[outputMessageNum].isFuzzed = True 325 | for subcomponent in fuzzerData.messageCollection.messages[outputMessageNum].subcomponents: 326 | subcomponent.isFuzzed = True 327 | 328 | 329 | outputFilePath = "{0}-{1}.fuzzer".format(os.path.splitext(inputFilePath)[0], outputFilenameEnd) 330 | actualPath = fuzzerData.writeToFile(outputFilePath, defaultComments=True, finalMessageNum=finalMessageNum) 331 | print(GREEN) 332 | print("Wrote .fuzzer file: {0}".format(actualPath)) 333 | print(CLEAR) 334 | 335 | if autogenerateAllClient: 336 | nextMessage = getNextMessage(outputMessageNum+1, Message.Direction.Outbound) 337 | # Will return None when we're out of messages to auto-output 338 | if nextMessage: 339 | promptAndOutput(nextMessage, autogenerateAllClient=True) 340 | return finalMessageNum 341 | 342 | ############# Call promptAndOutput() 343 | 344 | # See if they'd like us to just rip out a .fuzzer per client message 345 | # Default to no 346 | if prompt("\nWould you like to auto-generate a .fuzzer for each client message?", defaultIndex=1): 347 | promptAndOutput(getNextMessage(0, Message.Direction.Outbound), autogenerateAllClient=True) 348 | else: 349 | # Always run once 350 | outputMessageNum = promptAndOutput(getNextMessage(0, Message.Direction.Outbound)) 351 | 352 | # Allow creating multiple .fuzzers afterwards 353 | if not args.force: 354 | while prompt("\nDo you want to generate a .fuzzer for another message number?", defaultIndex=1): 355 | outputMessageNum = promptAndOutput(outputMessageNum) 356 | 357 | print("All files have been written.") 358 | -------------------------------------------------------------------------------- /radamsa-v0.6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cisco-Talos/mutiny-fuzzer/3f347d292ebf7e4dff596f54757663346222aeb1/radamsa-v0.6.tar.gz -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Quickstart: Mutiny tutorial 2 | 3 | Blog post here: 4 | * http://blog.talosintelligence.com/2018/01/tutorial-mutiny-fuzzing-framework-and.html 5 | 6 | Links to this YouTube video demo: 7 | * https://www.youtube.com/watch?v=FZyR6MgJCUs 8 | 9 | For more features geared towards fuzzing campaigns/feedback/harnesses: 10 | * https://github.com/Cisco-Talos/mutiny-fuzzer/tree/experiment 11 | 12 | # Mutiny Fuzzing Framework 13 | 14 | The Mutiny Fuzzing Framework is a network fuzzer that operates by replaying 15 | PCAPs through a mutational fuzzer. The goal is to begin network fuzzing as 16 | quickly as possible, at the expense of being thorough. 17 | 18 | The general workflow for Mutiny is to take a sample of legitimate traffic, such 19 | as a browser request, and feed it into a prep script to generate a .fuzzer file. 20 | Then, Mutiny can be run with this .fuzzer file to generate traffic against a 21 | target host, mutating whichever packets the user would like. 22 | 23 | There are extensions that allow changing how Mutiny behaves, including changing 24 | messages based on input/output, changing how Mutiny responds to network errors, 25 | and monitoring the target in a separate thread. 26 | 27 | Mutiny uses [Radamsa](https://github.com/aoh/radamsa) to perform mutations. 28 | 29 | The [Decept Proxy](https://github.com/Cisco-Talos/Decept) is a multi-purpose 30 | network proxy that can forward traffic from a plaintext or TLS TCP/UDP/domain 31 | socket connection to a plaintext or TLS TCP/UDP/domain socket connection, among 32 | other features. It makes a good companion for Mutiny, as it can both generate 33 | .fuzzer files directly, particularly helpful when fuzzing TLS connections, and 34 | allow Mutiny to communicate with TLS hosts. 35 | 36 | sample_apps give a basic idea of some things that can be done with the fuzzer, 37 | with a few different applications/clients to test with. 38 | 39 | Written by James Spadaro (jaspadar@cisco.com) and Lilith Wyatt 40 | (liwyatt@cisco.com) 41 | 42 | ## Setup 43 | 44 | Ensure python and scapy are installed. 45 | 46 | Untar Radamsa and `make` (You do not have to make install, unless you want it 47 | in /usr/bin - it will use the local Radamsa) Update `mutiny.py` with path to 48 | Radamsa if you changed it. 49 | 50 | ## Basic Usage 51 | 52 | Save pcap into a folder. Run `mutiny_prep.py` on `.pcap` (also optionally 53 | pass the directory of a custom processor if any, more below). Answer the 54 | questions, end up with a `.fuzzer` file in same folder as pcap. 55 | 56 | Run `mutiny.py .fuzzer ` This will start fuzzing. Logs will be 57 | saved in same folder, under directory 58 | `_logs//` 59 | 60 | ## More Detailed Usage 61 | 62 | ### .fuzzer Files 63 | 64 | The .fuzzer files are human-readable and commented. They allow changing various 65 | options on a per-fuzzer-file basis, including which message or message parts are 66 | fuzzed. 67 | 68 | ### Message Formatting 69 | 70 | Within a .fuzzer file is the message contents. These are simply lines that 71 | begin with either 'inbound' or 'outbound', signifying which direction the 72 | message goes. They are in Python string format, with '\xYY' being used for 73 | non-printable characters. These are autogenerated by 'mutiny_prep.py' and 74 | Decept, but sometimes need to be manually modified. 75 | 76 | ### Message Formatting - Manual Editing 77 | 78 | If a message has the 'fuzz' keyword after 'outbound', this indicates it is to be 79 | fuzzed through Radamsa. A given message can have line continuations, by simply 80 | putting more message data in quotes on a new line. In this case, this second 81 | line will be merged with the first. 82 | 83 | Alternatively, the 'sub' keyword can be used to indicate a subcomponent. This 84 | allows specifying a separate component of the message, in order to fuzz only 85 | certain parts and for convenience within a Message Processor. 86 | 87 | Here is an example arbitrary set of message data: 88 | ``` 89 | outbound 'say' 90 | ' hi' 91 | sub fuzz ' and fuzz' 92 | ' this' 93 | sub ' but not this\xde\xad\xbe\xef' 94 | inbound 'this is the server's' 95 | ' expected response' 96 | ``` 97 | 98 | This will cause Mutiny to transmit `say hi and fuzz this but not 99 | this(0xdeadbeef)`. `0xdeadbeef` will be transmitted as 4 hex bytes. `and fuzz 100 | this` will be passed through Radamsa for fuzzing, but `say hi` and ` but not 101 | this(0xdeadbeef)` will be left alone. 102 | 103 | Mutiny will wait for a response from the server after transmitting the single 104 | above message, due to the 'inbound' line. The server's expected response is 105 | `this is the server's expected response`. Mutiny won't do a whole lot with this 106 | data, aside from seeing if what the server actually sent matches this string. 107 | If a crash occurs, Mutiny will log both the expected output from the server and 108 | what the server actually replied with. 109 | 110 | ### Customization 111 | 112 | mutiny_classes/ contains base classes for the Message Processor, Monitor, and 113 | Exception Processor. Any of these files can be copied into the same folder as 114 | the .fuzzer (by default) or into a separate subfolder specified as the 115 | 'processor_dir' within the .fuzzer file. 116 | 117 | These three classes allow for storing server responses and changing outgoing 118 | messages, monitoring the target on a separate thread, and changing how Mutiny 119 | handles exceptions. 120 | 121 | ### Customization - Message Processor 122 | 123 | The Message Processor defines various callbacks that are called during a fuzzing 124 | run. Within these callbacks, any Python code can be run. Anecdotally, these 125 | are primarily used in three ways. 126 | 127 | The most common is when the server sends tokens that need to be added to future 128 | outbound messages. For example, if Mutiny's first message logs in, and the 129 | server responds with a session ID, the `postReceiveProcess()` callback can be used 130 | to store that session ID. Then, in `preSendProcess()`, the outgoing data can be 131 | fixed up with that session ID. An example of this is in 132 | `sample_apps/session_server`. 133 | 134 | Another common use of a Message Processor is to limit or change a fuzzed 135 | message. For example, if the server always drops messages greater than 1000 136 | bytes, it may not be worth sending any large messages. preSendProcess() can be 137 | used to shorten messages after fuzzing but before they are sent or to raise an 138 | exception. 139 | 140 | Raising an exception brings up the final way Message Processors are commonly 141 | used. Within a callback, any custom exceptions defined in 142 | `mutiny_classes/mutiny_exceptions.py` can be raised. There are several 143 | exceptions, all commented, that will cause various behaviors from Mutiny. These 144 | generally involve either logging, retrying, or aborting the current run. 145 | 146 | ### Customization - Monitor 147 | 148 | The Monitor has a `monitorTarget()` function that is run on a separate thread from 149 | the main Mutiny fuzzer. The purpose is to allow implementing a long-running 150 | process that can monitor a host in some fashion. This can be anything that can 151 | be done in Python, such as communicating with a monitor daemon running on the 152 | target, reading a long file, or even just pinging the host repeatedly, depending 153 | on the requirements of the fuzzing session. 154 | 155 | If the Monitor detects a crash, it can call `signalMain()` at any time. This will 156 | signal the main Mutiny thread that a crash has occurred, and it will log the 157 | crash. This function should generally operate in an infinite loop, as returning 158 | will cause the thread to terminate, and it will not be restarted. 159 | 160 | ### Customization - Exception Processor 161 | 162 | The Exception Processor determines what Mutiny should do with a given exception 163 | during a fuzz session. In the most general sense, the `processException()` 164 | function will translate Python and OS-level exceptions into Mutiny error 165 | handling actions as best as it can. 166 | 167 | For example, if Mutiny gets 'Connection Refused', the default response is to 168 | assume that the target server has died unrecoverably, so Mutiny will log the 169 | previous run and halt. This is true in most cases, but this behavior can be 170 | changed to that of any of the exceptions in 171 | `mutiny_classes/mutiny_exceptions.py` as needed, allowing tailoring of crash 172 | detection and error correction. 173 | -------------------------------------------------------------------------------- /sample_apps/pidlisten/data/pid_listen.fuzzer: -------------------------------------------------------------------------------- 1 | # Directory containing any custom exception/message/monitor processors 2 | # This should be either an absolute path or relative to the .fuzzer file 3 | # If set to "default", Mutiny will use any processors in the same 4 | # folder as the .fuzzer file 5 | processor_dir default 6 | # Number of times to retry a test case causing a crash 7 | failureThreshold 3 8 | # How long to wait between retrying test cases causing a crash 9 | failureTimeout 5 10 | # How long for recv() to block when waiting on data from server 11 | receiveTimeout 1.0 12 | # Whether to perform an unfuzzed test run before fuzzing 13 | shouldPerformTestRun 1 14 | # Protocol (udp or tcp) 15 | proto tcp 16 | # Port number to connect to 17 | port 9999 18 | # Port number to connect from 19 | sourcePort -1 20 | # Source IP to connect from 21 | sourceIP 0.0.0.0 22 | 23 | # The actual messages in the conversation 24 | # Each contains a message to be sent to or from the server, printably-formatted 25 | outbound fuzz '1234.4321' 26 | inbound '[^.^] Launching 4321 testcases for pid 4321' 27 | -------------------------------------------------------------------------------- /sample_apps/pidlisten/data/pidlisten.fuzzer: -------------------------------------------------------------------------------- 1 | # [;.;] Built by sadsoxxy [;.;] 2 | # ['sadsox/sadsoxxy.py', '127.0.0.1', '9090', '127.0.0.1', '9999', '--fuzzer', 'pidlisten.fuzzer'] 3 | #----------------------- 4 | failureThreshold 3 5 | failureTimeout 0 6 | proto tcp 7 | port 9999 8 | messagesToFuzz 0 9 | #unfuzzedBytes 0 1,2-3 10 | processor_dir default 11 | message 0 31,32,33,34,2e,34,33,32,31 12 | message 1 5b,5e,2e,5e,5d,20,4c,61,75,6e,63,68,69,6e,67,20,34,33,32,31,20,74,65,73,74,63,61,73,65,73,20,66,6f,72,20,70,69,64,20,34,33,32,31 13 | -------------------------------------------------------------------------------- /sample_apps/pidlisten/source/pid_listener.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/python 2 | #--------------------- 3 | # Vulnerable binary, doesn't really do much 4 | # Faulty pidlistener server for testing purposes 5 | # Integer overflow -> malloc crash 6 | # (Integer of 8192 causes crash) 7 | # 8 | # September 2015, created within ASIG 9 | # Author Lilith Wyatt (liwyatt) 10 | # 11 | # Copyright (c) 2015 by Cisco Systems, Inc. 12 | # All rights reserved. 13 | #------------------------------------------------------------------ 14 | 15 | import socket 16 | import os 17 | from ctypes import * 18 | from ctypes.util import find_library 19 | from sys import exit,argv 20 | import threading 21 | from time import localtime 22 | 23 | #----------- 24 | # ctype structs for holding testcase/session data 25 | 26 | class fuzz_tc(Structure): 27 | _fields_ = [("status",c_int), 28 | ("tc_id",c_int)] 29 | 30 | class fuzz_session(Structure): 31 | _packed_ = ("tc",) 32 | _fields_ = [("ip",c_char_p), 33 | ("port",c_int), 34 | ("pid",c_int), 35 | ("tc_len",c_short), 36 | ("tc",c_void_p)] 37 | #------------------------------- 38 | # Options for sessions 39 | TIMEOUT = 2 40 | MAX_SESSIONS = 5 41 | 42 | try: 43 | libc_loc = find_library("c") 44 | LIBC = CDLL(libc_loc) 45 | except: 46 | print("Unable to find Libc, exiting!") 47 | exit(-1) 48 | 49 | 50 | #------------------------------- 51 | # Vulnerable server initialization 52 | def server_init(): 53 | 54 | 55 | bindip = "127.0.0.1" 56 | socket_family = socket.AF_INET 57 | socket_type = socket.SOCK_STREAM 58 | 59 | try: 60 | if argv[1] == "-6": 61 | socket_family = socket.AF_INET6 62 | bindip = "::1" 63 | elif argv[1] == '-l': 64 | socket_family = socket.AF_UNIX 65 | bindip = "fdsa" 66 | elif argv[1] == '-u': 67 | socket_family = socket.AF_INET 68 | socket_type = socket.SOCK_DGRAM 69 | except IndexError: 70 | pass 71 | 72 | bindport = 9999 73 | pid = c_uint() 74 | 75 | try: 76 | serv = socket.socket(socket_family,socket_type) 77 | serv.bind((bindip,bindport)) 78 | if socket_type != socket.SOCK_DGRAM: 79 | serv.listen(MAX_SESSIONS) 80 | print('[^_^] Listening on %s:%d'%(bindip,bindport)) 81 | except TypeError as e: 82 | try: 83 | # this is for unix socket 84 | serv.bind(bindip) 85 | serv.listen(MAX_SESSIONS) 86 | except Exception as e: 87 | print(e) 88 | print("Unable to bind to %s,%d!!" % (bindip,bindport)) 89 | exit(-1) 90 | 91 | 92 | #Spawn thread for each fuzz session (if mulitple) 93 | while True: 94 | if socket_type == socket.SOCK_DGRAM: 95 | client_handler(serv,(bindip,bindport),udp=True) 96 | continue 97 | try: 98 | cli_sock,cli_addr = serv.accept() 99 | fuzz_session = threading.Thread(target=client_handler,args=(cli_sock,cli_addr)) 100 | except Exception as e: #UDS error 101 | print(e) 102 | cli_sock = serv.accept() 103 | fuzz_session = threading.Thread(target=client_handler,args=(cli_sock)) 104 | 105 | 106 | fuzz_session.start() 107 | 108 | 109 | # Code for handling a given fuzzing session 110 | # - Propagates fuzz_session struct with connection info 111 | # - Allocates enough memory to handle all the testcases 112 | # - Records the information into a file upon timeout/crash/normal exit 113 | #--------------------- 114 | 115 | def client_handler(cli_sock,cli_addr=None,udp=False): 116 | try: 117 | ip = cli_addr[0] 118 | port = cli_addr[1] 119 | cli_sock.settimeout(TIMEOUT) 120 | fs = fuzz_session(ip,port,-1,-1,None) 121 | except: 122 | ip = "fdsa" 123 | cli_sock.settimeout(TIMEOUT) 124 | fs = fuzz_session(ip,-1,-1,-1,None) 125 | 126 | #generate log file name 127 | timestamp = localtime() 128 | fname = "%d_%d_%d_%d_%d_%s" % (timestamp.tm_year, 129 | timestamp.tm_mon, 130 | timestamp.tm_mday, 131 | timestamp.tm_hour, 132 | timestamp.tm_min, 133 | ip) 134 | 135 | 136 | #listen for initialization message: 137 | # 4 byte - pid 138 | # . separator 139 | # 4 byte - number of test cases 140 | if not udp: 141 | try: 142 | msg = cli_sock.recv(4096).split('.') 143 | print("asdf") 144 | except: 145 | pass 146 | else: 147 | try: 148 | msg,addr = cli_sock.recvfrom(4096) 149 | msg = msg.split('.') 150 | print("Msg from %s:%d"%addr) 151 | except Exception as e: 152 | return 153 | 154 | if len(msg) != 2 or len(msg[0]) > 4 or len(msg[1].rstrip()) > 4: 155 | print("Invalid session init: %s" % (msg,)) 156 | if udp: 157 | return 158 | exit(-1) 159 | 160 | try: 161 | dirty_input_int = int(msg[0]) 162 | dirty_input_short = int(msg[1]) 163 | if dirty_input_int >= c_uint(-1).value or dirty_input_short >= c_ushort(-1).value: 164 | print("Invalid init values given: %d, %d" % (dirty_input_int, dirty_input_short)) 165 | if udp: 166 | return 167 | exit(-1) 168 | except: 169 | print("Unsavory input given: %s, %s" % (msg[0],msg[1])) 170 | 171 | #populate data to struct 172 | try: 173 | fs.pid = c_int(dirty_input_int) 174 | fs.tc_len = c_short(dirty_input_short) 175 | except: 176 | print("Unable to parse msg: %s" % (msg,)) 177 | 178 | #allocate enough space to hold fuzz_tc structs 179 | #! vulnerable line 180 | allocate_space = c_ushort(sizeof(fuzz_tc) * fs.tc_len) 181 | LIBC.malloc.restype = c_void_p 182 | ret = LIBC.malloc(allocate_space) 183 | 184 | print("Buffer (size: %d) allocated at 0x%x" % (allocate_space.value, ret)) 185 | 186 | if ret == 0: 187 | print("Unable to allocate memory! Exiting!") 188 | if udp: 189 | return 190 | exit(-1) 191 | 192 | fs.tc = cast(ret,c_void_p) 193 | print("fs.tc = 0x%x" % (fs.tc)) 194 | 195 | tmp = c_void_p(None) 196 | tmp_struct = None 197 | 198 | #write arbitrary value (for now) to each struct 199 | for i in range(0,fs.tc_len): 200 | tmp = c_void_p(fs.tc + (i*sizeof(fuzz_tc))) 201 | tmp_struct = fuzz_tc.from_address(tmp.value) 202 | tmp_struct.status = i 203 | print("status: %d" % (i,)) 204 | 205 | if udp: 206 | cli_sock.sendto("[^.^] Launching %d testcases for pid %d" % (fs.tc_len,fs.tc_len),addr) 207 | else: 208 | cli_sock.send("[^.^] Launching %d testcases for pid %d" % (fs.tc_len,fs.tc_len)) 209 | 210 | if __name__ == '__main__': 211 | 212 | try: 213 | os.remove("fdsa") 214 | except: 215 | pass 216 | server_init() 217 | -------------------------------------------------------------------------------- /sample_apps/pidlisten/source/test_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import socket 4 | import sys 5 | 6 | ip = "127.0.0.1" 7 | port = 9090 8 | 9 | #msg format : .<# of test cases> 10 | #although it doesn't really do much right now 11 | msg = "1234.4321" 12 | 13 | try: 14 | cli = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 15 | cli.connect((ip,port)) 16 | except: 17 | print("Could not connect to %s, %d, exiting!" % (ip,port)) 18 | sys.exit(0) 19 | 20 | cli.send(msg) 21 | print(cli.recv(4096)) 22 | 23 | -------------------------------------------------------------------------------- /sample_apps/server/data/message_processor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # Base processor for a fuzzing session 4 | # 5 | # Cisco Confidential 6 | # November 2014, created within ASIG 7 | # Author James Spadaro (jaspadar) 8 | # Contributor Lilith Wyatt (liwyatt) 9 | # 10 | # Copyright (c) 2014-2015 by Cisco Systems, Inc. 11 | # All rights reserved. 12 | # 13 | # This script is the base class for the MessageProcessor 14 | # Create a message_processor.py file into your project's subfolder 15 | # File will be imported into mutiny.py. 16 | #------------------------------------------------------------------ 17 | 18 | # Copy this file to your project's mutiny classes directory to 19 | # change message processing 20 | # This is useful to alter fuzzed messages before transmission, 21 | # such as updating outbound messages based on the server's responses 22 | 23 | import errno 24 | import socket 25 | import _thread 26 | from mutiny_classes.mutiny_exceptions import * 27 | 28 | # This class is used to provide extra parameters beyond only the message 29 | # contents to the MessageProcessor callbacks 30 | # Do not bother this here, as only the base mutiny_classes version will get 31 | # imported by design 32 | class MessageProcessorExtraParams(object): 33 | def __init__(self, messageNumber, subcomponentNumber, isFuzzed, originalSubcomponents, actualSubcomponents): 34 | # Which message number this is in the .fuzzer file list, 0-indexed 35 | self.messageNumber = messageNumber 36 | 37 | # Which subcomponent is being called within this specific callback 38 | # Is -1 if it doesn't apply (examples: preFuzzProcess/preSendProcess/postReceiveProcess) 39 | self.subcomponentNumber = subcomponentNumber 40 | 41 | # Will message / subcomponent be fuzzed? 42 | self.isFuzzed = isFuzzed 43 | 44 | # List of subcomponent data as they are recorded in the .fuzzer file 45 | self.originalSubcomponents = originalSubcomponents 46 | 47 | # List of subcomponent data as it was actually received or will be 48 | # transmitted after fuzzing 49 | self.actualSubcomponents = actualSubcomponents 50 | 51 | # Convenience variable that is literally just all the originalSubcomponents combined 52 | self.originalMessage = bytearray().join(self.originalSubcomponents) 53 | 54 | # Convenience variable that is literally just all the actualSubcomponents combined 55 | self.actualMessage = bytearray().join(self.actualSubcomponents) 56 | 57 | class MessageProcessor(object): 58 | def __init__(self): 59 | self.postReceiveStore = {} 60 | 61 | # runNumber = number of current run 62 | # targetIP = address to connect to 63 | # targetPort = port being connected to 64 | # Called when the fuzzer is about to connect for runNumber 65 | def preConnect(self, runNumber, targetIP, targetPort): 66 | pass 67 | 68 | # subcomponent = subcomponent of message 69 | # Called whether or not subcomponent will be fuzzed 70 | # Will not be called if message has no subcomponents 71 | # extraParams contains MessageProcessorExtraParams based on the Message this 72 | # is a subcomponent for 73 | # Return subcomponent with any required modifications made 74 | def preFuzzSubcomponentProcess(self, subcomponent, extraParams): 75 | return subcomponent 76 | 77 | # message = full message, called whether or not message will be fuzzed 78 | # ONLY called if message has no subcomponents 79 | # If you use subcomponents, handle in preFuzzSubcomponentProcess() 80 | # extraParams contains MessageProcessorExtraParams based on this Message 81 | # Return message with any required modifications made 82 | def preFuzzProcess(self, message, extraParams): 83 | return message 84 | 85 | # subcomponent = subcomponent of message about to be sent 86 | # Will not be called if message has no subcomponents 87 | # extraParams contains MessageProcessorExtraParams based on the Message this 88 | # is a subcomponent for 89 | # Return subcomponent with any required modifications made 90 | # If subcomponent was fuzzed, this is the post-fuzzing subcomponent 91 | def preSendSubcomponentProcess(self, subcomponent, extraParams): 92 | return subcomponent 93 | 94 | # message = full message about to be sent 95 | # Any fuzzing on this message has been performed by this point 96 | # Called after preSendSubcomponentProcess() is called for every subcomponent, 97 | # if applicable 98 | # extraParams contains MessageProcessorExtraParams based on this Message 99 | # Return message with any required modifications made 100 | def preSendProcess(self, message, extraParams): 101 | return message 102 | 103 | # message = message that was actually received 104 | # extraParams contains MessageProcessorExtraParams based on this Message 105 | # Does not return anything 106 | # Can store messages for later use in the class as shown 107 | def postReceiveProcess(self, message, extraParams): 108 | self.postReceiveStore[int(extraParams.messageNumber)] = message 109 | 110 | # if message indicates fault, raise LogCrashException("reason") 111 | if extraParams.messageNumber == 3: 112 | if len(message) == 0 or (message != bytearray("OK\n") and message != bytearray("INVALID\n")): 113 | print(message) 114 | raise LogCrashException("Server response was not OK or INVALID") 115 | -------------------------------------------------------------------------------- /sample_apps/server/data/server-0.fuzzer: -------------------------------------------------------------------------------- 1 | # Directory containing any custom exception/message/monitor processors 2 | # This should be either an absolute path or relative to the .fuzzer file 3 | # If set to "default", Mutiny will use any processors in the same 4 | # folder as the .fuzzer file 5 | processor_dir default 6 | # Number of times to retry a test case causing a crash 7 | failureThreshold 3 8 | # How long to wait between retrying test cases causing a crash 9 | failureTimeout 5 10 | # How long for recv() to block when waiting on data from server 11 | receiveTimeout 1.0 12 | # Whether to perform an unfuzzed test run before fuzzing 13 | shouldPerformTestRun 1 14 | # Protocol (udp or tcp) 15 | proto tcp 16 | # Port number to connect to 17 | port 2500 18 | # Port number to connect from 19 | sourcePort -1 20 | # Source IP to connect from 21 | sourceIP 0.0.0.0 22 | 23 | # The actual messages in the conversation 24 | # Each contains a message to be sent to or from the server, printably-formatted 25 | outbound fuzz 'auth\n' 26 | inbound 'OK\n' 27 | outbound 'quit\n' 28 | inbound 'OK\n' 29 | -------------------------------------------------------------------------------- /sample_apps/server/source/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import signal 4 | import socket 5 | import sys 6 | 7 | HOST = "127.0.0.1" 8 | PORT = 2500 9 | BUFFERSIZE = 1024 10 | STATES = ("Listening", "Authenticated", "Quit") 11 | STATE_COMMANDS = ("auth", "quit") 12 | 13 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 14 | s.bind((HOST, PORT)) 15 | s.listen(1) 16 | 17 | def sigint_handler(signal, frame): 18 | # Quit on ctrl-c 19 | print("\nSIGINT received, stopping\n") 20 | s.close() 21 | sys.exit(0) 22 | signal.signal(signal.SIGINT, sigint_handler) 23 | 24 | while 1: 25 | connection, address = s.accept() 26 | print("\nNew client from %s:%d" % (address[0], address[1])) 27 | state = STATES[0] 28 | try: 29 | while state != STATES[2]: 30 | data = connection.recv(BUFFERSIZE).rstrip() 31 | if not data: 32 | break 33 | print("Received: %s" % (data)) 34 | 35 | if state == STATES[0]: 36 | if data == STATE_COMMANDS[0]: 37 | print("Transitioning from %s to %s" % (STATES[0], STATES[1])) 38 | state = STATES[1] 39 | connection.send("OK\n") 40 | continue 41 | elif state == STATES[1]: 42 | if data == STATE_COMMANDS[1]: 43 | print("Transitioning from %s to %s" % (STATES[1], STATES[2])) 44 | state = STATES[2] 45 | connection.send("OK\n") 46 | continue 47 | # Should have done something by now on a valid command 48 | print("Invalid command '%s' for state '%s'" % (data, state)) 49 | connection.send("INVALID\n") 50 | except socket.error as e: 51 | print("Socket error %s, lost client" % (str(e))) 52 | 53 | connection.close() 54 | -------------------------------------------------------------------------------- /sample_apps/session_server/data/message_processor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # Base processor for a fuzzing session 4 | # 5 | # Cisco Confidential 6 | # November 2014, created within ASIG 7 | # Author James Spadaro (jaspadar) 8 | # Contributor Lilith Wyatt (liwyatt) 9 | # 10 | # Copyright (c) 2014-2015 by Cisco Systems, Inc. 11 | # All rights reserved. 12 | # 13 | # This script is the base class for the MessageProcessor 14 | # Create a message_processor.py file into your project's subfolder 15 | # File will be imported into mutiny.py. 16 | #------------------------------------------------------------------ 17 | 18 | # Copy this file to your project's mutiny classes directory to 19 | # change message processing 20 | # This is useful to alter fuzzed messages before transmission, 21 | # such as updating outbound messages based on the server's responses 22 | 23 | import errno 24 | import socket 25 | import _thread 26 | from mutiny_classes.mutiny_exceptions import * 27 | 28 | # This class is used to provide extra parameters beyond only the message 29 | # contents to the MessageProcessor callbacks 30 | # Do not bother this here, as only the base mutiny_classes version will get 31 | # imported by design 32 | class MessageProcessorExtraParams(object): 33 | def __init__(self, messageNumber, subcomponentNumber, isFuzzed, originalSubcomponents, actualSubcomponents): 34 | # Which message number this is in the .fuzzer file list, 0-indexed 35 | self.messageNumber = messageNumber 36 | 37 | # Which subcomponent is being called within this specific callback 38 | # Is -1 if it doesn't apply (examples: preFuzzProcess/preSendProcess/postReceiveProcess) 39 | self.subcomponentNumber = subcomponentNumber 40 | 41 | # Will message / subcomponent be fuzzed? 42 | self.isFuzzed = isFuzzed 43 | 44 | # List of subcomponent data as they are recorded in the .fuzzer file 45 | self.originalSubcomponents = originalSubcomponents 46 | 47 | # List of subcomponent data as it was actually received or will be 48 | # transmitted after fuzzing 49 | self.actualSubcomponents = actualSubcomponents 50 | 51 | # Convenience variable that is literally just all the originalSubcomponents combined 52 | self.originalMessage = "".join(self.originalSubcomponents) 53 | 54 | # Convenience variable that is literally just all the actualSubcomponents combined 55 | self.actualMessage = "".join(self.actualSubcomponents) 56 | 57 | class MessageProcessor(object): 58 | def __init__(self): 59 | self.postReceiveStore = {} 60 | 61 | # runNumber = number of current run 62 | # targetIP = address to connect to 63 | # targetPort = port being connected to 64 | # Called when the fuzzer is about to connect for runNumber 65 | def preConnect(self, runNumber, targetIP, targetPort): 66 | pass 67 | 68 | # subcomponent = subcomponent of message 69 | # Called whether or not subcomponent will be fuzzed 70 | # Will not be called if message has no subcomponents 71 | # extraParams contains MessageProcessorExtraParams based on the Message this 72 | # is a subcomponent for 73 | # Return subcomponent with any required modifications made 74 | def preFuzzSubcomponentProcess(self, subcomponent, extraParams): 75 | return subcomponent 76 | 77 | # message = full message, called whether or not message will be fuzzed 78 | # ONLY called if message has no subcomponents 79 | # If you use subcomponents, handle in preFuzzSubcomponentProcess() 80 | # extraParams contains MessageProcessorExtraParams based on this Message 81 | # Return message with any required modifications made 82 | def preFuzzProcess(self, message, extraParams): 83 | if extraParams.messageNumber > 0: 84 | # Message 0 is the initial auth message, but anything after 85 | # should have its token number fixed up before sending 86 | message = message.replace(self.oldSessionNumber, self.sessionNumber) 87 | return message 88 | 89 | # subcomponent = subcomponent of message about to be sent 90 | # Will not be called if message has no subcomponents 91 | # extraParams contains MessageProcessorExtraParams based on the Message this 92 | # is a subcomponent for 93 | # Return subcomponent with any required modifications made 94 | # If subcomponent was fuzzed, this is the post-fuzzing subcomponent 95 | def preSendSubcomponentProcess(self, subcomponent, extraParams): 96 | return subcomponent 97 | 98 | # message = full message about to be sent 99 | # Any fuzzing on this message has been performed by this point 100 | # Called after preSendSubcomponentProcess() is called for every subcomponent, 101 | # if applicable 102 | # extraParams contains MessageProcessorExtraParams based on this Message 103 | # Return message with any required modifications made 104 | def preSendProcess(self, message, extraParams): 105 | return message 106 | 107 | # message = message that was actually received 108 | # extraParams contains MessageProcessorExtraParams based on this Message 109 | # Does not return anything 110 | # Can store messages for later use in the class as shown 111 | def postReceiveProcess(self, message, extraParams): 112 | self.postReceiveStore[int(extraParams.messageNumber)] = message 113 | 114 | # If message indicates fault, raise LogCrashException("reason") 115 | if extraParams.messageNumber == 3 or extraParams.messageNumber == 5: 116 | if len(message) == 0 or (message != bytearray("OK\n") and message != bytearray("INVALID\n")): 117 | print(message) 118 | raise LogCrashException("Server response was not OK or INVALID") 119 | 120 | # The server should have sent a message number, store it 121 | if extraParams.messageNumber == 1: 122 | self.sessionNumber = str(message[:-1]) 123 | # A little kludgy, the expected message contains the token 124 | # from the originally recorded session, makes for an easy 125 | # substitution in preFuzzProcess() later 126 | self.oldSessionNumber = str(extraParams.originalMessage[:-1]) 127 | -------------------------------------------------------------------------------- /sample_apps/session_server/data/session_server-3.fuzzer: -------------------------------------------------------------------------------- 1 | # Directory containing any custom exception/message/monitor processors 2 | # This should be either an absolute path or relative to the .fuzzer file 3 | # If set to "default", Mutiny will use any processors in the same 4 | # folder as the .fuzzer file 5 | processor_dir default 6 | # Number of times to retry a test case causing a crash 7 | failureThreshold 3 8 | # How long to wait between retrying test cases causing a crash 9 | failureTimeout 5 10 | # How long for recv() to block when waiting on data from server 11 | receiveTimeout 1.0 12 | # Whether to perform an unfuzzed test run before fuzzing 13 | shouldPerformTestRun 1 14 | # Protocol (udp or tcp) 15 | proto tcp 16 | # Port number to connect to 17 | port 2500 18 | # Port number to connect from 19 | sourcePort -1 20 | # Source IP to connect from 21 | sourceIP 0.0.0.0 22 | 23 | # The actual messages in the conversation 24 | # Each contains a message to be sent to or from the server, printably-formatted 25 | outbound 'auth\n' 26 | inbound '92\n' 27 | outbound 'do_stuff 92\n' 28 | inbound 'OK\n' 29 | outbound fuzz 'do_stuff 92\n' 30 | inbound 'OK\n' 31 | outbound 'quit 92\n' 32 | inbound 'OK\n' 33 | -------------------------------------------------------------------------------- /sample_apps/session_server/source/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import random 4 | import signal 5 | import socket 6 | import sys 7 | 8 | HOST = "0.0.0.0" 9 | PORT = 2500 10 | BUFFERSIZE = 1024 11 | STATES = ("Listening", "Authenticated", "Quit") 12 | STATE_COMMANDS = ("auth", "quit", "do_stuff") 13 | 14 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 | s.bind((HOST, PORT)) 16 | s.listen(1) 17 | 18 | def sigint_handler(signal, frame): 19 | # Quit on ctrl-c 20 | print("\nSIGINT received, stopping\n") 21 | s.close() 22 | sys.exit(0) 23 | signal.signal(signal.SIGINT, sigint_handler) 24 | 25 | while 1: 26 | connection, address = s.accept() 27 | print("\nNew client from %s:%d" % (address[0], address[1])) 28 | state = STATES[0] 29 | try: 30 | token = None 31 | 32 | while state != STATES[2]: 33 | data = connection.recv(BUFFERSIZE).rstrip() 34 | if not data: 35 | break 36 | print("Received: %s" % (data)) 37 | 38 | if state == STATES[0]: 39 | if data == STATE_COMMANDS[0]: 40 | print("Transitioning from %s to %s" % (STATES[0], STATES[1])) 41 | state = STATES[1] 42 | token = str(random.randint(1, 100)) 43 | connection.send("%s\n" % (token)) 44 | continue 45 | elif state == STATES[1]: 46 | if data[-len(token):] == token: 47 | if data[0:len(STATE_COMMANDS[1])] == STATE_COMMANDS[1]: 48 | print("Transitioning from %s to %s" % (STATES[1], STATES[2])) 49 | state = STATES[2] 50 | connection.send("OK\n") 51 | continue 52 | elif data[0:len(STATE_COMMANDS[2])] == STATE_COMMANDS[2]: 53 | connection.send("OK\n") 54 | continue 55 | # Should have done something by now on a valid command 56 | print("Invalid command '%s' for state '%s'" % (data, state)) 57 | connection.send("INVALID\n") 58 | except socket.error as e: 59 | print("Socket error %s, lost client" % (str(e))) 60 | 61 | connection.close() 62 | -------------------------------------------------------------------------------- /sample_apps/subcomponent_server/data/message_processor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # Base processor for a fuzzing session 4 | # 5 | # Cisco Confidential 6 | # November 2014, created within ASIG 7 | # Author James Spadaro (jaspadar) 8 | # Contributor Lilith Wyatt (liwyatt) 9 | # 10 | # Copyright (c) 2014-2015 by Cisco Systems, Inc. 11 | # All rights reserved. 12 | # 13 | # This script is the base class for the MessageProcessor 14 | # Create a message_processor.py file into your project's subfolder 15 | # File will be imported into mutiny.py. 16 | #------------------------------------------------------------------ 17 | 18 | # Copy this file to your project's mutiny classes directory to 19 | # change message processing 20 | # This is useful to alter fuzzed messages before transmission, 21 | # such as updating outbound messages based on the server's responses 22 | 23 | import errno 24 | import socket 25 | import _thread 26 | from mutiny_classes.mutiny_exceptions import * 27 | 28 | # This class is used to provide extra parameters beyond only the message 29 | # contents to the MessageProcessor callbacks 30 | # Do not bother this here, as only the base mutiny_classes version will get 31 | # imported by design 32 | class MessageProcessorExtraParams(object): 33 | def __init__(self, messageNumber, subcomponentNumber, isFuzzed, originalSubcomponents, actualSubcomponents): 34 | # Which message number this is in the .fuzzer file list, 0-indexed 35 | self.messageNumber = messageNumber 36 | 37 | # Which subcomponent is being called within this specific callback 38 | # Is -1 if it doesn't apply (examples: preFuzzProcess/preSendProcess/postReceiveProcess) 39 | self.subcomponentNumber = subcomponentNumber 40 | 41 | # Will message / subcomponent be fuzzed? 42 | self.isFuzzed = isFuzzed 43 | 44 | # List of subcomponent data as they are recorded in the .fuzzer file 45 | self.originalSubcomponents = originalSubcomponents 46 | 47 | # List of subcomponent data as it was actually received or will be 48 | # transmitted after fuzzing 49 | self.actualSubcomponents = actualSubcomponents 50 | 51 | # Convenience variable that is literally just all the originalSubcomponents combined 52 | self.originalMessage = bytearray().join(self.originalSubcomponents) 53 | 54 | # Convenience variable that is literally just all the actualSubcomponents combined 55 | self.actualMessage = bytearray().join(self.actualSubcomponents) 56 | 57 | class MessageProcessor(object): 58 | def __init__(self): 59 | self.postReceiveStore = {} 60 | 61 | # runNumber = number of current run 62 | # targetIP = address to connect to 63 | # targetPort = port being connected to 64 | # Called when the fuzzer is about to connect for runNumber 65 | def preConnect(self, runNumber, targetIP, targetPort): 66 | pass 67 | 68 | # subcomponent = subcomponent of message 69 | # Called whether or not subcomponent will be fuzzed 70 | # Will not be called if message has no subcomponents 71 | # extraParams contains MessageProcessorExtraParams based on the Message this 72 | # is a subcomponent for 73 | # Return subcomponent with any required modifications made 74 | def preFuzzSubcomponentProcess(self, subcomponent, extraParams): 75 | return subcomponent 76 | 77 | # message = full message, called whether or not message will be fuzzed 78 | # ONLY called if message has no subcomponents 79 | # If you use subcomponents, handle in preFuzzSubcomponentProcess() 80 | # extraParams contains MessageProcessorExtraParams based on this Message 81 | # Return message with any required modifications made 82 | def preFuzzProcess(self, message, extraParams): 83 | return message 84 | 85 | # subcomponent = subcomponent of message about to be sent 86 | # Will not be called if message has no subcomponents 87 | # extraParams contains MessageProcessorExtraParams based on the Message this 88 | # is a subcomponent for 89 | # Return subcomponent with any required modifications made 90 | # If subcomponent was fuzzed, this is the post-fuzzing subcomponent 91 | def preSendSubcomponentProcess(self, subcomponent, extraParams): 92 | return subcomponent 93 | 94 | # message = full message about to be sent 95 | # Any fuzzing on this message has been performed by this point 96 | # Called after preSendSubcomponentProcess() is called for every subcomponent, 97 | # if applicable 98 | # extraParams contains MessageProcessorExtraParams based on this Message 99 | # Return message with any required modifications made 100 | def preSendProcess(self, message, extraParams): 101 | return message 102 | 103 | # message = message that was actually received 104 | # extraParams contains MessageProcessorExtraParams based on this Message 105 | # Does not return anything 106 | # Can store messages for later use in the class as shown 107 | def postReceiveProcess(self, message, extraParams): 108 | self.postReceiveStore[int(extraParams.messageNumber)] = message 109 | if extraParams.messageNumber == 3: 110 | print(("Server's echo response: {0}".format(message))) 111 | 112 | -------------------------------------------------------------------------------- /sample_apps/subcomponent_server/data/subcomponent-0.fuzzer: -------------------------------------------------------------------------------- 1 | # Directory containing any custom exception/message/monitor processors 2 | # This should be either an absolute path or relative to the .fuzzer file 3 | # If set to "default", Mutiny will use any processors in the same 4 | # folder as the .fuzzer file 5 | processor_dir default 6 | # Number of times to retry a test case causing a crash 7 | failureThreshold 3 8 | # How long to wait between retrying test cases causing a crash 9 | failureTimeout 5 10 | # How long for recv() to block when waiting on data from server 11 | receiveTimeout 1.0 12 | # Whether to perform an unfuzzed test run before fuzzing 13 | shouldPerformTestRun 1 14 | # Protocol (udp or tcp) 15 | proto tcp 16 | # Port number to connect to 17 | port 2500 18 | # Port number to connect from 19 | sourcePort -1 20 | # Source IP to connect from 21 | sourceIP 0.0.0.0 22 | 23 | # The actual messages in the conversation 24 | # Each contains a message to be sent to or from the server, printably-formatted 25 | outbound 'auth\n' 26 | inbound 'OK\n' 27 | # These show how subcomponents and split-line messages can be used 28 | outbound 'echo server will see this' 29 | ' as one message' 30 | ' and it will not be fuzzed. ' 31 | sub fuzz 'this will be included as well' 32 | ' but it will be fuzzed. ' 33 | sub 'but not the ending\n' 34 | inbound 'server will see this as one message and it will not be fuzzed. this will be included as well but it will be fuzzed. but not the ending\n' 35 | outbound 'quit\n' 36 | inbound 'OK\n' 37 | -------------------------------------------------------------------------------- /sample_apps/subcomponent_server/source/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import signal 4 | import socket 5 | import sys 6 | 7 | HOST = "127.0.0.1" 8 | PORT = 2500 9 | BUFFERSIZE = 1024 10 | STATES = ("Listening", "Authenticated", "Quit") 11 | STATE_COMMANDS = ("auth", "echo", "quit") 12 | 13 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 14 | s.bind((HOST, PORT)) 15 | s.listen(1) 16 | 17 | def sigint_handler(signal, frame): 18 | # Quit on ctrl-c 19 | print("\nSIGINT received, stopping\n") 20 | s.close() 21 | sys.exit(0) 22 | signal.signal(signal.SIGINT, sigint_handler) 23 | 24 | while 1: 25 | connection, address = s.accept() 26 | print("\nNew client from %s:%d" % (address[0], address[1])) 27 | state = STATES[0] 28 | try: 29 | while state != STATES[2]: 30 | data = connection.recv(BUFFERSIZE).rstrip() 31 | if not data: 32 | break 33 | print("Received: %s" % (data)) 34 | 35 | if state == STATES[0]: 36 | if data == STATE_COMMANDS[0]: 37 | print("Transitioning from %s to %s" % (STATES[0], STATES[1])) 38 | state = STATES[1] 39 | connection.send("OK\n") 40 | continue 41 | elif state == STATES[1]: 42 | if data[:4] == STATE_COMMANDS[1]: 43 | echoData = data[5:] 44 | print("Echoing %s back to user" % (echoData)) 45 | connection.send("%s\n" % (echoData)) 46 | elif data == STATE_COMMANDS[2]: 47 | print("Transitioning from %s to %s" % (STATES[1], STATES[2])) 48 | state = STATES[2] 49 | connection.send("OK\n") 50 | continue 51 | # Should have done something by now on a valid command 52 | print("Invalid command '%s' for state '%s'" % (data, state)) 53 | connection.send("INVALID\n") 54 | except socket.error as e: 55 | print("Socket error %s, lost client" % (str(e))) 56 | 57 | connection.close() 58 | -------------------------------------------------------------------------------- /sample_apps/vnc/data/vnc-1.fuzzer: -------------------------------------------------------------------------------- 1 | # Directory containing any custom exception/message/monitor processors 2 | # This should be either an absolute path or relative to the .fuzzer file 3 | # If set to "default", Mutiny will use any processors in the same 4 | # folder as the .fuzzer file 5 | processor_dir default 6 | # Number of times to retry a test case causing a crash 7 | failureThreshold 3 8 | # How long to wait between retrying test cases causing a crash 9 | failureTimeout 5 10 | # How long for recv() to block when waiting on data from server 11 | receiveTimeout 1.0 12 | # Whether to perform an unfuzzed test run before fuzzing 13 | shouldPerformTestRun 1 14 | # Protocol (udp or tcp) 15 | proto tcp 16 | # Port number to connect to 17 | port 5900 18 | # Port number to connect from 19 | sourcePort -1 20 | # Source IP to connect from 21 | sourceIP 0.0.0.0 22 | 23 | # The actual messages in the conversation 24 | # Each contains a message to be sent to or from the server, printably-formatted 25 | inbound 'RFB 003.008\n' 26 | outbound fuzz 'RFB 003.008\n' 27 | inbound '\x02\x02\x10' 28 | outbound '\x02' 29 | inbound '\xaa\xc3\xe3\x95\xd3|\xd7\xf9\xfd\x84\xe7\xf5R\x94\x93\x1c' 30 | outbound '2\xd1\xa0I\x93q\x03\x11e$\x83\x94\xc6t\x8e\x08' 31 | inbound '\x00\x00\x00\x00' 32 | -------------------------------------------------------------------------------- /sample_apps/vnc/data/vnc.c_arrays: -------------------------------------------------------------------------------- 1 | char peer1_0[] = { 2 | 3 | 0x52, 0x46, 0x42, 0x20, 0x30, 0x30, 0x33, 0x2e, 4 | 5 | 0x30, 0x30, 0x38, 0x0a }; 6 | 7 | char peer0_0[] = { 8 | 9 | 0x52, 0x46, 0x42, 0x20, 0x30, 0x30, 0x33, 0x2e, 10 | 11 | 0x30, 0x30, 0x38, 0x0a }; 12 | 13 | char peer1_1[] = { 14 | 15 | 0x02, 0x02, 0x10 }; 16 | 17 | char peer0_1[] = { 18 | 19 | 0x02 }; 20 | 21 | char peer1_2[] = { 22 | 23 | 0xaa, 0xc3, 0xe3, 0x95, 0xd3, 0x7c, 0xd7, 0xf9, 24 | 25 | 0xfd, 0x84, 0xe7, 0xf5, 0x52, 0x94, 0x93, 0x1c }; 26 | 27 | char peer0_2[] = { 28 | 29 | 0x32, 0xd1, 0xa0, 0x49, 0x93, 0x71, 0x03, 0x11, 30 | 31 | 0x65, 0x24, 0x83, 0x94, 0xc6, 0x74, 0x8e, 0x08 }; 32 | 33 | char peer1_3[] = { 34 | 35 | 0x00, 0x00, 0x00, 0x00 }; 36 | 37 | -------------------------------------------------------------------------------- /tests/mutator/mutator_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # Test the mutator for duplication, verify it consistently produces different results 4 | # 5 | # Cisco Confidential 6 | # October 2016, created within ASIG 7 | # Author James Spadaro (jaspadar) 8 | # 9 | # Copyright (c) 2014-2016 by Cisco Systems, Inc. 10 | # All rights reserved. 11 | # 12 | #------------------------------------------------------------------ 13 | 14 | import signal 15 | # Use sqlite to store generated strings 16 | import sqlite3 17 | import subprocess 18 | import sys 19 | import os 20 | 21 | # How many iterations to run 22 | ITERATIONS = 1000000 23 | 24 | # Sample seed string for fuzzing 25 | START_STRING = "GET /test1234 HTTP/1.1\r\nFrom: joebob@test.com\r\nUser-Agent: Mozilla/1.2\r\n\r\n" 26 | 27 | # Some other defines taken from mutiny.py 28 | RADAMSA=os.path.abspath( os.path.join(__file__, "../../../radamsa-0.3/bin/radamsa") ) 29 | 30 | # Flag to tell main execution to wrap up and exit 31 | exit_flag = False 32 | 33 | # Database is global for ctrl-c to write database before exit 34 | def ctrl_c_handler(signal, frame): 35 | # Ensure we use the global exit_flag 36 | global exit_flag 37 | 38 | print("Ctrl-C received, setting exit flag...") 39 | exit_flag = True 40 | 41 | signal.signal(signal.SIGINT, ctrl_c_handler) 42 | 43 | def runFuzzer(fuzzer_input, seed): 44 | radamsa = subprocess.Popen([RADAMSA, "--seed", str(seed)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 45 | (fuzzer_output, error_output) = radamsa.communicate(fuzzer_input) 46 | if error_output: 47 | print(("Seed {0} Error: {1}", seed, str(error_output))) 48 | return fuzzer_output 49 | 50 | def main(): 51 | # Create database in-memory, don't retain it 52 | #database = sqlite3.connect(":memory:") 53 | # Create database on disk for later analysis 54 | database = sqlite3.connect("./results.db", isolation_level="IMMEDIATE") 55 | cursor = database.cursor() 56 | cursor.execute("""CREATE TABLE fuzzer_outputs (output blob, count int)""") 57 | cursor.execute("""CREATE TABLE seed_tracking (fuzzer_output_index int, seed int)""") 58 | database.commit() 59 | 60 | # How many outputs were totally unique (doesn't include any that got duped) 61 | originalCount = 0 62 | # How many strings were duped (as in if AAA appears 3 times, it will be 1 here) 63 | uniqueDupCount = 0 64 | # How many total duped outputs (as in if AAA appears 3 times, it will be 3 here) 65 | dupCount = 0 66 | 67 | # Ensure we use the global exit_flag 68 | global exit_flag 69 | 70 | for i in range(0, ITERATIONS): 71 | if exit_flag: 72 | print(("Exit flag set, exiting. Counts at exit were {0} dup {1} original {2} uniqueDup".format(dupCount, originalCount, uniqueDupCount))) 73 | break 74 | 75 | # Avoid issues with non-printable characters, etc by casting as buffer 76 | fuzzedString = buffer(runFuzzer(START_STRING, i)) 77 | 78 | # Look to see if output has already appeared 79 | cursor.execute("""select count from fuzzer_outputs where output=?;""", (fuzzedString,)) 80 | result = cursor.fetchone() 81 | if not result: 82 | current_count = 1 83 | else: 84 | current_count = result[0] 85 | current_count += 1 86 | 87 | # Print output if duplicated 88 | if current_count > 1: 89 | cursor.execute("""update fuzzer_outputs set count=? where output=? and count=?;""", (current_count, fuzzedString, current_count-1)) 90 | if cursor.rowcount == 0: 91 | import pdb 92 | pdb.set_trace() 93 | if current_count == 2: 94 | # First time we see a dup, count it 95 | uniqueDupCount += 1 96 | # Decrement originalCount, because there was a unique here that isn't 97 | originalCount -= 1 98 | # Bump dupCount by two, as we've seen the string twice in this case 99 | dupCount += 2 100 | else: 101 | # Otherwise, just bump dupCount so we can track that this repeated 102 | dupCount += 1 103 | 104 | # Can't use lastrowid for this, because that's only populated on insert 105 | cursor.execute("""select rowid from fuzzer_outputs where output=?;""", (fuzzedString,)) 106 | cursor.execute("""insert into seed_tracking values (?, ?);""", (cursor.fetchone()[0], i)) 107 | else: 108 | cursor.execute("""insert into fuzzer_outputs values (?, ?);""", (fuzzedString, current_count)) 109 | cursor.execute("""insert into seed_tracking values (?, ?);""", (cursor.lastrowid, i)) 110 | originalCount += 1 111 | database.commit() 112 | 113 | if i % 1000 == 0: 114 | print(("Iteration {0}: {1} dups {2} originals {3} uniqueDups so far".format(i, dupCount, originalCount, uniqueDupCount))) 115 | 116 | if not exit_flag: 117 | print(("Run of {0} iterations complete, dumping into debugger for analysis.", ITERATIONS)) 118 | print(("Counts at exit were {0} dup {1} original {2} uniqueDup".format(dupCount, originalCount, uniqueDupCount))) 119 | import pdb 120 | pdb.set_trace() 121 | else: 122 | print("Run exited due to Ctrl-C. Closing database.") 123 | database.close() 124 | 125 | if __name__ == "__main__": 126 | main() 127 | -------------------------------------------------------------------------------- /tests/serialization/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # 4 | # Cisco Confidential 5 | # November 2014, created within ASIG 6 | # Author James Spadaro (jaspadar) 7 | # Codeveloper Lilith Wyatt (liwyatt) 8 | # 9 | # Copyright (c) 2014-2015 by Cisco Systems, Inc. 10 | # All rights reserved. 11 | # 12 | #------------------------------------------------------------------ 13 | -------------------------------------------------------------------------------- /tests/serialization/serialization_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # Test serialization to ensure every char is serialized/deserialized properly 4 | # 5 | # Cisco Confidential 6 | # October 2016, created within ASIG 7 | # Author James Spadaro (jaspadar) 8 | # 9 | # Copyright (c) 2014-2016 by Cisco Systems, Inc. 10 | # All rights reserved. 11 | # 12 | #------------------------------------------------------------------ 13 | 14 | import sys 15 | sys.path.append("../..") 16 | from backend.fuzzer_types import Message 17 | 18 | class Color: 19 | PURPLE = '\033[95m' 20 | CYAN = '\033[96m' 21 | DARKCYAN = '\033[36m' 22 | BLUE = '\033[94m' 23 | GREEN = '\033[92m' 24 | YELLOW = '\033[93m' 25 | RED = '\033[91m' 26 | BOLD = '\033[1m' 27 | UNDERLINE = '\033[4m' 28 | END = '\033[0m' 29 | 30 | def printResult(message, isPass): 31 | if isPass: 32 | resultStr = "Pass" 33 | resultColor = Color.GREEN 34 | else: 35 | resultStr = "Fail" 36 | resultColor = Color.RED 37 | 38 | print(("\n{}: {}{}{}\n".format(message, resultColor, resultStr, Color.END))) 39 | 40 | 41 | def testString(inputValue): 42 | # Test the serialization function itself 43 | # String needs to be bytearray 44 | inputValue = bytearray(inputValue.encode('utf-8')) 45 | try: 46 | print(("\n{}Testing direct serialization and deserialization...{}".format(Color.BOLD, Color.END))) 47 | serialized = Message.serializeByteArray(inputValue) 48 | deserialized = Message.deserializeByteArray(serialized) 49 | print(("\tSerialized: {0}".format(serialized))) 50 | print(("\tBefore: {0}".format(inputValue.decode('utf-8')))) 51 | print(("\t After: {0}".format(deserialized.decode('utf-8')))) 52 | except Exception as e: 53 | print(("Caught exception running test: {}".format(str(e)))) 54 | deserialized = "" 55 | printResult("Direct Serialization Test", inputValue == deserialized) 56 | 57 | # Also go a step further and test the inbound/outbound etc parsing 58 | try: 59 | print(("\n{}Testing full serialization with inbound/outbound lines...{}".format(Color.BOLD, Color.END))) 60 | message = Message() 61 | message.direction = Message.Direction.Outbound 62 | message.setMessageFrom(Message.Format.Raw, bytearray(inputValue), False) 63 | serialized = message.getSerialized() 64 | message.setFromSerialized(serialized) 65 | deserialized = message.getOriginalMessage() 66 | print(("\tSerialized: {0}".format(serialized))) 67 | print(("\tBefore: {0}".format(inputValue.decode('utf-8')))) 68 | print(("\t After: {0}".format(deserialized.decode('utf-8')))) 69 | except Exception as e: 70 | print(("Caught exception running test: {}".format(str(e)))) 71 | deserialized = "" 72 | printResult("Full Serialization Test", inputValue == deserialized) 73 | 74 | def main(): 75 | # Try all possible ASCII characters 76 | allchars = 'datadatadata unprintable chars:' 77 | for i in range (0, 256): 78 | allchars += chr(i) 79 | testString(allchars) 80 | 81 | # Added as a result of issue #2 in git 82 | # Strings that contain only single quotes apparently get wrapped in double quotes 83 | testString("test'") 84 | 85 | # Found to be causing problems 86 | testString("") 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /util/bsd_denull.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from scapy.all import * 4 | import sys 5 | 6 | 7 | dstMac = "\xff\xff\xff\xff\xff\xff" 8 | srcMac = "\xaa\xaa\xaa\xaa\xaa\xaa" 9 | L3type = "\x08\x00" 10 | headerlen = "\x45" 11 | 12 | def usage(): 13 | print("Usage: ./%s " % sys.argv[0]) 14 | exit() 15 | 16 | def main(): 17 | nulls = sys.argv[1] 18 | denulled = PacketList() 19 | 20 | print("Denulling pcap: %s" % nulls) 21 | try: 22 | nulled = rdpcap(nulls) 23 | except: 24 | usage() 25 | 26 | for packet in nulled: 27 | denulled.append(Ether(srcMac + dstMac + L3type)/TCP(str(packet)[4:])) 28 | print(denulled) 29 | 30 | wrpcap("denulled_%s" % sys.argv[1], denulled) 31 | 32 | 33 | if __name__ == "__main__": 34 | main() 35 | -------------------------------------------------------------------------------- /util/fuzzer_converter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------ 3 | # Convert .fuzzer messages to binary/ascii and vice-versa 4 | # 5 | # Cisco Confidential 6 | # November 2014, created within ASIG 7 | # Author James Spadaro (jaspadar) 8 | # 9 | # Copyright (c) 2014-2015 by Cisco Systems, Inc. 10 | # All rights reserved. 11 | # 12 | #------------------------------------------------------------------ 13 | 14 | import argparse 15 | import os.path 16 | import sys 17 | import re 18 | 19 | # Kind of dirty, grab libs from one directory up 20 | sys.path.insert(0, os.path.abspath( os.path.join(__file__, "../.."))) 21 | from backend.fuzzerdata import FuzzerData 22 | from backend.fuzzer_types import Message 23 | 24 | epilog = """Actions: 25 | fuzzer2bin - Pull binary message out of .fuzzer file 26 | bin2fuzzer - Update message in .fuzzer file with raw binary data 27 | list - List all messages in a .fuzzer 28 | """ 29 | parser = argparse.ArgumentParser(description="Script to convert and view .fuzzer data", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=epilog) 30 | parser.add_argument("action", help="Action to use, see below", choices=["fuzzer2bin", "bin2fuzzer", "list"]) 31 | parser.add_argument("-i", "--infile", help="File to read input from, uses stdin otherwise") 32 | parser.add_argument("-o", "--outfile", help="File to write results to, uses stdout otherwise") 33 | parser.add_argument("-f", "--fuzzerfile", help="File to get .fuzzer data from for bin2fuzzer, if it should differ from outfile or outfile is stdout") 34 | parser.add_argument("-m", "--messagenum", help="Message number to read/write (fuzzer2bin and bin2fuzzer)", type=int) 35 | args = parser.parse_args() 36 | 37 | if args.action != "bin2fuzzer" and args.fuzzerfile: 38 | print("Use --fuzzerfile with only the bin2fuzzer option, to populate .fuzzer data") 39 | exit(1) 40 | 41 | # Default file descriptors 42 | inFileDesc = sys.stdin 43 | outFileDesc = sys.stdout 44 | 45 | # If we get file paths instead, fix them up 46 | # Have to do outfile lower down or we'll blow away an output file we might be fixing up 47 | if args.infile: 48 | inFileDesc = open(args.infile, "r") 49 | 50 | if args.action == "list": 51 | fuzzerData = FuzzerData() 52 | # Allow a non-quiet read to list out messages 53 | fuzzerData.readFromFD(inFileDesc, quiet=False) 54 | 55 | elif args.action in ["fuzzer2bin", "bin2fuzzer"]: 56 | if args.messagenum == None: 57 | print(("Message number required for action {0}".format(args.action))) 58 | exit(1) 59 | 60 | fuzzerData = FuzzerData() 61 | 62 | if args.action == "fuzzer2bin": 63 | # Pull message out from .fuzzer file, output as binary 64 | fuzzerData.readFromFD(inFileDesc, quiet=True) 65 | 66 | messageCount = len(fuzzerData.messageCollection.messages) 67 | if args.messagenum < 0 or args.messagenum >= messageCount: 68 | print(("Message number out of range: {0}".format(args.messagenum))) 69 | exit(1) 70 | 71 | if args.outfile: 72 | outFileDesc = open(args.outfile, "w") 73 | outFileDesc.write(fuzzerData.messageCollection.messages[args.messagenum].getOriginalMessage()) 74 | elif args.action == "bin2fuzzer": 75 | if not args.outfile and not args.fuzzerfile: 76 | print(("outfile or fuzzerfile required for action {0}".format(args.action))) 77 | 78 | if args.fuzzerfile: 79 | fuzzerData.readFromFile(args.fuzzerfile, quiet=True) 80 | else: 81 | try: 82 | # readFromFile() since outFileDesc is opened for write 83 | fuzzerData.readFromFile(args.outfile, quiet=True) 84 | except Exception as ex: 85 | print(("Ignoring bad outfile, writing default .fuzzer data, error: {0}".format(str(ex)))) 86 | pass 87 | 88 | messageData = bytearray() 89 | for line in inFileDesc: 90 | messageData += line 91 | 92 | messageCount = len(fuzzerData.messageCollection.messages) 93 | if args.messagenum < 0 or args.messagenum >= messageCount: 94 | print(("Message number out of range: {0}".format(args.messagenum))) 95 | exit(1) 96 | message = fuzzerData.messageCollection.messages[args.messagenum] 97 | message.setMessageFrom(Message.Format.Raw, messageData, message.isFuzzed) 98 | if args.outfile: 99 | outFileDesc = open(args.outfile, "w") 100 | fuzzerData.writeToFD(outFileDesc) 101 | 102 | # Clean up file descriptors 103 | if args.infile: 104 | inFileDesc.close() 105 | if args.outfile: 106 | outFileDesc.close() 107 | -------------------------------------------------------------------------------- /util/pcap_dump.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | from scapy.all import * 4 | 5 | def main(): 6 | 7 | if len(sys.argv) < 2: 8 | sys.argv.append('-h') 9 | 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument("pcap", help="pcap to dump") 12 | parser.add_argument("-f", "--filename", help="file to write to") 13 | args = parser.parse_args() 14 | 15 | pcap = rdpcap(args.pcap) 16 | 17 | try: 18 | srcPort = pcap[0][TCP].sport 19 | except: 20 | srcPort = pcap[0][UDP].sport 21 | 22 | src = ( pcap[0][Ether].src, pcap[0][IP].src, srcPort) 23 | 24 | retbuff = [] 25 | for packet in pcap: 26 | # skip packets without data (syn/ack/synack) 27 | try: 28 | len(packet[Raw]) 29 | except IndexError: 30 | continue 31 | 32 | tmp = "" 33 | if isSrc(src,packet): 34 | try: 35 | for byte in str(packet[Raw]): 36 | tmp+="\\x0" if ord(byte) <= 0xf else "\\x" 37 | tmp+=hex(ord(byte))[2:] 38 | except IndexError: 39 | pass 40 | if tmp: 41 | retbuff.append("send(\"" + tmp + "\")") 42 | 43 | #recv(1024) data sent by server, don't really care what it is 44 | else: 45 | retbuff.append("recv(1024)") if len(packet) < 1024 else retbuff.append("recv(%d)" % len(packet)) 46 | 47 | if args.filename: 48 | with open(args.filename,'w') as f: 49 | for packet in retbuff: 50 | f.write(packet + "\n") 51 | else: 52 | for packet in retbuff: 53 | print(packet) 54 | 55 | def isSrc(srcInfo,packet): 56 | # info_tuple[0] = [Ether].src 57 | # info_tuple[1] = [IP].src 58 | # info_tuple[2] = [TCP/UDP].sport 59 | try: 60 | l4port = packet[TCP].sport 61 | except: 62 | l4port = packet[TCP].sport 63 | 64 | try: 65 | if packet[Ether].src == srcInfo[0] and packet[IP].src == srcInfo[1] and l4port == srcInfo[2]: 66 | return 1 67 | except: 68 | pass 69 | 70 | return 0 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | --------------------------------------------------------------------------------