1*e5dd7070Spatrick#!/usr/bin/env python 2*e5dd7070Spatrick 3*e5dd7070Spatrick""" 4*e5dd7070SpatrickThis is a generic fuzz testing tool, see --help for more information. 5*e5dd7070Spatrick""" 6*e5dd7070Spatrick 7*e5dd7070Spatrickimport os 8*e5dd7070Spatrickimport sys 9*e5dd7070Spatrickimport random 10*e5dd7070Spatrickimport subprocess 11*e5dd7070Spatrickimport itertools 12*e5dd7070Spatrick 13*e5dd7070Spatrickclass TestGenerator: 14*e5dd7070Spatrick def __init__(self, inputs, delete, insert, replace, 15*e5dd7070Spatrick insert_strings, pick_input): 16*e5dd7070Spatrick self.inputs = [(s, open(s).read()) for s in inputs] 17*e5dd7070Spatrick 18*e5dd7070Spatrick self.delete = bool(delete) 19*e5dd7070Spatrick self.insert = bool(insert) 20*e5dd7070Spatrick self.replace = bool(replace) 21*e5dd7070Spatrick self.pick_input = bool(pick_input) 22*e5dd7070Spatrick self.insert_strings = list(insert_strings) 23*e5dd7070Spatrick 24*e5dd7070Spatrick self.num_positions = sum([len(d) for _,d in self.inputs]) 25*e5dd7070Spatrick self.num_insert_strings = len(insert_strings) 26*e5dd7070Spatrick self.num_tests = ((delete + (insert + replace)*self.num_insert_strings) 27*e5dd7070Spatrick * self.num_positions) 28*e5dd7070Spatrick self.num_tests += 1 29*e5dd7070Spatrick 30*e5dd7070Spatrick if self.pick_input: 31*e5dd7070Spatrick self.num_tests *= self.num_positions 32*e5dd7070Spatrick 33*e5dd7070Spatrick def position_to_source_index(self, position): 34*e5dd7070Spatrick for i,(s,d) in enumerate(self.inputs): 35*e5dd7070Spatrick n = len(d) 36*e5dd7070Spatrick if position < n: 37*e5dd7070Spatrick return (i,position) 38*e5dd7070Spatrick position -= n 39*e5dd7070Spatrick raise ValueError,'Invalid position.' 40*e5dd7070Spatrick 41*e5dd7070Spatrick def get_test(self, index): 42*e5dd7070Spatrick assert 0 <= index < self.num_tests 43*e5dd7070Spatrick 44*e5dd7070Spatrick picked_position = None 45*e5dd7070Spatrick if self.pick_input: 46*e5dd7070Spatrick index,picked_position = divmod(index, self.num_positions) 47*e5dd7070Spatrick picked_position = self.position_to_source_index(picked_position) 48*e5dd7070Spatrick 49*e5dd7070Spatrick if index == 0: 50*e5dd7070Spatrick return ('nothing', None, None, picked_position) 51*e5dd7070Spatrick 52*e5dd7070Spatrick index -= 1 53*e5dd7070Spatrick index,position = divmod(index, self.num_positions) 54*e5dd7070Spatrick position = self.position_to_source_index(position) 55*e5dd7070Spatrick if self.delete: 56*e5dd7070Spatrick if index == 0: 57*e5dd7070Spatrick return ('delete', position, None, picked_position) 58*e5dd7070Spatrick index -= 1 59*e5dd7070Spatrick 60*e5dd7070Spatrick index,insert_index = divmod(index, self.num_insert_strings) 61*e5dd7070Spatrick insert_str = self.insert_strings[insert_index] 62*e5dd7070Spatrick if self.insert: 63*e5dd7070Spatrick if index == 0: 64*e5dd7070Spatrick return ('insert', position, insert_str, picked_position) 65*e5dd7070Spatrick index -= 1 66*e5dd7070Spatrick 67*e5dd7070Spatrick assert self.replace 68*e5dd7070Spatrick assert index == 0 69*e5dd7070Spatrick return ('replace', position, insert_str, picked_position) 70*e5dd7070Spatrick 71*e5dd7070Spatrickclass TestApplication: 72*e5dd7070Spatrick def __init__(self, tg, test): 73*e5dd7070Spatrick self.tg = tg 74*e5dd7070Spatrick self.test = test 75*e5dd7070Spatrick 76*e5dd7070Spatrick def apply(self): 77*e5dd7070Spatrick if self.test[0] == 'nothing': 78*e5dd7070Spatrick pass 79*e5dd7070Spatrick else: 80*e5dd7070Spatrick i,j = self.test[1] 81*e5dd7070Spatrick name,data = self.tg.inputs[i] 82*e5dd7070Spatrick if self.test[0] == 'delete': 83*e5dd7070Spatrick data = data[:j] + data[j+1:] 84*e5dd7070Spatrick elif self.test[0] == 'insert': 85*e5dd7070Spatrick data = data[:j] + self.test[2] + data[j:] 86*e5dd7070Spatrick elif self.test[0] == 'replace': 87*e5dd7070Spatrick data = data[:j] + self.test[2] + data[j+1:] 88*e5dd7070Spatrick else: 89*e5dd7070Spatrick raise ValueError,'Invalid test %r' % self.test 90*e5dd7070Spatrick open(name,'wb').write(data) 91*e5dd7070Spatrick 92*e5dd7070Spatrick def revert(self): 93*e5dd7070Spatrick if self.test[0] != 'nothing': 94*e5dd7070Spatrick i,j = self.test[1] 95*e5dd7070Spatrick name,data = self.tg.inputs[i] 96*e5dd7070Spatrick open(name,'wb').write(data) 97*e5dd7070Spatrick 98*e5dd7070Spatrickdef quote(str): 99*e5dd7070Spatrick return '"' + str + '"' 100*e5dd7070Spatrick 101*e5dd7070Spatrickdef run_one_test(test_application, index, input_files, args): 102*e5dd7070Spatrick test = test_application.test 103*e5dd7070Spatrick 104*e5dd7070Spatrick # Interpolate arguments. 105*e5dd7070Spatrick options = { 'index' : index, 106*e5dd7070Spatrick 'inputs' : ' '.join(quote(f) for f in input_files) } 107*e5dd7070Spatrick 108*e5dd7070Spatrick # Add picked input interpolation arguments, if used. 109*e5dd7070Spatrick if test[3] is not None: 110*e5dd7070Spatrick pos = test[3][1] 111*e5dd7070Spatrick options['picked_input'] = input_files[test[3][0]] 112*e5dd7070Spatrick options['picked_input_pos'] = pos 113*e5dd7070Spatrick # Compute the line and column. 114*e5dd7070Spatrick file_data = test_application.tg.inputs[test[3][0]][1] 115*e5dd7070Spatrick line = column = 1 116*e5dd7070Spatrick for i in range(pos): 117*e5dd7070Spatrick c = file_data[i] 118*e5dd7070Spatrick if c == '\n': 119*e5dd7070Spatrick line += 1 120*e5dd7070Spatrick column = 1 121*e5dd7070Spatrick else: 122*e5dd7070Spatrick column += 1 123*e5dd7070Spatrick options['picked_input_line'] = line 124*e5dd7070Spatrick options['picked_input_col'] = column 125*e5dd7070Spatrick 126*e5dd7070Spatrick test_args = [a % options for a in args] 127*e5dd7070Spatrick if opts.verbose: 128*e5dd7070Spatrick print '%s: note: executing %r' % (sys.argv[0], test_args) 129*e5dd7070Spatrick 130*e5dd7070Spatrick stdout = None 131*e5dd7070Spatrick stderr = None 132*e5dd7070Spatrick if opts.log_dir: 133*e5dd7070Spatrick stdout_log_path = os.path.join(opts.log_dir, '%s.out' % index) 134*e5dd7070Spatrick stderr_log_path = os.path.join(opts.log_dir, '%s.err' % index) 135*e5dd7070Spatrick stdout = open(stdout_log_path, 'wb') 136*e5dd7070Spatrick stderr = open(stderr_log_path, 'wb') 137*e5dd7070Spatrick else: 138*e5dd7070Spatrick sys.stdout.flush() 139*e5dd7070Spatrick p = subprocess.Popen(test_args, stdout=stdout, stderr=stderr) 140*e5dd7070Spatrick p.communicate() 141*e5dd7070Spatrick exit_code = p.wait() 142*e5dd7070Spatrick 143*e5dd7070Spatrick test_result = (exit_code == opts.expected_exit_code or 144*e5dd7070Spatrick exit_code in opts.extra_exit_codes) 145*e5dd7070Spatrick 146*e5dd7070Spatrick if stdout is not None: 147*e5dd7070Spatrick stdout.close() 148*e5dd7070Spatrick stderr.close() 149*e5dd7070Spatrick 150*e5dd7070Spatrick # Remove the logs for passes, unless logging all results. 151*e5dd7070Spatrick if not opts.log_all and test_result: 152*e5dd7070Spatrick os.remove(stdout_log_path) 153*e5dd7070Spatrick os.remove(stderr_log_path) 154*e5dd7070Spatrick 155*e5dd7070Spatrick if not test_result: 156*e5dd7070Spatrick print 'FAIL: %d' % index 157*e5dd7070Spatrick elif not opts.succinct: 158*e5dd7070Spatrick print 'PASS: %d' % index 159*e5dd7070Spatrick return test_result 160*e5dd7070Spatrick 161*e5dd7070Spatrickdef main(): 162*e5dd7070Spatrick global opts 163*e5dd7070Spatrick from optparse import OptionParser, OptionGroup 164*e5dd7070Spatrick parser = OptionParser("""%prog [options] ... test command args ... 165*e5dd7070Spatrick 166*e5dd7070Spatrick%prog is a tool for fuzzing inputs and testing them. 167*e5dd7070Spatrick 168*e5dd7070SpatrickThe most basic usage is something like: 169*e5dd7070Spatrick 170*e5dd7070Spatrick $ %prog --file foo.txt ./test.sh 171*e5dd7070Spatrick 172*e5dd7070Spatrickwhich will run a default list of fuzzing strategies on the input. For each 173*e5dd7070Spatrickfuzzed input, it will overwrite the input files (in place), run the test script, 174*e5dd7070Spatrickthen restore the files back to their original contents. 175*e5dd7070Spatrick 176*e5dd7070SpatrickNOTE: You should make sure you have a backup copy of your inputs, in case 177*e5dd7070Spatricksomething goes wrong!!! 178*e5dd7070Spatrick 179*e5dd7070SpatrickYou can cause the fuzzing to not restore the original files with 180*e5dd7070Spatrick'--no-revert'. Generally this is used with '--test <index>' to run one failing 181*e5dd7070Spatricktest and then leave the fuzzed inputs in place to examine the failure. 182*e5dd7070Spatrick 183*e5dd7070SpatrickFor each fuzzed input, %prog will run the test command given on the command 184*e5dd7070Spatrickline. Each argument in the command is subject to string interpolation before 185*e5dd7070Spatrickbeing executed. The syntax is "%(VARIABLE)FORMAT" where FORMAT is a standard 186*e5dd7070Spatrickprintf format, and VARIABLE is one of: 187*e5dd7070Spatrick 188*e5dd7070Spatrick 'index' - the test index being run 189*e5dd7070Spatrick 'inputs' - the full list of test inputs 190*e5dd7070Spatrick 'picked_input' - (with --pick-input) the selected input file 191*e5dd7070Spatrick 'picked_input_pos' - (with --pick-input) the selected input position 192*e5dd7070Spatrick 'picked_input_line' - (with --pick-input) the selected input line 193*e5dd7070Spatrick 'picked_input_col' - (with --pick-input) the selected input column 194*e5dd7070Spatrick 195*e5dd7070SpatrickBy default, the script will run forever continually picking new tests to 196*e5dd7070Spatrickrun. You can limit the number of tests that are run with '--max-tests <number>', 197*e5dd7070Spatrickand you can run a particular test with '--test <index>'. 198*e5dd7070Spatrick 199*e5dd7070SpatrickYou can specify '--stop-on-fail' to stop the script on the first failure 200*e5dd7070Spatrickwithout reverting the changes. 201*e5dd7070Spatrick 202*e5dd7070Spatrick""") 203*e5dd7070Spatrick parser.add_option("-v", "--verbose", help="Show more output", 204*e5dd7070Spatrick action='store_true', dest="verbose", default=False) 205*e5dd7070Spatrick parser.add_option("-s", "--succinct", help="Reduce amount of output", 206*e5dd7070Spatrick action="store_true", dest="succinct", default=False) 207*e5dd7070Spatrick 208*e5dd7070Spatrick group = OptionGroup(parser, "Test Execution") 209*e5dd7070Spatrick group.add_option("", "--expected-exit-code", help="Set expected exit code", 210*e5dd7070Spatrick type=int, dest="expected_exit_code", 211*e5dd7070Spatrick default=0) 212*e5dd7070Spatrick group.add_option("", "--extra-exit-code", 213*e5dd7070Spatrick help="Set additional expected exit code", 214*e5dd7070Spatrick type=int, action="append", dest="extra_exit_codes", 215*e5dd7070Spatrick default=[]) 216*e5dd7070Spatrick group.add_option("", "--log-dir", 217*e5dd7070Spatrick help="Capture test logs to an output directory", 218*e5dd7070Spatrick type=str, dest="log_dir", 219*e5dd7070Spatrick default=None) 220*e5dd7070Spatrick group.add_option("", "--log-all", 221*e5dd7070Spatrick help="Log all outputs (not just failures)", 222*e5dd7070Spatrick action="store_true", dest="log_all", default=False) 223*e5dd7070Spatrick parser.add_option_group(group) 224*e5dd7070Spatrick 225*e5dd7070Spatrick group = OptionGroup(parser, "Input Files") 226*e5dd7070Spatrick group.add_option("", "--file", metavar="PATH", 227*e5dd7070Spatrick help="Add an input file to fuzz", 228*e5dd7070Spatrick type=str, action="append", dest="input_files", default=[]) 229*e5dd7070Spatrick group.add_option("", "--filelist", metavar="LIST", 230*e5dd7070Spatrick help="Add a list of inputs files to fuzz (one per line)", 231*e5dd7070Spatrick type=str, action="append", dest="filelists", default=[]) 232*e5dd7070Spatrick parser.add_option_group(group) 233*e5dd7070Spatrick 234*e5dd7070Spatrick group = OptionGroup(parser, "Fuzz Options") 235*e5dd7070Spatrick group.add_option("", "--replacement-chars", dest="replacement_chars", 236*e5dd7070Spatrick help="Characters to insert/replace", 237*e5dd7070Spatrick default="0{}[]<>\;@#$^%& ") 238*e5dd7070Spatrick group.add_option("", "--replacement-string", dest="replacement_strings", 239*e5dd7070Spatrick action="append", help="Add a replacement string to use", 240*e5dd7070Spatrick default=[]) 241*e5dd7070Spatrick group.add_option("", "--replacement-list", dest="replacement_lists", 242*e5dd7070Spatrick help="Add a list of replacement strings (one per line)", 243*e5dd7070Spatrick action="append", default=[]) 244*e5dd7070Spatrick group.add_option("", "--no-delete", help="Don't delete characters", 245*e5dd7070Spatrick action='store_false', dest="enable_delete", default=True) 246*e5dd7070Spatrick group.add_option("", "--no-insert", help="Don't insert strings", 247*e5dd7070Spatrick action='store_false', dest="enable_insert", default=True) 248*e5dd7070Spatrick group.add_option("", "--no-replace", help="Don't replace strings", 249*e5dd7070Spatrick action='store_false', dest="enable_replace", default=True) 250*e5dd7070Spatrick group.add_option("", "--no-revert", help="Don't revert changes", 251*e5dd7070Spatrick action='store_false', dest="revert", default=True) 252*e5dd7070Spatrick group.add_option("", "--stop-on-fail", help="Stop on first failure", 253*e5dd7070Spatrick action='store_true', dest="stop_on_fail", default=False) 254*e5dd7070Spatrick parser.add_option_group(group) 255*e5dd7070Spatrick 256*e5dd7070Spatrick group = OptionGroup(parser, "Test Selection") 257*e5dd7070Spatrick group.add_option("", "--test", help="Run a particular test", 258*e5dd7070Spatrick type=int, dest="test", default=None, metavar="INDEX") 259*e5dd7070Spatrick group.add_option("", "--max-tests", help="Maximum number of tests", 260*e5dd7070Spatrick type=int, dest="max_tests", default=None, metavar="COUNT") 261*e5dd7070Spatrick group.add_option("", "--pick-input", 262*e5dd7070Spatrick help="Randomly select an input byte as well as fuzzing", 263*e5dd7070Spatrick action='store_true', dest="pick_input", default=False) 264*e5dd7070Spatrick parser.add_option_group(group) 265*e5dd7070Spatrick 266*e5dd7070Spatrick parser.disable_interspersed_args() 267*e5dd7070Spatrick 268*e5dd7070Spatrick (opts, args) = parser.parse_args() 269*e5dd7070Spatrick 270*e5dd7070Spatrick if not args: 271*e5dd7070Spatrick parser.error("Invalid number of arguments") 272*e5dd7070Spatrick 273*e5dd7070Spatrick # Collect the list of inputs. 274*e5dd7070Spatrick input_files = list(opts.input_files) 275*e5dd7070Spatrick for filelist in opts.filelists: 276*e5dd7070Spatrick f = open(filelist) 277*e5dd7070Spatrick try: 278*e5dd7070Spatrick for ln in f: 279*e5dd7070Spatrick ln = ln.strip() 280*e5dd7070Spatrick if ln: 281*e5dd7070Spatrick input_files.append(ln) 282*e5dd7070Spatrick finally: 283*e5dd7070Spatrick f.close() 284*e5dd7070Spatrick input_files.sort() 285*e5dd7070Spatrick 286*e5dd7070Spatrick if not input_files: 287*e5dd7070Spatrick parser.error("No input files!") 288*e5dd7070Spatrick 289*e5dd7070Spatrick print '%s: note: fuzzing %d files.' % (sys.argv[0], len(input_files)) 290*e5dd7070Spatrick 291*e5dd7070Spatrick # Make sure the log directory exists if used. 292*e5dd7070Spatrick if opts.log_dir: 293*e5dd7070Spatrick if not os.path.exists(opts.log_dir): 294*e5dd7070Spatrick try: 295*e5dd7070Spatrick os.mkdir(opts.log_dir) 296*e5dd7070Spatrick except OSError: 297*e5dd7070Spatrick print "%s: error: log directory couldn't be created!" % ( 298*e5dd7070Spatrick sys.argv[0],) 299*e5dd7070Spatrick raise SystemExit,1 300*e5dd7070Spatrick 301*e5dd7070Spatrick # Get the list if insert/replacement strings. 302*e5dd7070Spatrick replacements = list(opts.replacement_chars) 303*e5dd7070Spatrick replacements.extend(opts.replacement_strings) 304*e5dd7070Spatrick for replacement_list in opts.replacement_lists: 305*e5dd7070Spatrick f = open(replacement_list) 306*e5dd7070Spatrick try: 307*e5dd7070Spatrick for ln in f: 308*e5dd7070Spatrick ln = ln[:-1] 309*e5dd7070Spatrick if ln: 310*e5dd7070Spatrick replacements.append(ln) 311*e5dd7070Spatrick finally: 312*e5dd7070Spatrick f.close() 313*e5dd7070Spatrick 314*e5dd7070Spatrick # Unique and order the replacement list. 315*e5dd7070Spatrick replacements = list(set(replacements)) 316*e5dd7070Spatrick replacements.sort() 317*e5dd7070Spatrick 318*e5dd7070Spatrick # Create the test generator. 319*e5dd7070Spatrick tg = TestGenerator(input_files, opts.enable_delete, opts.enable_insert, 320*e5dd7070Spatrick opts.enable_replace, replacements, opts.pick_input) 321*e5dd7070Spatrick 322*e5dd7070Spatrick print '%s: note: %d input bytes.' % (sys.argv[0], tg.num_positions) 323*e5dd7070Spatrick print '%s: note: %d total tests.' % (sys.argv[0], tg.num_tests) 324*e5dd7070Spatrick if opts.test is not None: 325*e5dd7070Spatrick it = [opts.test] 326*e5dd7070Spatrick elif opts.max_tests is not None: 327*e5dd7070Spatrick it = itertools.imap(random.randrange, 328*e5dd7070Spatrick itertools.repeat(tg.num_tests, opts.max_tests)) 329*e5dd7070Spatrick else: 330*e5dd7070Spatrick it = itertools.imap(random.randrange, itertools.repeat(tg.num_tests)) 331*e5dd7070Spatrick for test in it: 332*e5dd7070Spatrick t = tg.get_test(test) 333*e5dd7070Spatrick 334*e5dd7070Spatrick if opts.verbose: 335*e5dd7070Spatrick print '%s: note: running test %d: %r' % (sys.argv[0], test, t) 336*e5dd7070Spatrick ta = TestApplication(tg, t) 337*e5dd7070Spatrick try: 338*e5dd7070Spatrick ta.apply() 339*e5dd7070Spatrick test_result = run_one_test(ta, test, input_files, args) 340*e5dd7070Spatrick if not test_result and opts.stop_on_fail: 341*e5dd7070Spatrick opts.revert = False 342*e5dd7070Spatrick sys.exit(1) 343*e5dd7070Spatrick finally: 344*e5dd7070Spatrick if opts.revert: 345*e5dd7070Spatrick ta.revert() 346*e5dd7070Spatrick 347*e5dd7070Spatrick sys.stdout.flush() 348*e5dd7070Spatrick 349*e5dd7070Spatrickif __name__ == '__main__': 350*e5dd7070Spatrick main() 351