├── .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 |
--------------------------------------------------------------------------------