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