├── .gitignore ├── BigStringVoodoo.py ├── Executor.py ├── LICENSE.txt ├── Mutator.py ├── README ├── client.py ├── commands.py ├── fuzzer.py ├── mutations.py └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | *~ 4 | files/ 5 | saved/ 6 | -------------------------------------------------------------------------------- /BigStringVoodoo.py: -------------------------------------------------------------------------------- 1 | ''' the twisted guys are crazy, here is some of their voodoo to send a whole file at once ;) ''' 2 | from twisted.protocols import amp 3 | import itertools 4 | 5 | def split_string(x, size): 6 | return list(x[i*size:(i+1)*size] for i in xrange((len(x)+size-1)//size)) 7 | 8 | class StringList(amp.Argument): 9 | def fromBox(self, name, strings, objects, proto): 10 | objects[name] = list(itertools.takewhile(bool, (strings.pop('%s.%d' % (name, i), None) for i in itertools.count()))) 11 | def toBox(self, name, strings, objects, proto): 12 | for i, elem in enumerate(objects.pop(name)): 13 | strings['%s.%d' % (name, i)] = elem 14 | 15 | class BigString(StringList): 16 | def fromBox(self, name, strings, objects, proto): 17 | StringList.fromBox(self, name, strings, objects, proto) 18 | objects[name] = ''.join((elem) for elem in objects[name]) 19 | def toBox(self, name, strings, objects, proto): 20 | objects[name] = split_string(objects[name], amp.MAX_VALUE_LENGTH) 21 | StringList.toBox(self, name, strings, objects, proto) 22 | -------------------------------------------------------------------------------- /Executor.py: -------------------------------------------------------------------------------- 1 | from pydbg import * 2 | from pydbg.defines import * 3 | from time import time, sleep 4 | import utils 5 | 6 | class Executor(): 7 | def __init__(self, timeout=5): 8 | self.timeout = timeout 9 | self.output = None 10 | 11 | def execute(self, command, args): 12 | self.output = None 13 | dbg = pydbg() 14 | dbg.set_callback(EXCEPTION_ACCESS_VIOLATION, self.handle_av) 15 | dbg.set_callback(0xC000001D, self.handle_av) # illegal instruction 16 | dbg.set_callback(USER_CALLBACK_DEBUG_EVENT, self.timeout_callback) 17 | dbg.load(command, command_line=args) 18 | dbg.start_time = time() 19 | dbg.run() 20 | 21 | return self.output 22 | 23 | def timeout_callback(self, dbg): 24 | if time() - dbg.start_time > self.timeout: 25 | dbg.terminate_process() 26 | return DBG_CONTINUE 27 | 28 | def handle_av(self, dbg): 29 | crash_bin = utils.crash_binning.crash_binning() 30 | crash_bin.record_crash(dbg) 31 | self.output = crash_bin.crash_synopsis() 32 | 33 | dbg.terminate_process() 34 | return DBG_EXCEPTION_NOT_HANDLED 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Mitchell Adair 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . 15 | 16 | -------------------------------------------------------------------------------- /Mutator.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from sys import exit 3 | from os.path import splitext, join 4 | 5 | import mutations 6 | 7 | class Mutator(): 8 | 9 | def __init__(self, original_file, mutation_types, original_file_name, directory): 10 | self.original_file = original_file # original contents of file 11 | self.mutation_types = mutation_types # list of possible mutations 12 | self.directory = directory 13 | self.original_file_name = original_file_name 14 | self.original_file_base = splitext(self.original_file_name) [0] 15 | self.original_file_ext = splitext(self.original_file_name) [1] 16 | 17 | def createMutatedFileName(self, offset, mutation_index): 18 | ''' create a file name that represents the current mutation, return it ''' 19 | fname = '%s-%d-%d%s' % (self.original_file_base, offset, mutation_index, self.original_file_ext) 20 | return fname 21 | 22 | def createMutatedFile(self, offset, mutation_index): 23 | ''' mutate the contents of the original file, at offset, with mutation at 24 | mutation_index, creating a new file. return the new file name ''' 25 | 26 | new_bytes = list(self.original_file[:]) 27 | mutation = self.mutation_types[mutation_index] 28 | 29 | if mutation['type'] == 'replace': 30 | new_bytes[offset:offset+mutation['size']] = mutation['value'] # if 'replace', then just substitute/replace the desired bytes 31 | elif mutation['type'] == 'insert': 32 | new_bytes = new_bytes[:offset] + list(mutation['value']) + new_bytes[offset:] # if 'insert', stick them in, shifting the rest of the bytes down 33 | else: 34 | raise Exception('[*] UNKNOWN mutation[\'type\'], %s' % mutation['type']) 35 | 36 | # create the new file name, then it's full path 37 | mutated_file_name = self.createMutatedFileName(offset, mutation_index) 38 | mutated_file_name = join(self.directory, mutated_file_name) 39 | 40 | # write the file 41 | try: 42 | with open(mutated_file_name, 'wb') as fopen: 43 | fopen.write( ''.join(new_bytes) ) 44 | except Exception as e: 45 | raise Exception('[*] unable to open tmp file for mutation! Error : %s' % e) 46 | 47 | return mutated_file_name 48 | 49 | class MutationGenerator(): 50 | 51 | def __init__(self, value_type, strings=False): 52 | self.generateValues(value_type, strings) 53 | 54 | def generateValues(self, value_type, strings): 55 | values = [] 56 | if value_type == 'byte': 57 | values.extend(mutations.values_8bit) 58 | elif value_type == 'word': 59 | values.extend(mutations.values_16bit) 60 | elif value_type == 'dword': 61 | values.extend(mutations.values_32bit) 62 | else: 63 | raise Exception('unknown value type passed to generateValues(...), %s' % value_type) 64 | 65 | if strings: 66 | values.extend(mutations.values_strings) 67 | 68 | # turn them into writeable bytes 69 | self.value_to_bytes(values, vtype=value_type) 70 | self.createWriteable(values) 71 | self.createStrings(values) 72 | self.values = values 73 | 74 | def createStrings(self, values): 75 | for value_dict in values: 76 | value_dict['value'] = ''.join(value_dict['value']) 77 | 78 | def value_to_bytes(self, values, vtype='dword'): 79 | '''Given a value as an int, return it in little endian bytes of length type. 80 | Example, given 0xabcdeff with type "dword" : (255, 222, 188, 10) ''' 81 | for value_dict in values: 82 | if value_dict['type'] == 'insert': 83 | continue 84 | value = value_dict['value'] 85 | try: 86 | if vtype == 'byte': 87 | value_dict['value'] = list(struct.unpack('B', struct.pack('B', value))) 88 | elif vtype == 'word': 89 | value_dict['value'] = list(struct.unpack('BB', struct.pack(' AAAABBBBAA 13 | # - insert: insert the bytes at a specific offset with the new bytes, shifting the rest of the bytes down 14 | # - aka: AAAAAAAAAA -> AAAABBBBAAAAAA 15 | values_8bit = [{'value':0x00, 'type':'replace', 'size':1}, {'value':0x01, 'type':'replace', 'size':1}, {'value':MAX8/2-16, 'type':'replace', 'size':1}, 16 | {'value':MAX8/2-1, 'type':'replace', 'size':1}, {'value':MAX8/2, 'type':'replace', 'size':1}, {'value':MAX8/2+1, 'type':'replace', 'size':1}, 17 | {'value':MAX8/2+16, 'type':'replace', 'size':1}, {'value':MAX8-1, 'type':'replace', 'size':1}, {'value':MAX8, 'type':'replace', 'size':1} ] 18 | 19 | values_16bit = [{'value':0x00, 'type':'replace', 'size':2}, {'value':0x01, 'type':'replace', 'size':2}, {'value':MAX16/2-16, 'type':'replace', 'size':2}, 20 | {'value':MAX16/2-1, 'type':'replace', 'size':2}, {'value':MAX16/2, 'type':'replace', 'size':2}, {'value':MAX16/2+1, 'type':'replace', 'size':2}, 21 | {'value':MAX16/2+16, 'type':'replace', 'size':2}, {'value':MAX16-1, 'type':'replace', 'size':2}, {'value':MAX16, 'type':'replace', 'size':2} ] 22 | 23 | values_32bit = [{'value':0x00, 'type':'replace', 'size':4}, {'value':0x01, 'type':'replace', 'size':4}, {'value':MAX32/2-16, 'type':'replace', 'size':4}, 24 | {'value':MAX32/2-1, 'type':'replace', 'size':4}, {'value':MAX32/2, 'type':'replace', 'size':4}, {'value':MAX32/2+1, 'type':'replace', 'size':4}, 25 | {'value':MAX32/2+16, 'type':'replace', 'size':4}, {'value':MAX32-1, 'type':'replace', 'size':4}, {'value':MAX32, 'type':'replace', 'size':4} ] 26 | 27 | values_strings = [{'value':list("B"*100), 'type':'insert', 'size':100}, \ 28 | {'value':list("B"*1000), 'type':'insert', 'size':1000}, \ 29 | {'value':list("B"*10000),'type':'insert', 'size':10000}, \ 30 | {'value':list("%s"*10), 'type':'insert', 'size':10}, \ 31 | {'value':list("%s"*100), 'type':'insert', 'size':100}] 32 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | # for twisted 2 | from twisted.internet import protocol, reactor 3 | from twisted.internet.protocol import ServerFactory 4 | from twisted.protocols import amp 5 | from time import time 6 | import commands 7 | 8 | # for mutations 9 | from Mutator import MutationGenerator, Mutator 10 | 11 | from optparse import OptionParser 12 | from os.path import split 13 | from sys import argv, exit 14 | from threading import Thread 15 | from time import sleep, ctime 16 | 17 | class FuzzerServerProtocol(amp.AMP): 18 | 19 | @commands.GetNextMutation.responder 20 | def getNextMutation(self): 21 | ret = self.factory.getNextMutation() 22 | return ret 23 | 24 | @commands.LogResults.responder 25 | def logResults(self, mutation_index, offset, output, filename): 26 | print 'Got a crash!' 27 | # log the crash 28 | self.factory.log_file.write('Offset: %d, Mutation_Index: %d, Filename: %s, Output:\n%s'% 29 | (offset, mutation_index, filename, output)) 30 | self.factory.log_file.flush() 31 | # add it to the servers list 32 | self.factory.crashes.append({'mutation_index':mutation_index, 'offset':offset, 'output':output, 'filename':filename}) 33 | # create the file locally 34 | print 'Created file :', self.factory.mutator.createMutatedFile(offset, mutation_index) 35 | return {} 36 | 37 | @commands.GetOriginalFile.responder 38 | def getOriginalFile(self): 39 | return {'original_file_name':self.factory.file_name, 'original_file':self.factory.contents} 40 | 41 | @commands.GetMutationTypes.responder 42 | def getMutationTypes(self): 43 | return {'mutation_types':self.factory.mutations} 44 | 45 | @commands.GetProgram.responder 46 | def getProgram(self): 47 | return {'program':self.factory.program} 48 | 49 | def connectionMade(self): 50 | ''' add new clients to the list ''' 51 | self.factory.clients.append(self.transport.getPeer()) 52 | 53 | def connectionLost(self, traceback): 54 | ''' remove clients from the list ''' 55 | self.factory.clients.remove(self.transport.getPeer()) 56 | 57 | class FuzzerFactory(ServerFactory): 58 | protocol = FuzzerServerProtocol 59 | 60 | def __init__(self, program, original_file, log_file_name, mutation_type, directory): 61 | print 'FuzzerFactory(...) started' 62 | self.mutation_generator = MutationGenerator(mutation_type) # create the list of mutations 63 | self.mutator = None # to create the files that reportedly cause crashes 64 | self.mutations = self.mutation_generator.getValues() 65 | self.mutations_range = range(len(self.mutations)) 66 | self.file_name = split(original_file)[1] # just the filename 67 | self.program = program 68 | self.log_file_name = log_file_name # just the name 69 | self.directory = directory # directory to save crash causing files 70 | self.log_file = None # the opened instance 71 | self.contents = None 72 | self.contents_range = None 73 | self.generator = self.createGenerator() 74 | self.clients = [] # list of clients 75 | self.crashes = [] # list of crashes 76 | self.mutations_executed = 0 # number of mutations executed so far 77 | self.paused = False 78 | 79 | # make sure we can read the original target file 80 | try: 81 | self.contents = open(original_file, 'rb').read() 82 | self.contents_range = range(len(self.contents)) 83 | except Exception as e: 84 | quit('Unable to open "%s" and read the contents. Error: %s' % (original_file, e)) 85 | 86 | # make sure we can write to the logfile 87 | try: 88 | self.log_file = open(self.log_file_name, 'w') 89 | except Exception as e: 90 | quit('Unable to open logfile "%s". Error: %s' % (self.log_file_name, self.log_file)) 91 | 92 | # we have all the pieces for the mutator now 93 | self.mutator = Mutator(self.contents, self.mutations, self.file_name, self.directory) 94 | 95 | # a thread to handle user input and print statistics 96 | menu_thread = Thread(target=self.menu) 97 | menu_thread.start() 98 | 99 | def createGenerator(self): 100 | for offset in self.contents_range: 101 | for mutation_index in self.mutations_range: 102 | yield {'offset':offset, 'mutation_index':mutation_index, 'stop':False, 'pause':False} 103 | 104 | def getNextMutation(self): 105 | if self.paused: 106 | return {'offset':0, 'mutation_index':0, 'stop':False, 'pause':True} 107 | try: 108 | n = self.generator.next() 109 | self.mutations_executed += 1 110 | return n 111 | except StopIteration: 112 | # no more mutations, close the logfile 113 | if not self.log_file.closed: 114 | self.log_file.close() 115 | # tell any clients to 'stop' 116 | return {'offset':0, 'mutation_index':0, 'stop':True, 'pause':False} 117 | 118 | def printStatistics(self, mutations=False, clients=False, crashes=False): 119 | ''' print some statistics information ''' 120 | 121 | print '' 122 | if mutations: 123 | total_mutations = len(self.contents) * len(self.mutations) 124 | print 'Mutations:' 125 | print ' - File size :', len(self.contents) 126 | print ' - Number of possible mutations :', len(self.mutations) 127 | print ' - Total number of mutations :', total_mutations 128 | print ' - Total executed so far : %d (%d%%)' % (self.mutations_executed, float(self.mutations_executed)/total_mutations*100) 129 | if clients: 130 | print 'Clients:' 131 | for client in self.clients: 132 | print ' - %s:%d' % (client.host, client.port) 133 | if crashes: 134 | print 'Crashes:' 135 | for crash in self.crashes: 136 | print ' - Offset :', crash['offset'] 137 | print ' - Mutation Index :', crash['mutation_index'] 138 | print ' - Filename :', crash['filename'] 139 | print ' - Output :\n', crash['output'] 140 | 141 | def menu(self): 142 | while True: 143 | print '\n' 144 | if self.paused: print '---- PAUSED ----' 145 | print 'Menu :' 146 | print '1. Show clients' 147 | print '2. Show crashes' 148 | print '3. Mutations' 149 | print '4. Show all' 150 | print '5. Pause/Resume' 151 | selection = raw_input('Enter Selection : ').rstrip() 152 | if selection == '1': 153 | self.printStatistics(clients=True) 154 | if selection == '2': 155 | self.printStatistics(crashes=True) 156 | if selection == '3': 157 | self.printStatistics(mutations=True) 158 | if selection == '4': 159 | self.printStatistics(clients=True, crashes=True, mutations=True) 160 | if selection == '5': 161 | self.paused = False if self.paused else True 162 | 163 | def quit(message=None): 164 | if message: 165 | print message 166 | print 'Exiting!' 167 | exit(1) 168 | 169 | def check_usage(args): 170 | ''' Parse command line - they're not really optional, shhh''' 171 | 172 | parser = OptionParser() 173 | parser.add_option('-e', action="store", dest="program_cmd_line", help='Executable program to launch, the full command line that will be executed', metavar="program") 174 | parser.add_option('-f', action="store", dest="original_file", help='File to be mutated', metavar="file") 175 | parser.add_option('-t', action="store", dest="mutation_type", help='Type of mutation ("byte", "word", "dword")', metavar="mutation_type") 176 | parser.add_option('-l', action="store", dest="log_file", help='Log file', metavar="log") 177 | parser.add_option('-p', action="store", dest="port", help='Port to listen on', type='int', metavar="port") 178 | parser.add_option('-d', action="store", dest="directory", help="Directory to save files that cause crashes", metavar="directory") 179 | parser.epilog = "Example:\n\n" 180 | parser.epilog += './server.py -e "C:\Program Files\Blah\prog.exe" -f original_file.mp3 -t dword -l log.txt -p 12345 -d save' 181 | options, args = parser.parse_args(args) 182 | 183 | # make sure enough args are passed 184 | if not all((options.program_cmd_line, options.original_file, options.mutation_type, options.log_file, options.port, options.directory)): 185 | parser.error("Incorrect number of arguments - must specify program, original_file, mutation_type, log_file, port, directory") 186 | 187 | return options 188 | 189 | if __name__ == '__main__': 190 | options = check_usage(argv) 191 | factory = FuzzerFactory(options.program_cmd_line, options.original_file, options.log_file, options.mutation_type, options.directory) 192 | reactor.listenTCP(options.port, factory) 193 | reactor.run() 194 | 195 | --------------------------------------------------------------------------------