From 32bb4f3cbbc5af4d5b6f4bddc12c92a61f0c33cb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 20 Dec 2013 10:42:50 -0500 Subject: [PATCH 0001/1169] pep8 cleanup certfuzz.analyzers.callgrind --- src/certfuzz/analyzers/callgrind/annotate.py | 58 ++++++++++--------- .../analyzers/callgrind/annotation_file.py | 1 + src/certfuzz/analyzers/callgrind/callgrind.py | 1 + .../analyzers/callgrind/calltree_file.py | 1 + src/certfuzz/analyzers/callgrind/errors.py | 4 ++ 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/certfuzz/analyzers/callgrind/annotate.py b/src/certfuzz/analyzers/callgrind/annotate.py index 7483119..027c1fb 100644 --- a/src/certfuzz/analyzers/callgrind/annotate.py +++ b/src/certfuzz/analyzers/callgrind/annotate.py @@ -3,18 +3,18 @@ @organization: cert.org ''' + + import os -#from subprocess import PIPE from subprocess import Popen import logging from optparse import OptionParser -#import re -#import sys from . import callgrind -from . import CallgrindAnnotateMissingInputFileError -from . import CallgrindAnnotateEmptyOutputFileError -from . import CallgrindAnnotateNoOutputFileError +from .errors import CallgrindAnnotateMissingInputFileError +from .errors import CallgrindAnnotateEmptyOutputFileError +from .errors import CallgrindAnnotateNoOutputFileError +from .annotation_file import AnnotationFile logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -22,25 +22,44 @@ OUTFILE_EXT = 'annotated' get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) -def annotate_callgrind(crash, file_ext='annotated', options={}): + +def main(): + parser = OptionParser() + parser.add_option('', '--debug', dest='debug', action='store_true', help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--outfile', dest='outfile', help='file to write output to') + options, args = parser.parse_args() + if options.debug: + logger.setLevel(logging.DEBUG) + for arg in args: + opts = {'threshold': 100} + cga = CallgrindAnnotate(arg, opts) + a = AnnotationFile(cga.outfile) + print a.__dict__ + + +def annotate_callgrind(crash, file_ext='annotated', options=None): infile = callgrind.get_file(crash.fuzzedfile.path) + if options is None: + options = {} options['threshold'] = '100' CallgrindAnnotate(infile, file_ext, options) + def annotate_callgrind_tree(crash): options = {'tree': 'calling'} file_ext = 'calltree' annotate_callgrind(crash, file_ext, options) + class CallgrindAnnotate(object): ''' Wrapper class for callgrind_annotate ''' - def __init__(self, callgrind_file, file_ext, options={}): + def __init__(self, callgrind_file, file_ext, options=None): ''' @param callgrind_file: A file containing output from valgrind --tool=callgrind @@ -53,7 +72,10 @@ def __init__(self, callgrind_file, file_ext, options={}): self.outfile = '%s.%s' % (self.callgrind_file, file_ext) - self.options = options + if options is None: + self.options = {} + else: + self.options = options self.annotate() @@ -82,20 +104,4 @@ def annotate(self): hdlr = logging.StreamHandler() logger.addHandler(hdlr) - parser = OptionParser() - parser.add_option('', '--debug', dest='debug', action='store_true', help='Enable debug messages (overrides --verbose)') -# parser.add_option('', '--tree', dest='tree', action='store_true', help='Enable call tree output') -# parser.add_option('', '--no-libc', dest='suppress', action='store_true', help='Ignore functions containing "libc" if set') - parser.add_option('', '--outfile', dest='outfile', help='file to write output to') - (options, args) = parser.parse_args() - - if options.debug: - logger.setLevel(logging.DEBUG) - - for arg in args: - - opts = {'threshold': 100} - cga = CallgrindAnnotate(arg, opts) - a = Annotation(cga.outfile) - - print a.__dict__ + main() diff --git a/src/certfuzz/analyzers/callgrind/annotation_file.py b/src/certfuzz/analyzers/callgrind/annotation_file.py index 610d09e..684fd90 100644 --- a/src/certfuzz/analyzers/callgrind/annotation_file.py +++ b/src/certfuzz/analyzers/callgrind/annotation_file.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) + class AnnotationFile(object): ''' Annotation File object. Reads in a callgrind annotation file and parses it into a dict (self.coverage) diff --git a/src/certfuzz/analyzers/callgrind/callgrind.py b/src/certfuzz/analyzers/callgrind/callgrind.py index aad015d..5d24f73 100644 --- a/src/certfuzz/analyzers/callgrind/callgrind.py +++ b/src/certfuzz/analyzers/callgrind/callgrind.py @@ -16,6 +16,7 @@ get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) + class Callgrind(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) diff --git a/src/certfuzz/analyzers/callgrind/calltree_file.py b/src/certfuzz/analyzers/callgrind/calltree_file.py index 8cdb576..8a8df4d 100644 --- a/src/certfuzz/analyzers/callgrind/calltree_file.py +++ b/src/certfuzz/analyzers/callgrind/calltree_file.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) + class CalltreeFile(object): ''' Annotation File object. Reads in a callgrind annotation file and parses it into a dict (self.coverage) diff --git a/src/certfuzz/analyzers/callgrind/errors.py b/src/certfuzz/analyzers/callgrind/errors.py index 3f78c37..432ac1f 100644 --- a/src/certfuzz/analyzers/callgrind/errors.py +++ b/src/certfuzz/analyzers/callgrind/errors.py @@ -5,9 +5,11 @@ ''' from .. import AnalyzerError + class CallgrindAnnotateError(AnalyzerError): pass + class CallgrindAnnotateMissingInputFileError(CallgrindAnnotateError): def __init__(self, f): self.file = f @@ -15,6 +17,7 @@ def __init__(self, f): def __str__(self): return "Input file does not exist: %s" % self.file + class CallgrindAnnotateNoOutputFileError(CallgrindAnnotateError): def __init__(self, f): self.file = f @@ -22,6 +25,7 @@ def __init__(self, f): def __str__(self): return "Output file does not exist: %s" % self.file + class CallgrindAnnotateEmptyOutputFileError(CallgrindAnnotateError): def __init__(self, f): self.file = f From ba226689e37ba4c41aff124dcb0e193cea4a38b4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 20 Dec 2013 10:44:27 -0500 Subject: [PATCH 0002/1169] peppep8 cleanup certfuzz.analyzers --- src/certfuzz/analyzers/analyzer_base.py | 4 ++++ src/certfuzz/analyzers/cw_gmalloc.py | 1 + src/certfuzz/analyzers/pin_calltrace.py | 3 ++- src/certfuzz/analyzers/stderr.py | 1 + src/certfuzz/analyzers/valgrind.py | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/analyzers/analyzer_base.py b/src/certfuzz/analyzers/analyzer_base.py index c548d38..fcadbea 100644 --- a/src/certfuzz/analyzers/analyzer_base.py +++ b/src/certfuzz/analyzers/analyzer_base.py @@ -11,9 +11,11 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) + class AnalyzerError(Exception): pass + class AnalyzerOutputMissingError(AnalyzerError): ''' Exception class for missing output files @@ -24,6 +26,7 @@ def __init__(self, f): def __str__(self): return "Expected output file is missing: %s" % self.file + class AnalyzerEmptyOutputError(AnalyzerError): ''' Exception class for missing output files @@ -34,6 +37,7 @@ def __init__(self, f): def __str__(self): return "Output file is empty: %s" % self.file + class Analyzer(object): ''' classdocs diff --git a/src/certfuzz/analyzers/cw_gmalloc.py b/src/certfuzz/analyzers/cw_gmalloc.py index c4c3a1b..cf90333 100644 --- a/src/certfuzz/analyzers/cw_gmalloc.py +++ b/src/certfuzz/analyzers/cw_gmalloc.py @@ -15,6 +15,7 @@ OUTFILE_EXT = "gmalloc" get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) + class CrashWranglerGmalloc(Analyzer): ''' classdocs diff --git a/src/certfuzz/analyzers/pin_calltrace.py b/src/certfuzz/analyzers/pin_calltrace.py index cba99b7..27c3b47 100644 --- a/src/certfuzz/analyzers/pin_calltrace.py +++ b/src/certfuzz/analyzers/pin_calltrace.py @@ -17,6 +17,7 @@ get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) + class Pin_calltrace(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) @@ -29,6 +30,6 @@ def __init__(self, cfg, crash): def _get_cmdline(self): pin = os.path.expanduser('~/pin/pin') pintool = os.path.expanduser('~/pintool/calltrace.so') - args = [pin, '-injection', 'child', '-t', pintool, '-o', self.outfile, '--'] + args = [pin, '-injection', 'child', '-t', pintool, '-o', self.outfile, '--'] args.extend(self.cmdargs) return args diff --git a/src/certfuzz/analyzers/stderr.py b/src/certfuzz/analyzers/stderr.py index 185415d..6c6fb4b 100644 --- a/src/certfuzz/analyzers/stderr.py +++ b/src/certfuzz/analyzers/stderr.py @@ -15,6 +15,7 @@ get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) + class StdErr(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) diff --git a/src/certfuzz/analyzers/valgrind.py b/src/certfuzz/analyzers/valgrind.py index 5c986c7..5e9c682 100644 --- a/src/certfuzz/analyzers/valgrind.py +++ b/src/certfuzz/analyzers/valgrind.py @@ -16,6 +16,7 @@ get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) + class Valgrind(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) From 9f8eb326986ae237c88d79e0a30148689d486d39 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 20 Dec 2013 14:18:03 -0500 Subject: [PATCH 0003/1169] peppep8 cleanup certfuzz.campaign.config --- src/certfuzz/campaign/config/android_config.py | 8 +++----- src/certfuzz/campaign/config/bff_config.py | 4 ++-- src/certfuzz/campaign/config/config_base.py | 2 ++ src/certfuzz/campaign/config/foe_config.py | 4 +++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/campaign/config/android_config.py b/src/certfuzz/campaign/config/android_config.py index f5b8f45..58c84b8 100644 --- a/src/certfuzz/campaign/config/android_config.py +++ b/src/certfuzz/campaign/config/android_config.py @@ -6,7 +6,6 @@ from .config_base import Config as ConfigBase #from ...android.controller.defaults import CONFIG as DEFAULT_CONFIG import logging -import os logger = logging.getLogger(__name__) @@ -40,7 +39,7 @@ def _validate_directories(self): # Make sure apk_dir exists if it is specified, otherwise it # should be None try: - apk_dir = self.config['directories']['apk_dir'] + self.config['directories']['apk_dir'] except KeyError: logger.warning('No APK installation directory specified. ' + 'Make sure any needed APKs are already installed.') @@ -58,7 +57,7 @@ def _validate_db_config(self): # Validate host try: - host = self.config['db']['host'] + self.config['db']['host'] except KeyError: logger.warning('No host specified in config. Defaulting to localhost.') self.config['db']['host'] = 'localhost' @@ -75,8 +74,7 @@ def _validate_db_config(self): # Validate dbname try: - dbname = self.config['db']['dbname'] + self.config['db']['dbname'] except KeyError: logger.warning('No dbname specified in config. Defaulting to bff.') self.config['db']['dbname'] = 'bff' - diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index 01d8f47..2b2cf3e 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -11,10 +11,8 @@ import os import ConfigParser import subprocess -import uuid import re import shlex -#import string import logging from ...fuzztools import filetools @@ -35,6 +33,7 @@ CACHED_SEEDFILESET_OBJECT_FILE = 'seedfile_set.pkl' SEEDFILE_REPLACE_STRING = '\$SEEDFILE' + def read_config_options(cfg_file): ''' Reads and parses , returning a ConfigHelper object @@ -44,6 +43,7 @@ def read_config_options(cfg_file): config.read(cfg_file) return ConfigHelper(config) + class ConfigHelper: '''ConfigHelper takes a generic ConfigParser raw object and provides the properties and helper methods necessary to configure a fuzz run.''' diff --git a/src/certfuzz/campaign/config/config_base.py b/src/certfuzz/campaign/config/config_base.py index c300a05..39b060c 100644 --- a/src/certfuzz/campaign/config/config_base.py +++ b/src/certfuzz/campaign/config/config_base.py @@ -13,9 +13,11 @@ def parse_yaml(yaml_file): return yaml.load(open(yaml_file, 'r')) + class ConfigError(Exception): pass + class Config(object): ''' If you are inheriting this class, add validation methods to self.validations diff --git a/src/certfuzz/campaign/config/foe_config.py b/src/certfuzz/campaign/config/foe_config.py index fadb04a..4809030 100644 --- a/src/certfuzz/campaign/config/foe_config.py +++ b/src/certfuzz/campaign/config/foe_config.py @@ -13,6 +13,7 @@ logger = logging.getLogger(__name__) + def get_command_args_list(cmd_template, infile, posix=True): ''' Given a command template and infile, will substitute infile into the @@ -27,6 +28,7 @@ def get_command_args_list(cmd_template, infile, posix=True): cmdlist = shlex.split(cmd, posix=posix) return cmd, cmdlist + class Config(ConfigBase): def _add_validations(self): self.validations.append(self._validate_debugger_timeout_exceeds_runner) @@ -39,7 +41,7 @@ def _set_derived_options(self): # self.config['target']['cmdline_template'] = t.safe_substitute(PROGRAM=self.config['target']['program']) self.config['target']['cmdline_template'] = t.safe_substitute(PROGRAM=quoted(self.config['target']['program']), SEEDFILE=quoted('$SEEDFILE')) - + def _validate_new_options(self): if 'minimizer_timeout' not in self.config['runoptions']: self.config['runoptions']['minimizer_timeout'] = 3600 From 0361b2a77447b6378f3e15660bf0c8e97938f505 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 20 Dec 2013 14:21:06 -0500 Subject: [PATCH 0004/1169] pep8 cleanup certfuzz.campaign --- src/certfuzz/campaign/campaign.py | 1 + src/certfuzz/campaign/campaign_base.py | 3 +++ src/certfuzz/campaign/errors.py | 3 +++ src/certfuzz/campaign/iteration.py | 1 - src/certfuzz/campaign/iteration_android.py | 2 +- src/certfuzz/campaign/iteration_base.py | 2 -- 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index 1eddd75..bf7a220 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -381,6 +381,7 @@ def go(self): logger.info('Seedfile set is empty. Nothing more to do.') return + class WindowsCampaign(Campaign): ''' Extends Campaign to add windows-specific features like ButtonClicker diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 624b2ce..620dc17 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -13,6 +13,7 @@ from . import __version__ from ..fuzztools import filetools + def import_module_by_name(name, logger=None): if logger: logger.debug('Importing module %s', name) @@ -20,9 +21,11 @@ def import_module_by_name(name, logger=None): module = sys.modules[name] return module + class CampaignError(Exception): pass + class CampaignBase(object): __metaclass__ = abc.ABCMeta diff --git a/src/certfuzz/campaign/errors.py b/src/certfuzz/campaign/errors.py index 1ec944d..a90a510 100644 --- a/src/certfuzz/campaign/errors.py +++ b/src/certfuzz/campaign/errors.py @@ -5,11 +5,14 @@ ''' from certfuzz.errors import CERTFuzzError + class CampaignError(CERTFuzzError): pass + class AndroidCampaignError(CampaignError): pass + class IterationError(CERTFuzzError): pass diff --git a/src/certfuzz/campaign/iteration.py b/src/certfuzz/campaign/iteration.py index da7cfc1..be9cb85 100644 --- a/src/certfuzz/campaign/iteration.py +++ b/src/certfuzz/campaign/iteration.py @@ -314,4 +314,3 @@ def _fuzz_and_run(self): self._build_crash(fuzzer, cmdlist, dbg_opts, fuzzed_file) else: logger.debug('...no crash') - diff --git a/src/certfuzz/campaign/iteration_android.py b/src/certfuzz/campaign/iteration_android.py index 87166e2..c77ee56 100644 --- a/src/certfuzz/campaign/iteration_android.py +++ b/src/certfuzz/campaign/iteration_android.py @@ -33,6 +33,7 @@ class EmuHandle(object): emu = EmuHandle() + def _get_seedfile_by_id(tcdb, sf_dir, sfid): # find the doc record by id doc = FileDoc.load(tcdb.db, sfid) @@ -200,4 +201,3 @@ def _fuzz_and_run(self): ) as testcase: logger.info('...store object to db') testcase.store(self.tcdb.db) - diff --git a/src/certfuzz/campaign/iteration_base.py b/src/certfuzz/campaign/iteration_base.py index a7ec448..f4f6203 100644 --- a/src/certfuzz/campaign/iteration_base.py +++ b/src/certfuzz/campaign/iteration_base.py @@ -58,5 +58,3 @@ def go(self): # process all the crashes for c in self.crashes: self._process_crash(c) - - From c1d5c9632caeb4990e0297e89b8bb4c6c51282e8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 20 Dec 2013 14:22:42 -0500 Subject: [PATCH 0005/1169] pep8 cleanup certfuzz.crash --- src/certfuzz/crash/bff_crash.py | 1 + src/certfuzz/crash/crash_base.py | 1 + src/certfuzz/crash/errors.py | 3 +++ src/certfuzz/crash/foe_crash.py | 19 +++++++++++-------- src/certfuzz/crash/testcase_base.py | 1 + 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index 7850150..b230396 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -23,6 +23,7 @@ debugger = None host_info = hostinfo.HostInfo() + class BffCrash(Crash): ''' classdocs diff --git a/src/certfuzz/crash/crash_base.py b/src/certfuzz/crash/crash_base.py index 58b6f55..d972d52 100644 --- a/src/certfuzz/crash/crash_base.py +++ b/src/certfuzz/crash/crash_base.py @@ -14,6 +14,7 @@ logger = logging.getLogger(__name__) + class Crash(TestCaseBase): tmpdir_pfx = 'crash-' diff --git a/src/certfuzz/crash/errors.py b/src/certfuzz/crash/errors.py index aee2731..df3f8bc 100644 --- a/src/certfuzz/crash/errors.py +++ b/src/certfuzz/crash/errors.py @@ -5,11 +5,14 @@ ''' from .. import CERTFuzzError + class TestCaseError(CERTFuzzError): pass + class CrashError(TestCaseError): pass + class AndroidTestCaseError(TestCaseError): pass diff --git a/src/certfuzz/crash/foe_crash.py b/src/certfuzz/crash/foe_crash.py index aa5fb1d..ed0b6d3 100644 --- a/src/certfuzz/crash/foe_crash.py +++ b/src/certfuzz/crash/foe_crash.py @@ -14,6 +14,8 @@ from ..campaign.config.foe_config import get_command_args_list logger = logging.getLogger(__name__) + + def logerror(func, path, excinfo): logger.warning('%s failed to remove %s: %s', func, path, excinfo) @@ -27,14 +29,16 @@ def logerror(func, path, excinfo): exp_rank = { 'EXPLOITABLE': 1, - 'PROBABLY_EXPLOITABLE': 2, + 'PROBABLY_EXPLOITABLE': 2, 'UNKNOWN': 3, 'PROBABLY_NOT_EXPLOITABLE': 4, 'HEISENBUG': 5, } + class FoeCrash(Crash): tmpdir_pfx = 'foe-crash-' + # TODO: do we still need fuzzer as an arg? def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, dbg_class, dbg_opts, workingdir_base, keep_faddr, program, @@ -104,8 +108,7 @@ def update_crash_details(self): def set_debugger_template(self, *args): pass - - + def debug_once(self): outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) @@ -120,13 +123,13 @@ def debug_once(self): self.parsed_outputs.append(debugger.go()) self.reached_secondchance = self.parsed_outputs[self.exception_depth].secondchance - + if self.reached_secondchance and self.exception_depth > 0: # No need to process second-chance exception # Note that some exceptions, such as Illegal Instructions have no first-chance: # In those cases, proceed... return - + # Store highest exploitability of every exception in the chain current_exception_exp = self.parsed_outputs[self.exception_depth].exp if current_exception_exp: @@ -134,7 +137,7 @@ def debug_once(self): self.exp = current_exception_exp elif exp_rank[current_exception_exp] < exp_rank[self.exp]: self.exp = current_exception_exp - + current_exception_hash = self.parsed_outputs[self.exception_depth].crash_hash current_exception_faddr = self.parsed_outputs[self.exception_depth].faddr if current_exception_hash: @@ -148,7 +151,7 @@ def debug_once(self): if self.keep_uniq_faddr and current_exception_faddr: self.crash_hash += '.' + current_exception_faddr - + # The first exception is the one that is representative for the crasher if self.exception_depth == 0: self.dbg_file = debugger.outfile @@ -179,7 +182,7 @@ def debug(self, tries_remaining=None): self.debug_once() if self.reached_secondchance or not self.parsed_outputs[self.exception_depth].is_crash: logger.debug("no more handled exceptions") - break + break # get the signature now that we've got all of the exceptions self.get_signature() else: diff --git a/src/certfuzz/crash/testcase_base.py b/src/certfuzz/crash/testcase_base.py index b2a2a24..1874b8d 100644 --- a/src/certfuzz/crash/testcase_base.py +++ b/src/certfuzz/crash/testcase_base.py @@ -10,6 +10,7 @@ logger = logging.getLogger(__name__) + class TestCaseBase(object): _tmp_sfx = '' _tmp_pfx = 'BFF_testcase_' From 7c0fc5962ae41328a08806b5c3beea0532e3e0ee Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 20 Dec 2013 15:11:22 -0500 Subject: [PATCH 0006/1169] pep8 cleanup certfuzz.debuggers --- src/certfuzz/debuggers/debugger_base.py | 1 + src/certfuzz/debuggers/errors.py | 5 +++++ src/certfuzz/debuggers/gdb.py | 1 + src/certfuzz/debuggers/jit.py | 2 +- src/certfuzz/debuggers/msec.py | 22 +++++++++++----------- src/certfuzz/debuggers/nulldebugger.py | 2 ++ 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/debuggers/debugger_base.py b/src/certfuzz/debuggers/debugger_base.py index a2d0c7e..141e652 100644 --- a/src/certfuzz/debuggers/debugger_base.py +++ b/src/certfuzz/debuggers/debugger_base.py @@ -10,6 +10,7 @@ logger = logging.getLogger(__name__) + class Debugger(object): ''' classdocs diff --git a/src/certfuzz/debuggers/errors.py b/src/certfuzz/debuggers/errors.py index f42a4ca..6c0bfb4 100644 --- a/src/certfuzz/debuggers/errors.py +++ b/src/certfuzz/debuggers/errors.py @@ -3,9 +3,12 @@ @organization: cert.org ''' + + class DebuggerError(Exception): pass + class UndefinedDebuggerError(DebuggerError): ''' Exception class for undefined debuggers @@ -15,6 +18,8 @@ def __init__(self, system): def __str__(self): return "No debugger defined for '%s'" % self.system + + class DebuggerNotFoundError(DebuggerError): def __init__(self, debugger): self.debugger = debugger diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index 9b6a91d..a382dac 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -19,6 +19,7 @@ logger = logging.getLogger(__name__) + class GDB(Debugger): _platform = 'Linux' _key = 'gdb' diff --git a/src/certfuzz/debuggers/jit.py b/src/certfuzz/debuggers/jit.py index b7fdc11..e1c562f 100644 --- a/src/certfuzz/debuggers/jit.py +++ b/src/certfuzz/debuggers/jit.py @@ -6,7 +6,7 @@ import sys from ctypes import windll, byref from ctypes.wintypes import HANDLE, BOOL, LPCWSTR -import time + def main(pid=None): PROCESS_TERMINATE = 0x0001 diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index f25f456..8bc79c3 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -83,7 +83,7 @@ def run_with_timer(self): targetdir = os.path.dirname(self.program) exename = os.path.basename(self.program) process_info = {} - id = None + _id = None done = False started = False wmiInterface = None @@ -99,14 +99,14 @@ def run_with_timer(self): while retrycount < 5 and not foundpid: for process in wmiInterface.Win32_Process(name=exename): # TODO: What if there's more than one? - id = process.ProcessID - logger.debug('Found %s PID: %s', exename, id) + _id = process.ProcessID + logger.debug('Found %s PID: %s', exename, _id) foundpid = True if not foundpid: logger.debug('%s not seen yet. Retrying...', exename) retrycount += 1 time.sleep(0.1) - if not id: + if not _id: logger.debug('Cannot find %s child process! Bailing.', exename) self.kill(p.pid, 99) return @@ -117,16 +117,16 @@ def run_with_timer(self): if self.watchcpu == True: # This is a race. In some cases, a GUI app could be done before we can even measure it # TODO: Do something about it - while p.poll() is None and not done and id: - for proc in wmiInterface.Win32_PerfRawData_PerfProc_Process (IDProcess=id): - n1, d1 = long (proc.PercentProcessorTime), long (proc.Timestamp_Sys100NS) - n0, d0 = process_info.get (id, (0, 0)) + while p.poll() is None and not done and _id: + for proc in wmiInterface.Win32_PerfRawData_PerfProc_Process(IDProcess=_id): + n1, d1 = long(proc.PercentProcessorTime), long(proc.Timestamp_Sys100NS) + n0, d0 = process_info.get(_id, (0, 0)) try: - percent_processor_time = (float (n1 - n0) / float (d1 - d0)) * 100.0 + percent_processor_time = (float(n1 - n0) / float(d1 - d0)) * 100.0 except ZeroDivisionError: percent_processor_time = 0.0 - process_info[id] = (n1, d1) - logger.debug('Process %s CPU usage: %s', id, percent_processor_time) + process_info[_id] = (n1, d1) + logger.debug('Process %s CPU usage: %s', _id, percent_processor_time) if percent_processor_time < 0.01: if started: logger.debug('killing %s due to CPU inactivity', p.pid) diff --git a/src/certfuzz/debuggers/nulldebugger.py b/src/certfuzz/debuggers/nulldebugger.py index 25c20f4..c1d03df 100644 --- a/src/certfuzz/debuggers/nulldebugger.py +++ b/src/certfuzz/debuggers/nulldebugger.py @@ -14,9 +14,11 @@ logger = logging.getLogger(__name__) + def factory(*args): return NullDebugger(*args) + class NullDebugger(Debugger): ''' classdocs From b31b2042e7216e6a58e820b25c0f0cde518de4c8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 20 Dec 2013 15:12:52 -0500 Subject: [PATCH 0007/1169] pep8 cleanup certfuzz.file_handlers --- src/certfuzz/file_handlers/directory.py | 2 ++ src/certfuzz/file_handlers/errors.py | 4 ++++ src/certfuzz/file_handlers/fuzzedfile.py | 1 + src/certfuzz/file_handlers/seedfile.py | 2 ++ src/certfuzz/file_handlers/seedfile_set.py | 2 +- src/certfuzz/file_handlers/tempdir.py | 2 +- src/certfuzz/file_handlers/tmp_reaper.py | 1 + 7 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/file_handlers/directory.py b/src/certfuzz/file_handlers/directory.py index e3e4a74..995b7fc 100644 --- a/src/certfuzz/file_handlers/directory.py +++ b/src/certfuzz/file_handlers/directory.py @@ -9,11 +9,13 @@ import logging logger = logging.getLogger(__name__) + class DirectoryError(Exception): pass blacklist = ['.DS_Store', ] + class Directory(object): def __init__(self, mydir, create=False): self.dir = mydir diff --git a/src/certfuzz/file_handlers/errors.py b/src/certfuzz/file_handlers/errors.py index 4b13555..685023b 100644 --- a/src/certfuzz/file_handlers/errors.py +++ b/src/certfuzz/file_handlers/errors.py @@ -5,14 +5,18 @@ ''' from .. import CERTFuzzError + class FileHandlerError(CERTFuzzError): pass + class BasicFileError(FileHandlerError): pass + class FuzzedFileError(BasicFileError): pass + class SeedFileError(BasicFileError): pass diff --git a/src/certfuzz/file_handlers/fuzzedfile.py b/src/certfuzz/file_handlers/fuzzedfile.py index 6e5b5bd..5334819 100644 --- a/src/certfuzz/file_handlers/fuzzedfile.py +++ b/src/certfuzz/file_handlers/fuzzedfile.py @@ -5,6 +5,7 @@ ''' from . import BasicFile + class FuzzedFile(BasicFile): ''' Adds a derived_from field to BasicFile object diff --git a/src/certfuzz/file_handlers/seedfile.py b/src/certfuzz/file_handlers/seedfile.py index 42a10b3..f4f32a0 100644 --- a/src/certfuzz/file_handlers/seedfile.py +++ b/src/certfuzz/file_handlers/seedfile.py @@ -12,6 +12,7 @@ from ..fuzztools import filetools from ..scoring.scorable_thing import ScorableThing + # TODO: replace with a common function in some helper module def print_dict(d, indent=0): for (k, v) in d.iteritems(): @@ -22,6 +23,7 @@ def print_dict(d, indent=0): else: print indent_str + "%s (%s): %s" % (k, type(v).__name__, v) + # ScorableThing mixin gives us the probability stuff needed for use as part of # a scorable set like SeedfileSet class SeedFile(BasicFile, ScorableThing): diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 438de65..f162a14 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -12,6 +12,7 @@ from ..scoring.scorable_set import ScorableSet2, EmptySetError logger = logging.getLogger(__name__) + class SeedfileSet(ScorableSet2): ''' classdocs @@ -121,7 +122,6 @@ def next_item(self): logger.warning('Seedfile no longer exists, removing from set: %s', sf.path) self.del_item(sf.md5) - def __setstate__(self, state): newstate = state.copy() diff --git a/src/certfuzz/file_handlers/tempdir.py b/src/certfuzz/file_handlers/tempdir.py index 54ab4b2..dd82f71 100644 --- a/src/certfuzz/file_handlers/tempdir.py +++ b/src/certfuzz/file_handlers/tempdir.py @@ -9,6 +9,7 @@ logger = logging.getLogger(__name__) + class TempDir(object): ''' Runtime context that creates a tempdir then cleans it up when exiting the @@ -37,4 +38,3 @@ def __exit__(self, etype, value, traceback): logger.debug('%s caught %s: %s', self.__class__.__name__, etype, value) logger.debug('Removing tempdir %s', self.tmpdir) shutil.rmtree(self.tmpdir, ignore_errors=True) - diff --git a/src/certfuzz/file_handlers/tmp_reaper.py b/src/certfuzz/file_handlers/tmp_reaper.py index d9afd3b..d51cf85 100644 --- a/src/certfuzz/file_handlers/tmp_reaper.py +++ b/src/certfuzz/file_handlers/tmp_reaper.py @@ -12,6 +12,7 @@ logger = logging.getLogger(__name__) + class TmpReaper(object): ''' classdocs From 9b3e203dd41c42824017687e4c62a4ebebd0d753 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 20 Dec 2013 15:16:02 -0500 Subject: [PATCH 0008/1169] pep8 cleanup certfuzz.fuzzers --- src/certfuzz/fuzzers/bitmut.py | 1 + src/certfuzz/fuzzers/copy.py | 1 + src/certfuzz/fuzzers/crlfmut.py | 1 + src/certfuzz/fuzzers/crmut.py | 1 + src/certfuzz/fuzzers/drop.py | 2 ++ src/certfuzz/fuzzers/errors.py | 4 ++++ src/certfuzz/fuzzers/fuzzer_base.py | 2 ++ src/certfuzz/fuzzers/insert.py | 2 ++ src/certfuzz/fuzzers/nullmut.py | 1 + src/certfuzz/fuzzers/swap.py | 2 ++ src/certfuzz/fuzzers/truncate.py | 2 ++ src/certfuzz/fuzzers/verify.py | 1 + src/certfuzz/fuzzers/wave.py | 3 +++ 13 files changed, 23 insertions(+) diff --git a/src/certfuzz/fuzzers/bitmut.py b/src/certfuzz/fuzzers/bitmut.py index 93dee8e..0f5e5d5 100644 --- a/src/certfuzz/fuzzers/bitmut.py +++ b/src/certfuzz/fuzzers/bitmut.py @@ -4,6 +4,7 @@ logger = logging.getLogger(__name__) + class BitMutFuzzer(MinimizableFuzzer): ''' This fuzzer module randomly selects bits in an input file and flips them. diff --git a/src/certfuzz/fuzzers/copy.py b/src/certfuzz/fuzzers/copy.py index 26736b4..253d5c0 100644 --- a/src/certfuzz/fuzzers/copy.py +++ b/src/certfuzz/fuzzers/copy.py @@ -1,5 +1,6 @@ from . import Fuzzer + class CopyFuzzer(Fuzzer): ''' This "fuzzer" copies input_file_path to output_file_path. Useful for diff --git a/src/certfuzz/fuzzers/crlfmut.py b/src/certfuzz/fuzzers/crlfmut.py index 12647c5..c5d88ba 100644 --- a/src/certfuzz/fuzzers/crlfmut.py +++ b/src/certfuzz/fuzzers/crlfmut.py @@ -4,6 +4,7 @@ from .bytemut import ByteMutFuzzer + class CRLFMutFuzzer(ByteMutFuzzer): ''' This fuzzer module randomly replaces single CR and LF characters 0x0D 0x0A with diff --git a/src/certfuzz/fuzzers/crmut.py b/src/certfuzz/fuzzers/crmut.py index 3981c58..ba2d0ef 100644 --- a/src/certfuzz/fuzzers/crmut.py +++ b/src/certfuzz/fuzzers/crmut.py @@ -4,6 +4,7 @@ from .bytemut import ByteMutFuzzer + class CRMutFuzzer(ByteMutFuzzer): ''' This fuzzer module randomly replaces single CR characters 0x0D with diff --git a/src/certfuzz/fuzzers/drop.py b/src/certfuzz/fuzzers/drop.py index 24a7430..c03235d 100644 --- a/src/certfuzz/fuzzers/drop.py +++ b/src/certfuzz/fuzzers/drop.py @@ -7,9 +7,11 @@ logger = logging.getLogger(__name__) + class DropFuzzerError(FuzzerError): pass + class DropFuzzer(Fuzzer): ''' This fuzzer module iterates through an input file, dropping each byte position diff --git a/src/certfuzz/fuzzers/errors.py b/src/certfuzz/fuzzers/errors.py index cec2dfb..9c8409e 100644 --- a/src/certfuzz/fuzzers/errors.py +++ b/src/certfuzz/fuzzers/errors.py @@ -3,13 +3,17 @@ @organization: cert.org ''' + + class FuzzerError(Exception): pass + # raise this exception if your fuzzer is out of ways to manipulate # the seed file class FuzzerExhaustedError(FuzzerError): pass + class FuzzerInputMatchesOutputError(FuzzerError): pass diff --git a/src/certfuzz/fuzzers/fuzzer_base.py b/src/certfuzz/fuzzers/fuzzer_base.py index 78bb6d8..5031965 100644 --- a/src/certfuzz/fuzzers/fuzzer_base.py +++ b/src/certfuzz/fuzzers/fuzzer_base.py @@ -26,6 +26,7 @@ def logerror(func, path, excinfo): logger.warning('%s failed to remove %s: %s', func, path, excinfo) + def is_fuzzable(x, exclude_list): ''' Returns true if x is not in any range in range_list @@ -40,6 +41,7 @@ def is_fuzzable(x, exclude_list): return False return True + class Fuzzer(object): ''' The Fuzzer class is intended to be used as the parent class for actual diff --git a/src/certfuzz/fuzzers/insert.py b/src/certfuzz/fuzzers/insert.py index 01829bc..90575b9 100644 --- a/src/certfuzz/fuzzers/insert.py +++ b/src/certfuzz/fuzzers/insert.py @@ -8,9 +8,11 @@ logger = logging.getLogger(__name__) + class InsertFuzzerError(FuzzerError): pass + class InsertFuzzer(Fuzzer): ''' This fuzzer module iterates through an input file, inserting a random byte diff --git a/src/certfuzz/fuzzers/nullmut.py b/src/certfuzz/fuzzers/nullmut.py index b296dde..863639e 100644 --- a/src/certfuzz/fuzzers/nullmut.py +++ b/src/certfuzz/fuzzers/nullmut.py @@ -4,6 +4,7 @@ from .bytemut import ByteMutFuzzer + class NullMutFuzzer(ByteMutFuzzer): ''' This fuzzer module randomly replaces single null characters 0x00 with diff --git a/src/certfuzz/fuzzers/swap.py b/src/certfuzz/fuzzers/swap.py index 28e4377..49e4351 100644 --- a/src/certfuzz/fuzzers/swap.py +++ b/src/certfuzz/fuzzers/swap.py @@ -6,9 +6,11 @@ logger = logging.getLogger(__name__) + class SwapFuzzerError(FuzzerError): pass + class SwapFuzzer(MinimizableFuzzer): ''' Step through the input file swapping each byte with its neighbor diff --git a/src/certfuzz/fuzzers/truncate.py b/src/certfuzz/fuzzers/truncate.py index 33e4c7f..a91d006 100644 --- a/src/certfuzz/fuzzers/truncate.py +++ b/src/certfuzz/fuzzers/truncate.py @@ -10,9 +10,11 @@ logger = logging.getLogger(__name__) + class TruncateFuzzerError(FuzzerError): pass + class TruncateFuzzer(Fuzzer): ''' This fuzzer module iterates through an input file, dropping an additional diff --git a/src/certfuzz/fuzzers/verify.py b/src/certfuzz/fuzzers/verify.py index b602a87..c575a13 100644 --- a/src/certfuzz/fuzzers/verify.py +++ b/src/certfuzz/fuzzers/verify.py @@ -8,6 +8,7 @@ _files_seen = set() + class VerifyFuzzer(CopyFuzzer): ''' Adds a uniquness function to the CopyFuzzer diff --git a/src/certfuzz/fuzzers/wave.py b/src/certfuzz/fuzzers/wave.py index cf183b3..419f932 100644 --- a/src/certfuzz/fuzzers/wave.py +++ b/src/certfuzz/fuzzers/wave.py @@ -8,12 +8,15 @@ logger = logging.getLogger(__name__) + def fuzz(*args): return WaveFuzzer(*args).fuzz() + class WaveFuzzerError(FuzzerError): pass + class WaveFuzzer(MinimizableFuzzer): def _fuzz(self): """Twiddle bytes of input_file_path and write output to output_file_path""" From 54a84344dab4b3dec40fe9ea2b9d488b769657dc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 3 Jan 2014 12:10:58 -0500 Subject: [PATCH 0009/1169] use new style class declaration --- src/certfuzz/minimizer/minimizer_base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 3ce2ea1..3551d31 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -34,9 +34,7 @@ MAX_OTHER_CRASHES = 20 - - -class Minimizer: +class Minimizer(object): use_watchdog = False def __init__(self, cfg=None, crash=None, crash_dst_dir=None, From 4bfc49675f2e7ee3b53debbad6b409dba8291517 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 3 Jan 2014 12:32:58 -0500 Subject: [PATCH 0010/1169] bump version numbers --- src/certfuzz/defaults.py | 2 +- src/linux/bff.py | 2 +- src/windows/foe2.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/defaults.py b/src/certfuzz/defaults.py index cd259fd..1098baa 100644 --- a/src/certfuzz/defaults.py +++ b/src/certfuzz/defaults.py @@ -3,4 +3,4 @@ @organization: cert.org ''' -__version__ = '2.7' +__version__ = '2.8' diff --git a/src/linux/bff.py b/src/linux/bff.py index 52b3447..5dc26ef 100644 --- a/src/linux/bff.py +++ b/src/linux/bff.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -__version__ = '2.7' +__version__ = '2.8' import os import sys diff --git a/src/windows/foe2.py b/src/windows/foe2.py index d04b593..007fe9a 100644 --- a/src/windows/foe2.py +++ b/src/windows/foe2.py @@ -4,7 +4,7 @@ @organization: cert.org ''' -__version__ = '2.1' +__version__ = '2.2' import sys import logging @@ -72,7 +72,7 @@ def parse_options(): help='Silence messages to screen (log file will remain at INFO level') parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Enable verbose logging messages to screen and log file (overrides --quiet)') - parser.add_option('-c', '--config', dest='configfile', help='Path to config file', + parser.add_option('-c', '--config', dest='configfile', help='Path to config file', default='configs/foe.yaml', metavar='FILE') parser.add_option('-l', '--logfile', dest='logfile', help='Path to log file', metavar='FILE') parser.add_option('-r', '--result-dir', dest='resultdir', help='Path to result directory (overrides config)', metavar='DIR') From 816dc70ec40152add7f8974323e806877591e8eb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 3 Jan 2014 13:24:31 -0500 Subject: [PATCH 0011/1169] create a stateful timer --- src/certfuzz/fuzztools/state_timer.py | 48 +++++++++++++++++++ .../test/fuzztools/test_state_timer.py | 43 +++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/certfuzz/fuzztools/state_timer.py create mode 100644 src/certfuzz/test/fuzztools/test_state_timer.py diff --git a/src/certfuzz/fuzztools/state_timer.py b/src/certfuzz/fuzztools/state_timer.py new file mode 100644 index 0000000..b5d4851 --- /dev/null +++ b/src/certfuzz/fuzztools/state_timer.py @@ -0,0 +1,48 @@ +''' +Created on Jan 3, 2014 + +@author: adh +''' +import time + +class StateTimer(object): + ''' + Implements a timer with multiple states + ''' + def __init__(self, states=None): + self.current_state = None + self.timers = {} + self._in = None + + def _reset(self): + self.current_state = None + self._in = None + + def states(self): + return self.timers.keys() + + def enter_state(self, new_state=None): + if new_state == self.current_state: + # nothing to do + return + # state change + # close out current timer + if self.current_state is not None: + _out = time.time() + _elapsed = _out - self._in + self.timers[self.current_state] += _elapsed + + # start new timer + if new_state is None: + self._reset() + else: + self.current_state = new_state + self._in = time.time() + if not self.current_state in self.timers: + self.timers[self.current_state] = 0.0 + + def time_in(self, state): + if state in self.timers: + return self.timers[state] + else: + return 0.0 diff --git a/src/certfuzz/test/fuzztools/test_state_timer.py b/src/certfuzz/test/fuzztools/test_state_timer.py new file mode 100644 index 0000000..88fefc1 --- /dev/null +++ b/src/certfuzz/test/fuzztools/test_state_timer.py @@ -0,0 +1,43 @@ +''' +Created on Jan 3, 2014 + +@author: adh +''' +import unittest +from certfuzz.fuzztools import state_timer +import time + + +class Test(unittest.TestCase): + def setUp(self): + self.st = state_timer.StateTimer() + + def tearDown(self): + pass + + def test_enter_state(self): + st = self.st + + self.assertEqual(0, len(st.timers)) + st.enter_state('alpha') + self.assertEqual(1, len(st.timers)) + time.sleep(1) + st.enter_state(None) + self.assertEqual(1, len(st.timers)) + self.assertAlmostEqual(1.0, st.time_in('alpha'), 1) + + st.enter_state('alpha') + self.assertEqual(1, len(st.timers)) + time.sleep(1) + st.enter_state('beta') + self.assertEqual(2, len(st.timers)) + self.assertAlmostEqual(2.0, st.time_in('alpha'), 1) + self.assertEqual(0.0, st.time_in('beta')) + time.sleep(1) + st.enter_state(None) + self.assertAlmostEqual(2.0, st.time_in('alpha'), 1) + self.assertAlmostEqual(1.0, st.time_in('beta'), 1) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 887e45387439e54d1be30350bdd350e677a1729b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 3 Jan 2014 14:36:05 -0500 Subject: [PATCH 0012/1169] add __str__ method to state timer object --- src/certfuzz/fuzztools/state_timer.py | 4 ++++ src/certfuzz/test/fuzztools/test_state_timer.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/certfuzz/fuzztools/state_timer.py b/src/certfuzz/fuzztools/state_timer.py index b5d4851..b9944f1 100644 --- a/src/certfuzz/fuzztools/state_timer.py +++ b/src/certfuzz/fuzztools/state_timer.py @@ -5,6 +5,7 @@ ''' import time + class StateTimer(object): ''' Implements a timer with multiple states @@ -14,6 +15,9 @@ def __init__(self, states=None): self.timers = {} self._in = None + def __str__(self): + return 'State Timer - ' + ', '.join('{}: {}'.format(k, v) for k, v in self.timers.iteritems()) + def _reset(self): self.current_state = None self._in = None diff --git a/src/certfuzz/test/fuzztools/test_state_timer.py b/src/certfuzz/test/fuzztools/test_state_timer.py index 88fefc1..6f82571 100644 --- a/src/certfuzz/test/fuzztools/test_state_timer.py +++ b/src/certfuzz/test/fuzztools/test_state_timer.py @@ -38,6 +38,9 @@ def test_enter_state(self): self.assertAlmostEqual(2.0, st.time_in('alpha'), 1) self.assertAlmostEqual(1.0, st.time_in('beta'), 1) + self.assertTrue('alpha' in str(st)) + self.assertTrue('beta' in str(st)) + if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 4a15db1856925ecabbd83389e74b4cb06627f8b9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 3 Jan 2014 14:36:30 -0500 Subject: [PATCH 0013/1169] dump an extra log line for state timers --- src/linux/bff.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/linux/bff.py b/src/linux/bff.py index 5dc26ef..f39422d 100644 --- a/src/linux/bff.py +++ b/src/linux/bff.py @@ -31,6 +31,7 @@ from certfuzz.fuzztools.object_caching import cache_state from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.seedrange import SeedRange +from certfuzz.fuzztools.state_timer import StateTimer from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.fuzztools.zzuf import Zzuf from certfuzz.fuzztools.zzuflog import ZzufLog @@ -56,6 +57,8 @@ SEED_TS = performance.TimeStamper() START_SEED = 0 +STATE_TIMER = StateTimer() + logger = logging.getLogger() logger.name = 'bff' logger.setLevel(0) @@ -111,6 +114,7 @@ def analyze_crasher(cfg, crash): logger.debug('Original debugger file: %s', dbg_out_file_orig) if cfg.minimizecrashers: + STATE_TIMER.enter_state('minimize_testcase') # try to reduce the Hamming Distance between the crasher file and the known good seedfile # crash.fuzzedfile will be replaced with the minimized result try: @@ -130,6 +134,7 @@ def analyze_crasher(cfg, crash): crash.calculate_hamming_distances() if cfg.minimize_to_string: + STATE_TIMER.enter_state('minimize_testcase_to_string') # Minimize to a string of 'x's # crash.fuzzedfile will be replaced with the minimized result try: @@ -144,6 +149,8 @@ def analyze_crasher(cfg, crash): min2string = None touch_watchdog_file(cfg) + STATE_TIMER.enter_state('analyze_testcase') + # get one last debugger output for the newly minimized file if crash.pc_in_function: # change the debugger template @@ -210,6 +217,7 @@ def verify_crasher(c, hashes, cfg, seedfile_set): for crash in crashes: # loop until we're out of crashes to verify logger.debug('crashes to verify: %d', len(crashes)) + STATE_TIMER.enter_state('verify_testcase') # crashes may be added as a result of minimization crash.is_unique = False @@ -439,6 +447,7 @@ def main(): r = sf.rangefinder.next_item() sr.set_s2() + logger.info(STATE_TIMER) while sr.in_range(): # interval.go @@ -464,6 +473,7 @@ def main(): else: quiet_flag = True + STATE_TIMER.enter_state('fuzzing') zzuf = Zzuf(cfg.local_dir, sr.s1, sr.s2, @@ -477,6 +487,7 @@ def main(): quiet_flag, ) saw_crash = zzuf.go() + STATE_TIMER.enter_state('checking_results') if not saw_crash: # we must have made it through this chunk without a crash From f1e2d6e0c05a8b44304ddc0b089aff45d5091047 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 3 Jan 2014 15:21:35 -0500 Subject: [PATCH 0014/1169] add unit test for __str__ method --- src/certfuzz/fuzztools/state_timer.py | 3 ++- src/certfuzz/test/fuzztools/test_state_timer.py | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/fuzztools/state_timer.py b/src/certfuzz/fuzztools/state_timer.py index b9944f1..f805c64 100644 --- a/src/certfuzz/fuzztools/state_timer.py +++ b/src/certfuzz/fuzztools/state_timer.py @@ -14,9 +14,10 @@ def __init__(self, states=None): self.current_state = None self.timers = {} self._in = None + self._delim = ', ' def __str__(self): - return 'State Timer - ' + ', '.join('{}: {}'.format(k, v) for k, v in self.timers.iteritems()) + return 'State Timer - ' + self._delim.join('{}: {}'.format(k, v) for k, v in self.timers.iteritems()) def _reset(self): self.current_state = None diff --git a/src/certfuzz/test/fuzztools/test_state_timer.py b/src/certfuzz/test/fuzztools/test_state_timer.py index 6f82571..8526df3 100644 --- a/src/certfuzz/test/fuzztools/test_state_timer.py +++ b/src/certfuzz/test/fuzztools/test_state_timer.py @@ -15,6 +15,18 @@ def setUp(self): def tearDown(self): pass + def test__str__(self): + # expect no commas in empty timer + delim = self.st._delim + self.assertFalse(delim in str(self.st)) + self.st.enter_state('foo') + self.assertTrue('foo' in str(self.st)) + self.assertFalse(delim in str(self.st)) + self.st.enter_state('bar') + self.assertTrue('foo' in str(self.st)) + self.assertTrue('bar' in str(self.st)) + self.assertTrue(delim in str(self.st)) + def test_enter_state(self): st = self.st @@ -38,9 +50,6 @@ def test_enter_state(self): self.assertAlmostEqual(2.0, st.time_in('alpha'), 1) self.assertAlmostEqual(1.0, st.time_in('beta'), 1) - self.assertTrue('alpha' in str(st)) - self.assertTrue('beta' in str(st)) - if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 951cea75d35e3932bfb182e47698a7684da032f2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 3 Jan 2014 15:31:46 -0500 Subject: [PATCH 0015/1169] add dev_builds to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..213fa5e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dev_builds From 6dde400d1fd26e245d3030216101362701fb60f4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 3 Jan 2014 15:32:12 -0500 Subject: [PATCH 0016/1169] fix import of dir_util --- build/dev/misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/dev/misc.py b/build/dev/misc.py index 9fb3f4d..e1b698f 100644 --- a/build/dev/misc.py +++ b/build/dev/misc.py @@ -5,7 +5,7 @@ ''' import logging import os -import distutils +from distutils import dir_util import shutil logger = logging.getLogger() @@ -36,7 +36,7 @@ def onerror(func, path, exc_info): def copydir(src, dst): logger.info('Copy dir %s -> %s', src, dst) - distutils.dir_util.copy_tree(src, dst) + dir_util.copy_tree(src, dst) def copyfile(src, dst): From 08819659a6012a975c1e6c991231fed480e42d34 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 3 Jan 2014 16:25:27 -0500 Subject: [PATCH 0017/1169] add total time method --- src/certfuzz/fuzztools/state_timer.py | 3 +++ src/certfuzz/test/fuzztools/test_state_timer.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/certfuzz/fuzztools/state_timer.py b/src/certfuzz/fuzztools/state_timer.py index f805c64..babdfef 100644 --- a/src/certfuzz/fuzztools/state_timer.py +++ b/src/certfuzz/fuzztools/state_timer.py @@ -46,6 +46,9 @@ def enter_state(self, new_state=None): if not self.current_state in self.timers: self.timers[self.current_state] = 0.0 + def total_time(self): + return sum(self.timers.itervalues()) + def time_in(self, state): if state in self.timers: return self.timers[state] diff --git a/src/certfuzz/test/fuzztools/test_state_timer.py b/src/certfuzz/test/fuzztools/test_state_timer.py index 8526df3..3811200 100644 --- a/src/certfuzz/test/fuzztools/test_state_timer.py +++ b/src/certfuzz/test/fuzztools/test_state_timer.py @@ -15,6 +15,11 @@ def setUp(self): def tearDown(self): pass + def test_total_time(self): + self.assertEqual(0.0, self.st.total_time()) + self.st.timers = {'a': 1.0, 'b': 2.0, 'c': 3.0} + self.assertEqual(6.0, self.st.total_time()) + def test__str__(self): # expect no commas in empty timer delim = self.st._delim From 5cfa0f29f117aed1eaad33914c382f71988da001 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 10:41:27 -0500 Subject: [PATCH 0018/1169] =?UTF-8?q?ignore=20meta-test=20for=20errors.py?= =?UTF-8?q?=20modules=20=E2=80=94=20there=E2=80=99s=20nothing=20to=20test?= =?UTF-8?q?=20in=20them=20remove=20test=5Ferrors=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/analyzers/callgrind/test_errors.py | 21 ------------------ src/certfuzz/test/android/api/test_errors.py | 22 ------------------- .../test/android/avd_mgr/test_errors.py | 22 ------------------- .../test/android/controller/test_errors.py | 22 ------------------- src/certfuzz/test/android/test_errors.py | 20 ----------------- .../test/android/worker/test_errors.py | 19 ---------------- src/certfuzz/test/campaign/test_errors.py | 21 ------------------ src/certfuzz/test/crash/test_errors.py | 21 ------------------ .../test/db/couchdb/datatypes/test_errors.py | 22 ------------------- src/certfuzz/test/db/couchdb/test_errors.py | 22 ------------------- src/certfuzz/test/db/test_errors.py | 22 ------------------- .../debuggers/output_parsers/test_errors.py | 5 ----- src/certfuzz/test/debuggers/test_errors.py | 21 ------------------ .../test/file_handlers/test_errors.py | 21 ------------------ src/certfuzz/test/fuzzers/test_errors.py | 21 ------------------ src/certfuzz/test/minimizer/test_errors.py | 21 ------------------ src/certfuzz/test/runners/test_errors.py | 21 ------------------ .../multiarmed_bandit/arms/test_errors.py | 22 ------------------- src/certfuzz/test/scoring/test_errors.py | 22 ------------------- src/certfuzz/test/test_errors.py | 21 ------------------ src/certfuzz/test/test_meta.py | 7 ++++++ 21 files changed, 7 insertions(+), 409 deletions(-) delete mode 100644 src/certfuzz/test/analyzers/callgrind/test_errors.py delete mode 100644 src/certfuzz/test/android/api/test_errors.py delete mode 100644 src/certfuzz/test/android/avd_mgr/test_errors.py delete mode 100644 src/certfuzz/test/android/controller/test_errors.py delete mode 100644 src/certfuzz/test/android/test_errors.py delete mode 100644 src/certfuzz/test/android/worker/test_errors.py delete mode 100644 src/certfuzz/test/campaign/test_errors.py delete mode 100644 src/certfuzz/test/crash/test_errors.py delete mode 100644 src/certfuzz/test/db/couchdb/datatypes/test_errors.py delete mode 100644 src/certfuzz/test/db/couchdb/test_errors.py delete mode 100644 src/certfuzz/test/db/test_errors.py delete mode 100644 src/certfuzz/test/debuggers/output_parsers/test_errors.py delete mode 100644 src/certfuzz/test/debuggers/test_errors.py delete mode 100644 src/certfuzz/test/file_handlers/test_errors.py delete mode 100644 src/certfuzz/test/fuzzers/test_errors.py delete mode 100644 src/certfuzz/test/minimizer/test_errors.py delete mode 100644 src/certfuzz/test/runners/test_errors.py delete mode 100644 src/certfuzz/test/scoring/multiarmed_bandit/arms/test_errors.py delete mode 100644 src/certfuzz/test/scoring/test_errors.py delete mode 100644 src/certfuzz/test/test_errors.py diff --git a/src/certfuzz/test/analyzers/callgrind/test_errors.py b/src/certfuzz/test/analyzers/callgrind/test_errors.py deleted file mode 100644 index cfce113..0000000 --- a/src/certfuzz/test/analyzers/callgrind/test_errors.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/api/test_errors.py b/src/certfuzz/test/android/api/test_errors.py deleted file mode 100644 index 115b88d..0000000 --- a/src/certfuzz/test/android/api/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.api import errors - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/avd_mgr/test_errors.py b/src/certfuzz/test/android/avd_mgr/test_errors.py deleted file mode 100644 index 78a03e7..0000000 --- a/src/certfuzz/test/android/avd_mgr/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.avd_mgr import errors - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/controller/test_errors.py b/src/certfuzz/test/android/controller/test_errors.py deleted file mode 100644 index 20a1f06..0000000 --- a/src/certfuzz/test/android/controller/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.android.controller import errors - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/test_errors.py b/src/certfuzz/test/android/test_errors.py deleted file mode 100644 index 0b7d568..0000000 --- a/src/certfuzz/test/android/test_errors.py +++ /dev/null @@ -1,20 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.android import errors -from certfuzz.errors import CERTFuzzError - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/worker/test_errors.py b/src/certfuzz/test/android/worker/test_errors.py deleted file mode 100644 index 085fd82..0000000 --- a/src/certfuzz/test/android/worker/test_errors.py +++ /dev/null @@ -1,19 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.android.worker import errors - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/campaign/test_errors.py b/src/certfuzz/test/campaign/test_errors.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/campaign/test_errors.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/crash/test_errors.py b/src/certfuzz/test/crash/test_errors.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/crash/test_errors.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/db/couchdb/datatypes/test_errors.py b/src/certfuzz/test/db/couchdb/datatypes/test_errors.py deleted file mode 100644 index 408beb1..0000000 --- a/src/certfuzz/test/db/couchdb/datatypes/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Mar 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.db.couchdb.datatypes import errors - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/db/couchdb/test_errors.py b/src/certfuzz/test/db/couchdb/test_errors.py deleted file mode 100644 index a073a53..0000000 --- a/src/certfuzz/test/db/couchdb/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Mar 18, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.db.couchdb import errors - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/db/test_errors.py b/src/certfuzz/test/db/test_errors.py deleted file mode 100644 index 71756d2..0000000 --- a/src/certfuzz/test/db/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Mar 18, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.db import errors - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/debuggers/output_parsers/test_errors.py b/src/certfuzz/test/debuggers/output_parsers/test_errors.py deleted file mode 100644 index cc43128..0000000 --- a/src/certfuzz/test/debuggers/output_parsers/test_errors.py +++ /dev/null @@ -1,5 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' diff --git a/src/certfuzz/test/debuggers/test_errors.py b/src/certfuzz/test/debuggers/test_errors.py deleted file mode 100644 index cfce113..0000000 --- a/src/certfuzz/test/debuggers/test_errors.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/file_handlers/test_errors.py b/src/certfuzz/test/file_handlers/test_errors.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/file_handlers/test_errors.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/fuzzers/test_errors.py b/src/certfuzz/test/fuzzers/test_errors.py deleted file mode 100644 index cfce113..0000000 --- a/src/certfuzz/test/fuzzers/test_errors.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/minimizer/test_errors.py b/src/certfuzz/test/minimizer/test_errors.py deleted file mode 100644 index 6e33993..0000000 --- a/src/certfuzz/test/minimizer/test_errors.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Apr 10, 2012 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/runners/test_errors.py b/src/certfuzz/test/runners/test_errors.py deleted file mode 100644 index cfce113..0000000 --- a/src/certfuzz/test/runners/test_errors.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_errors.py b/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_errors.py deleted file mode 100644 index 6fcd261..0000000 --- a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.scoring.multiarmed_bandit.arms import errors - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/scoring/test_errors.py b/src/certfuzz/test/scoring/test_errors.py deleted file mode 100644 index 2cbac22..0000000 --- a/src/certfuzz/test/scoring/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.scoring import errors - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/test_errors.py b/src/certfuzz/test/test_errors.py deleted file mode 100644 index 952e484..0000000 --- a/src/certfuzz/test/test_errors.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/test_meta.py b/src/certfuzz/test/test_meta.py index 7ca2337..8e65bc4 100644 --- a/src/certfuzz/test/test_meta.py +++ b/src/certfuzz/test/test_meta.py @@ -11,6 +11,7 @@ basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'certfuzz')) ignorelist = ['obsolete', 'dist'] + def find_packages(d): dirlist = [os.path.join(d, x) for x in os.listdir(d) if not x in ignorelist] dirs = [x for x in dirlist if os.path.isdir(x)] @@ -22,6 +23,7 @@ def find_packages(d): pkgs.extend(subpkgs) return pkgs + def non_tst_packages(d): pkglist = [] for path in find_packages(d): @@ -37,10 +39,12 @@ def find_all_modules(d): ignore = lambda f: any(['%s%s%s' % (os.sep, x, os.sep) in f for x in ignore_list]) return [x for x in filetools.all_files(d, "*.py") if not ignore(x)] + def find_modules(d): # ignore __init__.py modules return [x for x in find_all_modules(d) if not x.endswith('__init__.py')] + class Test(unittest.TestCase): def setUp(self): @@ -67,8 +71,11 @@ def test_each_package_has_a_test_package(self): def test_each_module_has_a_test_module(self): module_list = find_modules(self.basedir) missing_modules = [] + _ignored_modules = set(['errors']) for m in module_list: d, b = os.path.split(m) + if b in _ignored_modules: + continue test_b = 'test_%s' % b relpath = os.path.relpath(d, basedir) test_path = os.path.join(basedir, 'test', relpath, test_b) From a22522bb7913dc2eb4fc4fe1d1a72c70369fee2a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 11:34:03 -0500 Subject: [PATCH 0019/1169] fix file name --- src/certfuzz/test/test_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/test/test_meta.py b/src/certfuzz/test/test_meta.py index 8e65bc4..3ddeaaf 100644 --- a/src/certfuzz/test/test_meta.py +++ b/src/certfuzz/test/test_meta.py @@ -71,7 +71,7 @@ def test_each_package_has_a_test_package(self): def test_each_module_has_a_test_module(self): module_list = find_modules(self.basedir) missing_modules = [] - _ignored_modules = set(['errors']) + _ignored_modules = set(['errors.py']) for m in module_list: d, b = os.path.split(m) if b in _ignored_modules: From 6c8572aad18c0147d389c21b30f6fd338da1ce59 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 11:37:02 -0500 Subject: [PATCH 0020/1169] pep8 cleanup --- src/certfuzz/errors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/errors.py b/src/certfuzz/errors.py index 3799126..4c1f4ab 100644 --- a/src/certfuzz/errors.py +++ b/src/certfuzz/errors.py @@ -3,5 +3,7 @@ @organization: cert.org ''' + + class CERTFuzzError(Exception): pass From e6568f1cf0e070713046e4564102b8704013d94c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 12:57:35 -0500 Subject: [PATCH 0021/1169] move analyzer errors to their own module --- src/certfuzz/analyzers/analyzer_base.py | 27 +-------------------- src/certfuzz/analyzers/errors.py | 32 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 26 deletions(-) create mode 100644 src/certfuzz/analyzers/errors.py diff --git a/src/certfuzz/analyzers/analyzer_base.py b/src/certfuzz/analyzers/analyzer_base.py index fcadbea..6240f76 100644 --- a/src/certfuzz/analyzers/analyzer_base.py +++ b/src/certfuzz/analyzers/analyzer_base.py @@ -7,37 +7,12 @@ import os from ..fuzztools import subprocess_helper as subp +from .errors import AnalyzerOutputMissingError, AnalyzerEmptyOutputError logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -class AnalyzerError(Exception): - pass - - -class AnalyzerOutputMissingError(AnalyzerError): - ''' - Exception class for missing output files - ''' - def __init__(self, f): - self.file = f - - def __str__(self): - return "Expected output file is missing: %s" % self.file - - -class AnalyzerEmptyOutputError(AnalyzerError): - ''' - Exception class for missing output files - ''' - def __init__(self, f): - self.file = f - - def __str__(self): - return "Output file is empty: %s" % self.file - - class Analyzer(object): ''' classdocs diff --git a/src/certfuzz/analyzers/errors.py b/src/certfuzz/analyzers/errors.py new file mode 100644 index 0000000..ec6315a --- /dev/null +++ b/src/certfuzz/analyzers/errors.py @@ -0,0 +1,32 @@ +''' +Created on Jan 10, 2014 + +@author: adh +''' +from ..errors import CERTFuzzError + + +class AnalyzerError(CERTFuzzError): + pass + + +class AnalyzerOutputMissingError(AnalyzerError): + ''' + Exception class for missing output files + ''' + def __init__(self, f): + self.file = f + + def __str__(self): + return "Expected output file is missing: %s" % self.file + + +class AnalyzerEmptyOutputError(AnalyzerError): + ''' + Exception class for missing output files + ''' + def __init__(self, f): + self.file = f + + def __str__(self): + return "Output file is empty: %s" % self.file From 1b51f9f221058aa62e61f503e125d48b32db4c08 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 13:05:47 -0500 Subject: [PATCH 0022/1169] move campaign errors to their own module --- src/certfuzz/campaign/campaign_base.py | 4 ---- src/certfuzz/campaign/config/__init__.py | 1 - src/certfuzz/campaign/config/config_base.py | 4 ---- src/certfuzz/campaign/config/errors.py | 10 ++++++++++ src/certfuzz/campaign/config/foe_config.py | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 src/certfuzz/campaign/config/errors.py diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 620dc17..de30500 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -22,10 +22,6 @@ def import_module_by_name(name, logger=None): return module -class CampaignError(Exception): - pass - - class CampaignBase(object): __metaclass__ = abc.ABCMeta diff --git a/src/certfuzz/campaign/config/__init__.py b/src/certfuzz/campaign/config/__init__.py index c22455f..e2f7882 100644 --- a/src/certfuzz/campaign/config/__init__.py +++ b/src/certfuzz/campaign/config/__init__.py @@ -1,3 +1,2 @@ from .config_base import parse_yaml from .config_base import Config -from .config_base import ConfigError diff --git a/src/certfuzz/campaign/config/config_base.py b/src/certfuzz/campaign/config/config_base.py index 39b060c..534421d 100644 --- a/src/certfuzz/campaign/config/config_base.py +++ b/src/certfuzz/campaign/config/config_base.py @@ -14,10 +14,6 @@ def parse_yaml(yaml_file): return yaml.load(open(yaml_file, 'r')) -class ConfigError(Exception): - pass - - class Config(object): ''' If you are inheriting this class, add validation methods to self.validations diff --git a/src/certfuzz/campaign/config/errors.py b/src/certfuzz/campaign/config/errors.py new file mode 100644 index 0000000..5f778c1 --- /dev/null +++ b/src/certfuzz/campaign/config/errors.py @@ -0,0 +1,10 @@ +''' +Created on Jan 10, 2014 + +@author: adh +''' +from ..errors import CERTFuzzError + + +class ConfigError(CERTFuzzError): + pass diff --git a/src/certfuzz/campaign/config/foe_config.py b/src/certfuzz/campaign/config/foe_config.py index 4809030..c4803fa 100644 --- a/src/certfuzz/campaign/config/foe_config.py +++ b/src/certfuzz/campaign/config/foe_config.py @@ -7,7 +7,7 @@ from string import Template from . import Config as ConfigBase -from . import ConfigError +from .errors import ConfigError from ...helpers import quoted import shlex From 4916c5b8e51ff4ec457dd8ced8cff7cc7ca306c0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 13:11:13 -0500 Subject: [PATCH 0023/1169] move file_handler errors to their own module --- src/certfuzz/file_handlers/directory.py | 4 +--- src/certfuzz/file_handlers/errors.py | 4 ++++ src/certfuzz/file_handlers/seedfile_set.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/file_handlers/directory.py b/src/certfuzz/file_handlers/directory.py index 995b7fc..c8a22c7 100644 --- a/src/certfuzz/file_handlers/directory.py +++ b/src/certfuzz/file_handlers/directory.py @@ -7,12 +7,10 @@ from ..fuzztools import filetools from .basicfile import BasicFile import logging +from .errors import DirectoryError logger = logging.getLogger(__name__) -class DirectoryError(Exception): - pass - blacklist = ['.DS_Store', ] diff --git a/src/certfuzz/file_handlers/errors.py b/src/certfuzz/file_handlers/errors.py index 685023b..0afca7f 100644 --- a/src/certfuzz/file_handlers/errors.py +++ b/src/certfuzz/file_handlers/errors.py @@ -20,3 +20,7 @@ class FuzzedFileError(BasicFileError): class SeedFileError(BasicFileError): pass + + +class DirectoryError(FileHandlerError): + pass diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index f162a14..2a58871 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -8,7 +8,8 @@ from .directory import Directory from ..fuzztools import filetools -from .seedfile import SeedFile, SeedFileError +from .seedfile import SeedFile +from .errors import SeedFileError from ..scoring.scorable_set import ScorableSet2, EmptySetError logger = logging.getLogger(__name__) From 2f93c55d052ff4e757ede88bf0f3017d4dcabc53 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 13:20:43 -0500 Subject: [PATCH 0024/1169] move fuzzer errors to their own module --- src/certfuzz/fuzzers/__init__.py | 3 --- src/certfuzz/fuzzers/drop.py | 7 +----- src/certfuzz/fuzzers/fuzzer_base.py | 25 ------------------- src/certfuzz/fuzzers/insert.py | 7 +----- src/certfuzz/fuzzers/swap.py | 7 ++---- src/certfuzz/fuzzers/truncate.py | 7 +----- src/certfuzz/fuzzers/wave.py | 7 +----- src/certfuzz/test/fuzzers/test_drop.py | 4 +-- src/certfuzz/test/fuzzers/test_fuzzers_pkg.py | 4 +-- src/certfuzz/test/fuzzers/test_insert.py | 4 +-- src/certfuzz/test/fuzzers/test_swap.py | 4 +-- src/certfuzz/test/fuzzers/test_truncate.py | 4 +-- src/certfuzz/test/fuzzers/test_wave.py | 4 +-- 13 files changed, 17 insertions(+), 70 deletions(-) diff --git a/src/certfuzz/fuzzers/__init__.py b/src/certfuzz/fuzzers/__init__.py index b29e912..fb387ed 100644 --- a/src/certfuzz/fuzzers/__init__.py +++ b/src/certfuzz/fuzzers/__init__.py @@ -1,5 +1,2 @@ from .fuzzer_base import Fuzzer from .fuzzer_base import MinimizableFuzzer -from .errors import FuzzerError -from .errors import FuzzerExhaustedError -from .errors import FuzzerInputMatchesOutputError diff --git a/src/certfuzz/fuzzers/drop.py b/src/certfuzz/fuzzers/drop.py index c03235d..9925a48 100644 --- a/src/certfuzz/fuzzers/drop.py +++ b/src/certfuzz/fuzzers/drop.py @@ -1,17 +1,12 @@ """ """ from . import Fuzzer -from . import FuzzerError -from . import FuzzerExhaustedError +from .errors import FuzzerExhaustedError import logging logger = logging.getLogger(__name__) -class DropFuzzerError(FuzzerError): - pass - - class DropFuzzer(Fuzzer): ''' This fuzzer module iterates through an input file, dropping each byte position diff --git a/src/certfuzz/fuzzers/fuzzer_base.py b/src/certfuzz/fuzzers/fuzzer_base.py index 5031965..d424578 100644 --- a/src/certfuzz/fuzzers/fuzzer_base.py +++ b/src/certfuzz/fuzzers/fuzzer_base.py @@ -9,12 +9,9 @@ import zipfile import collections -from ..fuzztools.hamming import bytewise_hd, bitwise_hd from ..fuzztools.filetools import write_file from ..fuzztools.filetools import find_or_create_dir from ..helpers import log_object -from .errors import FuzzerInputMatchesOutputError - MAXDEPTH = 3 SLEEPTIMER = 0.5 @@ -247,25 +244,3 @@ def _postfuzz(self): # get the byte string version of the archive and put in self.fuzzed self.fuzzed = inmemzip.getvalue() inmemzip.close() - -# def bitwise_hd(self): -# ''' -# Convenience function that returns the bitwise hamming distance -# between input and fuzzed -# ''' -# fuzzed = [chr(x) for x in self.fuzzed] -# return bitwise_hd(self.input, fuzzed) -# -# def bytewise_hd(self): -# ''' -# Convenience function that returns the bytewise hamming distance -# between input and fuzzed -# ''' -# fuzzed = [chr(x) for x in self.fuzzed] -# return bytewise_hd(self.input, fuzzed) -# -# def fuzzed_bit_ratio(self): -# return self.bitwise_hd() / float(len(self.input) * 8) -# -# def fuzzed_byte_ratio(self): -# return self.bytewise_hd() / float(len(self.input)) diff --git a/src/certfuzz/fuzzers/insert.py b/src/certfuzz/fuzzers/insert.py index 90575b9..ece821e 100644 --- a/src/certfuzz/fuzzers/insert.py +++ b/src/certfuzz/fuzzers/insert.py @@ -1,18 +1,13 @@ """ """ from . import Fuzzer -from . import FuzzerError -from . import FuzzerExhaustedError +from .errors import FuzzerExhaustedError import logging from random import getrandbits logger = logging.getLogger(__name__) -class InsertFuzzerError(FuzzerError): - pass - - class InsertFuzzer(Fuzzer): ''' This fuzzer module iterates through an input file, inserting a random byte diff --git a/src/certfuzz/fuzzers/swap.py b/src/certfuzz/fuzzers/swap.py index 49e4351..879318d 100644 --- a/src/certfuzz/fuzzers/swap.py +++ b/src/certfuzz/fuzzers/swap.py @@ -2,15 +2,12 @@ bytes. """ import logging -from . import MinimizableFuzzer, FuzzerError, FuzzerExhaustedError +from . import MinimizableFuzzer +from .errors import FuzzerExhaustedError logger = logging.getLogger(__name__) -class SwapFuzzerError(FuzzerError): - pass - - class SwapFuzzer(MinimizableFuzzer): ''' Step through the input file swapping each byte with its neighbor diff --git a/src/certfuzz/fuzzers/truncate.py b/src/certfuzz/fuzzers/truncate.py index a91d006..c67ed44 100644 --- a/src/certfuzz/fuzzers/truncate.py +++ b/src/certfuzz/fuzzers/truncate.py @@ -4,17 +4,12 @@ @organization: cert.org ''' from . import Fuzzer -from . import FuzzerError -from . import FuzzerExhaustedError +from .errors import FuzzerExhaustedError import logging logger = logging.getLogger(__name__) -class TruncateFuzzerError(FuzzerError): - pass - - class TruncateFuzzer(Fuzzer): ''' This fuzzer module iterates through an input file, dropping an additional diff --git a/src/certfuzz/fuzzers/wave.py b/src/certfuzz/fuzzers/wave.py index 419f932..ad2e79e 100644 --- a/src/certfuzz/fuzzers/wave.py +++ b/src/certfuzz/fuzzers/wave.py @@ -2,8 +2,7 @@ as it goes. E.g. try 0-255 for the first byte, 0-255 for the second byte, etc. """ from . import MinimizableFuzzer -from . import FuzzerError -from . import FuzzerExhaustedError +from .errors import FuzzerExhaustedError import logging logger = logging.getLogger(__name__) @@ -13,10 +12,6 @@ def fuzz(*args): return WaveFuzzer(*args).fuzz() -class WaveFuzzerError(FuzzerError): - pass - - class WaveFuzzer(MinimizableFuzzer): def _fuzz(self): """Twiddle bytes of input_file_path and write output to output_file_path""" diff --git a/src/certfuzz/test/fuzzers/test_drop.py b/src/certfuzz/test/fuzzers/test_drop.py index e35d80c..02eff41 100644 --- a/src/certfuzz/test/fuzzers/test_drop.py +++ b/src/certfuzz/test/fuzzers/test_drop.py @@ -8,14 +8,14 @@ from certfuzz.fuzzers.drop import DropFuzzer import certfuzz.fuzzers.drop import shutil -import os -from certfuzz.fuzzers import FuzzerExhaustedError +from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging from certfuzz.test import MockSeedfile import tempfile certfuzz.fuzzers.drop.logger.setLevel(logging.WARNING) + class Test(unittest.TestCase): def setUp(self): diff --git a/src/certfuzz/test/fuzzers/test_fuzzers_pkg.py b/src/certfuzz/test/fuzzers/test_fuzzers_pkg.py index 2f3b3d7..7843f04 100644 --- a/src/certfuzz/test/fuzzers/test_fuzzers_pkg.py +++ b/src/certfuzz/test/fuzzers/test_fuzzers_pkg.py @@ -7,6 +7,7 @@ from certfuzz.test import misc import certfuzz.fuzzers + class Test(unittest.TestCase): def setUp(self): @@ -19,9 +20,6 @@ def test_api(self): module = certfuzz.fuzzers api_list = ['Fuzzer', 'MinimizableFuzzer', - 'FuzzerError', - 'FuzzerExhaustedError', - 'FuzzerInputMatchesOutputError', ] (is_fail, msg) = misc.check_for_apis(module, api_list) self.assertFalse(is_fail, msg) diff --git a/src/certfuzz/test/fuzzers/test_insert.py b/src/certfuzz/test/fuzzers/test_insert.py index 4438651..bac59e4 100644 --- a/src/certfuzz/test/fuzzers/test_insert.py +++ b/src/certfuzz/test/fuzzers/test_insert.py @@ -8,14 +8,14 @@ from certfuzz.fuzzers.insert import InsertFuzzer import certfuzz.fuzzers.insert import shutil -import os -from certfuzz.fuzzers import FuzzerExhaustedError +from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging from certfuzz.test import MockSeedfile import tempfile certfuzz.fuzzers.insert.logger.setLevel(logging.WARNING) + class Test(unittest.TestCase): def setUp(self): diff --git a/src/certfuzz/test/fuzzers/test_swap.py b/src/certfuzz/test/fuzzers/test_swap.py index 3a4fcb0..902085b 100644 --- a/src/certfuzz/test/fuzzers/test_swap.py +++ b/src/certfuzz/test/fuzzers/test_swap.py @@ -6,11 +6,11 @@ import unittest from certfuzz.fuzzers.swap import SwapFuzzer from certfuzz.test import MockSeedfile -from certfuzz.fuzzers import FuzzerExhaustedError -import os +from certfuzz.fuzzers.errors import FuzzerExhaustedError import shutil import tempfile + class Test(unittest.TestCase): def setUp(self): diff --git a/src/certfuzz/test/fuzzers/test_truncate.py b/src/certfuzz/test/fuzzers/test_truncate.py index b87f503..e1d0442 100644 --- a/src/certfuzz/test/fuzzers/test_truncate.py +++ b/src/certfuzz/test/fuzzers/test_truncate.py @@ -8,14 +8,14 @@ from certfuzz.fuzzers.truncate import TruncateFuzzer import certfuzz.fuzzers.drop import shutil -import os -from certfuzz.fuzzers import FuzzerExhaustedError +from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging from certfuzz.test import MockSeedfile import tempfile certfuzz.fuzzers.drop.logger.setLevel(logging.WARNING) + class Test(unittest.TestCase): def setUp(self): diff --git a/src/certfuzz/test/fuzzers/test_wave.py b/src/certfuzz/test/fuzzers/test_wave.py index 6b71fe3..e93838d 100644 --- a/src/certfuzz/test/fuzzers/test_wave.py +++ b/src/certfuzz/test/fuzzers/test_wave.py @@ -8,14 +8,14 @@ from certfuzz.fuzzers.wave import WaveFuzzer import certfuzz.fuzzers.wave import shutil -import os -from certfuzz.fuzzers import FuzzerExhaustedError +from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging from certfuzz.test import MockSeedfile import tempfile certfuzz.fuzzers.wave.logger.setLevel(logging.WARNING) + class Test(unittest.TestCase): def setUp(self): From bf58ad9d78a87f01d38addd4e2010d74a686e7da Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 13:47:54 -0500 Subject: [PATCH 0025/1169] move fuzztools errors to their own module --- src/certfuzz/fuzztools/distance_matrix.py | 5 +---- src/certfuzz/fuzztools/errors.py | 22 +++++++++++++++++++++ src/certfuzz/fuzztools/hostinfo.py | 8 -------- src/certfuzz/fuzztools/rangefinder.py | 4 ++-- src/certfuzz/fuzztools/similarity_matrix.py | 7 +++---- 5 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 src/certfuzz/fuzztools/errors.py diff --git a/src/certfuzz/fuzztools/distance_matrix.py b/src/certfuzz/fuzztools/distance_matrix.py index a9d8672..c0de2bf 100644 --- a/src/certfuzz/fuzztools/distance_matrix.py +++ b/src/certfuzz/fuzztools/distance_matrix.py @@ -8,15 +8,12 @@ import numpy import hcluster import logging +from .errors import DistanceMatrixError logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -class DistanceMatrixError(Exception): - pass - - class DistanceMatrix(object): def __init__(self, similarity_dict): self.sim = similarity_dict diff --git a/src/certfuzz/fuzztools/errors.py b/src/certfuzz/fuzztools/errors.py new file mode 100644 index 0000000..2e9d132 --- /dev/null +++ b/src/certfuzz/fuzztools/errors.py @@ -0,0 +1,22 @@ +''' +Created on Jan 10, 2014 + +@author: adh +''' +from ..errors import CERTFuzzError + + +class FuzztoolError(CERTFuzzError): + pass + + +class DistanceMatrixError(FuzztoolError): + pass + + +class RangeFinderError(Exception): + pass + + +class SimilarityMatrixError(Exception): + pass diff --git a/src/certfuzz/fuzztools/hostinfo.py b/src/certfuzz/fuzztools/hostinfo.py index c9012a3..2839e9b 100644 --- a/src/certfuzz/fuzztools/hostinfo.py +++ b/src/certfuzz/fuzztools/hostinfo.py @@ -8,14 +8,6 @@ system = platform.system() -class HostInfoError(Exception): - pass - -class UnsupportedPlatformError(HostInfoError): - def __init__(self, system): - self.system = system - def __str__(self): - return "'%s' is not a supported platform." % self.system class HostInfo(object): def __init__(self): diff --git a/src/certfuzz/fuzztools/rangefinder.py b/src/certfuzz/fuzztools/rangefinder.py index afa5985..28a631c 100644 --- a/src/certfuzz/fuzztools/rangefinder.py +++ b/src/certfuzz/fuzztools/rangefinder.py @@ -8,12 +8,12 @@ from ..scoring.scorable_set import ScorableSet2 from .range import Range +from .errors import RangeFinderError range_scale_factor = (math.sqrt(5) + 1.0) / 2.0 logger = logging.getLogger(__name__) -class RangeFinderError(Exception): - pass + class RangeFinder(ScorableSet2): ''' diff --git a/src/certfuzz/fuzztools/similarity_matrix.py b/src/certfuzz/fuzztools/similarity_matrix.py index 311718c..7e094b9 100644 --- a/src/certfuzz/fuzztools/similarity_matrix.py +++ b/src/certfuzz/fuzztools/similarity_matrix.py @@ -9,15 +9,14 @@ import sys import operator -from ..fuzztools.filetools import all_files_nonzero_length -from ..fuzztools.vectors import compare +from .filetools import all_files_nonzero_length +from .vectors import compare from ..analyzers.callgrind.annotation_file import AnnotationFile +from .errors import SimilarityMatrixError logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -class SimilarityMatrixError(Exception): - pass class SimilarityMatrix(object): def __init__(self, dirs): From 2f0fe9ccd391cbc760b8d881a0988d3ed75f3bfb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 13:50:31 -0500 Subject: [PATCH 0026/1169] move scoring errors to their own module --- src/certfuzz/scoring/errors.py | 6 +++--- src/certfuzz/scoring/scorable_thing.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/scoring/errors.py b/src/certfuzz/scoring/errors.py index 9cc71f6..152d776 100644 --- a/src/certfuzz/scoring/errors.py +++ b/src/certfuzz/scoring/errors.py @@ -5,14 +5,14 @@ ''' from certfuzz.errors import CERTFuzzError + class ScoringError(CERTFuzzError): pass + class ScorableSetError(ScoringError): pass -class EmptySetError(ScorableSetError): - pass -class ScorableThingError(ScoringError): +class EmptySetError(ScorableSetError): pass diff --git a/src/certfuzz/scoring/scorable_thing.py b/src/certfuzz/scoring/scorable_thing.py index e26b1dd..cdcb692 100644 --- a/src/certfuzz/scoring/scorable_thing.py +++ b/src/certfuzz/scoring/scorable_thing.py @@ -6,6 +6,7 @@ import json from ..helpers import random_str + def beta_estimate(m, N, a_prior=1.0, b_prior=1.0): numerator = alpha = m + a_prior l = N - m @@ -14,8 +15,6 @@ def beta_estimate(m, N, a_prior=1.0, b_prior=1.0): p_success = float(numerator) / float(denominator) return (alpha, beta, p_success) -class ScorableThingError(Exception): - pass class ScorableThing(object): def __init__(self, key=None, a=None, b=None, uniques_only=True): From ca7104970c56c66b1b2308eceb0c5056612e54a0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 13:56:00 -0500 Subject: [PATCH 0027/1169] fix imports so unit tests run --- src/certfuzz/analyzers/__init__.py | 3 --- src/certfuzz/analyzers/callgrind/errors.py | 2 +- src/certfuzz/campaign/__init__.py | 1 - src/certfuzz/campaign/campaign.py | 4 ++-- src/certfuzz/scoring/__init__.py | 1 - src/certfuzz/test/scoring/test_scorable_thing.py | 3 ++- 6 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/analyzers/__init__.py b/src/certfuzz/analyzers/__init__.py index b7e8c52..a46b468 100644 --- a/src/certfuzz/analyzers/__init__.py +++ b/src/certfuzz/analyzers/__init__.py @@ -1,4 +1 @@ from .analyzer_base import Analyzer -from .analyzer_base import AnalyzerEmptyOutputError -from .analyzer_base import AnalyzerError -from .analyzer_base import AnalyzerOutputMissingError diff --git a/src/certfuzz/analyzers/callgrind/errors.py b/src/certfuzz/analyzers/callgrind/errors.py index 432ac1f..97de912 100644 --- a/src/certfuzz/analyzers/callgrind/errors.py +++ b/src/certfuzz/analyzers/callgrind/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from .. import AnalyzerError +from ..errors import AnalyzerError class CallgrindAnnotateError(AnalyzerError): diff --git a/src/certfuzz/campaign/__init__.py b/src/certfuzz/campaign/__init__.py index 1d78550..ae27afd 100644 --- a/src/certfuzz/campaign/__init__.py +++ b/src/certfuzz/campaign/__init__.py @@ -1,4 +1,3 @@ from .. import __version__ from .campaign_base import import_module_by_name -from .campaign_base import CampaignError from .campaign_base import CampaignBase diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index bf7a220..b98a6b0 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -19,14 +19,14 @@ from . import __version__ from . import import_module_by_name from . import CampaignBase -from . import CampaignError +from .errors import CampaignError from .config.foe_config import Config from .iteration import Iteration from ..debuggers import registration from ..fuzztools import filetools from ..file_handlers.seedfile_set import SeedfileSet -from ..fuzzers import FuzzerExhaustedError +from ..fuzzers.errors import FuzzerExhaustedError from ..scoring.scorable_set import EmptySetError from ..runners import RunnerArchitectureError from ..runners.killableprocess import Popen diff --git a/src/certfuzz/scoring/__init__.py b/src/certfuzz/scoring/__init__.py index 7f12bd7..df6bfdf 100644 --- a/src/certfuzz/scoring/__init__.py +++ b/src/certfuzz/scoring/__init__.py @@ -1,3 +1,2 @@ -from .errors import ScorableSetError, ScorableThingError, ScoringError from .scorable_set import ScorableSet2 from .scorable_thing import ScorableThing diff --git a/src/certfuzz/test/scoring/test_scorable_thing.py b/src/certfuzz/test/scoring/test_scorable_thing.py index 9d4a2eb..546dc32 100644 --- a/src/certfuzz/test/scoring/test_scorable_thing.py +++ b/src/certfuzz/test/scoring/test_scorable_thing.py @@ -4,7 +4,8 @@ @organization: cert.org ''' import unittest -from certfuzz.scoring.scorable_thing import ScorableThing, ScorableThingError +from certfuzz.scoring.scorable_thing import ScorableThing + class Test(unittest.TestCase): From 00b21633c6e2eb7a6ef63dee2e827b71df22e8d1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 14:29:44 -0500 Subject: [PATCH 0028/1169] refactor windows campaign into separate module --- src/certfuzz/campaign/campaign.py | 96 +------------------- src/certfuzz/campaign/campaign_windows.py | 102 ++++++++++++++++++++++ src/windows/foe2.py | 2 +- 3 files changed, 106 insertions(+), 94 deletions(-) create mode 100644 src/certfuzz/campaign/campaign_windows.py diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index b98a6b0..9c65c54 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -6,14 +6,11 @@ import logging import os -import sys import shutil import tempfile import traceback import re import cPickle as pickle -from threading import Timer -import platform import gc from . import __version__ @@ -27,10 +24,9 @@ from ..fuzztools import filetools from ..file_handlers.seedfile_set import SeedfileSet from ..fuzzers.errors import FuzzerExhaustedError -from ..scoring.scorable_set import EmptySetError -from ..runners import RunnerArchitectureError -from ..runners.killableprocess import Popen -from certfuzz.runners import RunnerPlatformVersionError +from ..scoring.errors import EmptySetError +from ..runners.errors import RunnerArchitectureError +from ..runners.errors import RunnerPlatformVersionError from ..fuzztools.object_caching import dump_obj_to_file logger = logging.getLogger(__name__) @@ -380,89 +376,3 @@ def go(self): except EmptySetError: logger.info('Seedfile set is empty. Nothing more to do.') return - - -class WindowsCampaign(Campaign): - ''' - Extends Campaign to add windows-specific features like ButtonClicker - ''' - def __enter__(self): - if sys.platform == 'win32': - winver = sys.getwindowsversion().major - machine = platform.machine() - hook_incompat = (winver > 5) or (machine == 'AMD64') - if hook_incompat and self.runner_module_name == 'certfuzz.runners.winrun': - logger.debug('winrun is not compatible with Windows %s %s. Overriding.', winver, machine) - self.runner_module_name = None - self = Campaign.__enter__(self) - self._start_buttonclicker() - self._cache_app() - return self - - def __exit__(self, etype, value, mytraceback): - self._stop_buttonclicker() - return Campaign.__exit__(self, etype, value, mytraceback) - - def _cache_app(self): - logger.debug('Caching application %s and determining if we need to watch the CPU...', self.prog) - targetdir = os.path.dirname(self.prog) - # Use overriden Popen that uses a job object to make sure that - # child processes are killed - p = Popen(self.prog, cwd=targetdir) - runtimeout = self.config['runner']['runtimeout'] - logger.debug('...Timer: %f', runtimeout) - t = Timer(runtimeout, self.kill, args=[p]) - logger.debug('...timer start') - t.start() - p.wait() - logger.debug('...timer stop') - t.cancel() - if not self.gui_app: - logger.debug('This seems to be a CLI application.') - try: - runner_watchcpu = str(self.config['runner']['watchcpu']).lower() - debugger_watchcpu = str(self.config['debugger']['watchcpu']).lower() - except KeyError: - self.config['runner']['watchcpu'] = 'auto' - self.config['debugger']['watchcpu'] = 'auto' - runner_watchcpu = 'auto' - debugger_watchcpu = 'auto' - if runner_watchcpu == 'auto': - logger.debug('Disabling runner CPU monitoring for dynamic timeout') - self.config['runner']['watchcpu'] = False - if debugger_watchcpu == 'auto': - logger.debug('Disabling debugger CPU monitoring for dynamic timeout') - self.config['debugger']['watchcpu'] = False - - def kill(self, p): - # The app didn't complete within the timeout. Assume it's a GUI app - logger.debug('This seems to be a GUI application.') - self.gui_app = True - try: - runner_watchcpu = str(self.config['runner']['watchcpu']).lower() - debugger_watchcpu = str(self.config['debugger']['watchcpu']).lower() - except KeyError: - self.config['runner']['watchcpu'] = 'auto' - self.config['debugger']['watchcpu'] = 'auto' - runner_watchcpu = 'auto' - debugger_watchcpu = 'auto' - if runner_watchcpu == 'auto': - logger.debug('Enabling runner CPU monitoring for dynamic timeout') - self.config['runner']['watchcpu'] = True - logger.debug('kill runner watchcpu: %s', self.config['runner']['watchcpu']) - if debugger_watchcpu == 'auto': - logger.debug('Enabling debugger CPU monitoring for dynamic timeout') - self.config['debugger']['watchcpu'] = True - logger.debug('kill debugger watchcpu: %s', self.config['debugger']['watchcpu']) - logger.debug('kill %s', p) - p.kill() - - def _start_buttonclicker(self): - if self.use_buttonclicker: - rootpath = os.path.dirname(sys.argv[0]) - buttonclicker = os.path.join(rootpath, 'buttonclicker', 'buttonclicker.exe') - os.startfile(buttonclicker) # @UndefinedVariable - - def _stop_buttonclicker(self): - if self.use_buttonclicker: - os.system('taskkill /im buttonclicker.exe') diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py new file mode 100644 index 0000000..c73c126 --- /dev/null +++ b/src/certfuzz/campaign/campaign_windows.py @@ -0,0 +1,102 @@ +''' +Created on Jan 10, 2014 + +@author: adh +''' +import logging +import sys +import os +from threading import Timer +import platform + +from .campaign import Campaign + +from ..runners.killableprocess import Popen + +logger = logging.getLogger(__name__) + + +class WindowsCampaign(Campaign): + ''' + Extends Campaign to add windows-specific features like ButtonClicker + ''' + def __enter__(self): + if sys.platform == 'win32': + winver = sys.getwindowsversion().major + machine = platform.machine() + hook_incompat = (winver > 5) or (machine == 'AMD64') + if hook_incompat and self.runner_module_name == 'certfuzz.runners.winrun': + logger.debug('winrun is not compatible with Windows %s %s. Overriding.', winver, machine) + self.runner_module_name = None + self = Campaign.__enter__(self) + self._start_buttonclicker() + self._cache_app() + return self + + def __exit__(self, etype, value, mytraceback): + self._stop_buttonclicker() + return Campaign.__exit__(self, etype, value, mytraceback) + + def _cache_app(self): + logger.debug('Caching application %s and determining if we need to watch the CPU...', self.prog) + targetdir = os.path.dirname(self.prog) + # Use overriden Popen that uses a job object to make sure that + # child processes are killed + p = Popen(self.prog, cwd=targetdir) + runtimeout = self.config['runner']['runtimeout'] + logger.debug('...Timer: %f', runtimeout) + t = Timer(runtimeout, self.kill, args=[p]) + logger.debug('...timer start') + t.start() + p.wait() + logger.debug('...timer stop') + t.cancel() + if not self.gui_app: + logger.debug('This seems to be a CLI application.') + try: + runner_watchcpu = str(self.config['runner']['watchcpu']).lower() + debugger_watchcpu = str(self.config['debugger']['watchcpu']).lower() + except KeyError: + self.config['runner']['watchcpu'] = 'auto' + self.config['debugger']['watchcpu'] = 'auto' + runner_watchcpu = 'auto' + debugger_watchcpu = 'auto' + if runner_watchcpu == 'auto': + logger.debug('Disabling runner CPU monitoring for dynamic timeout') + self.config['runner']['watchcpu'] = False + if debugger_watchcpu == 'auto': + logger.debug('Disabling debugger CPU monitoring for dynamic timeout') + self.config['debugger']['watchcpu'] = False + + def kill(self, p): + # The app didn't complete within the timeout. Assume it's a GUI app + logger.debug('This seems to be a GUI application.') + self.gui_app = True + try: + runner_watchcpu = str(self.config['runner']['watchcpu']).lower() + debugger_watchcpu = str(self.config['debugger']['watchcpu']).lower() + except KeyError: + self.config['runner']['watchcpu'] = 'auto' + self.config['debugger']['watchcpu'] = 'auto' + runner_watchcpu = 'auto' + debugger_watchcpu = 'auto' + if runner_watchcpu == 'auto': + logger.debug('Enabling runner CPU monitoring for dynamic timeout') + self.config['runner']['watchcpu'] = True + logger.debug('kill runner watchcpu: %s', self.config['runner']['watchcpu']) + if debugger_watchcpu == 'auto': + logger.debug('Enabling debugger CPU monitoring for dynamic timeout') + self.config['debugger']['watchcpu'] = True + logger.debug('kill debugger watchcpu: %s', self.config['debugger']['watchcpu']) + logger.debug('kill %s', p) + p.kill() + + def _start_buttonclicker(self): + if self.use_buttonclicker: + rootpath = os.path.dirname(sys.argv[0]) + buttonclicker = os.path.join(rootpath, 'buttonclicker', 'buttonclicker.exe') + os.startfile(buttonclicker) # @UndefinedVariable + + def _stop_buttonclicker(self): + if self.use_buttonclicker: + os.system('taskkill /im buttonclicker.exe') diff --git a/src/windows/foe2.py b/src/windows/foe2.py index 007fe9a..5d073cb 100644 --- a/src/windows/foe2.py +++ b/src/windows/foe2.py @@ -11,7 +11,7 @@ import os from optparse import OptionParser -from certfuzz.campaign.campaign import WindowsCampaign +from certfuzz.campaign.campaign_windows import WindowsCampaign from logging.handlers import RotatingFileHandler From 0479870f0ac07ec1655d857e7c0a50c830b1bdbf Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 14:10:16 -0500 Subject: [PATCH 0029/1169] drop unneeded apis --- src/certfuzz/test/analyzers/test_analyzers_pkg.py | 4 ++-- src/certfuzz/test/campaign/test_campaign_pkg.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/test/analyzers/test_analyzers_pkg.py b/src/certfuzz/test/analyzers/test_analyzers_pkg.py index 73be030..d93670d 100644 --- a/src/certfuzz/test/analyzers/test_analyzers_pkg.py +++ b/src/certfuzz/test/analyzers/test_analyzers_pkg.py @@ -7,6 +7,7 @@ import certfuzz.analyzers from certfuzz.test import misc + class Test(unittest.TestCase): def setUp(self): @@ -17,8 +18,7 @@ def tearDown(self): def test_api(self): module = certfuzz.analyzers - api_list = ['Analyzer', 'AnalyzerEmptyOutputError', - 'AnalyzerError', 'AnalyzerOutputMissingError'] + api_list = ['Analyzer'] (is_fail, msg) = misc.check_for_apis(module, api_list) self.assertFalse(is_fail, msg) diff --git a/src/certfuzz/test/campaign/test_campaign_pkg.py b/src/certfuzz/test/campaign/test_campaign_pkg.py index f50fa62..cd3bebc 100644 --- a/src/certfuzz/test/campaign/test_campaign_pkg.py +++ b/src/certfuzz/test/campaign/test_campaign_pkg.py @@ -19,7 +19,6 @@ def test_api(self): module = certfuzz.campaign api_list = ['__version__', 'import_module_by_name', - 'CampaignError', 'CampaignBase', ] (is_fail, msg) = misc.check_for_apis(module, api_list) From 34c3ecc315c2f5996a56a10a6cf9dd2dab766483 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 14:54:20 -0500 Subject: [PATCH 0030/1169] fix imports so unit tests run --- src/certfuzz/campaign/iteration.py | 2 +- .../test/campaign/config/test_config_pkg.py | 2 +- .../test/campaign/config/test_foe_config.py | 4 +- src/certfuzz/test/campaign/test_campaign.py | 38 +------------------ .../test/campaign/test_campaign_windows.py | 24 ++++++++++++ 5 files changed, 30 insertions(+), 40 deletions(-) create mode 100644 src/certfuzz/test/campaign/test_campaign_windows.py diff --git a/src/certfuzz/campaign/iteration.py b/src/certfuzz/campaign/iteration.py index be9cb85..57269ed 100644 --- a/src/certfuzz/campaign/iteration.py +++ b/src/certfuzz/campaign/iteration.py @@ -8,7 +8,7 @@ from ..debuggers.output_parsers import DebuggerFileError from ..file_handlers.basicfile import BasicFile from ..file_handlers.tmp_reaper import TmpReaper -from ..fuzzers import FuzzerError, FuzzerExhaustedError, \ +from ..fuzzers.errors import FuzzerError, FuzzerExhaustedError, \ FuzzerInputMatchesOutputError from ..fuzztools import filetools from ..fuzztools.filetools import delete_files_or_dirs diff --git a/src/certfuzz/test/campaign/config/test_config_pkg.py b/src/certfuzz/test/campaign/config/test_config_pkg.py index b9aee4a..56565c6 100644 --- a/src/certfuzz/test/campaign/config/test_config_pkg.py +++ b/src/certfuzz/test/campaign/config/test_config_pkg.py @@ -7,6 +7,7 @@ from certfuzz.test import misc import certfuzz.campaign.config + class Test(unittest.TestCase): def setUp(self): @@ -19,7 +20,6 @@ def test_api(self): module = certfuzz.campaign.config api_list = ['parse_yaml', 'Config', - 'ConfigError', ] (is_fail, msg) = misc.check_for_apis(module, api_list) self.assertFalse(is_fail, msg) diff --git a/src/certfuzz/test/campaign/config/test_foe_config.py b/src/certfuzz/test/campaign/config/test_foe_config.py index 2de6965..439fe89 100644 --- a/src/certfuzz/test/campaign/config/test_foe_config.py +++ b/src/certfuzz/test/campaign/config/test_foe_config.py @@ -9,14 +9,14 @@ import yaml import tempfile import shutil -#import pprint -from certfuzz.campaign.config import ConfigError +from certfuzz.campaign.config.errors import ConfigError import logging logger = logging.getLogger() hdlr = logging.FileHandler(os.devnull) logger.addHandler(hdlr) + class Test(unittest.TestCase): def _get_minimal_config(self): diff --git a/src/certfuzz/test/campaign/test_campaign.py b/src/certfuzz/test/campaign/test_campaign.py index d857c97..f5c444f 100644 --- a/src/certfuzz/test/campaign/test_campaign.py +++ b/src/certfuzz/test/campaign/test_campaign.py @@ -13,10 +13,12 @@ import tempfile from certfuzz.fuzztools import filetools + class Mock(object): def __getstate__(self): return dict(x=1, y=2, z=3) + class Test(unittest.TestCase): def _dump_test_config(self): @@ -82,27 +84,6 @@ def test_getstate(self): except Exception, e: self.fail(e) -# def test_to_json(self): -# self.campaign.seedfile_set = Mock() -# -# # make sure we can write -# as_json = self.campaign.to_json() -# -# pprint.pprint(as_json) -# from_json = json.loads(as_json) -# -# for k, v in self.campaign.__getstate__().iteritems(): -# self.assertTrue(k in from_json, '%s not found' % k) -# if k == 'seedfile_set': -# # make sure everything in the json version -# # matches what was in our original sfs -# sfs = from_json[k] -# for k1, v1 in Mock().__getstate__().iteritems(): -# self.assertTrue(k1 in sfs) -# self.assertEqual(sfs[k1], v1) -# else: -# self.assertEqual(from_json[k], v) - def counter(self, *args): self.count += 1 @@ -113,21 +94,6 @@ def test_save_state(self): self.campaign._save_state() self.assertTrue(os.path.exists(self.campaign.cached_state_file)) -# @unittest.skip("JSON has been removed") -# def test_read_state(self): -# state = {'a': 1, 'b': 2, 'c': 3} -# fd, f = tempfile.mkstemp(dir=self.tmpdir) -# os.close(fd) -# json.dump(state, open(f, 'wb')) -# -# self.assertTrue(os.path.exists(f)) -# self.assertTrue(os.path.getsize(f) > 0) -# -# self.count = 0 -# self.campaign.__setstate__ = self.counter -# self.campaign._read_state(f) -# self.assertEqual(self.count, 1) - def test_set_state(self): state = {'crashes_seen': [1, 2, 3, 3], 'seedfile_set': {'things': {}}, diff --git a/src/certfuzz/test/campaign/test_campaign_windows.py b/src/certfuzz/test/campaign/test_campaign_windows.py new file mode 100644 index 0000000..693062a --- /dev/null +++ b/src/certfuzz/test/campaign/test_campaign_windows.py @@ -0,0 +1,24 @@ +''' +Created on Jan 10, 2014 + +@author: adh +''' +import unittest +from certfuzz.campaign.campaign_windows import WindowsCampaign + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 6896416268b2e36ddcd7db08e859b3207267c219 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 14:10:16 -0500 Subject: [PATCH 0031/1169] fix imports --- src/certfuzz/file_handlers/seedfile_set.py | 3 ++- src/certfuzz/scoring/__init__.py | 2 -- src/certfuzz/scoring/scorable_set.py | 1 + src/certfuzz/test/scoring/test_scoring_pkg.py | 7 ++----- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 2a58871..15e8843 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -10,7 +10,8 @@ from ..fuzztools import filetools from .seedfile import SeedFile from .errors import SeedFileError -from ..scoring.scorable_set import ScorableSet2, EmptySetError +from ..scoring.scorable_set import ScorableSet2 +from ..scoring.errors import EmptySetError logger = logging.getLogger(__name__) diff --git a/src/certfuzz/scoring/__init__.py b/src/certfuzz/scoring/__init__.py index df6bfdf..e69de29 100644 --- a/src/certfuzz/scoring/__init__.py +++ b/src/certfuzz/scoring/__init__.py @@ -1,2 +0,0 @@ -from .scorable_set import ScorableSet2 -from .scorable_thing import ScorableThing diff --git a/src/certfuzz/scoring/scorable_set.py b/src/certfuzz/scoring/scorable_set.py index 99bbf58..b0ac0d2 100644 --- a/src/certfuzz/scoring/scorable_set.py +++ b/src/certfuzz/scoring/scorable_set.py @@ -13,6 +13,7 @@ logger = logging.getLogger(__name__) + # Simplified reimplementation of ScorableSet with a Bayesian approach class ScorableSet2(object): ''' diff --git a/src/certfuzz/test/scoring/test_scoring_pkg.py b/src/certfuzz/test/scoring/test_scoring_pkg.py index 57d9a15..27d2c1e 100644 --- a/src/certfuzz/test/scoring/test_scoring_pkg.py +++ b/src/certfuzz/test/scoring/test_scoring_pkg.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz import scoring + class Test(unittest.TestCase): @@ -15,10 +15,7 @@ def tearDown(self): pass def testName(self): - names = ['ScorableSetError', 'ScorableThingError', 'ScoringError', 'ScorableSet2', 'ScorableThing'] - - for name in names: - self.assertTrue(hasattr(scoring, name)) + pass if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] From c09112a4106af25a21e2544de6122ee01505a153 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 12:29:58 -0500 Subject: [PATCH 0032/1169] move bff linux code to certfuzz.bff.linux --- src/certfuzz/bff/__init__.py | 0 src/certfuzz/bff/linux.py | 573 +++++++++++++++++++++++++++++++++++ src/linux/bff.py | 569 +--------------------------------- 3 files changed, 574 insertions(+), 568 deletions(-) create mode 100644 src/certfuzz/bff/__init__.py create mode 100644 src/certfuzz/bff/linux.py diff --git a/src/certfuzz/bff/__init__.py b/src/certfuzz/bff/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py new file mode 100644 index 0000000..c25602f --- /dev/null +++ b/src/certfuzz/bff/linux.py @@ -0,0 +1,573 @@ +''' +Created on Jan 13, 2014 + +@author: adh +''' +__version__ = '2.8' + +import os +import sys +from optparse import OptionParser +import time +import logging +import platform +from logging.handlers import RotatingFileHandler + +from certfuzz import file_handlers +from certfuzz import debuggers +from certfuzz.debuggers import gdb # @UnusedImport +from certfuzz.debuggers import crashwrangler # @UnusedImport + +from certfuzz.fuzztools import bff_helper as z +from certfuzz.campaign.config import bff_config as cfg_helper +from certfuzz.fuzztools import filetools +from certfuzz.fuzztools import performance + +from certfuzz.file_handlers.seedfile_set import SeedfileSet +from certfuzz.file_handlers.tmp_reaper import TmpReaper + +from certfuzz.crash.bff_crash import BffCrash +from certfuzz.fuzztools.object_caching import get_cached_state +from certfuzz.fuzztools.object_caching import cache_state +from certfuzz.fuzztools.process_killer import ProcessKiller +from certfuzz.fuzztools.seedrange import SeedRange +from certfuzz.fuzztools.state_timer import StateTimer +from certfuzz.fuzztools.watchdog import WatchDog +from certfuzz.fuzztools.zzuf import Zzuf +from certfuzz.fuzztools.zzuflog import ZzufLog + +from certfuzz.analyzers import valgrind +from certfuzz.analyzers import cw_gmalloc +from certfuzz.analyzers import stderr +from certfuzz.analyzers import pin_calltrace +from certfuzz.analyzers.errors import AnalyzerEmptyOutputError +from certfuzz.analyzers.callgrind import callgrind +from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError +from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError +from certfuzz.analyzers.callgrind.annotate import annotate_callgrind +from certfuzz.analyzers.callgrind.annotate import annotate_callgrind_tree +from certfuzz.minimizer import UnixMinimizer as Minimizer +from certfuzz.minimizer import MinimizerError + + +DEBUG = True + +SEED_INTERVAL = 500 + +SEED_TS = performance.TimeStamper() +START_SEED = 0 + +STATE_TIMER = StateTimer() + +logger = logging.getLogger() +logger.name = 'bff' +logger.setLevel(0) + + +def get_rate(current_seed): + seeds = current_seed - START_SEED + rate = seeds / SEED_TS.since_start() + return rate + + +def get_uniq_logger(logfile): + l = logging.getLogger('uniq_crash') + if len(l.handlers) == 0: + hdlr = logging.FileHandler(logfile) + l.addHandler(hdlr) + return l + + +def determine_uniqueness(crash, hashes): + ''' + Gets the crash signature, then compares it against known crashes. + Sets crash.is_unique = True if it is new + ''' + + # short-circuit on crashes with no signature + if not crash.signature: + logger.warning('Crash has no signature, cleaning up') + crash.delete_files() + return + + if crash.signature in hashes: + crash.is_unique = False + return + + # fall back to checking if the crash directory exists + crash_dir_found = filetools.find_or_create_dir(crash.result_dir) + + crash.is_unique = not crash_dir_found + + +def analyze_crasher(cfg, crash): + ''' + Runs multiple analyses and collects data about a crash. Returns a list of other crashes + encountered during the process of analyzing the current crash. + @param cfg: A BFF config object + @param crash: A crash object + @return: a list of Crasher objects + ''' + other_crashers_found = [] + + dbg_out_file_orig = crash.dbg.file + logger.debug('Original debugger file: %s', dbg_out_file_orig) + + if cfg.minimizecrashers: + STATE_TIMER.enter_state('minimize_testcase') + # try to reduce the Hamming Distance between the crasher file and the known good seedfile + # crash.fuzzedfile will be replaced with the minimized result + try: + with Minimizer(cfg=cfg, crash=crash, bitwise=False, + seedfile_as_target=True, confidence=0.999, + tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout + ) as minimizer: + minimizer.go() + other_crashers_found.extend(minimizer.other_crashes.values()) + except MinimizerError, e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) + minimizer = None + + touch_watchdog_file(cfg) + # calculate the hamming distances for this crash + # between the original seedfile and the minimized fuzzed file + crash.calculate_hamming_distances() + + if cfg.minimize_to_string: + STATE_TIMER.enter_state('minimize_testcase_to_string') + # Minimize to a string of 'x's + # crash.fuzzedfile will be replaced with the minimized result + try: + with Minimizer(cfg=cfg, crash=crash, bitwise=False, + seedfile_as_target=False, confidence=0.9, + tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout + ) as min2string: + min2string.go() + other_crashers_found.extend(min2string.other_crashes.values()) + except MinimizerError, e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) + min2string = None + touch_watchdog_file(cfg) + + STATE_TIMER.enter_state('analyze_testcase') + + # get one last debugger output for the newly minimized file + if crash.pc_in_function: + # change the debugger template + crash.set_debugger_template('complete') + else: + # use a debugger template that specifies fixed offsets from $pc for disassembly + crash.set_debugger_template('complete_nofunction') + logger.info('Getting complete debugger output for crash: %s', crash.fuzzedfile.path) + crash.get_debug_output(crash.fuzzedfile.path) + + if dbg_out_file_orig != crash.dbg.file: + # we have a new debugger output + # remove the old one + filetools.delete_files(dbg_out_file_orig) + if os.path.exists(dbg_out_file_orig): + logger.warning('Failed to remove old debugger file %s', dbg_out_file_orig) + else: + logger.debug('Removed old debug file %s', dbg_out_file_orig) + + # use the minimized file for the rest of the analyses + analyzers = [ + stderr.StdErr, + cw_gmalloc.CrashWranglerGmalloc, + ] + if cfg.use_valgrind: + analyzers.extend([ + valgrind.Valgrind, + callgrind.Callgrind, + ]) + if cfg.use_pin_calltrace: + analyzers.extend([ + pin_calltrace.Pin_calltrace, + ]) + + for analyzer in analyzers: + touch_watchdog_file(cfg) + + analyzer_instance = analyzer(cfg, crash) + if analyzer_instance: + try: + analyzer_instance.go() + except AnalyzerEmptyOutputError: + logger.warning('Unexpected empty output from analyzer. Continuing') + + logger.info('Annotating callgrind output') + try: + annotate_callgrind(crash) + annotate_callgrind_tree(crash) + except CallgrindAnnotateEmptyOutputFileError: + logger.warning('Unexpected empty output from annotate_callgrind. Continuing') + except CallgrindAnnotateMissingInputFileError: + logger.warning('Missing callgrind output. Continuing') + + return other_crashers_found + + +def verify_crasher(c, hashes, cfg, seedfile_set): + logger.debug('verifying crash') + found_new_crash = False + + crashes = [] + crashes.append(c) + + for crash in crashes: + # loop until we're out of crashes to verify + logger.debug('crashes to verify: %d', len(crashes)) + STATE_TIMER.enter_state('verify_testcase') + + # crashes may be added as a result of minimization + crash.is_unique = False + determine_uniqueness(crash, hashes) + crash.get_logger() + if crash.is_unique: + hashes.append(crash) + # only toggle it once + if not found_new_crash: + found_new_crash = True + + logger.debug("%s did not exist in cache, crash is unique", crash.signature) + more_crashes = analyze_crasher(cfg, crash) + + if cfg.recycle_crashers: + logger.debug('Recycling crash as seedfile') + iterstring = crash.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + crash.seedfile.md5 + '-' + iterstring + crash.seedfile.ext + crasherseed_path = os.path.join(cfg.seedfile_origin_dir, crasherseedname) + filetools.copy_file(crash.fuzzedfile.path, crasherseed_path) + seedfile_set.add_file(crasherseed_path) + # add new crashes to the queue + crashes.extend(more_crashes) + crash.copy_files() + + uniqlogger = get_uniq_logger(cfg.uniq_log) + uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', crash.seedfile.basename, crash.signature, crash.seednum, crash.range, crash.hd_bits, crash.hd_bytes) + logger.info('%s first seen at %d', crash.signature, crash.seednum) + else: + logger.debug('%s was found, not unique', crash.signature) + # always clean up after yourself + crash.clean_tmpdir() + + # clean up + crash.delete_files() + # whether it was unique or not, record some details for posterity + # record the details of this crash so we can regenerate it later if needed + crash.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', crash.seedfile.basename, crash.seednum, crash.range, crash.fuzzedfile.path) + crash.logger.info('PC=%s', crash.pc) + + # score this crash for the seedfile + crash.seedfile.record_success(crash.signature, tries=0) + if crash.range: + # ...and for the range + crash.range.record_success(crash.signature, tries=0) + + return found_new_crash + + +def check_for_script(cfg): + if cfg.program_is_script(): + logger.warning("Target application is a shell script.") + raise + #cfg.disable_verification() + #time.sleep(10) + + +def touch_watchdog_file(cfg): + if cfg.watchdogtimeout: + # this one just checks the permission + if os.access(cfg.remote_dir, os.W_OK): + # equivalent to 'touch cfg.watchdogfile' + open(cfg.watchdogfile, 'w').close() + + +def start_process_killer(scriptpath, cfg): + # set up and spawn the process killer + killscript = cfg.get_killscript_path(scriptpath) + ProcessKiller(killscript, cfg.killprocname, cfg.killproctimeout).go() + logger.debug("Process killer started: %s %s %d", killscript, cfg.killprocname, cfg.killproctimeout) + + +def add_log_handler(log_obj, level, hdlr, formatter): + hdlr.setLevel(level) + hdlr.setFormatter(formatter) + log_obj.addHandler(hdlr) + + +def setup_logging_to_console(log_obj, level): + hdlr = logging.StreamHandler() + formatter = logging.Formatter('%(name)s %(message)s') + add_log_handler(log_obj, level, hdlr, formatter) + + +def setup_logfile(logdir, log_basename='bff.log', level=logging.DEBUG, + max_bytes=1e8, backup_count=5): + ''' + Creates a log file in / at level + @param logdir: the directory where the log file should reside + @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') + @param level: the logging level (defaults to logging.DEBUG) + ''' + filetools.make_directories(logdir) + logfile = os.path.join(logdir, log_basename) + handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) + formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") + add_log_handler(logger, level, handler, formatter) + logger.info('Logging %s at %s', logging.getLevelName(level), logfile) + + +def get_config_file(basedir): + config_dir = os.path.join(basedir, 'conf.d') + + # check for a platform-specific file + platform_cfg = 'bff-%s.cfg' % platform.system() + + fullpath_platform_cfg = os.path.join(config_dir, platform_cfg) + + if os.path.exists(fullpath_platform_cfg): + config_file = fullpath_platform_cfg + else: + # default if nothing else is around + config_file = os.path.join(config_dir, "bff.cfg") + + return config_file + + +def main(): + global START_SEED + hashes = [] + + # give up if we don't have a debugger + debuggers.verify_supported_platform() + + setup_logging_to_console(logger, logging.INFO) + logger.info("Welcome to BFF!") + + scriptpath = os.path.dirname(sys.argv[0]) + logger.info('Scriptpath is %s', scriptpath) + + # parse command line options + logger.info('Parsing command line options') + parser = OptionParser() + parser.add_option('', '--debug', dest='debug', help='Turn on debugging output', action='store_true') + parser.add_option('-c', '--config', dest='cfg', help='Config file location') + (options, args) = parser.parse_args() #@UnusedVariable + + # Get the cfg file name + if options.cfg: + remote_cfg_file = options.cfg + else: + remote_cfg_file = get_config_file(scriptpath) + + # die unless the remote config is present + assert os.path.exists(remote_cfg_file), 'Cannot find remote config file: %s, Please create it or use --config option to specify a different location.' % remote_cfg_file + + # copy remote config to local: + local_cfg_file = os.path.expanduser('~/bff.cfg') + filetools.copy_file(remote_cfg_file, local_cfg_file) + + # Read the cfg file + logger.info('Reading config from %s', local_cfg_file) + cfg = cfg_helper.read_config_options(local_cfg_file) + + # set up local logging + setup_logfile(cfg.local_dir, log_basename='bff.log', level=logging.DEBUG, + max_bytes=1e8, backup_count=3) + + # set up remote logging + setup_logfile(cfg.output_dir, log_basename='bff.log', level=logging.INFO, + max_bytes=1e7, backup_count=5) + + try: + check_for_script(cfg) + except: + logger.warning("Please configure BFF to fuzz a binary. Exiting...") + sys.exit() + + z.setup_dirs_and_files(local_cfg_file, cfg) + + # make sure we cache it for the next run +# cache_state(cfg.campaign_id, 'cfg', cfg, cfg.cached_config_file) + + sr = get_cached_state('seedrange', cfg.campaign_id, cfg.cached_seedrange_file) + if not sr: + sr = SeedRange(cfg.start_seed, cfg.seed_interval, cfg.max_seed) + + # set START_SEED global for timestamping + START_SEED = sr.s1 + + start_process_killer(scriptpath, cfg) + + z.set_unbuffered_stdout() + + # set up the seedfile set so we can pick seedfiles for everything else... + seedfile_set = get_cached_state('seedfile_set', cfg.campaign_id, cfg.cached_seedfile_set) + if not seedfile_set: + logger.info('Building seedfile set') + sfs_logfile = os.path.join(cfg.seedfile_output_dir, 'seedfile_set.log') + with SeedfileSet(campaign_id=cfg.campaign_id, + originpath=cfg.seedfile_origin_dir, + localpath=cfg.seedfile_local_dir, + outputpath=cfg.seedfile_output_dir, + logfile=sfs_logfile, + ) as sfset: + seedfile_set = sfset + + # set up the watchdog timeout within the VM and restart the daemon + if cfg.watchdogtimeout: + watchdog = WatchDog(cfg.watchdogfile, cfg.watchdogtimeout) + touch_watchdog_file(cfg) + watchdog.go() + + cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) + + sf = seedfile_set.next_item() + + # Run the program once to cache it into memory + z.cache_program_once(cfg, sf.path) + + # Give target time to die + time.sleep(1) + + # flag to indicate whether this is a fresh script start up or not + first_chunk = True + + # remember our parent process id so we can tell if it changes later + _last_ppid = os.getppid() + + # campaign.go + while sr.in_max_range(): + + # wipe the tmp dir clean to try to avoid filling the VM disk + TmpReaper().clean_tmp() + + sf = seedfile_set.next_item() + + r = sf.rangefinder.next_item() + sr.set_s2() + logger.info(STATE_TIMER) + + while sr.in_range(): + # interval.go + logger.debug('Starting interval %d-%d', sr.s1, sr.s2) + + # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot + touch_watchdog_file(cfg) + + # check parent process id + _ppid_now = os.getppid() + if not _ppid_now == _last_ppid: + logger.warning('Parent process ID changed from %d to %d', _last_ppid, _ppid_now) + _last_ppid = _ppid_now + + # do the fuzz + cmdline = cfg.get_command(sf.path) + + if first_chunk: + # disable the --quiet option in zzuf + # on the first chunk only + quiet_flag = False + first_chunk = False + else: + quiet_flag = True + + STATE_TIMER.enter_state('fuzzing') + zzuf = Zzuf(cfg.local_dir, + sr.s1, + sr.s2, + cmdline, + sf.path, + cfg.zzuf_log_file, + cfg.copymode, + r.min, + r.max, + cfg.progtimeout, + quiet_flag, + ) + saw_crash = zzuf.go() + STATE_TIMER.enter_state('checking_results') + + if not saw_crash: + # we must have made it through this chunk without a crash + # so go to next chunk + try_count = sr.s1_s2_delta() + sf.record_tries(tries=try_count) + r.record_tries(tries=try_count) + + # emit a log entry + crashcount = z.get_crashcount(cfg.crashers_dir) + rate = get_rate(sr.s1) + seed_str = "seeds=%d-%d" % (sr.s1, sr.s2) + range_str = "range=%.6f-%.6f" % (r.min, r.max) + rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) + expected_density = seedfile_set.expected_crash_density + xd_str = "expected=%.9f" % expected_density + xr_str = 'expected_rate=%.6f uniq/day' % (expected_density * rate * 86400) + logger.info('Fuzzing %s %s %s %s %s %s crash_count=%d', + sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) + + # set s1 to s2 so that as soon as we continue we'll break out of the sr.in_range() loop + sr.set_s1_to_s2() + continue + + # we must have seen a crash + + # get the results + zzuf_log = ZzufLog(cfg.zzuf_log_file, cfg.zzuf_log_out(sf.output_dir)) + + # Don't generate cases for killed process or out-of-memory + # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will + # report the exit code in its output log. The exit code is 128 + the signal number. + crash_status = zzuf_log.crash_logged(cfg.copymode) + sr.bookmark_s1() + sr.s1 = zzuf_log.seed + + # record the fact that we've made it this far + try_count = sr.s1_delta() + sf.record_tries(tries=try_count) + r.record_tries(tries=try_count) + + new_uniq_crash = False + if crash_status: + logger.info('Generating testcase for %s', zzuf_log.line) + # a true crash + zzuf_range = zzuf_log.range + # create the temp dir for the results + cfg.create_tmpdir() + outfile = cfg.get_testcase_outfile(sf.path, sr.s1) + logger.debug('Output file is %s', outfile) + testcase = zzuf.generate_test_case(sf.path, sr.s1, zzuf_range, outfile) + + # Do internal verification using GDB / Valgrind / Stderr + fuzzedfile = file_handlers.basicfile.BasicFile(outfile) + + with BffCrash(cfg, sf, fuzzedfile, cfg.program, cfg.debugger_timeout, + cfg.killprocname, cfg.backtracelevels, + cfg.crashers_dir, sr.s1, r) as c: + if c.is_crash: + new_uniq_crash = verify_crasher(c, hashes, cfg, seedfile_set) + + # record the zzuf log line for this crash + if not c.logger: + c.get_logger() + c.logger.debug("zzuflog: %s", zzuf_log.line) + c.logger.info('Command: %s', testcase.cmdline) + + cfg.clean_tmpdir() + + sr.increment_seed() + + # cache objects in case of reboot + cache_state(cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) + pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) + cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) + cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) + + if new_uniq_crash: + # we had a hit, so break the inner while() loop + # so we can pick a new range. This is to avoid + # having a crash-rich range run away with the + # probability before other ranges have been tried + break diff --git a/src/linux/bff.py b/src/linux/bff.py index f39422d..274caaf 100644 --- a/src/linux/bff.py +++ b/src/linux/bff.py @@ -3,574 +3,7 @@ @organization: cert.org ''' -__version__ = '2.8' - -import os -import sys -from optparse import OptionParser -import time -import logging -import platform -from logging.handlers import RotatingFileHandler - -from certfuzz import file_handlers -from certfuzz import debuggers -from certfuzz.debuggers import gdb # @UnusedImport -from certfuzz.debuggers import crashwrangler # @UnusedImport - -from certfuzz.fuzztools import bff_helper as z -from certfuzz.campaign.config import bff_config as cfg_helper -from certfuzz.fuzztools import filetools -from certfuzz.fuzztools import performance - -from certfuzz.file_handlers.seedfile_set import SeedfileSet -from certfuzz.file_handlers.tmp_reaper import TmpReaper - -from certfuzz.crash.bff_crash import BffCrash -from certfuzz.fuzztools.object_caching import get_cached_state -from certfuzz.fuzztools.object_caching import cache_state -from certfuzz.fuzztools.process_killer import ProcessKiller -from certfuzz.fuzztools.seedrange import SeedRange -from certfuzz.fuzztools.state_timer import StateTimer -from certfuzz.fuzztools.watchdog import WatchDog -from certfuzz.fuzztools.zzuf import Zzuf -from certfuzz.fuzztools.zzuflog import ZzufLog - -from certfuzz.analyzers import valgrind -from certfuzz.analyzers import cw_gmalloc -from certfuzz.analyzers import stderr -from certfuzz.analyzers import pin_calltrace -from certfuzz.analyzers import AnalyzerEmptyOutputError -from certfuzz.analyzers.callgrind import callgrind -from certfuzz.analyzers.callgrind import CallgrindAnnotateEmptyOutputFileError -from certfuzz.analyzers.callgrind import CallgrindAnnotateMissingInputFileError -from certfuzz.analyzers.callgrind.annotate import annotate_callgrind -from certfuzz.analyzers.callgrind.annotate import annotate_callgrind_tree -from certfuzz.minimizer import UnixMinimizer as Minimizer -from certfuzz.minimizer import MinimizerError - - -DEBUG = True - -SEED_INTERVAL = 500 - -SEED_TS = performance.TimeStamper() -START_SEED = 0 - -STATE_TIMER = StateTimer() - -logger = logging.getLogger() -logger.name = 'bff' -logger.setLevel(0) - - -def get_rate(current_seed): - seeds = current_seed - START_SEED - rate = seeds / SEED_TS.since_start() - return rate - - -def get_uniq_logger(logfile): - l = logging.getLogger('uniq_crash') - if len(l.handlers) == 0: - hdlr = logging.FileHandler(logfile) - l.addHandler(hdlr) - return l - - -def determine_uniqueness(crash, hashes): - ''' - Gets the crash signature, then compares it against known crashes. - Sets crash.is_unique = True if it is new - ''' - - # short-circuit on crashes with no signature - if not crash.signature: - logger.warning('Crash has no signature, cleaning up') - crash.delete_files() - return - - if crash.signature in hashes: - crash.is_unique = False - return - - # fall back to checking if the crash directory exists - crash_dir_found = filetools.find_or_create_dir(crash.result_dir) - - crash.is_unique = not crash_dir_found - - -def analyze_crasher(cfg, crash): - ''' - Runs multiple analyses and collects data about a crash. Returns a list of other crashes - encountered during the process of analyzing the current crash. - @param cfg: A BFF config object - @param crash: A crash object - @return: a list of Crasher objects - ''' - other_crashers_found = [] - - dbg_out_file_orig = crash.dbg.file - logger.debug('Original debugger file: %s', dbg_out_file_orig) - - if cfg.minimizecrashers: - STATE_TIMER.enter_state('minimize_testcase') - # try to reduce the Hamming Distance between the crasher file and the known good seedfile - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=cfg, crash=crash, bitwise=False, - seedfile_as_target=True, confidence=0.999, - tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout - ) as minimizer: - minimizer.go() - other_crashers_found.extend(minimizer.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) - minimizer = None - - touch_watchdog_file(cfg) - # calculate the hamming distances for this crash - # between the original seedfile and the minimized fuzzed file - crash.calculate_hamming_distances() - - if cfg.minimize_to_string: - STATE_TIMER.enter_state('minimize_testcase_to_string') - # Minimize to a string of 'x's - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=cfg, crash=crash, bitwise=False, - seedfile_as_target=False, confidence=0.9, - tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout - ) as min2string: - min2string.go() - other_crashers_found.extend(min2string.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) - min2string = None - touch_watchdog_file(cfg) - - STATE_TIMER.enter_state('analyze_testcase') - - # get one last debugger output for the newly minimized file - if crash.pc_in_function: - # change the debugger template - crash.set_debugger_template('complete') - else: - # use a debugger template that specifies fixed offsets from $pc for disassembly - crash.set_debugger_template('complete_nofunction') - logger.info('Getting complete debugger output for crash: %s', crash.fuzzedfile.path) - crash.get_debug_output(crash.fuzzedfile.path) - - if dbg_out_file_orig != crash.dbg.file: - # we have a new debugger output - # remove the old one - filetools.delete_files(dbg_out_file_orig) - if os.path.exists(dbg_out_file_orig): - logger.warning('Failed to remove old debugger file %s', dbg_out_file_orig) - else: - logger.debug('Removed old debug file %s', dbg_out_file_orig) - - # use the minimized file for the rest of the analyses - analyzers = [ - stderr.StdErr, - cw_gmalloc.CrashWranglerGmalloc, - ] - if cfg.use_valgrind: - analyzers.extend([ - valgrind.Valgrind, - callgrind.Callgrind, - ]) - if cfg.use_pin_calltrace: - analyzers.extend([ - pin_calltrace.Pin_calltrace, - ]) - - for analyzer in analyzers: - touch_watchdog_file(cfg) - - analyzer_instance = analyzer(cfg, crash) - if analyzer_instance: - try: - analyzer_instance.go() - except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from analyzer. Continuing') - - logger.info('Annotating callgrind output') - try: - annotate_callgrind(crash) - annotate_callgrind_tree(crash) - except CallgrindAnnotateEmptyOutputFileError: - logger.warning('Unexpected empty output from annotate_callgrind. Continuing') - except CallgrindAnnotateMissingInputFileError: - logger.warning('Missing callgrind output. Continuing') - - return other_crashers_found - - -def verify_crasher(c, hashes, cfg, seedfile_set): - logger.debug('verifying crash') - found_new_crash = False - - crashes = [] - crashes.append(c) - - for crash in crashes: - # loop until we're out of crashes to verify - logger.debug('crashes to verify: %d', len(crashes)) - STATE_TIMER.enter_state('verify_testcase') - - # crashes may be added as a result of minimization - crash.is_unique = False - determine_uniqueness(crash, hashes) - crash.get_logger() - if crash.is_unique: - hashes.append(crash) - # only toggle it once - if not found_new_crash: - found_new_crash = True - - logger.debug("%s did not exist in cache, crash is unique", crash.signature) - more_crashes = analyze_crasher(cfg, crash) - - if cfg.recycle_crashers: - logger.debug('Recycling crash as seedfile') - iterstring = crash.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + crash.seedfile.md5 + '-' + iterstring + crash.seedfile.ext - crasherseed_path = os.path.join(cfg.seedfile_origin_dir, crasherseedname) - filetools.copy_file(crash.fuzzedfile.path, crasherseed_path) - seedfile_set.add_file(crasherseed_path) - # add new crashes to the queue - crashes.extend(more_crashes) - crash.copy_files() - - uniqlogger = get_uniq_logger(cfg.uniq_log) - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', crash.seedfile.basename, crash.signature, crash.seednum, crash.range, crash.hd_bits, crash.hd_bytes) - logger.info('%s first seen at %d', crash.signature, crash.seednum) - else: - logger.debug('%s was found, not unique', crash.signature) - # always clean up after yourself - crash.clean_tmpdir() - - # clean up - crash.delete_files() - # whether it was unique or not, record some details for posterity - # record the details of this crash so we can regenerate it later if needed - crash.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', crash.seedfile.basename, crash.seednum, crash.range, crash.fuzzedfile.path) - crash.logger.info('PC=%s', crash.pc) - - # score this crash for the seedfile - crash.seedfile.record_success(crash.signature, tries=0) - if crash.range: - # ...and for the range - crash.range.record_success(crash.signature, tries=0) - - return found_new_crash - - -def check_for_script(cfg): - if cfg.program_is_script(): - logger.warning("Target application is a shell script.") - raise - #cfg.disable_verification() - #time.sleep(10) - - -def touch_watchdog_file(cfg): - if cfg.watchdogtimeout: - # this one just checks the permission - if os.access(cfg.remote_dir, os.W_OK): - # equivalent to 'touch cfg.watchdogfile' - open(cfg.watchdogfile, 'w').close() - - -def start_process_killer(scriptpath, cfg): - # set up and spawn the process killer - killscript = cfg.get_killscript_path(scriptpath) - ProcessKiller(killscript, cfg.killprocname, cfg.killproctimeout).go() - logger.debug("Process killer started: %s %s %d", killscript, cfg.killprocname, cfg.killproctimeout) - - -def add_log_handler(log_obj, level, hdlr, formatter): - hdlr.setLevel(level) - hdlr.setFormatter(formatter) - log_obj.addHandler(hdlr) - - -def setup_logging_to_console(log_obj, level): - hdlr = logging.StreamHandler() - formatter = logging.Formatter('%(name)s %(message)s') - add_log_handler(log_obj, level, hdlr, formatter) - - -def setup_logfile(logdir, log_basename='bff.log', level=logging.DEBUG, - max_bytes=1e8, backup_count=5): - ''' - Creates a log file in / at level - @param logdir: the directory where the log file should reside - @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') - @param level: the logging level (defaults to logging.DEBUG) - ''' - filetools.make_directories(logdir) - logfile = os.path.join(logdir, log_basename) - handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) - formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") - add_log_handler(logger, level, handler, formatter) - logger.info('Logging %s at %s', logging.getLevelName(level), logfile) - - -def get_config_file(basedir): - config_dir = os.path.join(basedir, 'conf.d') - - # check for a platform-specific file - platform_cfg = 'bff-%s.cfg' % platform.system() - - fullpath_platform_cfg = os.path.join(config_dir, platform_cfg) - - if os.path.exists(fullpath_platform_cfg): - config_file = fullpath_platform_cfg - else: - # default if nothing else is around - config_file = os.path.join(config_dir, "bff.cfg") - - return config_file - - -def main(): - global START_SEED - hashes = [] - - # give up if we don't have a debugger - debuggers.verify_supported_platform() - - setup_logging_to_console(logger, logging.INFO) - logger.info("Welcome to BFF!") - - scriptpath = os.path.dirname(sys.argv[0]) - logger.info('Scriptpath is %s', scriptpath) - - # parse command line options - logger.info('Parsing command line options') - parser = OptionParser() - parser.add_option('', '--debug', dest='debug', help='Turn on debugging output', action='store_true') - parser.add_option('-c', '--config', dest='cfg', help='Config file location') - (options, args) = parser.parse_args() #@UnusedVariable - - # Get the cfg file name - if options.cfg: - remote_cfg_file = options.cfg - else: - remote_cfg_file = get_config_file(scriptpath) - - # die unless the remote config is present - assert os.path.exists(remote_cfg_file), 'Cannot find remote config file: %s, Please create it or use --config option to specify a different location.' % remote_cfg_file - - # copy remote config to local: - local_cfg_file = os.path.expanduser('~/bff.cfg') - filetools.copy_file(remote_cfg_file, local_cfg_file) - - # Read the cfg file - logger.info('Reading config from %s', local_cfg_file) - cfg = cfg_helper.read_config_options(local_cfg_file) - - # set up local logging - setup_logfile(cfg.local_dir, log_basename='bff.log', level=logging.DEBUG, - max_bytes=1e8, backup_count=3) - - # set up remote logging - setup_logfile(cfg.output_dir, log_basename='bff.log', level=logging.INFO, - max_bytes=1e7, backup_count=5) - - try: - check_for_script(cfg) - except: - logger.warning("Please configure BFF to fuzz a binary. Exiting...") - sys.exit() - - z.setup_dirs_and_files(local_cfg_file, cfg) - - # make sure we cache it for the next run -# cache_state(cfg.campaign_id, 'cfg', cfg, cfg.cached_config_file) - - sr = get_cached_state('seedrange', cfg.campaign_id, cfg.cached_seedrange_file) - if not sr: - sr = SeedRange(cfg.start_seed, cfg.seed_interval, cfg.max_seed) - - # set START_SEED global for timestamping - START_SEED = sr.s1 - - start_process_killer(scriptpath, cfg) - - z.set_unbuffered_stdout() - - # set up the seedfile set so we can pick seedfiles for everything else... - seedfile_set = get_cached_state('seedfile_set', cfg.campaign_id, cfg.cached_seedfile_set) - if not seedfile_set: - logger.info('Building seedfile set') - sfs_logfile = os.path.join(cfg.seedfile_output_dir, 'seedfile_set.log') - with SeedfileSet(campaign_id=cfg.campaign_id, - originpath=cfg.seedfile_origin_dir, - localpath=cfg.seedfile_local_dir, - outputpath=cfg.seedfile_output_dir, - logfile=sfs_logfile, - ) as sfset: - seedfile_set = sfset - - # set up the watchdog timeout within the VM and restart the daemon - if cfg.watchdogtimeout: - watchdog = WatchDog(cfg.watchdogfile, cfg.watchdogtimeout) - touch_watchdog_file(cfg) - watchdog.go() - - cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) - - sf = seedfile_set.next_item() - - # Run the program once to cache it into memory - z.cache_program_once(cfg, sf.path) - - # Give target time to die - time.sleep(1) - - # flag to indicate whether this is a fresh script start up or not - first_chunk = True - - # remember our parent process id so we can tell if it changes later - _last_ppid = os.getppid() - - # campaign.go - while sr.in_max_range(): - - # wipe the tmp dir clean to try to avoid filling the VM disk - TmpReaper().clean_tmp() - - sf = seedfile_set.next_item() - - r = sf.rangefinder.next_item() - sr.set_s2() - logger.info(STATE_TIMER) - - while sr.in_range(): - # interval.go - logger.debug('Starting interval %d-%d', sr.s1, sr.s2) - - # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot - touch_watchdog_file(cfg) - - # check parent process id - _ppid_now = os.getppid() - if not _ppid_now == _last_ppid: - logger.warning('Parent process ID changed from %d to %d', _last_ppid, _ppid_now) - _last_ppid = _ppid_now - - # do the fuzz - cmdline = cfg.get_command(sf.path) - - if first_chunk: - # disable the --quiet option in zzuf - # on the first chunk only - quiet_flag = False - first_chunk = False - else: - quiet_flag = True - - STATE_TIMER.enter_state('fuzzing') - zzuf = Zzuf(cfg.local_dir, - sr.s1, - sr.s2, - cmdline, - sf.path, - cfg.zzuf_log_file, - cfg.copymode, - r.min, - r.max, - cfg.progtimeout, - quiet_flag, - ) - saw_crash = zzuf.go() - STATE_TIMER.enter_state('checking_results') - - if not saw_crash: - # we must have made it through this chunk without a crash - # so go to next chunk - try_count = sr.s1_s2_delta() - sf.record_tries(tries=try_count) - r.record_tries(tries=try_count) - - # emit a log entry - crashcount = z.get_crashcount(cfg.crashers_dir) - rate = get_rate(sr.s1) - seed_str = "seeds=%d-%d" % (sr.s1, sr.s2) - range_str = "range=%.6f-%.6f" % (r.min, r.max) - rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) - expected_density = seedfile_set.expected_crash_density - xd_str = "expected=%.9f" % expected_density - xr_str = 'expected_rate=%.6f uniq/day' % (expected_density * rate * 86400) - logger.info('Fuzzing %s %s %s %s %s %s crash_count=%d', - sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) - - # set s1 to s2 so that as soon as we continue we'll break out of the sr.in_range() loop - sr.set_s1_to_s2() - continue - - # we must have seen a crash - - # get the results - zzuf_log = ZzufLog(cfg.zzuf_log_file, cfg.zzuf_log_out(sf.output_dir)) - - # Don't generate cases for killed process or out-of-memory - # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will - # report the exit code in its output log. The exit code is 128 + the signal number. - crash_status = zzuf_log.crash_logged(cfg.copymode) - sr.bookmark_s1() - sr.s1 = zzuf_log.seed - - # record the fact that we've made it this far - try_count = sr.s1_delta() - sf.record_tries(tries=try_count) - r.record_tries(tries=try_count) - - new_uniq_crash = False - if crash_status: - logger.info('Generating testcase for %s', zzuf_log.line) - # a true crash - zzuf_range = zzuf_log.range - # create the temp dir for the results - cfg.create_tmpdir() - outfile = cfg.get_testcase_outfile(sf.path, sr.s1) - logger.debug('Output file is %s', outfile) - testcase = zzuf.generate_test_case(sf.path, sr.s1, zzuf_range, outfile) - - # Do internal verification using GDB / Valgrind / Stderr - fuzzedfile = file_handlers.basicfile.BasicFile(outfile) - - with BffCrash(cfg, sf, fuzzedfile, cfg.program, cfg.debugger_timeout, - cfg.killprocname, cfg.backtracelevels, - cfg.crashers_dir, sr.s1, r) as c: - if c.is_crash: - new_uniq_crash = verify_crasher(c, hashes, cfg, seedfile_set) - - # record the zzuf log line for this crash - if not c.logger: - c.get_logger() - c.logger.debug("zzuflog: %s", zzuf_log.line) - c.logger.info('Command: %s', testcase.cmdline) - - cfg.clean_tmpdir() - - sr.increment_seed() - - # cache objects in case of reboot - cache_state(cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) - pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) - cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) - cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) - - if new_uniq_crash: - # we had a hit, so break the inner while() loop - # so we can pick a new range. This is to avoid - # having a crash-rich range run away with the - # probability before other ranges have been tried - break +from certfuzz.bff.linux import main if __name__ == '__main__': main() From 1f5bdd65619e8e1c44692f04b59b796782263ac1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 12:30:22 -0500 Subject: [PATCH 0033/1169] add python builder to project --- .externalToolBuilders/dev_builder.launch | 9 +++++++++ .project | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .externalToolBuilders/dev_builder.launch diff --git a/.externalToolBuilders/dev_builder.launch b/.externalToolBuilders/dev_builder.launch new file mode 100644 index 0000000..8ee4ffa --- /dev/null +++ b/.externalToolBuilders/dev_builder.launch @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.project b/.project index 54c09c8..5cd433d 100644 --- a/.project +++ b/.project @@ -10,6 +10,16 @@ + + org.eclipse.ui.externaltools.ExternalToolBuilder + full,incremental, + + + LaunchConfigHandle + <project>/.externalToolBuilders/dev_builder.launch + + + org.python.pydev.pythonNature From 2010d6766f163bbb69ba04a12ab9fb11ad201cce Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 12:33:34 -0500 Subject: [PATCH 0034/1169] move foe entry point into certfuzz.bff.windows module --- src/certfuzz/bff/windows.py | 118 ++++++++++++++++++++++++++++++++++++ src/windows/foe2.py | 114 +--------------------------------- 2 files changed, 119 insertions(+), 113 deletions(-) create mode 100644 src/certfuzz/bff/windows.py diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py new file mode 100644 index 0000000..a7c0432 --- /dev/null +++ b/src/certfuzz/bff/windows.py @@ -0,0 +1,118 @@ +''' +Created on Jan 13, 2014 + +@author: adh +''' + +__version__ = '2.2' + +import sys +import logging +import os +from optparse import OptionParser + +from certfuzz.campaign.campaign_windows import WindowsCampaign +from logging.handlers import RotatingFileHandler + + +def _setup_logging_to_screen(options, logger, fmt): + # logging to screen + hdlr = logging.StreamHandler() + hdlr.setFormatter(fmt) + hdlr.setLevel(logging.INFO) + # override if debug or quiet + if options.debug: + hdlr.setLevel(logging.DEBUG) + elif options.quiet and not options.verbose: + hdlr.setLevel(logging.WARNING) + logger.addHandler(hdlr) + + +def _setup_logging_to_file(options, logger, fmt): + # logging to file + # override if option specified + if options.logfile: + logfile = options.logfile + else: + logfile = os.path.join('log', 'foe2log.txt') + + hdlr = RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) + + hdlr.setFormatter(fmt) + hdlr.setLevel(logging.WARNING) + # override if debug + if options.debug: + hdlr.setLevel(logging.DEBUG) + elif options.verbose: + hdlr.setLevel(logging.INFO) + logger.addHandler(hdlr) + + +def setup_logging(options): + logger = logging.getLogger() + + if options.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + fmt = logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(message)s') + _setup_logging_to_screen(options, logger, fmt) + _setup_logging_to_file(options, logger, fmt) + return logger + + +def parse_options(): + u = '%prog [options]' + v = ' '.join(['%prog', 'v%s' % __version__]) + parser = OptionParser(usage=u, version=v) + parser.add_option('-d', '--debug', dest='debug', action='store_true', + help='Enable debug messages to screen and log file (overrides --quiet)') + parser.add_option('-q', '--quiet', dest='quiet', action='store_true', + help='Silence messages to screen (log file will remain at INFO level') + parser.add_option('-v', '--verbose', dest='verbose', action='store_true', + help='Enable verbose logging messages to screen and log file (overrides --quiet)') + parser.add_option('-c', '--config', dest='configfile', help='Path to config file', + default='configs/foe.yaml', metavar='FILE') + parser.add_option('-l', '--logfile', dest='logfile', help='Path to log file', metavar='FILE') + parser.add_option('-r', '--result-dir', dest='resultdir', help='Path to result directory (overrides config)', metavar='DIR') + + (options, args) = parser.parse_args() + + return options, args + + +def setup_debugging(logger): + logger.debug('Instantiating embedded rpdb2 debugger with password "foe"...') + try: + import rpdb2 + rpdb2.start_embedded_debugger("foe", timeout=0.0) + except ImportError: + logger.debug('Error importing rpdb2. Is Winpdb installed?') + + logger.debug('Enabling heapy remote monitoring...') + try: + from guppy import hpy # @UnusedImport + import guppy.heapy.RM # @UnusedImport + except ImportError: + logger.debug('Error importing heapy. Is Guppy-PE installed?') + + +def main(): + # parse command line + options, args = parse_options() + + # start logging + logger = setup_logging(options) + logger.info('Welcome to %s version %s', sys.argv[0], __version__) + for a in args: + logger.warning('Ignoring unrecognized argument: %s', a) + + if options.debug: + setup_debugging(logger) + + with WindowsCampaign(config_file=options.configfile, result_dir=options.resultdir, debug=options.debug) as campaign: + logger.info('Initiating campaign') + campaign.go() + + logger.info('Campaign complete') diff --git a/src/windows/foe2.py b/src/windows/foe2.py index 5d073cb..7992a75 100644 --- a/src/windows/foe2.py +++ b/src/windows/foe2.py @@ -3,119 +3,7 @@ @organization: cert.org ''' - -__version__ = '2.2' - -import sys -import logging -import os -from optparse import OptionParser - -from certfuzz.campaign.campaign_windows import WindowsCampaign -from logging.handlers import RotatingFileHandler - - -def _setup_logging_to_screen(options, logger, fmt): - # logging to screen - hdlr = logging.StreamHandler() - hdlr.setFormatter(fmt) - hdlr.setLevel(logging.INFO) - # override if debug or quiet - if options.debug: - hdlr.setLevel(logging.DEBUG) - elif options.quiet and not options.verbose: - hdlr.setLevel(logging.WARNING) - logger.addHandler(hdlr) - - -def _setup_logging_to_file(options, logger, fmt): - # logging to file - # override if option specified - if options.logfile: - logfile = options.logfile - else: - logfile = os.path.join('log', 'foe2log.txt') - - hdlr = RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) - - hdlr.setFormatter(fmt) - hdlr.setLevel(logging.WARNING) - # override if debug - if options.debug: - hdlr.setLevel(logging.DEBUG) - elif options.verbose: - hdlr.setLevel(logging.INFO) - logger.addHandler(hdlr) - - -def setup_logging(options): - logger = logging.getLogger() - - if options.debug: - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - - fmt = logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(message)s') - _setup_logging_to_screen(options, logger, fmt) - _setup_logging_to_file(options, logger, fmt) - return logger - - -def parse_options(): - u = '%prog [options]' - v = ' '.join(['%prog', 'v%s' % __version__]) - parser = OptionParser(usage=u, version=v) - parser.add_option('-d', '--debug', dest='debug', action='store_true', - help='Enable debug messages to screen and log file (overrides --quiet)') - parser.add_option('-q', '--quiet', dest='quiet', action='store_true', - help='Silence messages to screen (log file will remain at INFO level') - parser.add_option('-v', '--verbose', dest='verbose', action='store_true', - help='Enable verbose logging messages to screen and log file (overrides --quiet)') - parser.add_option('-c', '--config', dest='configfile', help='Path to config file', - default='configs/foe.yaml', metavar='FILE') - parser.add_option('-l', '--logfile', dest='logfile', help='Path to log file', metavar='FILE') - parser.add_option('-r', '--result-dir', dest='resultdir', help='Path to result directory (overrides config)', metavar='DIR') - - (options, args) = parser.parse_args() - - return options, args - - -def setup_debugging(logger): - logger.debug('Instantiating embedded rpdb2 debugger with password "foe"...') - try: - import rpdb2 - rpdb2.start_embedded_debugger("foe", timeout=0.0) - except ImportError: - logger.debug('Error importing rpdb2. Is Winpdb installed?') - - logger.debug('Enabling heapy remote monitoring...') - try: - from guppy import hpy # @UnusedImport - import guppy.heapy.RM # @UnusedImport - except ImportError: - logger.debug('Error importing heapy. Is Guppy-PE installed?') - - -def main(): - # parse command line - options, args = parse_options() - - # start logging - logger = setup_logging(options) - logger.info('Welcome to %s version %s', sys.argv[0], __version__) - for a in args: - logger.warning('Ignoring unrecognized argument: %s', a) - - if options.debug: - setup_debugging(logger) - - with WindowsCampaign(config_file=options.configfile, result_dir=options.resultdir, debug=options.debug) as campaign: - logger.info('Initiating campaign') - campaign.go() - - logger.info('Campaign complete') +from certfuzz.bff.windows import main if __name__ == '__main__': main() From 190148832f123de260ff0ce30826d19b718c6996 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 12:44:59 -0500 Subject: [PATCH 0035/1169] fix import in unit test --- src/linux/test/test_bff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/test/test_bff.py b/src/linux/test/test_bff.py index 95de3a4..163719d 100644 --- a/src/linux/test/test_bff.py +++ b/src/linux/test/test_bff.py @@ -14,7 +14,7 @@ parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) -import bff +import certfuzz.bff.linux as bff class Mock(object): def __init__(self): From ed40cde7ec659f75de37148f7e79eca837173e88 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 11:20:37 -0500 Subject: [PATCH 0036/1169] pep8 cleanup --- src/certfuzz/test/test_meta.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/test/test_meta.py b/src/certfuzz/test/test_meta.py index 3ddeaaf..9dd91c5 100644 --- a/src/certfuzz/test/test_meta.py +++ b/src/certfuzz/test/test_meta.py @@ -74,11 +74,14 @@ def test_each_module_has_a_test_module(self): _ignored_modules = set(['errors.py']) for m in module_list: d, b = os.path.split(m) + if b in _ignored_modules: continue + test_b = 'test_%s' % b relpath = os.path.relpath(d, basedir) test_path = os.path.join(basedir, 'test', relpath, test_b) + if not os.path.exists(test_path): missing_modules.append((os.path.relpath(m, basedir), os.path.relpath(test_path, basedir))) fail_lines = ['Module %s has no corresponding test module %s' % mm for mm in missing_modules] From 70b59944a9dba0c69a9209bd81da85351051fab3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 13:02:13 -0500 Subject: [PATCH 0037/1169] add empty test harnesses --- src/certfuzz/test/bff/__init__.py | 0 src/certfuzz/test/bff/test_linux.py | 23 +++++++++++++++++++++++ src/certfuzz/test/bff/test_windows.py | 26 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 src/certfuzz/test/bff/__init__.py create mode 100644 src/certfuzz/test/bff/test_linux.py create mode 100644 src/certfuzz/test/bff/test_windows.py diff --git a/src/certfuzz/test/bff/__init__.py b/src/certfuzz/test/bff/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/test/bff/test_linux.py b/src/certfuzz/test/bff/test_linux.py new file mode 100644 index 0000000..2d99a8c --- /dev/null +++ b/src/certfuzz/test/bff/test_linux.py @@ -0,0 +1,23 @@ +''' +Created on Jan 13, 2014 + +@author: adh +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/bff/test_windows.py b/src/certfuzz/test/bff/test_windows.py new file mode 100644 index 0000000..5f20a14 --- /dev/null +++ b/src/certfuzz/test/bff/test_windows.py @@ -0,0 +1,26 @@ +''' +Created on Jan 13, 2014 + +@author: adh +''' +import unittest + + +class Test(unittest.TestCase): + + + def setUp(self): + pass + + + def tearDown(self): + pass + + + def testName(self): + pass + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file From 7e3998c815f205187be50842470364f3e8e3eb80 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 16 Jan 2014 09:23:06 -0500 Subject: [PATCH 0038/1169] update license per BFF-498 --- src/certfuzz/hooks/winxp/dllmain.cpp | 13 +++---------- src/windows/COPYING.txt | 13 +++---------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/certfuzz/hooks/winxp/dllmain.cpp b/src/certfuzz/hooks/winxp/dllmain.cpp index cb7b6d1..4d583f9 100644 --- a/src/certfuzz/hooks/winxp/dllmain.cpp +++ b/src/certfuzz/hooks/winxp/dllmain.cpp @@ -18,24 +18,17 @@ * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * - * 3. All advertising materials for third-party software mentioning - * features or use of this software must display the following - * disclaimer: - * - * "Neither Carnegie Mellon University nor its Software Engineering - * Institute have reviewed or endorsed this software" - * - * 4. The names "Department of Homeland Security," "Carnegie Mellon + * 3. The names "Department of Homeland Security," "Carnegie Mellon * University," "CERT" and/or "Software Engineering Institute" shall * not be used to endorse or promote products derived from this software * without prior written permission. For written permission, please * contact permission@sei.cmu.edu. * - * 5. Products derived from this software may not be called "CERT" nor + * 4. Products derived from this software may not be called "CERT" nor * may "CERT" appear in their names without prior written permission of * permission@sei.cmu.edu. * - * 6. Redistributions of any form whatsoever must retain the following + * 5. Redistributions of any form whatsoever must retain the following * acknowledgment: * * "This product includes software developed by CERT with funding diff --git a/src/windows/COPYING.txt b/src/windows/COPYING.txt index 71692ad..5d7819c 100644 --- a/src/windows/COPYING.txt +++ b/src/windows/COPYING.txt @@ -18,24 +18,17 @@ met: notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -3. All advertising materials for third-party software mentioning - features or use of this software must display the following - disclaimer: - - "Neither Carnegie Mellon University nor its Software Engineering - Institute have reviewed or endorsed this software" - -4. The names "Department of Homeland Security," "Carnegie Mellon +3. The names "Department of Homeland Security," "Carnegie Mellon University," "CERT" and/or "Software Engineering Institute" shall not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact permission@sei.cmu.edu. -5. Products derived from this software may not be called "CERT" nor +4. Products derived from this software may not be called "CERT" nor may "CERT" appear in their names without prior written permission of permission@sei.cmu.edu. -6. Redistributions of any form whatsoever must retain the following +5. Redistributions of any form whatsoever must retain the following acknowledgment: "This product includes software developed by CERT with funding From c1fa02595bbdb8b786d923b0f2a6417eebb44900 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jan 2014 13:26:01 -0500 Subject: [PATCH 0039/1169] reorganize imports --- src/certfuzz/bff/linux.py | 66 +++++++++++++++---------------------- src/certfuzz/bff/windows.py | 8 ++--- 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index c25602f..7e6d7cd 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -5,49 +5,37 @@ ''' __version__ = '2.8' +import logging +from logging.handlers import RotatingFileHandler +from optparse import OptionParser import os +import platform import sys -from optparse import OptionParser import time -import logging -import platform -from logging.handlers import RotatingFileHandler -from certfuzz import file_handlers -from certfuzz import debuggers -from certfuzz.debuggers import gdb # @UnusedImport -from certfuzz.debuggers import crashwrangler # @UnusedImport - -from certfuzz.fuzztools import bff_helper as z -from certfuzz.campaign.config import bff_config as cfg_helper -from certfuzz.fuzztools import filetools -from certfuzz.fuzztools import performance - -from certfuzz.file_handlers.seedfile_set import SeedfileSet -from certfuzz.file_handlers.tmp_reaper import TmpReaper - -from certfuzz.crash.bff_crash import BffCrash -from certfuzz.fuzztools.object_caching import get_cached_state -from certfuzz.fuzztools.object_caching import cache_state -from certfuzz.fuzztools.process_killer import ProcessKiller -from certfuzz.fuzztools.seedrange import SeedRange -from certfuzz.fuzztools.state_timer import StateTimer -from certfuzz.fuzztools.watchdog import WatchDog -from certfuzz.fuzztools.zzuf import Zzuf -from certfuzz.fuzztools.zzuflog import ZzufLog - -from certfuzz.analyzers import valgrind -from certfuzz.analyzers import cw_gmalloc -from certfuzz.analyzers import stderr -from certfuzz.analyzers import pin_calltrace -from certfuzz.analyzers.errors import AnalyzerEmptyOutputError -from certfuzz.analyzers.callgrind import callgrind -from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError -from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError -from certfuzz.analyzers.callgrind.annotate import annotate_callgrind -from certfuzz.analyzers.callgrind.annotate import annotate_callgrind_tree -from certfuzz.minimizer import UnixMinimizer as Minimizer -from certfuzz.minimizer import MinimizerError +from .. import debuggers, file_handlers +from ..analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind +from ..analyzers.callgrind import callgrind +from ..analyzers.callgrind.annotate import annotate_callgrind +from ..analyzers.callgrind.annotate import annotate_callgrind_tree +from ..analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError +from ..analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError +from ..analyzers.errors import AnalyzerEmptyOutputError +from ..campaign.config import bff_config as cfg_helper +from ..crash.bff_crash import BffCrash +from ..debuggers import crashwrangler # @UnusedImport +from ..debuggers import gdb # @UnusedImport +from ..file_handlers.seedfile_set import SeedfileSet +from ..file_handlers.tmp_reaper import TmpReaper +from ..fuzztools import bff_helper as z, filetools, performance +from ..fuzztools.object_caching import cache_state, get_cached_state +from ..fuzztools.process_killer import ProcessKiller +from ..fuzztools.seedrange import SeedRange +from ..fuzztools.state_timer import StateTimer +from ..fuzztools.watchdog import WatchDog +from ..fuzztools.zzuf import Zzuf +from ..fuzztools.zzuflog import ZzufLog +from ..minimizer import MinimizerError, UnixMinimizer as Minimizer DEBUG = True diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index a7c0432..fdee7d1 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -6,13 +6,13 @@ __version__ = '2.2' -import sys import logging -import os +from logging.handlers import RotatingFileHandler from optparse import OptionParser +import os +import sys -from certfuzz.campaign.campaign_windows import WindowsCampaign -from logging.handlers import RotatingFileHandler +from ..campaign.campaign_windows import WindowsCampaign def _setup_logging_to_screen(options, logger, fmt): From ff25e9b380c9f9cb9daa2d2e0741a62675e3e054 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jan 2014 13:36:09 -0500 Subject: [PATCH 0040/1169] remove empty package test --- src/certfuzz/test/scoring/test_scoring_pkg.py | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/certfuzz/test/scoring/test_scoring_pkg.py diff --git a/src/certfuzz/test/scoring/test_scoring_pkg.py b/src/certfuzz/test/scoring/test_scoring_pkg.py deleted file mode 100644 index 27d2c1e..0000000 --- a/src/certfuzz/test/scoring/test_scoring_pkg.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest - - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From 755a83875f671f0a9ab60d160d413f47772eea6b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 08:31:12 -0500 Subject: [PATCH 0041/1169] add new tools packages --- src/certfuzz/tools/__init__.py | 0 src/certfuzz/tools/linux/__init__.py | 0 src/certfuzz/tools/windows/__init__.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/certfuzz/tools/__init__.py create mode 100644 src/certfuzz/tools/linux/__init__.py create mode 100644 src/certfuzz/tools/windows/__init__.py diff --git a/src/certfuzz/tools/__init__.py b/src/certfuzz/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/tools/linux/__init__.py b/src/certfuzz/tools/linux/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/tools/windows/__init__.py b/src/certfuzz/tools/windows/__init__.py new file mode 100644 index 0000000..e69de29 From c5801be07f4ee001e967a44a4410445b4feef720 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 08:38:38 -0500 Subject: [PATCH 0042/1169] move bff_stats.py into certfuzz.tools.linux --- src/certfuzz/tools/linux/bff_stats.py | 176 ++++++++++++++++++++++++++ src/linux/tools/bff_stats.py | 156 +---------------------- 2 files changed, 178 insertions(+), 154 deletions(-) create mode 100755 src/certfuzz/tools/linux/bff_stats.py diff --git a/src/certfuzz/tools/linux/bff_stats.py b/src/certfuzz/tools/linux/bff_stats.py new file mode 100755 index 0000000..d18cf00 --- /dev/null +++ b/src/certfuzz/tools/linux/bff_stats.py @@ -0,0 +1,176 @@ +''' +Created on Jan 12, 2011 + +@organization: cert.org + +''' +import os +import logging +import sys +import re +from optparse import OptionParser +from certfuzz.campaign.config.bff_config import read_config_options + +parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, parent_path) + + +logger = logging.getLogger(__name__) +# set default logging level (override with command line options) +logger.setLevel(logging.INFO) + +hdlr = logging.StreamHandler(sys.stdout) +logger.addHandler(hdlr) + + +def _fmt_ln(formats, parts): + return '\t'.join(fmt % val for (fmt, val) in zip(formats, parts)) + + +def format_header(parts): + formats = ['%31s', '%12s', '%12s', '%12s', '%12s', '%12s'] + return '#' + _fmt_ln(formats, parts) + + +def format_line(parts): + formats = ['%32s', '%12d', '%12d', '%12d', '%12d', '%12d'] + return _fmt_ln(formats, parts) + + +def record_stats(key, seed_list, counters, first_seeds, last_seeds): + uniq_seeds = list(set(seed_list)) + logger.debug('seeds=%s', uniq_seeds) + first_seeds[key] = min(uniq_seeds) + last_seeds[key] = max(uniq_seeds) + counters[key] = len(uniq_seeds) + logger.debug('%s first=%d last=%d count=%d', key, first_seeds[key], last_seeds[key], counters[key]) + + +def get_sort_key(options, counters, bit_hds, byte_hds, first_seeds, last_seeds): + if options.sort_by_first: + sort_by = first_seeds + reverse = False + elif options.sort_by_last: + sort_by = last_seeds + reverse = False + elif options.sort_by_bits: + sort_by = bit_hds + reverse = False + elif options.sort_by_bytes: + sort_by = byte_hds + reverse = False + else: + sort_by = counters + reverse = True + return sort_by, reverse + + +def prepare_output(options, counters, bit_hds, byte_hds, first_seeds, last_seeds): + output_lines = [] + header_line = format_header(('crash_id', 'count', 'first_seed', 'last_seed', 'bitwise_hd', 'bytewise_hd')) + output_lines.append(header_line) + sort_by, reverse = get_sort_key(options, counters, bit_hds, byte_hds, first_seeds, last_seeds) + for dummy, k in sorted([(value, key) for (key, value) in sort_by.items()], reverse=reverse): + parts = [k, counters[k], first_seeds[k], last_seeds[k], bit_hds[k], byte_hds[k]] + output_lines.append(format_line(parts)) + return output_lines + + +def parse_cmdline_args(): + parser = OptionParser() + parser.add_option("-d", "--debug", dest="debug", help="Turn on debugging output", action='store_true', default=False) + parser.add_option("-F", "--config", dest="cfgfile", help="read config data from FILENAME") + parser.add_option('', '--first', dest='sort_by_first', help="Sort output by first_seed", action='store_true', default=False) + parser.add_option('', '--last', dest='sort_by_last', help="Sort output by last_seed", action='store_true', default=False) + parser.add_option('', '--bits', dest='sort_by_bits', help="Sort output by bitwise_hd", action='store_true', default=False) + parser.add_option('', '--bytes', dest='sort_by_bytes', help="Sort output by bytewise_hd", action='store_true', default=False) + options, dummy = parser.parse_args() + return options + + +def main(): + options = parse_cmdline_args() + + if options.debug: + logger.setLevel(logging.DEBUG) + + if options.cfgfile: + cfg_file = options.cfgfile + else: + cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') + + logger.debug('Using config file: %s', cfg_file) + cfg = read_config_options(cfg_file) + + result_dir = cfg.crashers_dir + logger.debug('Reading results from %s', result_dir) + + counters = {} + bit_hds = {} + byte_hds = {} + first_seeds = {} + last_seeds = {} + + if not os.path.isdir(result_dir): + logger.info('No results dir found at %s', result_dir) + sys.exit() + + for x in os.listdir(result_dir): + fullpath = os.path.join(result_dir, x) + + # skip non-directories + if not os.path.isdir(fullpath): + logger.debug('Skipping %s - not a dir', fullpath) + continue + + logger.debug('Entering %s', fullpath) + + crashlog = '%s.log' % x + fullpath_crashlog = os.path.join(fullpath, crashlog) + + # skip non-existent logs + if not os.path.isfile(fullpath_crashlog): + logger.debug('No crash log found at %s', fullpath_crashlog) + continue + + logger.debug('Processing %s', fullpath_crashlog) + seed_list = [] + + f = open(fullpath_crashlog, 'r') + try: + for l in f.readlines(): + m = re.search('at seed=(\d+)', l) + if m: + seed_list.append(int(m.group(1))) + logger.debug('%s seed=%s', x, m.group(1)) + continue + + m = re.match('^bitwise_hd=(\d+)', l) + if m: + bit_hds[x] = int(m.group(1)) + logger.debug('%s bitwise_hd=%s', x, m.group(1)) + continue + + m = re.match('^bytewise_hd=(\d+)', l) + if m: + byte_hds[x] = int(m.group(1)) + logger.debug('%s bytewise_hd=%s', x, m.group(1)) + continue + finally: + f.close() + + if len(seed_list) == 0: + logger.debug('%s seed_list was empty', x) + continue + + record_stats(x, seed_list, counters, first_seeds, last_seeds) + + output_lines = prepare_output(options, counters, bit_hds, byte_hds, first_seeds, last_seeds) + + # print your output + [logger.info(l) for l in output_lines] + + +if __name__ == '__main__': + main() + diff --git a/src/linux/tools/bff_stats.py b/src/linux/tools/bff_stats.py index 78b8f8a..24410f2 100755 --- a/src/linux/tools/bff_stats.py +++ b/src/linux/tools/bff_stats.py @@ -5,160 +5,8 @@ @organization: cert.org ''' -import os -import logging -import sys -import re -from optparse import OptionParser +from certfuzz.tools.linux.bff_stats import main -parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, parent_path) - -from certfuzz.campaign.config.bff_config import read_config_options - -logger = logging.getLogger(__name__) -# set default logging level (override with command line options) -logger.setLevel(logging.INFO) - -hdlr = logging.StreamHandler(sys.stdout) -logger.addHandler(hdlr) - -def _fmt_ln(formats, parts): - return '\t'.join(fmt % val for (fmt, val) in zip(formats, parts)) - -def format_header(parts): - formats = ['%31s', '%12s', '%12s', '%12s', '%12s', '%12s'] - return '#' + _fmt_ln(formats, parts) - -def format_line(parts): - formats = ['%32s', '%12d', '%12d', '%12d', '%12d', '%12d'] - return _fmt_ln(formats, parts) - -def record_stats(key, seed_list, counters, first_seeds, last_seeds): - uniq_seeds = list(set(seed_list)) - logger.debug('seeds=%s', uniq_seeds) - first_seeds[key] = min(uniq_seeds) - last_seeds[key] = max(uniq_seeds) - counters[key] = len(uniq_seeds) - logger.debug('%s first=%d last=%d count=%d', key, first_seeds[key], last_seeds[key], counters[key]) - -def get_sort_key(options, counters, bit_hds, byte_hds, first_seeds, last_seeds): - if options.sort_by_first: - sort_by = first_seeds - reverse = False - elif options.sort_by_last: - sort_by = last_seeds - reverse = False - elif options.sort_by_bits: - sort_by = bit_hds - reverse = False - elif options.sort_by_bytes: - sort_by = byte_hds - reverse = False - else: - sort_by = counters - reverse = True - return sort_by, reverse - -def prepare_output(options, counters, bit_hds, byte_hds, first_seeds, last_seeds): - output_lines = [] - header_line = format_header(('crash_id', 'count', 'first_seed', 'last_seed', 'bitwise_hd', 'bytewise_hd')) - output_lines.append(header_line) - sort_by, reverse = get_sort_key(options, counters, bit_hds, byte_hds, first_seeds, last_seeds) - for dummy, k in sorted([(value, key) for (key, value) in sort_by.items()], reverse=reverse): - parts = [k, counters[k], first_seeds[k], last_seeds[k], bit_hds[k], byte_hds[k]] - output_lines.append(format_line(parts)) - return output_lines - -def parse_cmdline_args(): - parser = OptionParser() - parser.add_option("-d", "--debug", dest="debug", help="Turn on debugging output", action='store_true', default=False) - parser.add_option("-F", "--config", dest="cfgfile", help="read config data from FILENAME") - parser.add_option('', '--first', dest='sort_by_first', help="Sort output by first_seed", action='store_true', default=False) - parser.add_option('', '--last', dest='sort_by_last', help="Sort output by last_seed", action='store_true', default=False) - parser.add_option('', '--bits', dest='sort_by_bits', help="Sort output by bitwise_hd", action='store_true', default=False) - parser.add_option('', '--bytes', dest='sort_by_bytes', help="Sort output by bytewise_hd", action='store_true', default=False) - options, dummy = parser.parse_args() - return options if __name__ == '__main__': - options = parse_cmdline_args() - - if options.debug: - logger.setLevel(logging.DEBUG) - - if options.cfgfile: - cfg_file = options.cfgfile - else: - cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') - - logger.debug('Using config file: %s', cfg_file) - cfg = read_config_options(cfg_file) - - result_dir = cfg.crashers_dir - logger.debug('Reading results from %s', result_dir) - - counters = {} - bit_hds = {} - byte_hds = {} - first_seeds = {} - last_seeds = {} - - if not os.path.isdir(result_dir): - logger.info('No results dir found at %s', result_dir) - sys.exit() - - for x in os.listdir(result_dir): - fullpath = os.path.join(result_dir, x) - - # skip non-directories - if not os.path.isdir(fullpath): - logger.debug('Skipping %s - not a dir', fullpath) - continue - - logger.debug('Entering %s', fullpath) - - crashlog = '%s.log' % x - fullpath_crashlog = os.path.join(fullpath, crashlog) - - # skip non-existent logs - if not os.path.isfile(fullpath_crashlog): - logger.debug('No crash log found at %s', fullpath_crashlog) - continue - - logger.debug('Processing %s', fullpath_crashlog) - seed_list = [] - - f = open(fullpath_crashlog, 'r') - try: - for l in f.readlines(): - m = re.search('at seed=(\d+)', l) - if m: - seed_list.append(int(m.group(1))) - logger.debug('%s seed=%s', x, m.group(1)) - continue - - m = re.match('^bitwise_hd=(\d+)', l) - if m: - bit_hds[x] = int(m.group(1)) - logger.debug('%s bitwise_hd=%s', x, m.group(1)) - continue - - m = re.match('^bytewise_hd=(\d+)', l) - if m: - byte_hds[x] = int(m.group(1)) - logger.debug('%s bytewise_hd=%s', x, m.group(1)) - continue - finally: - f.close() - - if len(seed_list) == 0: - logger.debug('%s seed_list was empty', x) - continue - - record_stats(x, seed_list, counters, first_seeds, last_seeds) - - output_lines = prepare_output(options, counters, bit_hds, byte_hds, first_seeds, last_seeds) - - # print your output - [logger.info(l) for l in output_lines] + main() From 3616415dc4a4c62f9c7f0a77734daae72d0dbbb8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 08:42:13 -0500 Subject: [PATCH 0043/1169] move callsim.py into certfuzz.tools.linux --- src/certfuzz/tools/linux/callsim.py | 82 +++++++++++++++++++++++++++++ src/linux/tools/callsim.py | 78 +-------------------------- 2 files changed, 84 insertions(+), 76 deletions(-) create mode 100755 src/certfuzz/tools/linux/callsim.py diff --git a/src/certfuzz/tools/linux/callsim.py b/src/certfuzz/tools/linux/callsim.py new file mode 100755 index 0000000..120b8eb --- /dev/null +++ b/src/certfuzz/tools/linux/callsim.py @@ -0,0 +1,82 @@ +''' +Created on Aug 16, 2011 + +@organization: cert.org +''' +import logging +from optparse import OptionParser + +from ...fuzztools.similarity_matrix import SimilarityMatrix +from ...fuzztools.similarity_matrix import SimilarityMatrixError +from ...fuzztools.distance_matrix import DistanceMatrixError + +logger = logging.getLogger() +logger.setLevel(logging.WARNING) + + +def main(): + parser = OptionParser(usage='%prog [options] ... ') + parser.add_option('', '--debug', dest='debug', action='store_true', help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') + parser.add_option('', '--outfile', dest='outfile', help='file to write output to') + parser.add_option('', '--precision', dest='precision', help='Number of digits to print in similarity') + parser.add_option('', '--style', dest='style', help='Either "list" or "tree"') + + (options, args) = parser.parse_args() + + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + if options.verbose: + logger.setLevel(logging.INFO) + if options.debug: + logger.setLevel(logging.DEBUG) + + if not len(args): + print "You must specify at least one dir to crawl.\n" + parser.print_help() + exit(-1) + else: + logger.debug('Args: %s', args) + + try: + sim = SimilarityMatrix(args) + except SimilarityMatrixError, e: + print 'Error:', e + exit(-1) + + if options.precision: + sim.precision = options.precision + + if not options.style or options.style == 'list': + # Print the results + if options.outfile: + target = options.outfile + else: + # default goes to sys.stdout + target = None + + sim.print_to(target) + + elif options.style == 'tree': + from certfuzz.fuzztools.distance_matrix import DistanceMatrix + + if options.outfile: + target = options.outfile + else: + target = 'cluster.png' + + dm = DistanceMatrix(sim.sim) + try: + dm.to_image(target) + except DistanceMatrixError, e: + print "PIL not installed, skipping image creation." + else: + # it's something other than None, list, or tree + print "The only allowed values for --style are 'list' and 'tree': %s" % options.style + parser.print_help() + exit(-1) + + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/callsim.py b/src/linux/tools/callsim.py index 32752b6..d64a848 100755 --- a/src/linux/tools/callsim.py +++ b/src/linux/tools/callsim.py @@ -4,81 +4,7 @@ @organization: cert.org ''' -import os -import sys -import logging -from optparse import OptionParser - -parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, parent_path) - -from certfuzz.fuzztools.similarity_matrix import SimilarityMatrix -from certfuzz.fuzztools.similarity_matrix import SimilarityMatrixError -from certfuzz.fuzztools.distance_matrix import DistanceMatrixError - -logger = logging.getLogger() -logger.setLevel(logging.WARNING) +from certfuzz.tools.linux.callsim import main if __name__ == '__main__': - - parser = OptionParser(usage='%prog [options] ... ') - parser.add_option('', '--debug', dest='debug', action='store_true', help='Enable debug messages (overrides --verbose)') - parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') - parser.add_option('', '--outfile', dest='outfile', help='file to write output to') - parser.add_option('', '--precision', dest='precision', help='Number of digits to print in similarity') - parser.add_option('', '--style', dest='style', help='Either "list" or "tree"') - - (options, args) = parser.parse_args() - - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - if options.verbose: - logger.setLevel(logging.INFO) - if options.debug: - logger.setLevel(logging.DEBUG) - - if not len(args): - print "You must specify at least one dir to crawl.\n" - parser.print_help() - exit(-1) - else: - logger.debug('Args: %s', args) - - try: - sim = SimilarityMatrix(args) - except SimilarityMatrixError, e: - print 'Error:', e - exit(-1) - - if options.precision: - sim.precision = options.precision - - if not options.style or options.style == 'list': - # Print the results - if options.outfile: - target = options.outfile - else: - # default goes to sys.stdout - target = None - - sim.print_to(target) - - elif options.style == 'tree': - from certfuzz.fuzztools.distance_matrix import DistanceMatrix - - if options.outfile: - target = options.outfile - else: - target = 'cluster.png' - - dm = DistanceMatrix(sim.sim) - try: - dm.to_image(target) - except DistanceMatrixError, e: - print "PIL not installed, skipping image creation." - else: - # it's something other than None, list, or tree - print "The only allowed values for --style are 'list' and 'tree': %s" % options.style - parser.print_help() - exit(-1) + main() From 5c0acabf3cc52d30852ca28cd0e0c07f9b8dd32b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 08:47:26 -0500 Subject: [PATCH 0044/1169] move create_crasher_script into certfuzz.tools.linux --- .../tools/linux/create_crasher_script.py | 122 ++++++++++++++++++ src/linux/tools/create_crasher_script.py | 110 +--------------- 2 files changed, 124 insertions(+), 108 deletions(-) create mode 100755 src/certfuzz/tools/linux/create_crasher_script.py diff --git a/src/certfuzz/tools/linux/create_crasher_script.py b/src/certfuzz/tools/linux/create_crasher_script.py new file mode 100755 index 0000000..2dc3a52 --- /dev/null +++ b/src/certfuzz/tools/linux/create_crasher_script.py @@ -0,0 +1,122 @@ +''' +Created on Jan 13, 2011 + +@organization: cert.org + +''' + +from optparse import OptionParser +import logging +import sys +import os +import re + +parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, parent_path) + +from certfuzz.campaign.config.bff_config import read_config_options + +logger = logging.getLogger(__name__) +# set default logging level (override with command line options) +logger.setLevel(logging.INFO) +stdout_hdlr = logging.StreamHandler(sys.stdout) +stderr_hdlr = logging.StreamHandler(sys.stderr) +stderr_hdlr.setLevel(logging.WARNING) + +logger.addHandler(stdout_hdlr) +logger.addHandler(stderr_hdlr) + + +def parse_options(): + usage = "usage: %prog [options] " + + parser = OptionParser(usage) + parser.add_option("-d", "--debug", dest="debug", help="Turn on debugging output", action='store_true', default=False) + parser.add_option("-F", "--config", dest="cfgfile", help="read config data from CFGFILE") + parser.add_option("-o", '--outfile', dest='outfile', help="write script to OUTFILE instead of stdout") + parser.add_option("", "--force", dest="force", help="Force overwrite of existing OUTFILE", action='store_true', default=False) + parser.add_option('', '--destination', dest='dest', help="Replace output location in script with DEST") + + options, args = parser.parse_args() + if not len(args): + parser.print_help() + parser.error("Please specify a crash_id") + return options, args + + +def main(): + options, args = parse_options() + + if options.debug: + logger.setLevel(logging.DEBUG) + + file_logger = False + if options.outfile: + file_logger = logging.getLogger('outfile') + file_logger.setLevel(logging.INFO) + if os.path.exists(options.outfile) and not options.force: + logging.warning('%s exists, use --force to overwrite', options.outfile) + sys.exit() + hdlr = logging.FileHandler(options.outfile, 'w') + fmt = logging.Formatter('%(message)s') + hdlr.setFormatter(fmt) + hdlr.setLevel(logging.INFO) + file_logger.addHandler(hdlr) + # since we're logging to a file, we can suppress output to stdout + # but we still want to keep warnings + if not options.debug: + logger.removeHandler(stdout_hdlr) + + if options.cfgfile: + cfg_file = options.cfgfile + else: + cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') + + logger.debug('Using config file: %s', cfg_file) + cfg = read_config_options(cfg_file) + + result_dir = cfg.crashers_dir + logger.debug('Reading results from %s', result_dir) + + for crash_id in args: + logger.debug('Crash_id=%s', crash_id) + crash_dir = os.path.join(result_dir, crash_id) + if not os.path.isdir(crash_dir): + logger.debug('%s is not a dir', crash_dir) + continue + + logger.debug('Looking for crash log in %s', crash_dir) + log = os.path.join(crash_dir, crash_id + '.log') + if not os.path.exists(log): + logger.warning('No log found at %s', log) + continue + + logger.debug('Found log at %s', log) + + f = open(log, 'r') + try: + for l in f.readlines(): + m = re.match('^Command:\s+(.+)$', l) + if not m: + continue + + cmdline = m.group(1) + + if options.dest: + (cmd, dst) = [x.strip() for x in cmdline.split('>')] + filename = os.path.basename(dst) + logger.debug(filename) + new_dst = os.path.join(options.dest, filename) + logger.debug(new_dst) + cmdline = ' > '.join((cmd, new_dst)) + + if file_logger: + file_logger.info(cmdline) + else: + logger.info(cmdline) + finally: + f.close() + + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/create_crasher_script.py b/src/linux/tools/create_crasher_script.py index f9e3add..c33d70f 100755 --- a/src/linux/tools/create_crasher_script.py +++ b/src/linux/tools/create_crasher_script.py @@ -5,113 +5,7 @@ @organization: cert.org ''' - -from optparse import OptionParser -import logging -import sys -import os -import re - -parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, parent_path) - -from certfuzz.campaign.config.bff_config import read_config_options - -logger = logging.getLogger(__name__) -# set default logging level (override with command line options) -logger.setLevel(logging.INFO) -stdout_hdlr = logging.StreamHandler(sys.stdout) -stderr_hdlr = logging.StreamHandler(sys.stderr) -stderr_hdlr.setLevel(logging.WARNING) - -logger.addHandler(stdout_hdlr) -logger.addHandler(stderr_hdlr) - -def parse_options(): - usage = "usage: %prog [options] " - - parser = OptionParser(usage) - parser.add_option("-d", "--debug", dest="debug", help="Turn on debugging output", action='store_true', default=False) - parser.add_option("-F", "--config", dest="cfgfile", help="read config data from CFGFILE") - parser.add_option("-o", '--outfile', dest='outfile', help="write script to OUTFILE instead of stdout") - parser.add_option("", "--force", dest="force", help="Force overwrite of existing OUTFILE", action='store_true', default=False) - parser.add_option('', '--destination', dest='dest', help="Replace output location in script with DEST") - - options, args = parser.parse_args() - if not len(args): - parser.print_help() - parser.error("Please specify a crash_id") - return options, args +from certfuzz.tools.linux.create_crasher_script import main if __name__ == '__main__': - options, args = parse_options() - - if options.debug: - logger.setLevel(logging.DEBUG) - - file_logger = False - if options.outfile: - file_logger = logging.getLogger('outfile') - file_logger.setLevel(logging.INFO) - if os.path.exists(options.outfile) and not options.force: - logging.warning('%s exists, use --force to overwrite', options.outfile) - sys.exit() - hdlr = logging.FileHandler(options.outfile, 'w') - fmt = logging.Formatter('%(message)s') - hdlr.setFormatter(fmt) - hdlr.setLevel(logging.INFO) - file_logger.addHandler(hdlr) - # since we're logging to a file, we can suppress output to stdout - # but we still want to keep warnings - if not options.debug: - logger.removeHandler(stdout_hdlr) - - if options.cfgfile: - cfg_file = options.cfgfile - else: - cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') - - logger.debug('Using config file: %s', cfg_file) - cfg = read_config_options(cfg_file) - - result_dir = cfg.crashers_dir - logger.debug('Reading results from %s', result_dir) - - for crash_id in args: - logger.debug('Crash_id=%s', crash_id) - crash_dir = os.path.join(result_dir, crash_id) - if not os.path.isdir(crash_dir): - logger.debug('%s is not a dir', crash_dir) - continue - - logger.debug('Looking for crash log in %s', crash_dir) - log = os.path.join(crash_dir, crash_id + '.log') - if not os.path.exists(log): - logger.warning('No log found at %s', log) - continue - - logger.debug('Found log at %s', log) - - f = open(log, 'r') - try: - for l in f.readlines(): - m = re.match('^Command:\s+(.+)$', l) - if not m: - continue - - cmdline = m.group(1) - - if options.dest: - (cmd, dst) = [x.strip() for x in cmdline.split('>')] - filename = os.path.basename(dst) - logger.debug(filename) - new_dst = os.path.join(options.dest, filename) - logger.debug(new_dst) - cmdline = ' > '.join((cmd, new_dst)) - - if file_logger: - file_logger.info(cmdline) - else: - logger.info(cmdline) - finally: - f.close() + main() From 4b482a0cb899fbe8506264a89426171120971c10 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 08:50:59 -0500 Subject: [PATCH 0045/1169] move debuggerfile into certfuzz.tools.linux --- src/certfuzz/tools/linux/debuggerfile.py | 52 ++++++++++++++++++++++++ src/linux/tools/debuggerfile.py | 42 +------------------ 2 files changed, 54 insertions(+), 40 deletions(-) create mode 100755 src/certfuzz/tools/linux/debuggerfile.py diff --git a/src/certfuzz/tools/linux/debuggerfile.py b/src/certfuzz/tools/linux/debuggerfile.py new file mode 100755 index 0000000..1aee794 --- /dev/null +++ b/src/certfuzz/tools/linux/debuggerfile.py @@ -0,0 +1,52 @@ +''' +Created on Oct 15, 2012 + +@organization: cert.org +''' + +import logging +from optparse import OptionParser +import sys +import os + +mydir = os.path.dirname(os.path.abspath(__file__)) +parentdir = os.path.abspath(os.path.join(mydir, '..')) +sys.path.append(parentdir) + +from certfuzz.debuggers.output_parsers.gdbfile import GDBfile +from certfuzz.debuggers.output_parsers.cwfile import CWfile + +logger = logging.getLogger(__name__) +logger.setLevel(logging.WARNING) + + +def main(): + logger = logging.getLogger() + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + parser = OptionParser() + parser.add_option('', '--debug', dest='debug', action='store_true', help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') + (options, args) = parser.parse_args() + + if options.debug: + logger.setLevel(logging.DEBUG) + elif options.verbose: + logger.setLevel(logging.INFO) + else: + logger.setLevel(logging.WARNING) + + for f in args: + debugger = os.path.splitext(f)[1] + if debugger == '.gdb': + g = GDBfile(f) + elif debugger == '.cw': + g = CWfile(f) + print 'Signature=%s' % g.get_crash_signature(5) + if g.registers_hex.get(g.pc_name): + print 'PC=%s' % g.registers_hex[g.pc_name] + + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/debuggerfile.py b/src/linux/tools/debuggerfile.py index 005cb1a..e08d882 100755 --- a/src/linux/tools/debuggerfile.py +++ b/src/linux/tools/debuggerfile.py @@ -4,45 +4,7 @@ @organization: cert.org ''' - -import logging -from optparse import OptionParser -import sys -import os - -mydir = os.path.dirname(os.path.abspath(__file__)) -parentdir = os.path.abspath(os.path.join(mydir, '..')) -sys.path.append(parentdir) - -from certfuzz.debuggers.output_parsers.gdbfile import GDBfile -from certfuzz.debuggers.output_parsers.cwfile import CWfile - -logger = logging.getLogger(__name__) -logger.setLevel(logging.WARNING) +from certfuzz.tools.linux.debuggerfile import main if __name__ == '__main__': - logger = logging.getLogger() - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - parser = OptionParser() - parser.add_option('', '--debug', dest='debug', action='store_true', help='Enable debug messages (overrides --verbose)') - parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') - (options, args) = parser.parse_args() - - if options.debug: - logger.setLevel(logging.DEBUG) - elif options.verbose: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.WARNING) - - for f in args: - debugger = os.path.splitext(f)[1] - if debugger == '.gdb': - g = GDBfile(f) - elif debugger == '.cw': - g = CWfile(f) - print 'Signature=%s' % g.get_crash_signature(5) - if g.registers_hex.get(g.pc_name): - print 'PC=%s' % g.registers_hex[g.pc_name] + main() From 4060b59d74276dc2dbbd0227cc83ff4320160664 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 08:53:40 -0500 Subject: [PATCH 0046/1169] move drillresults into certfuzz.tools.linux --- src/certfuzz/tools/linux/drillresults.py | 548 +++++++++++++++++++++++ src/linux/tools/drillresults.py | 543 +--------------------- 2 files changed, 549 insertions(+), 542 deletions(-) create mode 100755 src/certfuzz/tools/linux/drillresults.py diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py new file mode 100755 index 0000000..3f79271 --- /dev/null +++ b/src/certfuzz/tools/linux/drillresults.py @@ -0,0 +1,548 @@ +''' +This script looks for interesting crashes and rate them by potential exploitability +''' + +import os +import struct +import binascii +import re +from optparse import OptionParser + +regex = { + 'gdb_report': re.compile(r'.+.gdb$'), + 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), + 'frame0': re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+'), + 'regs1': re.compile(r'^eax=.+'), + 'regs2': re.compile(r'^eip=.+'), + 'bt_addr': re.compile(r'(0x[0-9a-fA-F]+)\s+.+$'), + '64bit_debugger': re.compile(r'^Microsoft.*AMD64$'), + 'syswow64': re.compile(r'ModLoad:.*syswow64.*', re.IGNORECASE), + 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)'), + 'vdso': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+\[vdso\]'), + 'mapped_address': re.compile(r'^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), + 'mapped_address64': re.compile(r'^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), + 'syswow64': re.compile(r'ModLoad:.*syswow64.*', re.IGNORECASE), + 'dbg_prompt': re.compile(r'^[0-9]:[0-9][0-9][0-9]> (.*)'), + } + +registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', + 'edi', 'eip') + +registers64 = ('rax', 'rcx', 'rdx', 'rbx', 'rsp', 'rbp', 'rsi', + 'rdi', 'rip', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', + 'r14', 'r15') + +# These !exploitable short descriptions indicate a very interesting crash +really_exploitable = [ + 'SegFaultOnPc', + 'BranchAv', + 'StackCodeExection', + 'BadInstruction', + 'ReturnAv', + ] +reg_set = set(registers) +reg64_set = set(registers64) +re_set = set(really_exploitable) + +results = {} +scoredcrashes = {} +#regdict = {} +gdblist = [] +ignorejit = False +_64bit_debugger = False + + +def check_64bit(reporttext): + ''' + Check if the debugger and target app are 64-bit + ''' + global _64bit_debugger + + if _64bit_debugger: + return + + for line in reporttext.splitlines(): + m = re.match(regex['bt_addr'], line) + if m: + start_addr = m.group(1) + #print '%s length: %s'% (start_addr, len(start_addr)) + if len(start_addr) > 10: + _64bit_debugger = True + #print 'Target process is 64-bit' + break + + +def pc_in_mapped_address(reporttext, instraddr): + ''' + Check if the instruction pointer is in a loaded module + ''' + if not instraddr: + # The gdb file doesn't have anything in it that'll tell us + # where the PC is. + return '' +# print 'checking if %s is mapped...' % instraddr + mapped_module = 'unloaded' + + instraddr = int(instraddr, 16) +# print 'instraddr: %d' % instraddr + for line in reporttext.splitlines(): + #print 'checking: %s for %s' % (line,regex['mapped_frame']) + n = re.search(regex['mapped_frame'], line) + if n: +# print '*** found mapped address regex!' + # Strip out backticks present on 64-bit systems + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + if begin_address < instraddr < end_address: + mapped_module = n.group(4) + #print 'mapped_module: %s' % mapped_module + else: + # [vdso] still counts as a mapped module + n = re.search(regex['vdso'], line) + if n: + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + if begin_address < instraddr < end_address: + mapped_module = '[vdso]' + + return mapped_module + + +def fixefabug(reporttext, instraddr, faultaddr): + ''' + !exploitable often reports an incorrect EFA for 64-bit targets. + If we're dealing with a 64-bit target, we can second-guess the reported EFA + ''' + instructionline = getinstr(reporttext, instraddr) + if not instructionline: + return faultaddr + ds = carve(instructionline, "ds:", "=") + if ds: + faultaddr = ds.replace('`', '') + return faultaddr + + +def readfile(textfile): + ''' + Read text file + ''' + f = open(textfile, 'r') + text = f.read() + return text + + +def readbinfile(textfile): + ''' + Read binary file + ''' + f = open(textfile, 'rb') + text = f.read() + return text + + +def carve(string, token1, token2): + startindex = string.find(token1) + if startindex == -1: + # can't find token1 + return "" + startindex = startindex + len(token1) + endindex = string.find(token2, startindex) + if endindex == -1: + # can't find token2 + return "" + return string[startindex:endindex] + + +# Todo: fix this up. Was added to bring gdb support +def carve2(string): + delims = [("Exception Faulting Address: ", "\n"), + ("si_addr:$2 = (void *)", "\n")] + for token1, token2 in delims: + startindex = string.find(token1) + if startindex == -1: + # can't find token1 + continue + startindex = startindex + len(token1) + endindex = string.find(token2, startindex) + return string[startindex:endindex] + + +def getexnum(reporttext): + ''' + Get the exception number by counting the number of continues + ''' + exception = 0 + for line in reporttext.splitlines(): + n = re.match(regex['dbg_prompt'], line) + if n: + cdbcmd = n.group(1) + cmds = cdbcmd.split(';') + for cmd in cmds: + if cmd == 'g': + exception = exception + 1 + return exception + + +def getinstraddr(reporttext): + ''' + Find the address for the current (crashing) instruction + ''' + instraddr = None + for line in reporttext.splitlines(): + #print 'checking: %s' % line + n = re.match(regex['current_instr'], line) + if n: + instraddr = n.group(1) + #print 'Found instruction address: %s' % instraddr + if not instraddr: + for line in reporttext.splitlines(): + #No disassembly. Resort to frame 0 address + n = re.match(regex['frame0'], line) + if n: + instraddr = n.group(1) + #print 'Found instruction address: %s' % instraddr + return instraddr + + +def getinstr(reporttext, instraddr): + ''' + Find the disassembly line for the current (crashing) instruction + ''' + instructionline = '' + + for line in reporttext.splitlines(): + n = re.match(regex['current_instr'], line) + if n: + instructionline = n.group(3) + break + return instructionline + + +def formataddr(faultaddr): + ''' + Format a 64- or 32-bit memory address to a fixed width + ''' + + if not faultaddr: + return + else: + faultaddr = faultaddr.strip() + faultaddr = faultaddr.replace('0x', '') + + if _64bit_debugger: + # Due to a bug in !exploitable, the Exception Faulting Address is + # often wrong with 64-bit targets + if len(faultaddr) < 10: + # pad faultaddr + faultaddr = faultaddr.zfill(16) + else: + if len(faultaddr) > 10: # 0x12345678 = 10 chars + faultaddr = faultaddr[-8:] + elif len(faultaddr) < 10: + # pad faultaddr + faultaddr = faultaddr.zfill(8) + + return faultaddr + + +def ratchetscore(crasher, score): + ''' + Only allow a crasher to increase score + ''' + try: + currentscore = scoredcrashes[crasher] + except KeyError: + currentscore = 100 + if score < currentscore: + scoredcrashes[crasher] = score + + +def fixefaoffset(instructionline, faultaddr): + ''' + Adjust faulting address for instructions that use offsets + Currently only works for instructions like CALL [reg + offset] + ''' + global reg_set + + if '0x' not in faultaddr: + faultaddr = '0x' + faultaddr + instructionpieces = instructionline.split() + for index, piece in enumerate(instructionpieces): + if piece == 'call': + # CALL instruction + if len(instructionpieces) <= index + 3: + # CALL to just a register. No offset + return faultaddr + address = instructionpieces[index + 3] + if '+' in address: + splitaddress = address.split('+') + reg = splitaddress[0] + reg = reg.replace('[', '') + if reg not in reg_set: + return faultaddr + offset = splitaddress[1] + offset = offset.replace('h', '') + offset = offset.replace(']', '') + if '0x' not in offset: + offset = '0x' + offset + if int(offset, 16) > int(faultaddr, 16): + # TODO: fix up negative numbers + return faultaddr + # Subtract offset to get actual interesting pattern + faultaddr = hex(eval(faultaddr) - eval(offset)) + faultaddr = formataddr(faultaddr.replace('L', '')) + return faultaddr + + +def checkreport(reportfile, crasherfile, crash_hash): + ''' + Parse the gdb file + ''' + + global _64bit_debugger + + #print('checking %s against %s: %s' % (reportfile, crasherfile, crash_hash)) + crashid = results[crash_hash] + + reporttext = readfile(reportfile) + current_dir = os.path.dirname(reportfile) + exceptionnum = 0 + classification = carve(reporttext, "Classification: ", "\n") + #print 'classification: %s' % classification + try: + if classification: + # Create a new exception dictionary to add to the crash + exception = {} + crashid['exceptions'][exceptionnum] = exception + except KeyError: + # Crash ID (crash_hash) not yet seen + # Default it to not being "really exploitable" + crashid['reallyexploitable'] = False + # Create a dictionary of exceptions for the crash id + exceptions = {} + crashid['exceptions'] = exceptions + # Create a dictionary for the exception + crashid['exceptions'][exceptionnum] = exception + + # Set !exploitable classification for the exception + if classification: + crashid['exceptions'][exceptionnum]['classification'] = classification + + shortdesc = carve(reporttext, "Short description: ", " (") + #print 'shortdesc: %s' % shortdesc + if shortdesc: + # Set !exploitable Short Description for the exception + crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc + if shortdesc in re_set: + # Flag the entire crash ID as really exploitable if this is a good + # exception + crashid['reallyexploitable'] = True + + if not os.path.isfile(crasherfile): + # Can't find the crasher file + #print "WTF! Cannot find %s" % crasherfile + return + # Set the "fuzzedfile" property for the crash ID + crashid['fuzzedfile'] = crasherfile + # See if we're dealing with 64-bit debugger or target app + check_64bit(reporttext) + faultaddr = carve2(reporttext) + #print 'faultaddr: %s' % faultaddr + instraddr = getinstraddr(reporttext) + #instraddr = carve(reporttext, "Instruction Address:", "\n") + faultaddr = formataddr(faultaddr) + instraddr = formataddr(instraddr) + #print 'instruction address: %s' % instraddr + + # No faulting address means no crash. + if not faultaddr: + return + + if instraddr: + crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr) + + # Get the cdb line that contains the crashing instruction + instructionline = getinstr(reporttext, instraddr) + crashid['exceptions'][exceptionnum]['instructionline'] = instructionline + if instructionline: + faultaddr = fixefaoffset(instructionline, faultaddr) + + # Fix faulting pattern endian + faultaddr = faultaddr.replace('0x', '') + crashid['exceptions'][exceptionnum]['efa'] = faultaddr + if _64bit_debugger: + # 64-bit target app + faultaddr = faultaddr.zfill(16) + #print 'faultaddr: %s' % faultaddr + efaptr = struct.unpack('\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), - 'frame0': re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+'), - 'regs1': re.compile(r'^eax=.+'), - 'regs2': re.compile(r'^eip=.+'), - 'bt_addr': re.compile(r'(0x[0-9a-fA-F]+)\s+.+$'), - '64bit_debugger': re.compile(r'^Microsoft.*AMD64$'), - 'syswow64' : re.compile(r'ModLoad:.*syswow64.*', re.IGNORECASE), - 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)'), - 'vdso': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+\[vdso\]'), - 'mapped_address': re.compile(r'^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), - 'mapped_address64': re.compile(r'^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), - 'syswow64' : re.compile(r'ModLoad:.*syswow64.*', re.IGNORECASE), - 'dbg_prompt' : re.compile(r'^[0-9]:[0-9][0-9][0-9]> (.*)'), - } - -registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', - 'edi', 'eip') - -registers64 = ('rax', 'rcx', 'rdx', 'rbx', 'rsp', 'rbp', 'rsi', - 'rdi', 'rip', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', - 'r14', 'r15') - -# These !exploitable short descriptions indicate a very interesting crash -really_exploitable = [ - 'SegFaultOnPc', - 'BranchAv', - 'StackCodeExection', - 'BadInstruction', - 'ReturnAv', - ] -reg_set = set(registers) -reg64_set = set(registers64) -re_set = set(really_exploitable) - -results = {} -scoredcrashes = {} -#regdict = {} -gdblist = [] -ignorejit = False -_64bit_debugger = False - - -def check_64bit(reporttext): - ''' - Check if the debugger and target app are 64-bit - ''' - global _64bit_debugger - - if _64bit_debugger: - return - - for line in reporttext.splitlines(): - m = re.match(regex['bt_addr'], line) - if m: - start_addr = m.group(1) - #print '%s length: %s'% (start_addr, len(start_addr)) - if len(start_addr) > 10: - _64bit_debugger = True - #print 'Target process is 64-bit' - break - - -def pc_in_mapped_address(reporttext, instraddr): - ''' - Check if the instruction pointer is in a loaded module - ''' - if not instraddr: - # The gdb file doesn't have anything in it that'll tell us - # where the PC is. - return '' -# print 'checking if %s is mapped...' % instraddr - mapped_module = 'unloaded' - - instraddr = int(instraddr, 16) -# print 'instraddr: %d' % instraddr - for line in reporttext.splitlines(): - #print 'checking: %s for %s' % (line,regex['mapped_frame']) - n = re.search(regex['mapped_frame'], line) - if n: -# print '*** found mapped address regex!' - # Strip out backticks present on 64-bit systems - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = n.group(4) - #print 'mapped_module: %s' % mapped_module - else: - # [vdso] still counts as a mapped module - n = re.search(regex['vdso'], line) - if n: - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = '[vdso]' - - return mapped_module - - -def fixefabug(reporttext, instraddr, faultaddr): - ''' - !exploitable often reports an incorrect EFA for 64-bit targets. - If we're dealing with a 64-bit target, we can second-guess the reported EFA - ''' - instructionline = getinstr(reporttext, instraddr) - if not instructionline: - return faultaddr - ds = carve(instructionline, "ds:", "=") - if ds: - faultaddr = ds.replace('`', '') - return faultaddr - - -def readfile(textfile): - ''' - Read text file - ''' - f = open(textfile, 'r') - text = f.read() - return text - - -def readbinfile(textfile): - ''' - Read binary file - ''' - f = open(textfile, 'rb') - text = f.read() - return text - - -def carve(string, token1, token2): - startindex = string.find(token1) - if startindex == -1: - # can't find token1 - return "" - startindex = startindex + len(token1) - endindex = string.find(token2, startindex) - if endindex == -1: - # can't find token2 - return "" - return string[startindex:endindex] - - -# Todo: fix this up. Was added to bring gdb support -def carve2(string): - delims = [("Exception Faulting Address: ", "\n"), - ("si_addr:$2 = (void *)", "\n")] - for token1, token2 in delims: - startindex = string.find(token1) - if startindex == -1: - # can't find token1 - continue - startindex = startindex + len(token1) - endindex = string.find(token2, startindex) - return string[startindex:endindex] - - -def getexnum(reporttext): - ''' - Get the exception number by counting the number of continues - ''' - exception = 0 - for line in reporttext.splitlines(): - n = re.match(regex['dbg_prompt'], line) - if n: - cdbcmd = n.group(1) - cmds = cdbcmd.split(';') - for cmd in cmds: - if cmd == 'g': - exception = exception + 1 - return exception - - -def getinstraddr(reporttext): - ''' - Find the address for the current (crashing) instruction - ''' - instraddr = None - for line in reporttext.splitlines(): - #print 'checking: %s' % line - n = re.match(regex['current_instr'], line) - if n: - instraddr = n.group(1) - #print 'Found instruction address: %s' % instraddr - if not instraddr: - for line in reporttext.splitlines(): - #No disassembly. Resort to frame 0 address - n = re.match(regex['frame0'], line) - if n: - instraddr = n.group(1) - #print 'Found instruction address: %s' % instraddr - return instraddr - - -def getinstr(reporttext, instraddr): - ''' - Find the disassembly line for the current (crashing) instruction - ''' - instructionline = '' - - for line in reporttext.splitlines(): - n = re.match(regex['current_instr'], line) - if n: - instructionline = n.group(3) - break - return instructionline - - -def formataddr(faultaddr): - ''' - Format a 64- or 32-bit memory address to a fixed width - ''' - - if not faultaddr: - return - else: - faultaddr = faultaddr.strip() - faultaddr = faultaddr.replace('0x', '') - - if _64bit_debugger: - # Due to a bug in !exploitable, the Exception Faulting Address is - # often wrong with 64-bit targets - if len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(16) - else: - if len(faultaddr) > 10: # 0x12345678 = 10 chars - faultaddr = faultaddr[-8:] - elif len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(8) - - return faultaddr - - -def ratchetscore(crasher, score): - ''' - Only allow a crasher to increase score - ''' - try: - currentscore = scoredcrashes[crasher] - except KeyError: - currentscore = 100 - if score < currentscore: - scoredcrashes[crasher] = score - - -def fixefaoffset(instructionline, faultaddr): - ''' - Adjust faulting address for instructions that use offsets - Currently only works for instructions like CALL [reg + offset] - ''' - global reg_set - - if '0x' not in faultaddr: - faultaddr = '0x' + faultaddr - instructionpieces = instructionline.split() - for index, piece in enumerate(instructionpieces): - if piece == 'call': - # CALL instruction - if len(instructionpieces) <= index + 3: - # CALL to just a register. No offset - return faultaddr - address = instructionpieces[index + 3] - if '+' in address: - splitaddress = address.split('+') - reg = splitaddress[0] - reg = reg.replace('[', '') - if reg not in reg_set: - return faultaddr - offset = splitaddress[1] - offset = offset.replace('h', '') - offset = offset.replace(']', '') - if '0x' not in offset: - offset = '0x' + offset - if int(offset, 16) > int(faultaddr, 16): - # TODO: fix up negative numbers - return faultaddr - # Subtract offset to get actual interesting pattern - faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = formataddr(faultaddr.replace('L', '')) - return faultaddr - - -def checkreport(reportfile, crasherfile, crash_hash): - ''' - Parse the gdb file - ''' - - global _64bit_debugger - - #print('checking %s against %s: %s' % (reportfile, crasherfile, crash_hash)) - crashid = results[crash_hash] - - reporttext = readfile(reportfile) - current_dir = os.path.dirname(reportfile) - exceptionnum = 0 - classification = carve(reporttext, "Classification: ", "\n") - #print 'classification: %s' % classification - try: - if classification: - # Create a new exception dictionary to add to the crash - exception = {} - crashid['exceptions'][exceptionnum] = exception - except KeyError: - # Crash ID (crash_hash) not yet seen - # Default it to not being "really exploitable" - crashid['reallyexploitable'] = False - # Create a dictionary of exceptions for the crash id - exceptions = {} - crashid['exceptions'] = exceptions - # Create a dictionary for the exception - crashid['exceptions'][exceptionnum] = exception - - # Set !exploitable classification for the exception - if classification: - crashid['exceptions'][exceptionnum]['classification'] = classification - - shortdesc = carve(reporttext, "Short description: ", " (") - #print 'shortdesc: %s' % shortdesc - if shortdesc: - # Set !exploitable Short Description for the exception - crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc - if shortdesc in re_set: - # Flag the entire crash ID as really exploitable if this is a good - # exception - crashid['reallyexploitable'] = True - - if not os.path.isfile(crasherfile): - # Can't find the crasher file - #print "WTF! Cannot find %s" % crasherfile - return - # Set the "fuzzedfile" property for the crash ID - crashid['fuzzedfile'] = crasherfile - # See if we're dealing with 64-bit debugger or target app - check_64bit(reporttext) - faultaddr = carve2(reporttext) - #print 'faultaddr: %s' % faultaddr - instraddr = getinstraddr(reporttext) - #instraddr = carve(reporttext, "Instruction Address:", "\n") - faultaddr = formataddr(faultaddr) - instraddr = formataddr(instraddr) - #print 'instruction address: %s' % instraddr - - # No faulting address means no crash. - if not faultaddr: - return - - if instraddr: - crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr) - - # Get the cdb line that contains the crashing instruction - instructionline = getinstr(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['instructionline'] = instructionline - if instructionline: - faultaddr = fixefaoffset(instructionline, faultaddr) - - # Fix faulting pattern endian - faultaddr = faultaddr.replace('0x', '') - crashid['exceptions'][exceptionnum]['efa'] = faultaddr - if _64bit_debugger: - # 64-bit target app - faultaddr = faultaddr.zfill(16) - #print 'faultaddr: %s' % faultaddr - efaptr = struct.unpack(' Date: Thu, 23 Jan 2014 08:55:19 -0500 Subject: [PATCH 0047/1169] move minimize into certfuzz.tools.linux --- src/certfuzz/tools/linux/minimize.py | 185 +++++++++++++++++++++++++++ src/linux/tools/minimize.py | 178 +------------------------- 2 files changed, 186 insertions(+), 177 deletions(-) create mode 100755 src/certfuzz/tools/linux/minimize.py diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py new file mode 100755 index 0000000..f14d855 --- /dev/null +++ b/src/certfuzz/tools/linux/minimize.py @@ -0,0 +1,185 @@ +''' +Created on Apr 9, 2012 + +@organization: cert.org +''' +import logging +import os +import sys + +mydir = os.path.dirname(os.path.abspath(__file__)) +parentdir = os.path.abspath(os.path.join(mydir, '..')) +sys.path.append(parentdir) + +from certfuzz import debuggers +from certfuzz.fuzztools import filetools, text +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.campaign.config.bff_config import read_config_options +from certfuzz.crash.bff_crash import BffCrash +from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer +from certfuzz.debuggers import gdb # @UnusedImport +from certfuzz.debuggers import crashwrangler # @UnusedImport + +logger = logging.getLogger() + + +def main(): + debuggers.verify_supported_platform() + + from optparse import OptionParser + + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + usage = "usage: %prog [options] fuzzedfile" + parser = OptionParser(usage) + parser.add_option('', '--debug', dest='debug', action='store_true', + help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--verbose', dest='verbose', action='store_true', + help='Enable verbose messages') + parser.add_option('-t', '--target', dest='target', + help='the file to minimize to (typically the seedfile)') + parser.add_option('-o', '--outdir', dest='outdir', + help='dir to write output to') + parser.add_option('-s', '--stringmode', dest='stringmode', action='store_true', + help='minimize to a string rather than to a target file') + parser.add_option('-x', '--preferx', dest='prefer_x_target', + action='store_true', + help='Minimize to \'x\' characters instead of Metasploit string pattern') + parser.add_option('-f', '--faddr', dest='keep_uniq_faddr', + action='store_true', + help='Use exception faulting addresses as part of crash signature') + parser.add_option('-b', '--bitwise', dest='bitwise', action='store_true', + help='if set, use bitwise hamming distance. Default is bytewise') + parser.add_option('-c', '--confidence', dest='confidence', + help='The desired confidence level (default: 0.999)', + type='float') + parser.add_option('-g', '--target-size-guess', dest='initial_target_size', + help='A guess at the minimal value (int)', type='int') + parser.add_option('', '--config', dest='config', + help='path to the configuration file to use') + parser.add_option('', '--timeout', dest='timeout', + metavar='N', type='int', default=0, + help='Stop minimizing after N seconds (default is 0, never time out).') + + (options, args) = parser.parse_args() + + if options.debug: + logger.setLevel(logging.DEBUG) + elif options.verbose: + logger.setLevel(logging.INFO) + + if options.config: + cfg_file = os.path.expanduser(options.config) + else: + if os.path.isfile("../conf.d/bff.cfg"): + cfg_file = "../conf.d/bff.cfg" + elif os.path.isfile("conf.d/bff.cfg"): + cfg_file = "conf.d/bff.cfg" + else: + parser.error('Configuration file (--config) option must be specified.') + logger.debug('Config file: %s', cfg_file) + + if options.stringmode and options.target: + parser.error('Options --stringmode and --target are mutually exclusive.') + + # Set some default options. Fast and loose if in string mode + # More precise with minimize to seedfile + if not options.confidence: + if options.stringmode: + options.confidence = 0.5 + else: + options.confidence = 0.999 + if not options.initial_target_size: + if options.stringmode: + options.initial_target_size = 100 + else: + options.initial_target_size = 1 + + if options.confidence: + try: + options.confidence = float(options.confidence) + except: + parser.error('Confidence must be a float.') + if not 0.0 < options.confidence < 1.0: + parser.error('Confidence must be in the range 0.0 < c < 1.0') + + confidence = options.confidence + + if options.outdir: + outdir = options.outdir + else: + outdir = "./minimizer_out" + + if not os.path.exists(outdir): + filetools.make_directories(outdir) + + if not os.path.isdir(outdir): + parser.error('--outdir must either already be a dir or not exist: %s' % outdir) + + if len(args) and os.path.exists(args[0]): + fuzzed_file = BasicFile(args[0]) + logger.info('Fuzzed file is %s', fuzzed_file) + else: + parser.error('fuzzedfile must be specified') + + cfg = read_config_options(cfg_file) + + if options.target: + seedfile = BasicFile(options.target) + else: + seedfile = None + + min2seed = not options.stringmode + filename_modifier = '' + + crashers_dir = '.' + + with BffCrash(cfg, seedfile, fuzzed_file, cfg.program, + cfg.debugger_timeout, cfg.killprocname, cfg.backtracelevels, + crashers_dir, options.keep_uniq_faddr) as crash: + + filetools.make_directories(crash.tempdir) + logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) + filetools.copy_file(fuzzed_file.path, crash.tempdir) + + with Minimizer(cfg=cfg, crash=crash, crash_dst_dir=outdir, + seedfile_as_target=min2seed, + bitwise=options.bitwise, + confidence=confidence, + logfile='./min_log.txt', + tempdir=crash.tempdir, + maxtime=options.timeout, + preferx=options.prefer_x_target, + keep_uniq_faddr=options.keep_uniq_faddr) as minimize: + minimize.save_others = False + minimize.target_size_guess = int(options.initial_target_size) + minimize.go() + + if options.stringmode: + logger.debug('x character substitution') + length = len(minimize.fuzzed) + if options.prefer_x_target: + #We minimized to 'x', so we attempt to get metasploit as a freebie + targetstring = list(text.metasploit_pattern_orig(length)) + filename_modifier = '-mtsp' + else: + #We minimized to metasploit, so we attempt to get 'x' as a freebie + targetstring = list('x' * length) + filename_modifier = '-x' + + fuzzed = list(minimize.fuzzed) + for idx in minimize.bytemap: + logger.debug('Swapping index %d', idx) + targetstring[idx] = fuzzed[idx] + filename = ''.join((crash.fuzzedfile.root, filename_modifier, crash.fuzzedfile.ext)) + metasploit_file = os.path.join(crash.tempdir, filename) + + with open(metasploit_file, 'wb') as f: + f.writelines(targetstring) + + crash.copy_files(outdir) + crash.clean_tmpdir() + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/minimize.py b/src/linux/tools/minimize.py index 5b2d334..7391e0a 100755 --- a/src/linux/tools/minimize.py +++ b/src/linux/tools/minimize.py @@ -4,183 +4,7 @@ @organization: cert.org ''' -import logging -import os -import sys - -mydir = os.path.dirname(os.path.abspath(__file__)) -parentdir = os.path.abspath(os.path.join(mydir, '..')) -sys.path.append(parentdir) - -from certfuzz import debuggers -from certfuzz.fuzztools import filetools, text -from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.campaign.config.bff_config import read_config_options -from certfuzz.crash.bff_crash import BffCrash -from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer -from certfuzz.minimizer import MinimizerError -from certfuzz.debuggers import gdb # @UnusedImport -from certfuzz.debuggers import crashwrangler # @UnusedImport - -logger = logging.getLogger() - -def main(): - debuggers.verify_supported_platform() - - from optparse import OptionParser - - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - usage = "usage: %prog [options] fuzzedfile" - parser = OptionParser(usage) - parser.add_option('', '--debug', dest='debug', action='store_true', - help='Enable debug messages (overrides --verbose)') - parser.add_option('', '--verbose', dest='verbose', action='store_true', - help='Enable verbose messages') - parser.add_option('-t', '--target', dest='target', - help='the file to minimize to (typically the seedfile)') - parser.add_option('-o', '--outdir', dest='outdir', - help='dir to write output to') - parser.add_option('-s', '--stringmode', dest='stringmode', action='store_true', - help='minimize to a string rather than to a target file') - parser.add_option('-x', '--preferx', dest='prefer_x_target', - action='store_true', - help='Minimize to \'x\' characters instead of Metasploit string pattern') - parser.add_option('-f', '--faddr', dest='keep_uniq_faddr', - action='store_true', - help='Use exception faulting addresses as part of crash signature') - parser.add_option('-b', '--bitwise', dest='bitwise', action='store_true', - help='if set, use bitwise hamming distance. Default is bytewise') - parser.add_option('-c', '--confidence', dest='confidence', - help='The desired confidence level (default: 0.999)', - type='float') - parser.add_option('-g', '--target-size-guess', dest='initial_target_size', - help='A guess at the minimal value (int)', type='int') - parser.add_option('', '--config', dest='config', - help='path to the configuration file to use') - parser.add_option('', '--timeout', dest='timeout', - metavar='N', type='int', default=0, - help='Stop minimizing after N seconds (default is 0, never time out).') - - (options, args) = parser.parse_args() - - if options.debug: - logger.setLevel(logging.DEBUG) - elif options.verbose: - logger.setLevel(logging.INFO) - - if options.config: - cfg_file = os.path.expanduser(options.config) - else: - if os.path.isfile("../conf.d/bff.cfg"): - cfg_file = "../conf.d/bff.cfg" - elif os.path.isfile("conf.d/bff.cfg"): - cfg_file = "conf.d/bff.cfg" - else: - parser.error('Configuration file (--config) option must be specified.') - logger.debug('Config file: %s', cfg_file) - - if options.stringmode and options.target: - parser.error('Options --stringmode and --target are mutually exclusive.') - - # Set some default options. Fast and loose if in string mode - # More precise with minimize to seedfile - if not options.confidence: - if options.stringmode: - options.confidence = 0.5 - else: - options.confidence = 0.999 - if not options.initial_target_size: - if options.stringmode: - options.initial_target_size = 100 - else: - options.initial_target_size = 1 - - if options.confidence: - try: - options.confidence = float(options.confidence) - except: - parser.error('Confidence must be a float.') - if not 0.0 < options.confidence < 1.0: - parser.error('Confidence must be in the range 0.0 < c < 1.0') - - confidence = options.confidence - - if options.outdir: - outdir = options.outdir - else: - outdir = "./minimizer_out" - - if not os.path.exists(outdir): - filetools.make_directories(outdir) - - if not os.path.isdir(outdir): - parser.error('--outdir must either already be a dir or not exist: %s' % outdir) - - if len(args) and os.path.exists(args[0]): - fuzzed_file = BasicFile(args[0]) - logger.info('Fuzzed file is %s', fuzzed_file) - else: - parser.error('fuzzedfile must be specified') - - cfg = read_config_options(cfg_file) - - if options.target: - seedfile = BasicFile(options.target) - else: - seedfile = None - - min2seed = not options.stringmode - filename_modifier = '' - - crashers_dir = '.' - - with BffCrash(cfg, seedfile, fuzzed_file, cfg.program, - cfg.debugger_timeout, cfg.killprocname, cfg.backtracelevels, - crashers_dir, options.keep_uniq_faddr) as crash: - - filetools.make_directories(crash.tempdir) - logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) - filetools.copy_file(fuzzed_file.path, crash.tempdir) - - with Minimizer(cfg=cfg, crash=crash, crash_dst_dir=outdir, - seedfile_as_target=min2seed, - bitwise=options.bitwise, - confidence=confidence, - logfile='./min_log.txt', - tempdir=crash.tempdir, - maxtime=options.timeout, - preferx=options.prefer_x_target, - keep_uniq_faddr=options.keep_uniq_faddr) as minimize: - minimize.save_others = False - minimize.target_size_guess = int(options.initial_target_size) - minimize.go() - - if options.stringmode: - logger.debug('x character substitution') - length = len(minimize.fuzzed) - if options.prefer_x_target: - #We minimized to 'x', so we attempt to get metasploit as a freebie - targetstring = list(text.metasploit_pattern_orig(length)) - filename_modifier = '-mtsp' - else: - #We minimized to metasploit, so we attempt to get 'x' as a freebie - targetstring = list('x' * length) - filename_modifier = '-x' - - fuzzed = list(minimize.fuzzed) - for idx in minimize.bytemap: - logger.debug('Swapping index %d', idx) - targetstring[idx] = fuzzed[idx] - filename = ''.join((crash.fuzzedfile.root, filename_modifier, crash.fuzzedfile.ext)) - metasploit_file = os.path.join(crash.tempdir, filename) - - with open(metasploit_file, 'wb') as f: - f.writelines(targetstring) - - crash.copy_files(outdir) - crash.clean_tmpdir() +from certfuzz.tools.linux.minimize import main if __name__ == '__main__': main() From 8777ed735c97f520a7e6311ee23cf93bda26d836 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 08:57:21 -0500 Subject: [PATCH 0048/1169] move minimizer_plot into certfuzz.tools.linux --- src/certfuzz/tools/linux/minimizer_plot.py | 180 +++++++++++++++++++++ src/linux/tools/minimizer_plot.py | 166 +------------------ 2 files changed, 182 insertions(+), 164 deletions(-) create mode 100755 src/certfuzz/tools/linux/minimizer_plot.py diff --git a/src/certfuzz/tools/linux/minimizer_plot.py b/src/certfuzz/tools/linux/minimizer_plot.py new file mode 100755 index 0000000..d882d5a --- /dev/null +++ b/src/certfuzz/tools/linux/minimizer_plot.py @@ -0,0 +1,180 @@ +''' +Created on Jan 13, 2011 + +@organization: cert.org +''' + +from optparse import OptionParser +import logging +import sys +import os +import re +import matplotlib.pyplot as plt + +parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, parent_path) + +from certfuzz.campaign.config.bff_config import read_config_options + +logger = logging.getLogger(__name__) +# set default logging level (override with command line options) +logger.setLevel(logging.INFO) + +hdlr = logging.StreamHandler(sys.stdout) +logger.addHandler(hdlr) + + +def parse_options(): + usage = "usage: %prog [options] " + parser = OptionParser(usage) + parser.add_option("-d", "--debug", dest="debug", help="Turn on debugging output (overrides --verbose)", action='store_true', default=False) + parser.add_option("-v", "--verbose", dest="verbose", help="Turn on verbose output", action='store_true', default=False) + parser.add_option('', '--dir', dest='dir', help='Specify crasher parent dir') + parser.add_option("-F", "--config", dest="cfgfile", help="read config data from CFGFILE", metavar='CFGFILE') + parser.add_option('', '--ylin', dest='linear_y', help="use linear scale on Y axis (default is logarithmic)", action='store_true', default=False) + parser.add_option('', '--xlog', dest='log_x', help="use log scale on X axis (default is linear)", action='store_true', default=False) + parser.add_option('', '--no-crash-id', dest='include_crash_id', help='suppress inclusion of crash_id in chart title', action='store_false', default=True) + parser.add_option('', '--infile', dest="infile", help="read minimizer log from FILE", metavar='FILE') + options, args = parser.parse_args() + if not len(args): + parser.print_help() + parser.error("Please specify a crash md5 to plot") + return options, args + + +def plot(options, crash_id, log): + logfile = LogFile(log) +# results = logfile.results + starts = [] + mins = [] + targets = [] + currents = [] + for item in logfile.results: + [x.append(y) for x, y in zip((starts, mins, targets, currents), item)] + + logger.debug("Got data to plot:") + [logger.debug('%s', str(x)) for x in (starts, mins, targets, currents)] + fig = plt.figure(figsize=(8, 8)) + ax = fig.add_subplot(1, 1, 1) + if not options.linear_y: + logger.info('Setting log scale on y-axis') + ax.set_yscale('log') + if options.log_x: + logger.info('Setting linear scale on x-axis') + ax.set_xscale('log') + logger.info('Building plot') + plt.xlabel('Iteration') + plt.ylabel('Hamming Distance') + title = 'Crash Minimization' +# prepend the crash_id unless the user told us not to + if options.include_crash_id: + title = '\n'.join((crash_id, title)) + plt.title(title) + plt.plot(starts, label='start_hd') + plt.plot(mins, label='min_found') + plt.plot(targets, label='target_guess') + plt.plot(currents, label='current_try') + logger.debug('Add legend to plot') + plt.legend() + logger.debug('Draw the plot') + plt.show() + + +class Line(): + def __init__(self, line): + self.line = line.strip() + self.value = False + self._process() + + def _process(self): + m = re.match('^start=(\d+)\s+min=(\d+)\s+target_guess=(\d+)\s+curr=(\d+)', self.line) + if not m: + return + + (start, minimum, target, current) = (int(x) for x in (m.group(1), m.group(2), m.group(3), m.group(4))) + logger.debug('start: %d', start) + logger.debug('min: %d', minimum) + logger.debug('target: %d', target) + logger.debug('current: %d', current) + self.value = (start, minimum, target, current) + + +class LogFile(): + def __init__(self, logfile): + self.file = logfile + self.results = [] + self.uniqresults = [] + self.results_read = False + self._process() + logger.debug('Created LogFile object for %s', self.file) + + def _process(self): + if self.results_read: + return self.results + + f = open(self.file, 'r') + try: + for l in f.readlines(): + result = Line(l).value + if result: + self.results.append(result) + self.results_read = True + finally: + f.close() + + def unique_results(self): + if self.uniqresults: + return self.uniqresults + + self.uniqresults = list(set(self.results)) + return self.uniqresults + + +def main(): + options, args = parse_options() + + if options.debug: + logger.setLevel(logging.DEBUG) + elif options.verbose: + logger.setLevel(logging.INFO) + + if options.cfgfile: + cfg_file = options.cfgfile + else: + cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') + + if options.dir: + result_dir = options.dir + else: + logger.info('Using config file: %s', cfg_file) + cfg = read_config_options(cfg_file) + result_dir = cfg.crashers_dir + logger.info('Reading results from %s', result_dir) + + log = None + crash_id = None + if len(args): + crash_id = args.pop(0) + logger.debug('Crash_id=%s', crash_id) + crashdir = os.path.join(result_dir, crash_id) + if not os.path.isdir(crashdir): + logger.debug('%s is not a dir', crashdir) + raise + + logger.debug('Looking for minimizer log in %s', crashdir) + log = os.path.join(crashdir, 'minimizer_log.txt') + elif options.infile: + crash_id = os.path.basename(options.infile) + log = options.infile + + if not os.path.exists(log): + logger.warning('No minimizer log found at %s', log) + raise + logger.info('Found log at %s', log) + plot(options, crash_id, log) + + logger.info('All done. Bye.') + + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/minimizer_plot.py b/src/linux/tools/minimizer_plot.py index a74dabc..2cd3a2c 100755 --- a/src/linux/tools/minimizer_plot.py +++ b/src/linux/tools/minimizer_plot.py @@ -4,169 +4,7 @@ @organization: cert.org ''' - -from optparse import OptionParser -import logging -import sys -import os -import re -import matplotlib.pyplot as plt - -parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, parent_path) - -from certfuzz.campaign.config.bff_config import read_config_options - -logger = logging.getLogger(__name__) -# set default logging level (override with command line options) -logger.setLevel(logging.INFO) - -hdlr = logging.StreamHandler(sys.stdout) -logger.addHandler(hdlr) - -def parse_options(): - usage = "usage: %prog [options] " - parser = OptionParser(usage) - parser.add_option("-d", "--debug", dest="debug", help="Turn on debugging output (overrides --verbose)", action='store_true', default=False) - parser.add_option("-v", "--verbose", dest="verbose", help="Turn on verbose output", action='store_true', default=False) - parser.add_option('', '--dir', dest='dir', help='Specify crasher parent dir') - parser.add_option("-F", "--config", dest="cfgfile", help="read config data from CFGFILE", metavar='CFGFILE') - parser.add_option('', '--ylin', dest='linear_y', help="use linear scale on Y axis (default is logarithmic)", action='store_true', default=False) - parser.add_option('', '--xlog', dest='log_x', help="use log scale on X axis (default is linear)", action='store_true', default=False) - parser.add_option('', '--no-crash-id', dest='include_crash_id', help='suppress inclusion of crash_id in chart title', action='store_false', default=True) - parser.add_option('', '--infile', dest="infile", help="read minimizer log from FILE", metavar='FILE') - options, args = parser.parse_args() - if not len(args): - parser.print_help() - parser.error("Please specify a crash md5 to plot") - return options, args - -def plot(options, crash_id, log): - logfile = LogFile(log) -# results = logfile.results - starts = [] - mins = [] - targets = [] - currents = [] - for item in logfile.results: - [x.append(y) for x, y in zip((starts, mins, targets, currents), item)] - - logger.debug("Got data to plot:") - [logger.debug('%s', str(x)) for x in (starts, mins, targets, currents)] - fig = plt.figure(figsize=(8, 8)) - ax = fig.add_subplot(1, 1, 1) - if not options.linear_y: - logger.info('Setting log scale on y-axis') - ax.set_yscale('log') - if options.log_x: - logger.info('Setting linear scale on x-axis') - ax.set_xscale('log') - logger.info('Building plot') - plt.xlabel('Iteration') - plt.ylabel('Hamming Distance') - title = 'Crash Minimization' -# prepend the crash_id unless the user told us not to - if options.include_crash_id: - title = '\n'.join((crash_id, title)) - plt.title(title) - plt.plot(starts, label='start_hd') - plt.plot(mins, label='min_found') - plt.plot(targets, label='target_guess') - plt.plot(currents, label='current_try') - logger.debug('Add legend to plot') - plt.legend() - logger.debug('Draw the plot') - plt.show() - -class Line(): - def __init__(self, line): - self.line = line.strip() - self.value = False - self._process() - - def _process(self): - m = re.match('^start=(\d+)\s+min=(\d+)\s+target_guess=(\d+)\s+curr=(\d+)', self.line) - if not m: - return - - (start, minimum, target, current) = (int(x) for x in (m.group(1), m.group(2), m.group(3), m.group(4))) - logger.debug('start: %d', start) - logger.debug('min: %d', minimum) - logger.debug('target: %d', target) - logger.debug('current: %d', current) - self.value = (start, minimum, target, current) - -class LogFile(): - def __init__(self, logfile): - self.file = logfile - self.results = [] - self.uniqresults = [] - self.results_read = False - self._process() - logger.debug('Created LogFile object for %s', self.file) - - def _process(self): - if self.results_read: - return self.results - - f = open(self.file, 'r') - try: - for l in f.readlines(): - result = Line(l).value - if result: - self.results.append(result) - self.results_read = True - finally: - f.close() - - def unique_results(self): - if self.uniqresults: - return self.uniqresults - - self.uniqresults = list(set(self.results)) - return self.uniqresults +from certfuzz.tools.linux.minimizer_plot import main if __name__ == '__main__': - options, args = parse_options() - - if options.debug: - logger.setLevel(logging.DEBUG) - elif options.verbose: - logger.setLevel(logging.INFO) - - if options.cfgfile: - cfg_file = options.cfgfile - else: - cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') - - if options.dir: - result_dir = options.dir - else: - logger.info('Using config file: %s', cfg_file) - cfg = read_config_options(cfg_file) - result_dir = cfg.crashers_dir - logger.info('Reading results from %s', result_dir) - - log = None - crash_id = None - if len(args): - crash_id = args.pop(0) - logger.debug('Crash_id=%s', crash_id) - crashdir = os.path.join(result_dir, crash_id) - if not os.path.isdir(crashdir): - logger.debug('%s is not a dir', crashdir) - raise - - logger.debug('Looking for minimizer log in %s', crashdir) - log = os.path.join(crashdir, 'minimizer_log.txt') - elif options.infile: - crash_id = os.path.basename(options.infile) - log = options.infile - - if not os.path.exists(log): - logger.warning('No minimizer log found at %s', log) - raise - logger.info('Found log at %s', log) - plot(options, crash_id, log) - - logger.info('All done. Bye.') + main() From 757ee4bb7f2c7a1490b873986ee39bb9dfac67c4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 08:58:50 -0500 Subject: [PATCH 0049/1169] move mtsp_enum into certfuzz.tools.linux --- src/certfuzz/tools/linux/mtsp_enum.py | 49 +++++++++++++++++++++++++++ src/linux/tools/mtsp_enum.py | 41 +--------------------- 2 files changed, 50 insertions(+), 40 deletions(-) create mode 100755 src/certfuzz/tools/linux/mtsp_enum.py diff --git a/src/certfuzz/tools/linux/mtsp_enum.py b/src/certfuzz/tools/linux/mtsp_enum.py new file mode 100755 index 0000000..3215d63 --- /dev/null +++ b/src/certfuzz/tools/linux/mtsp_enum.py @@ -0,0 +1,49 @@ +''' +Created on Mar 8, 2013 + +@organization: cert.org +''' +import logging +import argparse +import os.path + +try: + from certfuzz.fuzztools.text import enumerate_string +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + import sys + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.fuzztools.text import enumerate_string + + +def main(): + logger = logging.getLogger() + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + group.add_argument('--debug', help='', action='store_true') + group.add_argument('-v', '--verbose', help='', action='store_true') + parser.add_argument('searchstring', + type=str, + help='The string to enumerate') + parser.add_argument('fuzzedfile', + help='Path to a fuzzedfile', + type=str) + + args = parser.parse_args() + + if args.debug: + logger.setLevel(logging.DEBUG) + elif args.verbose: + logger.setLevel(logging.INFO) + else: + logger.setLevel(logging.WARNING) + + enumerate_string(path=args.fuzzedfile, str_to_enum=args.searchstring) + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/mtsp_enum.py b/src/linux/tools/mtsp_enum.py index 974e3ef..46e5849 100755 --- a/src/linux/tools/mtsp_enum.py +++ b/src/linux/tools/mtsp_enum.py @@ -4,46 +4,7 @@ @organization: cert.org ''' -import logging -import argparse -import os.path - -try: - from certfuzz.fuzztools.text import enumerate_string -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - import sys - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.fuzztools.text import enumerate_string - -def main(): - logger = logging.getLogger() - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument('--debug', help='', action='store_true') - group.add_argument('-v', '--verbose', help='', action='store_true') - parser.add_argument('searchstring', - type=str, - help='The string to enumerate') - parser.add_argument('fuzzedfile', - help='Path to a fuzzedfile', - type=str) - - args = parser.parse_args() - - if args.debug: - logger.setLevel(logging.DEBUG) - elif args.verbose: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.WARNING) - - enumerate_string(path=args.fuzzedfile, str_to_enum=args.searchstring) +from certfuzz.tools.linux.mtsp_enum import main if __name__ == '__main__': main() From 1b4ce4a7cba576831f72424ca44eaeda7abf5669 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:00:49 -0500 Subject: [PATCH 0050/1169] move repro into certfuzz.tools.linux --- src/certfuzz/tools/linux/repro.py | 150 ++++++++++++++++++++++++++++++ src/linux/tools/repro.py | 142 +--------------------------- 2 files changed, 151 insertions(+), 141 deletions(-) create mode 100755 src/certfuzz/tools/linux/repro.py diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py new file mode 100755 index 0000000..afa7265 --- /dev/null +++ b/src/certfuzz/tools/linux/repro.py @@ -0,0 +1,150 @@ +''' +Created on April 28, 2013 + +@organization: cert.org +''' +import logging +import os +import re +from subprocess import Popen + +try: + from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file + from certfuzz import debuggers + from certfuzz.file_handlers.basicfile import BasicFile + from certfuzz.runners.runner_base import get_command_args_list + from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options + from certfuzz.debuggers import gdb # @UnusedImport +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + import sys + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file + from certfuzz import debuggers + from certfuzz.file_handlers.basicfile import BasicFile + from certfuzz.runners.runner_base import get_command_args_list + from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options + from certfuzz.debuggers import gdb # @UnusedImport + +logger = logging.getLogger() +logger.setLevel(logging.WARNING) + + +def parseiterpath(commandline): + # Return the path to the iteration's fuzzed file + for part in commandline.split(): + if 'bff-crash' in part: + return part + + +def getiterpath(gdbfile): + # Find the commandline in an msec file + with open(gdbfile) as gdblines: + for line in gdblines: + m = re.match('Running: ', line) + if m: + return parseiterpath(line) + + +def main(): + debuggers.verify_supported_platform() + + from optparse import OptionParser + + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + usage = "usage: %prog [options] fuzzedfile" + parser = OptionParser(usage) + parser.add_option('', '--debug', dest='debug', action='store_true', + help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--verbose', dest='verbose', action='store_true', + help='Enable verbose messages') + parser.add_option('-c', '--config', default='conf.d/bff.cfg', + dest='config', help='path to the configuration file to use') + parser.add_option('-e', '--edb', dest='use_edb', + action='store_true', + help='Use edb instead of gdb') + parser.add_option('-p', '--debugger', dest='debugger', + help='Use specified debugger') + parser.add_option('-f', '--filepath', dest='filepath', + action='store_true', help='Recreate original file path') + + (options, args) = parser.parse_args() + + if options.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + cfg_file = options.config + logger.debug('Config file: %s', cfg_file) + + if len(args) and os.path.exists(args[0]): + fullpath_fuzzed_file = os.path.abspath(args[0]) + fuzzed_file = BasicFile(fullpath_fuzzed_file) + logger.info('Fuzzed file is %s', fuzzed_file) + else: + parser.error('fuzzedfile must be specified') + + iterationpath = '' + + if options.filepath: + # Recreate same file path as fuzz iteration + resultdir = os.path.dirname(fuzzed_file.path) + for gdbfile in all_files(resultdir, '*.gdb'): + print '** using gdb: %s' % gdbfile + iterationpath = getiterpath(gdbfile) + break + iterationdir = os.path.dirname(iterationpath) + iterationfile = os.path.basename(iterationpath) + if iterationdir: + mkdir_p(iterationdir) + copy_file(fuzzed_file.path, + os.path.join(iterationdir, iterationfile)) + fullpath_fuzzed_file = iterationpath + + config = read_config_options(cfg_file) + + cmd_as_args = config.get_command_list(fullpath_fuzzed_file) + program = cmd_as_args[0] + if not os.path.exists(program): + # edb wants a full path to the target app, so let's find it + for path in os.environ["PATH"].split(":"): + if os.path.exists(os.path.join(path, program)): + program = os.path.join(path, program) + + # Recreate command args list with full path to target + cmd_as_args = [] + cmd_as_args.append(program) + cmd_as_args.extend(config.get_command_args_list(fullpath_fuzzed_file)) + + args = [] + + if options.use_edb and options.debugger: + parser.error('Options --edb and --debugger are mutually exclusive.') + + if options.debugger: + debugger_app = options.debugger + elif options.use_edb: + debugger_app = 'edb' + else: + debugger_app = 'gdb' + args.append(debugger_app) + + if options.use_edb: + args.append('--run') + else: + # Using gdb + args.append('--args') + args.extend(cmd_as_args) + logger.info('args %s' % args) + + p = Popen(args, universal_newlines=True) + p.wait() + + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/repro.py b/src/linux/tools/repro.py index ab00dd0..3a07df6 100755 --- a/src/linux/tools/repro.py +++ b/src/linux/tools/repro.py @@ -4,147 +4,7 @@ @organization: cert.org ''' -import logging -import os -import re -from subprocess import Popen - -try: - from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file - from certfuzz import debuggers - from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.runners.runner_base import get_command_args_list - from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options - from certfuzz.debuggers import gdb # @UnusedImport -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - import sys - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file - from certfuzz import debuggers - from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.runners.runner_base import get_command_args_list - from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options - from certfuzz.debuggers import gdb # @UnusedImport - -logger = logging.getLogger() -logger.setLevel(logging.WARNING) - - -def parseiterpath(commandline): - # Return the path to the iteration's fuzzed file - for part in commandline.split(): - if 'bff-crash' in part: - return part - - -def getiterpath(gdbfile): - # Find the commandline in an msec file - with open(gdbfile) as gdblines: - for line in gdblines: - m = re.match('Running: ', line) - if m: - return parseiterpath(line) - - -def main(): - debuggers.verify_supported_platform() - - from optparse import OptionParser - - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - usage = "usage: %prog [options] fuzzedfile" - parser = OptionParser(usage) - parser.add_option('', '--debug', dest='debug', action='store_true', - help='Enable debug messages (overrides --verbose)') - parser.add_option('', '--verbose', dest='verbose', action='store_true', - help='Enable verbose messages') - parser.add_option('-c', '--config', default='conf.d/bff.cfg', - dest='config', help='path to the configuration file to use') - parser.add_option('-e', '--edb', dest='use_edb', - action='store_true', - help='Use edb instead of gdb') - parser.add_option('-p', '--debugger', dest='debugger', - help='Use specified debugger') - parser.add_option('-f', '--filepath', dest='filepath', - action='store_true', help='Recreate original file path') - - (options, args) = parser.parse_args() - - if options.debug: - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - - cfg_file = options.config - logger.debug('Config file: %s', cfg_file) - - if len(args) and os.path.exists(args[0]): - fullpath_fuzzed_file = os.path.abspath(args[0]) - fuzzed_file = BasicFile(fullpath_fuzzed_file) - logger.info('Fuzzed file is %s', fuzzed_file) - else: - parser.error('fuzzedfile must be specified') - - iterationpath = '' - - if options.filepath: - # Recreate same file path as fuzz iteration - resultdir = os.path.dirname(fuzzed_file.path) - for gdbfile in all_files(resultdir, '*.gdb'): - print '** using gdb: %s' % gdbfile - iterationpath = getiterpath(gdbfile) - break - iterationdir = os.path.dirname(iterationpath) - iterationfile = os.path.basename(iterationpath) - if iterationdir: - mkdir_p(iterationdir) - copy_file(fuzzed_file.path, - os.path.join(iterationdir, iterationfile)) - fullpath_fuzzed_file = iterationpath - - config = read_config_options(cfg_file) - - cmd_as_args = config.get_command_list(fullpath_fuzzed_file) - program = cmd_as_args[0] - if not os.path.exists(program): - # edb wants a full path to the target app, so let's find it - for path in os.environ["PATH"].split(":"): - if os.path.exists(os.path.join(path, program)): - program = os.path.join(path, program) - - # Recreate command args list with full path to target - cmd_as_args = [] - cmd_as_args.append(program) - cmd_as_args.extend(config.get_command_args_list(fullpath_fuzzed_file)) - - args = [] - - if options.use_edb and options.debugger: - parser.error('Options --edb and --debugger are mutually exclusive.') - - if options.debugger: - debugger_app = options.debugger - elif options.use_edb: - debugger_app = 'edb' - else: - debugger_app = 'gdb' - args.append(debugger_app) - - if options.use_edb: - args.append('--run') - else: - # Using gdb - args.append('--args') - args.extend(cmd_as_args) - logger.info('args %s' % args) - - p = Popen(args, universal_newlines=True) - p.wait() +from certfuzz.tools.linux.repro import main if __name__ == '__main__': main() From e734b3b55209656158345b850a961cc2c63b071f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:03:07 -0500 Subject: [PATCH 0051/1169] move clean_foe into certfuzz.tools.windows --- src/certfuzz/tools/windows/clean_foe.py | 112 ++++++++++++++++++++++++ src/windows/tools/clean_foe.py | 104 +--------------------- 2 files changed, 114 insertions(+), 102 deletions(-) create mode 100644 src/certfuzz/tools/windows/clean_foe.py diff --git a/src/certfuzz/tools/windows/clean_foe.py b/src/certfuzz/tools/windows/clean_foe.py new file mode 100644 index 0000000..fe40c2c --- /dev/null +++ b/src/certfuzz/tools/windows/clean_foe.py @@ -0,0 +1,112 @@ +''' +Created on Feb 28, 2012 + +@author: adh +''' + +import os +import time +import tempfile +import pprint + +defaults = {'config': 'configs/foe.yaml', + 'remove_results': False, + 'pretend': False, + 'retry': 3, + 'debug': False, + 'nuke': False, + } + +SLEEPTIMER = 0.5 +BACKOFF_FACTOR = 2 + +def main(): + import optparse + try: + from certfuzz.fuzztools.filetools import delete_contents_of + from certfuzz.campaign.config import Config + except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + import sys + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.fuzztools.filetools import delete_contents_of + from certfuzz.campaign.config import Config + if not os.path.exists(defaults['config']): + defaults['config'] = '../configs/foe.yaml' + + parser = optparse.OptionParser() + parser.add_option('-c', '--config', dest='configfile', default=defaults['config'], metavar='FILE') + parser.add_option('-p', '--pretend', dest='pretend', action='store_true', default=defaults['pretend'], help='Do not actually remove files') + parser.add_option('-r', '--retry', dest='retries', default=defaults['retry'], type='int', metavar='INT') + parser.add_option('', '--remove-results', dest='remove_results', action='store_true', default=defaults['remove_results'], help='Removes results dir contents') + parser.add_option('', '--all', dest='nuke', action='store_true', default=defaults['nuke'], help='Equivalent to --remove-results') + parser.add_option('', '--debug', dest='debug', action='store_true', default=defaults['debug']) + options, args = parser.parse_args() + + cfgobj = Config(options.configfile) + c = cfgobj.config + + if options.debug: + pprint.pprint(c) + + dirs = set() + + if options.nuke: + options.remove_results = True + + dirs.add(os.path.abspath(c['directories']['working_dir'])) + dirs.add(os.path.join(os.path.abspath(c['directories']['results_dir']), c['campaign']['id'], 'seedfiles')) + if options.remove_results: + dirs.add(os.path.join(os.path.abspath(c['directories']['results_dir']), c['campaign']['id'],)) + + # add temp dir(s) if available + if tempfile.gettempdir().lower() != os.getcwd().lower(): + # Only add tempdir if it's valid. Otherwise you get cwd + dirs.add(tempfile.gettempdir()) + try: + dirs.add(os.environ['TMP']) + except KeyError: + pass + + try: + dirs.add(os.environ['TEMP']) + except KeyError: + pass + + if not options.pretend: + tries = 0 + done = False + skipped = [] + while not done: + skipped = delete_contents_of(dirs, print_via_log=False) + # if we got here, no exceptions were thrown + # so we're done + if skipped: + if tries < options.retries: + # typically exceptions happen because the OS hasn't + # caught up with file lock status, so give it a chance + # to do so before the next iteration + nap_length = SLEEPTIMER * pow(BACKOFF_FACTOR, tries) + tries += 1 + print '%d files skipped, waiting %0.1fs to retry (%d of %d)' % (len(skipped), nap_length, tries, options.retries) + time.sleep(nap_length) + else: + print 'Maximum retries (%d) exceeded.' % options.retries + done = True + else: + done = True + + for (skipped_item, reason) in skipped: + print "Skipped file %s: %s" % (skipped_item, reason) + + else: + parser.print_help() + print + print 'Would have deleted the contents of:' + for d in dirs: + print '... %s' % d + +if __name__ == '__main__': + main() diff --git a/src/windows/tools/clean_foe.py b/src/windows/tools/clean_foe.py index 2d78c82..6481d42 100644 --- a/src/windows/tools/clean_foe.py +++ b/src/windows/tools/clean_foe.py @@ -3,107 +3,7 @@ @author: adh ''' - -import os -import time -import tempfile -import pprint - -defaults = {'config': 'configs/foe.yaml', - 'remove_results': False, - 'pretend': False, - 'retry': 3, - 'debug': False, - 'nuke': False, - } - -SLEEPTIMER = 0.5 -BACKOFF_FACTOR = 2 +from certfuzz.tools.windows.clean_foe import main if __name__ == '__main__': - import optparse - try: - from certfuzz.fuzztools.filetools import delete_contents_of - from certfuzz.campaign.config import Config - except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - import sys - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.fuzztools.filetools import delete_contents_of - from certfuzz.campaign.config import Config - if not os.path.exists(defaults['config']): - defaults['config'] = '../configs/foe.yaml' - - parser = optparse.OptionParser() - parser.add_option('-c', '--config', dest='configfile', default=defaults['config'], metavar='FILE') - parser.add_option('-p', '--pretend', dest='pretend', action='store_true', default=defaults['pretend'], help='Do not actually remove files') - parser.add_option('-r', '--retry', dest='retries', default=defaults['retry'], type='int', metavar='INT') - parser.add_option('', '--remove-results', dest='remove_results', action='store_true', default=defaults['remove_results'], help='Removes results dir contents') - parser.add_option('', '--all', dest='nuke', action='store_true', default=defaults['nuke'], help='Equivalent to --remove-results') - parser.add_option('', '--debug', dest='debug', action='store_true', default=defaults['debug']) - options, args = parser.parse_args() - - cfgobj = Config(options.configfile) - c = cfgobj.config - - if options.debug: - pprint.pprint(c) - - dirs = set() - - if options.nuke: - options.remove_results = True - - dirs.add(os.path.abspath(c['directories']['working_dir'])) - dirs.add(os.path.join(os.path.abspath(c['directories']['results_dir']), c['campaign']['id'], 'seedfiles')) - if options.remove_results: - dirs.add(os.path.join(os.path.abspath(c['directories']['results_dir']), c['campaign']['id'],)) - - # add temp dir(s) if available - if tempfile.gettempdir().lower() != os.getcwd().lower(): - # Only add tempdir if it's valid. Otherwise you get cwd - dirs.add(tempfile.gettempdir()) - try: - dirs.add(os.environ['TMP']) - except KeyError: - pass - - try: - dirs.add(os.environ['TEMP']) - except KeyError: - pass - - if not options.pretend: - tries = 0 - done = False - skipped = [] - while not done: - skipped = delete_contents_of(dirs, print_via_log=False) - # if we got here, no exceptions were thrown - # so we're done - if skipped: - if tries < options.retries: - # typically exceptions happen because the OS hasn't - # caught up with file lock status, so give it a chance - # to do so before the next iteration - nap_length = SLEEPTIMER * pow(BACKOFF_FACTOR, tries) - tries += 1 - print '%d files skipped, waiting %0.1fs to retry (%d of %d)' % (len(skipped), nap_length, tries, options.retries) - time.sleep(nap_length) - else: - print 'Maximum retries (%d) exceeded.' % options.retries - done = True - else: - done = True - - for (skipped_item, reason) in skipped: - print "Skipped file %s: %s" % (skipped_item, reason) - - else: - parser.print_help() - print - print 'Would have deleted the contents of:' - for d in dirs: - print '... %s' % d + main() From cb617332e32b59bd39349cd671b42fcf8e05d841 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:04:52 -0500 Subject: [PATCH 0052/1169] move copycrashers into certfuzz.tools.windows --- src/certfuzz/tools/windows/copycrashers.py | 64 ++++++++++++++++++++++ src/windows/tools/copycrashers.py | 58 +------------------- 2 files changed, 65 insertions(+), 57 deletions(-) create mode 100644 src/certfuzz/tools/windows/copycrashers.py diff --git a/src/certfuzz/tools/windows/copycrashers.py b/src/certfuzz/tools/windows/copycrashers.py new file mode 100644 index 0000000..eb6be15 --- /dev/null +++ b/src/certfuzz/tools/windows/copycrashers.py @@ -0,0 +1,64 @@ +''' +Created on Jun 14, 2013 + +''' + +import os +import re +import sys +import shutil +from optparse import OptionParser + +regex = { + 'crasher': re.compile('^sf_.+-\w+.\w+$'), + } + + +def copycrashers(tld, outputdir): + # Walk the results directory + for root, dirs, files in os.walk(tld): + crash_hash = os.path.basename(root) + # Only use directories that are hashes + if "0x" in crash_hash: + # Check each of the files in the hash directory + for current_file in files: + # This gives us the crasher file name + if regex['crasher'].match(current_file) and 'minimized' not in current_file: + crasher_file = os.path.join(root, current_file) + print 'Copying %s to %s ...' % (crasher_file, outputdir) + shutil.copy(crasher_file, outputdir) + + +def main(): + # If user doesn't specify a directory to crawl, use "results" + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + parser.add_option('-d', '--dir', + help='directory to look for results in. Default is "results"', + dest='resultsdir', default='results') + parser.add_option('-o', '--outputdir', dest='outputdir', default='seedfiles', + help='Directory to put crashing testcases') + (options, args) = parser.parse_args() + outputdir = options.outputdir + tld = options.resultsdir + if not os.path.isdir(tld): + if os.path.isdir('../results'): + tld = '../results' + elif os.path.isdir('crashers'): + # Probably using FOE 1.0, which defaults to "crashers" for output + tld = 'crashers' + else: + print 'Cannot find resuls directory %s' % tld + sys.exit(0) + + if not os.path.isdir(outputdir): + if os.path.isdir('../seedfiles'): + outputdir = '../seedfiles' + else: + print 'cannot find output directory %s' % outputdir + sys.exit(0) + + copycrashers(tld, outputdir) + +if __name__ == '__main__': + main() diff --git a/src/windows/tools/copycrashers.py b/src/windows/tools/copycrashers.py index 33d7519..cb82389 100644 --- a/src/windows/tools/copycrashers.py +++ b/src/windows/tools/copycrashers.py @@ -2,63 +2,7 @@ Created on Jun 14, 2013 ''' - -import os -import re -import sys -import shutil -from optparse import OptionParser - -regex = { - 'crasher': re.compile('^sf_.+-\w+.\w+$'), - } - - -def copycrashers(tld, outputdir): - # Walk the results directory - for root, dirs, files in os.walk(tld): - crash_hash = os.path.basename(root) - # Only use directories that are hashes - if "0x" in crash_hash: - # Check each of the files in the hash directory - for current_file in files: - # This gives us the crasher file name - if regex['crasher'].match(current_file) and 'minimized' not in current_file: - crasher_file = os.path.join(root, current_file) - print 'Copying %s to %s ...' % (crasher_file, outputdir) - shutil.copy(crasher_file, outputdir) - -def main(): - # If user doesn't specify a directory to crawl, use "results" - usage = "usage: %prog [options]" - parser = OptionParser(usage=usage) - parser.add_option('-d', '--dir', - help='directory to look for results in. Default is "results"', - dest='resultsdir', default='results') - parser.add_option('-o', '--outputdir', dest='outputdir', default='seedfiles', - help='Directory to put crashing testcases') - (options, args) = parser.parse_args() - outputdir = options.outputdir - tld = options.resultsdir - if not os.path.isdir(tld): - if os.path.isdir('../results'): - tld = '../results' - elif os.path.isdir('crashers'): - # Probably using FOE 1.0, which defaults to "crashers" for output - tld = 'crashers' - else: - print 'Cannot find resuls directory %s' % tld - sys.exit(0) - - if not os.path.isdir(outputdir): - if os.path.isdir('../seedfiles'): - outputdir = '../seedfiles' - else: - print 'cannot find output directory %s' % outputdir - sys.exit(0) - - copycrashers(tld, outputdir) +from certfuzz.tools.windows.copycrashers import main if __name__ == '__main__': main() - From 8ab4b66e9fc969ca223ba95d3f4b41bf90dbc1ab Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:06:31 -0500 Subject: [PATCH 0053/1169] calf -> lf & cleanup bad copy/paste on drillresults (+1 squashed commit) Squashed commits: [0ceeb09] move drillresults into certfuzz.tools.windows --- src/certfuzz/tools/windows/drillresults.py | 629 +++++++++++++++++++++ src/windows/tools/drillresults.py | 624 +------------------- 2 files changed, 630 insertions(+), 623 deletions(-) create mode 100644 src/certfuzz/tools/windows/drillresults.py diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py new file mode 100644 index 0000000..48ff75b --- /dev/null +++ b/src/certfuzz/tools/windows/drillresults.py @@ -0,0 +1,629 @@ +''' +This script looks for interesting crashes and rate them by potential exploitability +''' + +import os +import struct +import binascii +import re +from optparse import OptionParser +import StringIO +import zipfile +import cPickle as pickle + +regex = { + 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]'), + 'msec_report': re.compile('.+.msec$'), + 'regs1': re.compile('^eax=.+'), + 'regs2': re.compile('^eip=.+'), + '64bit_debugger': re.compile('^Microsoft.*AMD64$'), + 'syswow64': re.compile('ModLoad:.*syswow64.*', re.IGNORECASE), + 'mapped_address': re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), + 'mapped_address64': re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), + 'syswow64': re.compile('ModLoad:.*syswow64.*', re.IGNORECASE), + 'dbg_prompt': re.compile('^[0-9]:[0-9][0-9][0-9]> (.*)'), + 'wow64_dbg_prompt': re.compile('^[0-9]:[0-9][0-9][0-9]:x86> (.*)'), + } + +registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', + 'edi', 'eip') + +registers64 = ('rax', 'rcx', 'rdx', 'rbx', 'rsp', 'rbp', 'rsi', + 'rdi', 'rip', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', + 'r14', 'r15') + +# These !exploitable short descriptions indicate a very interesting crash +really_exploitable = [ + 'ReadAVonIP', + 'TaintedDataControlsCodeFlow', + 'ReadAVonControlFlow', + 'DEPViolation', + 'IllegalInstruction', + 'PrivilegedInstruction', + ] +reg_set = set(registers) +reg64_set = set(registers64) +re_set = set(really_exploitable) + +results = {} +cached_results = {} +scoredcrashes = {} +regdict = {} +mseclist = [] +_64bit_debugger = False +wow64_app = False +ignorejit = False +pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') + + +def check_64bit(reporttext): + ''' + Check if the debugger and target app are 64-bit + ''' + global _64bit_debugger + global wow64_app + for line in reporttext.splitlines(): + n = re.match(regex['64bit_debugger'], line) + if n: + _64bit_debugger = True + if _64bit_debugger: + n = re.match(regex['syswow64'], line) + if n: + wow64_app = True + + +def pc_in_mapped_address(reporttext, instraddr): + ''' + Check if the instruction pointer is in a loaded module + ''' + global _64bit_debugger + global wow64_app + ma_regex = 'mapped_address' + mapped_module = 'unloaded' + if _64bit_debugger: + ma_regex = 'mapped_address64' + + instraddr = instraddr.replace('`', '') + instraddr = int(instraddr, 16) + for line in reporttext.splitlines(): + n = re.match(regex[ma_regex], line) + if n: + # Strip out backticks present on 64-bit systems + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + if begin_address < instraddr < end_address: + mapped_module = n.group(3) + return mapped_module + + +def fixefabug(reporttext, instraddr, faultaddr): + ''' + !exploitable often reports an incorrect EFA for 64-bit targets. + If we're dealing with a 64-bit target, we can second-guess the reported EFA + ''' + instructionline = getinstr(reporttext, instraddr) + if not instructionline or "=" not in instructionline: + # Nothing to fix + return faultaddr + if 'ds:' in instructionline: + # There's a target address in the msec file + if '??' in instructionline: + # The AV is on dereferencing where to call + ds = carve(instructionline, "ds:", "=") + else: + # The AV is on accessing the code location + ds = instructionline.split("=")[-1] + else: + # AV must be on current instruction + ds = instructionline.split(' ')[0] + if ds: + faultaddr = ds.replace('`', '') + return faultaddr + + +def readfile(textfile): + ''' + Read text file + ''' + f = open(textfile, 'r') + text = f.read() + f.close() + return text + + +def readbinfile(inputfile): + ''' + Read binary file + ''' + f = open(inputfile, 'rb') + filebytes = f.read() + # For zip files, return the uncompressed bytes + file_like_content = StringIO.StringIO(filebytes) + if zipfile.is_zipfile(file_like_content): + # Make sure that it's not an embedded zip + # (e.g. a DOC file from Office 2007) + file_like_content.seek(0) + zipmagic = file_like_content.read(2) + if zipmagic == 'PK': + try: + # The file begins with the PK header + z = zipfile.ZipFile(file_like_content, 'r') + for filename in z.namelist(): + try: + filebytes += z.read(filename) + except: + pass + except: + # If the zip container is fuzzed we may get here + pass + file_like_content.close() + f.close + return filebytes + + +def carve(string, token1, token2): + startindex = string.find(token1) + if startindex == -1: + # can't find token1 + return "" + startindex = startindex + len(token1) + endindex = string.find(token2, startindex) + if endindex == -1: + # can't find token2 + return "" + return string[startindex:endindex] + + +# Todo: fix this up. Was added to bring gdb support +def carve2(string): + delims = [("Exception Faulting Address: ", "\n"), + ("si_addr:$2 = (void *)", "\n")] + for token1, token2 in delims: + startindex = string.find(token1) + if startindex == -1: + # can't find token1 + continue + startindex = startindex + len(token1) + endindex = string.find(token2, startindex) + return string[startindex:endindex] + + +def getexnum(reporttext): + ''' + Get the exception number by counting the number of continues + ''' + global wow64_app + + if wow64_app: + dbg_prompt = 'wow64_dbg_prompt' + else: + dbg_prompt = 'dbg_prompt' + exception = 0 + for line in reporttext.splitlines(): + n = re.match(regex[dbg_prompt], line) + if n: + cdbcmd = n.group(1) + cmds = cdbcmd.split(';') + for cmd in cmds: + if cmd == 'g': + exception = exception + 1 + return exception + + +def getregs(reporttext): + ''' + Populate the register dictionary with register values at crash + ''' + for line in reporttext.splitlines(): + if regex['regs1'].match(line) or regex['regs2'].match(line): + regs1 = line.split() + for reg in regs1: + if "=" in reg: + splitreg = reg.split("=") + regdict[splitreg[0]] = splitreg[1] + + +def getinstr(reporttext, instraddr): + ''' + Find the disassembly line for the current (crashing) instruction + ''' + regex = re.compile('^%s\s.+.+\s+' % instraddr) + for line in reporttext.splitlines(): + if regex.match(line): + return line + + +def formataddr(faultaddr): + ''' + Format a 64- or 32-bit memory address to a fixed width + ''' + global _64bit_debugger + global wow64_app + + if not faultaddr: + return + else: + faultaddr = faultaddr.strip() + faultaddr = faultaddr.replace('0x', '') + + if _64bit_debugger and not wow64_app: + # Due to a bug in !exploitable, the Exception Faulting Address is + # often wrong with 64-bit targets + if len(faultaddr) < 10: + # pad faultaddr + faultaddr = faultaddr.zfill(16) + else: + if len(faultaddr) > 10: # 0x12345678 = 10 chars + faultaddr = faultaddr[-8:] + elif len(faultaddr) < 10: + # pad faultaddr + faultaddr = faultaddr.zfill(8) + + return faultaddr + + +def ratchetscore(crasher, score): + ''' + Only allow a crasher to increase score + ''' + try: + currentscore = scoredcrashes[crasher] + except KeyError: + currentscore = 100 + if score < currentscore: + scoredcrashes[crasher] = score + + +def fixefaoffset(instructionline, faultaddr): + ''' + Adjust faulting address for instructions that use offsets + Currently only works for instructions like CALL [reg + offset] + ''' + global reg_set + if _64bit_debugger and not wow64_app: + reg_set = reg64_set + + if '0x' not in faultaddr: + faultaddr = '0x' + faultaddr + instructionpieces = instructionline.split() + if '??' not in instructionpieces[-1]: + # The av is on the address of the code called, not the address + # of the call + return faultaddr + for index, piece in enumerate(instructionpieces): + if piece == 'call': + # CALL instruction + if len(instructionpieces) <= index + 3: + # CALL to just a register. No offset + return faultaddr + address = instructionpieces[index + 3] + if '+' in address: + splitaddress = address.split('+') + reg = splitaddress[0] + reg = reg.replace('[', '') + if reg not in reg_set: + return faultaddr + offset = splitaddress[1] + offset = offset.replace('h', '') + offset = offset.replace(']', '') + if '0x' not in offset: + offset = '0x' + offset + if int(offset, 16) > int(faultaddr, 16): + # TODO: fix up negative numbers + return faultaddr + # Subtract offset to get actual interesting pattern + faultaddr = hex(eval(faultaddr) - eval(offset)) + faultaddr = formataddr(faultaddr.replace('L', '')) + return faultaddr + + +def checkreport(reportfile, crasherfile, crash_hash): + ''' + Parse the msec file + ''' + global _64bit_debugger + global wow64_app + global cached_results + + if cached_results: + if cached_results.get(crash_hash): + results[crash_hash] = cached_results[crash_hash] + return + + crashid = results[crash_hash] + + if crasherfile == '': + # Old FOE version that didn't do multiple exceptions or rename msec + # file with exploitability + crasherfile, reportfileext = os.path.splitext(reportfile) + + reporttext = readfile(reportfile) + getregs(reporttext) + current_dir = os.path.dirname(reportfile) + exceptionnum = getexnum(reporttext) + classification = carve(reporttext, "Exploitability Classification: ", "\n") + try: + if classification: + # Create a new exception dictionary to add to the crash + exception = {} + crashid['exceptions'][exceptionnum] = exception + except KeyError: + # Crash ID (crash_hash) not yet seen + # Default it to not being "really exploitable" + crashid['reallyexploitable'] = False + # Create a dictionary of exceptions for the crash id + exceptions = {} + crashid['exceptions'] = exceptions + # Create a dictionary for the exception + crashid['exceptions'][exceptionnum] = exception + + # Set !exploitable classification for the exception + if classification: + crashid['exceptions'][exceptionnum]['classification'] = classification + + shortdesc = carve(reporttext, "Short Description: ", "\n") + if shortdesc: + # Set !exploitable Short Description for the exception + crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc + if shortdesc in re_set: + # Flag the entire crash ID as really exploitable if this is a good + # exception + crashid['reallyexploitable'] = True + # Check if the expected crasher file (fuzzed file) exists + if not os.path.isfile(crasherfile): + # It's not there, so try to extract the filename from the cdb + # commandline + commandline = carve(reporttext, "CommandLine: ", "\n") + args = commandline.split() + for arg in args: + if "sf_" in arg: + crasherfile = os.path.basename(arg) + if "-" in crasherfile: + # FOE 2.0 verify mode puts a '-' part on the + # filename when invoking cdb, however the resulting file + # is really just 'sf_.' + fileparts = crasherfile.split('-') + m = re.search('\..+', fileparts[1]) + # Recreate the original file name, minus the iteration + crasherfile = os.path.join(current_dir, fileparts[0] + m.group(0)) + else: + crasherfile = os.path.join(current_dir, crasherfile) + if not os.path.isfile(crasherfile): + # Can't find the crasher file + return + # Set the "fuzzedfile" property for the crash ID + crashid['fuzzedfile'] = crasherfile + # See if we're dealing with 64-bit debugger or target app + check_64bit(reporttext) + faultaddr = carve2(reporttext) + instraddr = carve(reporttext, "Instruction Address:", "\n") + faultaddr = formataddr(faultaddr) + instraddr = formataddr(instraddr) + + # No faulting address means no crash. + if not faultaddr or not instraddr: + return + + if _64bit_debugger and not wow64_app and instraddr: + # Put backtick into instruction address for pattern matching + instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) + if shortdesc != 'DEPViolation': + faultaddr = fixefabug(reporttext, instraddr, faultaddr) + + +# pc_module = pc_in_mapped_address(reporttext, instraddr) + crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr) + + # Get the cdb line that contains the crashing instruction + instructionline = getinstr(reporttext, instraddr) + crashid['exceptions'][exceptionnum]['instructionline'] = instructionline + if instructionline: + faultaddr = fixefaoffset(instructionline, faultaddr) + + # Fix faulting pattern endian + faultaddr = faultaddr.replace('0x', '') + crashid['exceptions'][exceptionnum]['efa'] = faultaddr + if _64bit_debugger and not wow64_app: + # 64-bit target app + faultaddr = faultaddr.zfill(16) + efaptr = struct.unpack(' (.*)'), - 'wow64_dbg_prompt': re.compile('^[0-9]:[0-9][0-9][0-9]:x86> (.*)'), - } - -registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', - 'edi', 'eip') - -registers64 = ('rax', 'rcx', 'rdx', 'rbx', 'rsp', 'rbp', 'rsi', - 'rdi', 'rip', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', - 'r14', 'r15') - -# These !exploitable short descriptions indicate a very interesting crash -really_exploitable = [ - 'ReadAVonIP', - 'TaintedDataControlsCodeFlow', - 'ReadAVonControlFlow', - 'DEPViolation', - 'IllegalInstruction', - 'PrivilegedInstruction', - ] -reg_set = set(registers) -reg64_set = set(registers64) -re_set = set(really_exploitable) - -results = {} -cached_results = {} -scoredcrashes = {} -regdict = {} -mseclist = [] -_64bit_debugger = False -wow64_app = False -ignorejit = False -pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') - - -def check_64bit(reporttext): - ''' - Check if the debugger and target app are 64-bit - ''' - global _64bit_debugger - global wow64_app - for line in reporttext.splitlines(): - n = re.match(regex['64bit_debugger'], line) - if n: - _64bit_debugger = True - if _64bit_debugger: - n = re.match(regex['syswow64'], line) - if n: - wow64_app = True - - -def pc_in_mapped_address(reporttext, instraddr): - ''' - Check if the instruction pointer is in a loaded module - ''' - global _64bit_debugger - global wow64_app - ma_regex = 'mapped_address' - mapped_module = 'unloaded' - if _64bit_debugger: - ma_regex = 'mapped_address64' - - instraddr = instraddr.replace('`', '') - instraddr = int(instraddr, 16) - for line in reporttext.splitlines(): - n = re.match(regex[ma_regex], line) - if n: - # Strip out backticks present on 64-bit systems - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = n.group(3) - return mapped_module - - -def fixefabug(reporttext, instraddr, faultaddr): - ''' - !exploitable often reports an incorrect EFA for 64-bit targets. - If we're dealing with a 64-bit target, we can second-guess the reported EFA - ''' - instructionline = getinstr(reporttext, instraddr) - if not instructionline or "=" not in instructionline: - # Nothing to fix - return faultaddr - if 'ds:' in instructionline: - # There's a target address in the msec file - if '??' in instructionline: - # The AV is on dereferencing where to call - ds = carve(instructionline, "ds:", "=") - else: - # The AV is on accessing the code location - ds = instructionline.split("=")[-1] - else: - # AV must be on current instruction - ds = instructionline.split(' ')[0] - if ds: - faultaddr = ds.replace('`', '') - return faultaddr - - -def readfile(textfile): - ''' - Read text file - ''' - f = open(textfile, 'r') - text = f.read() - f.close() - return text - - -def readbinfile(inputfile): - ''' - Read binary file - ''' - f = open(inputfile, 'rb') - filebytes = f.read() - # For zip files, return the uncompressed bytes - file_like_content = StringIO.StringIO(filebytes) - if zipfile.is_zipfile(file_like_content): - # Make sure that it's not an embedded zip - # (e.g. a DOC file from Office 2007) - file_like_content.seek(0) - zipmagic = file_like_content.read(2) - if zipmagic == 'PK': - try: - # The file begins with the PK header - z = zipfile.ZipFile(file_like_content, 'r') - for filename in z.namelist(): - try: - filebytes += z.read(filename) - except: - pass - except: - # If the zip container is fuzzed we may get here - pass - file_like_content.close() - f.close - return filebytes - - -def carve(string, token1, token2): - startindex = string.find(token1) - if startindex == -1: - # can't find token1 - return "" - startindex = startindex + len(token1) - endindex = string.find(token2, startindex) - if endindex == -1: - # can't find token2 - return "" - return string[startindex:endindex] - - -# Todo: fix this up. Was added to bring gdb support -def carve2(string): - delims = [("Exception Faulting Address: ", "\n"), - ("si_addr:$2 = (void *)", "\n")] - for token1, token2 in delims: - startindex = string.find(token1) - if startindex == -1: - # can't find token1 - continue - startindex = startindex + len(token1) - endindex = string.find(token2, startindex) - return string[startindex:endindex] - - -def getexnum(reporttext): - ''' - Get the exception number by counting the number of continues - ''' - global wow64_app - - if wow64_app: - dbg_prompt = 'wow64_dbg_prompt' - else: - dbg_prompt = 'dbg_prompt' - exception = 0 - for line in reporttext.splitlines(): - n = re.match(regex[dbg_prompt], line) - if n: - cdbcmd = n.group(1) - cmds = cdbcmd.split(';') - for cmd in cmds: - if cmd == 'g': - exception = exception + 1 - return exception - - -def getregs(reporttext): - ''' - Populate the register dictionary with register values at crash - ''' - for line in reporttext.splitlines(): - if regex['regs1'].match(line) or regex['regs2'].match(line): - regs1 = line.split() - for reg in regs1: - if "=" in reg: - splitreg = reg.split("=") - regdict[splitreg[0]] = splitreg[1] - - -def getinstr(reporttext, instraddr): - ''' - Find the disassembly line for the current (crashing) instruction - ''' - regex = re.compile('^%s\s.+.+\s+' % instraddr) - for line in reporttext.splitlines(): - if regex.match(line): - return line - - -def formataddr(faultaddr): - ''' - Format a 64- or 32-bit memory address to a fixed width - ''' - global _64bit_debugger - global wow64_app - - if not faultaddr: - return - else: - faultaddr = faultaddr.strip() - faultaddr = faultaddr.replace('0x', '') - - if _64bit_debugger and not wow64_app: - # Due to a bug in !exploitable, the Exception Faulting Address is - # often wrong with 64-bit targets - if len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(16) - else: - if len(faultaddr) > 10: # 0x12345678 = 10 chars - faultaddr = faultaddr[-8:] - elif len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(8) - - return faultaddr - - -def ratchetscore(crasher, score): - ''' - Only allow a crasher to increase score - ''' - try: - currentscore = scoredcrashes[crasher] - except KeyError: - currentscore = 100 - if score < currentscore: - scoredcrashes[crasher] = score - - -def fixefaoffset(instructionline, faultaddr): - ''' - Adjust faulting address for instructions that use offsets - Currently only works for instructions like CALL [reg + offset] - ''' - global reg_set - if _64bit_debugger and not wow64_app: - reg_set = reg64_set - - if '0x' not in faultaddr: - faultaddr = '0x' + faultaddr - instructionpieces = instructionline.split() - if '??' not in instructionpieces[-1]: - # The av is on the address of the code called, not the address - # of the call - return faultaddr - for index, piece in enumerate(instructionpieces): - if piece == 'call': - # CALL instruction - if len(instructionpieces) <= index + 3: - # CALL to just a register. No offset - return faultaddr - address = instructionpieces[index + 3] - if '+' in address: - splitaddress = address.split('+') - reg = splitaddress[0] - reg = reg.replace('[', '') - if reg not in reg_set: - return faultaddr - offset = splitaddress[1] - offset = offset.replace('h', '') - offset = offset.replace(']', '') - if '0x' not in offset: - offset = '0x' + offset - if int(offset, 16) > int(faultaddr, 16): - # TODO: fix up negative numbers - return faultaddr - # Subtract offset to get actual interesting pattern - faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = formataddr(faultaddr.replace('L', '')) - return faultaddr - - -def checkreport(reportfile, crasherfile, crash_hash): - ''' - Parse the msec file - ''' - global _64bit_debugger - global wow64_app - global cached_results - - if cached_results: - if cached_results.get(crash_hash): - results[crash_hash] = cached_results[crash_hash] - return - - crashid = results[crash_hash] - - if crasherfile == '': - # Old FOE version that didn't do multiple exceptions or rename msec - # file with exploitability - crasherfile, reportfileext = os.path.splitext(reportfile) - - reporttext = readfile(reportfile) - getregs(reporttext) - current_dir = os.path.dirname(reportfile) - exceptionnum = getexnum(reporttext) - classification = carve(reporttext, "Exploitability Classification: ", "\n") - try: - if classification: - # Create a new exception dictionary to add to the crash - exception = {} - crashid['exceptions'][exceptionnum] = exception - except KeyError: - # Crash ID (crash_hash) not yet seen - # Default it to not being "really exploitable" - crashid['reallyexploitable'] = False - # Create a dictionary of exceptions for the crash id - exceptions = {} - crashid['exceptions'] = exceptions - # Create a dictionary for the exception - crashid['exceptions'][exceptionnum] = exception - - # Set !exploitable classification for the exception - if classification: - crashid['exceptions'][exceptionnum]['classification'] = classification - - shortdesc = carve(reporttext, "Short Description: ", "\n") - if shortdesc: - # Set !exploitable Short Description for the exception - crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc - if shortdesc in re_set: - # Flag the entire crash ID as really exploitable if this is a good - # exception - crashid['reallyexploitable'] = True - # Check if the expected crasher file (fuzzed file) exists - if not os.path.isfile(crasherfile): - # It's not there, so try to extract the filename from the cdb - # commandline - commandline = carve(reporttext, "CommandLine: ", "\n") - args = commandline.split() - for arg in args: - if "sf_" in arg: - crasherfile = os.path.basename(arg) - if "-" in crasherfile: - # FOE 2.0 verify mode puts a '-' part on the - # filename when invoking cdb, however the resulting file - # is really just 'sf_.' - fileparts = crasherfile.split('-') - m = re.search('\..+', fileparts[1]) - # Recreate the original file name, minus the iteration - crasherfile = os.path.join(current_dir, fileparts[0] + m.group(0)) - else: - crasherfile = os.path.join(current_dir, crasherfile) - if not os.path.isfile(crasherfile): - # Can't find the crasher file - return - # Set the "fuzzedfile" property for the crash ID - crashid['fuzzedfile'] = crasherfile - # See if we're dealing with 64-bit debugger or target app - check_64bit(reporttext) - faultaddr = carve2(reporttext) - instraddr = carve(reporttext, "Instruction Address:", "\n") - faultaddr = formataddr(faultaddr) - instraddr = formataddr(instraddr) - - # No faulting address means no crash. - if not faultaddr or not instraddr: - return - - if _64bit_debugger and not wow64_app and instraddr: - # Put backtick into instruction address for pattern matching - instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) - if shortdesc != 'DEPViolation': - faultaddr = fixefabug(reporttext, instraddr, faultaddr) - - -# pc_module = pc_in_mapped_address(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr) - - # Get the cdb line that contains the crashing instruction - instructionline = getinstr(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['instructionline'] = instructionline - if instructionline: - faultaddr = fixefaoffset(instructionline, faultaddr) - - # Fix faulting pattern endian - faultaddr = faultaddr.replace('0x', '') - crashid['exceptions'][exceptionnum]['efa'] = faultaddr - if _64bit_debugger and not wow64_app: - # 64-bit target app - faultaddr = faultaddr.zfill(16) - efaptr = struct.unpack(' Date: Thu, 23 Jan 2014 09:07:59 -0500 Subject: [PATCH 0054/1169] move minimize into certfuzz.tools.windows --- src/certfuzz/tools/windows/minimize.py | 209 +++++++++++++++++++++++++ src/windows/tools/minimize.py | 199 +---------------------- 2 files changed, 210 insertions(+), 198 deletions(-) create mode 100644 src/certfuzz/tools/windows/minimize.py diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py new file mode 100644 index 0000000..ab03b24 --- /dev/null +++ b/src/certfuzz/tools/windows/minimize.py @@ -0,0 +1,209 @@ +''' +Created on Apr 9, 2012 + +@organization: cert.org +''' + +import logging +import os +import sys +import string + +try: + from certfuzz import debuggers + from certfuzz.fuzztools import filetools, text + from certfuzz.file_handlers.basicfile import BasicFile + from certfuzz.minimizer import WindowsMinimizer as Minimizer + from certfuzz.campaign.config.foe_config import Config, get_command_args_list + from certfuzz.crash import FoeCrash + from certfuzz.debuggers import msec # @UnusedImport +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + import sys + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz import debuggers + from certfuzz.fuzztools import filetools, text + from certfuzz.file_handlers.basicfile import BasicFile + from certfuzz.minimizer import WindowsMinimizer as Minimizer + from certfuzz.campaign.config.foe_config import Config, get_command_args_list + from certfuzz.crash import FoeCrash + from certfuzz.debuggers import msec # @UnusedImport + +logger = logging.getLogger() +logger.setLevel(logging.WARNING) + + +def _create_minimizer_cfg(cfg): + class DummyCfg(object): + pass + config = DummyCfg() + config.backtracelevels = 5 # doesn't matter what this is, we don't use it + config.debugger_timeout = cfg['debugger']['runtimeout'] + template = string.Template(cfg['target']['cmdline_template']) + config.get_command_args_list = lambda x: get_command_args_list(template, x)[1] + config.program = cfg['target']['program'] + config.killprocname = None + config.exclude_unmapped_frames = False + config.watchdogfile = os.devnull + return config + + +def main(): + debuggers.verify_supported_platform() + + from optparse import OptionParser + + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + usage = "usage: %prog [options] fuzzedfile" + parser = OptionParser(usage) + parser.add_option('', '--debug', dest='debug', action='store_true', + help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--verbose', dest='verbose', action='store_true', + help='Enable verbose messages') + parser.add_option('-t', '--target', dest='target', + help='the file to minimize to (typically the seedfile)') + parser.add_option('-o', '--outdir', dest='outdir', + help='dir to write output to') + parser.add_option('-s', '--stringmode', dest='stringmode', + action='store_true', + help='minimize to a string rather than to a target file') + parser.add_option('-x', '--preferx', dest='prefer_x_target', + action='store_true', + help='Minimize to \'x\' characters instead of Metasploit string pattern') + parser.add_option('-f', '--faddr', dest='keep_uniq_faddr', + action='store_true', + help='Use exception faulting addresses as part of crash signature') + parser.add_option('-b', '--bitwise', dest='bitwise', action='store_true', + help='if set, use bitwise hamming distance. Default is bytewise') + parser.add_option('-c', '--confidence', dest='confidence', + help='The desired confidence level (default: 0.999)') + parser.add_option('-g', '--target-size-guess', dest='initial_target_size', + help='A guess at the minimal value (int)') + parser.add_option('', '--config', default='configs/foe.yaml', + dest='config', help='path to the configuration file to use') + parser.add_option('', '--timeout', dest='timeout', + metavar='N', type='int', default=0, + help='Stop minimizing after N seconds (default is 0, never time out).') + parser.add_option('-k', '--keepothers', dest='keep_other_crashes', + action='store_true', + help='Keep other crash hashes encountered during minimization') + (options, args) = parser.parse_args() + + if options.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + if options.config: + cfg_file = options.config + else: + cfg_file = "../conf.d/bff.cfg" + logger.debug('Config file: %s', cfg_file) + + if options.stringmode and options.target: + parser.error('Options --stringmode and --target are mutually exclusive.') + + # Set some default options. Fast and loose if in string mode + # More precise with minimize to seedfile + if not options.confidence: + if options.stringmode: + options.confidence = 0.5 + else: + options.confidence = 0.999 + if not options.initial_target_size: + if options.stringmode: + options.initial_target_size = 100 + else: + options.initial_target_size = 1 + + if options.confidence: + try: + options.confidence = float(options.confidence) + except: + parser.error('Confidence must be a float.') + if not 0.0 < options.confidence < 1.0: + parser.error('Confidence must be in the range 0.0 < c < 1.0') + + confidence = options.confidence + + if options.outdir: + outdir = options.outdir + else: + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + outdir = os.path.abspath(os.path.join(parentdir, 'minimizer_out')) + + filetools.make_directories(outdir) + + if len(args) and os.path.exists(args[0]): + fuzzed_file = BasicFile(args[0]) + logger.info('Fuzzed file is %s', fuzzed_file) + else: + parser.error('fuzzedfile must be specified') + + config = Config(cfg_file).config + cfg = _create_minimizer_cfg(config) + + if options.target: + seedfile = BasicFile(options.target) + else: + seedfile = None + + min2seed = not options.stringmode + filename_modifier = '' + retries = 0 + debugger_class = msec.MsecDebugger + template = string.Template(config['target']['cmdline_template']) + cmd_as_args = get_command_args_list(template, fuzzed_file.path)[1] + with FoeCrash(template, seedfile, fuzzed_file, cmd_as_args, None, debugger_class, + config['debugger'], outdir, options.keep_uniq_faddr, config['target']['program'], + retries) as crash: + filetools.make_directories(crash.tempdir) + logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) + filetools.copy_file(fuzzed_file.path, crash.tempdir) + + minlog = os.path.join(outdir, 'min_log.txt') + + with Minimizer(cfg=cfg, crash=crash, crash_dst_dir=outdir, + seedfile_as_target=min2seed, bitwise=options.bitwise, + confidence=confidence, tempdir=outdir, + logfile=minlog, maxtime=options.timeout, + preferx=options.prefer_x_target) as minimize: + minimize.save_others = options.keep_other_crashes + minimize.target_size_guess = int(options.initial_target_size) + minimize.go() + + if options.stringmode: + logger.debug('x character substitution') + length = len(minimize.fuzzed) + if options.prefer_x_target: + #We minimized to 'x', so we attempt to get metasploit as a freebie + targetstring = list(text.metasploit_pattern_orig(length)) + filename_modifier = '-mtsp' + else: + #We minimized to metasploit, so we attempt to get 'x' as a freebie + targetstring = list('x' * length) + filename_modifier = '-x' + + fuzzed = list(minimize.fuzzed) + for idx in minimize.bytemap: + logger.debug('Swapping index %d', idx) + targetstring[idx] = fuzzed[idx] + filename = ''.join((crash.fuzzedfile.root, filename_modifier, crash.fuzzedfile.ext)) + metasploit_file = os.path.join(crash.tempdir, filename) + + f = open(metasploit_file, 'wb') + try: + f.writelines(targetstring) + finally: + f.close() + crash.copy_files(outdir) + crash.clean_tmpdir() + + +if __name__ == '__main__': + main() diff --git a/src/windows/tools/minimize.py b/src/windows/tools/minimize.py index 188b278..b60d159 100644 --- a/src/windows/tools/minimize.py +++ b/src/windows/tools/minimize.py @@ -3,204 +3,7 @@ @organization: cert.org ''' - -import logging -import os -import sys -import string - -try: - from certfuzz import debuggers - from certfuzz.fuzztools import filetools, text - from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.minimizer import WindowsMinimizer as Minimizer - from certfuzz.campaign.config.foe_config import Config, get_command_args_list - from certfuzz.crash import FoeCrash - from certfuzz.debuggers import msec # @UnusedImport -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - import sys - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz import debuggers - from certfuzz.fuzztools import filetools, text - from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.minimizer import WindowsMinimizer as Minimizer - from certfuzz.campaign.config.foe_config import Config, get_command_args_list - from certfuzz.crash import FoeCrash - from certfuzz.debuggers import msec # @UnusedImport - -logger = logging.getLogger() -logger.setLevel(logging.WARNING) - -def _create_minimizer_cfg(cfg): - class DummyCfg(object): - pass - config = DummyCfg() - config.backtracelevels = 5 # doesn't matter what this is, we don't use it - config.debugger_timeout = cfg['debugger']['runtimeout'] - template = string.Template(cfg['target']['cmdline_template']) - config.get_command_args_list = lambda x: get_command_args_list(template, x)[1] - config.program = cfg['target']['program'] - config.killprocname = None - config.exclude_unmapped_frames = False - config.watchdogfile = os.devnull - return config - -def main(): - debuggers.verify_supported_platform() - - from optparse import OptionParser - - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - usage = "usage: %prog [options] fuzzedfile" - parser = OptionParser(usage) - parser.add_option('', '--debug', dest='debug', action='store_true', - help='Enable debug messages (overrides --verbose)') - parser.add_option('', '--verbose', dest='verbose', action='store_true', - help='Enable verbose messages') - parser.add_option('-t', '--target', dest='target', - help='the file to minimize to (typically the seedfile)') - parser.add_option('-o', '--outdir', dest='outdir', - help='dir to write output to') - parser.add_option('-s', '--stringmode', dest='stringmode', - action='store_true', - help='minimize to a string rather than to a target file') - parser.add_option('-x', '--preferx', dest='prefer_x_target', - action='store_true', - help='Minimize to \'x\' characters instead of Metasploit string pattern') - parser.add_option('-f', '--faddr', dest='keep_uniq_faddr', - action='store_true', - help='Use exception faulting addresses as part of crash signature') - parser.add_option('-b', '--bitwise', dest='bitwise', action='store_true', - help='if set, use bitwise hamming distance. Default is bytewise') - parser.add_option('-c', '--confidence', dest='confidence', - help='The desired confidence level (default: 0.999)') - parser.add_option('-g', '--target-size-guess', dest='initial_target_size', - help='A guess at the minimal value (int)') - parser.add_option('', '--config', default='configs/foe.yaml', - dest='config', help='path to the configuration file to use') - parser.add_option('', '--timeout', dest='timeout', - metavar='N', type='int', default=0, - help='Stop minimizing after N seconds (default is 0, never time out).') - parser.add_option('-k', '--keepothers', dest='keep_other_crashes', - action='store_true', - help='Keep other crash hashes encountered during minimization') - (options, args) = parser.parse_args() - - if options.debug: - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - - if options.config: - cfg_file = options.config - else: - cfg_file = "../conf.d/bff.cfg" - logger.debug('Config file: %s', cfg_file) - - if options.stringmode and options.target: - parser.error('Options --stringmode and --target are mutually exclusive.') - - # Set some default options. Fast and loose if in string mode - # More precise with minimize to seedfile - if not options.confidence: - if options.stringmode: - options.confidence = 0.5 - else: - options.confidence = 0.999 - if not options.initial_target_size: - if options.stringmode: - options.initial_target_size = 100 - else: - options.initial_target_size = 1 - - if options.confidence: - try: - options.confidence = float(options.confidence) - except: - parser.error('Confidence must be a float.') - if not 0.0 < options.confidence < 1.0: - parser.error('Confidence must be in the range 0.0 < c < 1.0') - - confidence = options.confidence - - if options.outdir: - outdir = options.outdir - else: - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - outdir = os.path.abspath(os.path.join(parentdir, 'minimizer_out')) - - filetools.make_directories(outdir) - - if len(args) and os.path.exists(args[0]): - fuzzed_file = BasicFile(args[0]) - logger.info('Fuzzed file is %s', fuzzed_file) - else: - parser.error('fuzzedfile must be specified') - - config = Config(cfg_file).config - cfg = _create_minimizer_cfg(config) - - if options.target: - seedfile = BasicFile(options.target) - else: - seedfile = None - - min2seed = not options.stringmode - filename_modifier = '' - retries = 0 - debugger_class = msec.MsecDebugger - template = string.Template(config['target']['cmdline_template']) - cmd_as_args = get_command_args_list(template, fuzzed_file.path)[1] - with FoeCrash(template, seedfile, fuzzed_file, cmd_as_args, None, debugger_class, - config['debugger'], outdir, options.keep_uniq_faddr, config['target']['program'], - retries) as crash: - filetools.make_directories(crash.tempdir) - logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) - filetools.copy_file(fuzzed_file.path, crash.tempdir) - - minlog = os.path.join(outdir, 'min_log.txt') - - with Minimizer(cfg=cfg, crash=crash, crash_dst_dir=outdir, - seedfile_as_target=min2seed, bitwise=options.bitwise, - confidence=confidence, tempdir=outdir, - logfile=minlog, maxtime=options.timeout, - preferx=options.prefer_x_target) as minimize: - minimize.save_others = options.keep_other_crashes - minimize.target_size_guess = int(options.initial_target_size) - minimize.go() - - if options.stringmode: - logger.debug('x character substitution') - length = len(minimize.fuzzed) - if options.prefer_x_target: - #We minimized to 'x', so we attempt to get metasploit as a freebie - targetstring = list(text.metasploit_pattern_orig(length)) - filename_modifier = '-mtsp' - else: - #We minimized to metasploit, so we attempt to get 'x' as a freebie - targetstring = list('x' * length) - filename_modifier = '-x' - - fuzzed = list(minimize.fuzzed) - for idx in minimize.bytemap: - logger.debug('Swapping index %d', idx) - targetstring[idx] = fuzzed[idx] - filename = ''.join((crash.fuzzedfile.root, filename_modifier, crash.fuzzedfile.ext)) - metasploit_file = os.path.join(crash.tempdir, filename) - - f = open(metasploit_file, 'wb') - try: - f.writelines(targetstring) - finally: - f.close() - crash.copy_files(outdir) - crash.clean_tmpdir() +from certfuzz.tools.windows.minimize import main if __name__ == '__main__': main() From 58694d6b083c15fb08d1a2cd043a26d1e5fcfa87 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:08:56 -0500 Subject: [PATCH 0055/1169] move mtsp_enum into certfuzz.tools.windows --- src/certfuzz/tools/windows/mtsp_enum.py | 50 +++++++++++++++++++++++++ src/windows/tools/mtsp_enum.py | 41 +------------------- 2 files changed, 51 insertions(+), 40 deletions(-) create mode 100644 src/certfuzz/tools/windows/mtsp_enum.py diff --git a/src/certfuzz/tools/windows/mtsp_enum.py b/src/certfuzz/tools/windows/mtsp_enum.py new file mode 100644 index 0000000..a8e1ccd --- /dev/null +++ b/src/certfuzz/tools/windows/mtsp_enum.py @@ -0,0 +1,50 @@ +''' +Created on Mar 8, 2013 + +@organization: cert.org +''' +import logging +import argparse +import os.path + +try: + from certfuzz.fuzztools.text import enumerate_string +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + import sys + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.fuzztools.text import enumerate_string + + +def main(): + logger = logging.getLogger() + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + group.add_argument('--debug', help='', action='store_true') + group.add_argument('-v', '--verbose', help='', action='store_true') + parser.add_argument('searchstring', + type=str, + help='The string to enumerate') + parser.add_argument('fuzzedfile', + help='Path to a fuzzedfile', + type=str) + + args = parser.parse_args() + + if args.debug: + logger.setLevel(logging.DEBUG) + elif args.verbose: + logger.setLevel(logging.INFO) + else: + logger.setLevel(logging.WARNING) + + enumerate_string(path=args.fuzzedfile, str_to_enum=args.searchstring) + + +if __name__ == '__main__': + main() diff --git a/src/windows/tools/mtsp_enum.py b/src/windows/tools/mtsp_enum.py index 10d034e..01c702f 100644 --- a/src/windows/tools/mtsp_enum.py +++ b/src/windows/tools/mtsp_enum.py @@ -3,46 +3,7 @@ @organization: cert.org ''' -import logging -import argparse -import os.path - -try: - from certfuzz.fuzztools.text import enumerate_string -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - import sys - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.fuzztools.text import enumerate_string - -def main(): - logger = logging.getLogger() - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument('--debug', help='', action='store_true') - group.add_argument('-v', '--verbose', help='', action='store_true') - parser.add_argument('searchstring', - type=str, - help='The string to enumerate') - parser.add_argument('fuzzedfile', - help='Path to a fuzzedfile', - type=str) - - args = parser.parse_args() - - if args.debug: - logger.setLevel(logging.DEBUG) - elif args.verbose: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.WARNING) - - enumerate_string(path=args.fuzzedfile, str_to_enum=args.searchstring) +from certfuzz.tools.windows.mtsp_enum import main if __name__ == '__main__': main() From b2d6d2925b4cd136232f2b01bd4081ba917d89c4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:10:18 -0500 Subject: [PATCH 0056/1169] move quickstats into certfuzz.tools.windows --- src/certfuzz/tools/windows/quickstats.py | 51 ++++++++++++++++++++++++ src/windows/tools/quickstats.py | 43 +------------------- 2 files changed, 52 insertions(+), 42 deletions(-) create mode 100644 src/certfuzz/tools/windows/quickstats.py diff --git a/src/certfuzz/tools/windows/quickstats.py b/src/certfuzz/tools/windows/quickstats.py new file mode 100644 index 0000000..f67b8eb --- /dev/null +++ b/src/certfuzz/tools/windows/quickstats.py @@ -0,0 +1,51 @@ +''' +Created on Jun 14, 2013 + +''' + +import os +import sys +from optparse import OptionParser + +explevels = ['EXPLOITABLE', 'PROBABLY_EXPLOITABLE', + 'PROBABLY_NOT_EXPLOITABLE', 'UNKNOWN', 'HEISENBUG'] + + +def countcrashers(tld): + totalcrashers = 0 + # Walk the results directory + for root, dirs, files in os.walk(tld): + curdir = os.path.basename(root) + for explevel in explevels: + if curdir == explevel: + explevelcount = len([hash for hash in os.listdir(root) if '0x' in hash]) + totalcrashers = totalcrashers + explevelcount + print '%s: %s' % (explevel, explevelcount) + print 'Total: %s' % totalcrashers + + +def main(): + # If user doesn't specify a directory to crawl, use "results" + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + parser.add_option('-d', '--dir', + help='directory to look for results in. Default is "results"', + dest='resultsdir', default='results') + (options, args) = parser.parse_args() + tld = options.resultsdir + if not os.path.isdir(tld): + if os.path.isdir('../results'): + tld = '../results' + elif os.path.isdir('crashers'): + # Probably using FOE 1.0, which defaults to "crashers" for output + tld = 'crashers' + else: + print 'Cannot find resuls directory %s' % tld + sys.exit(0) + + countcrashers(tld) + + +if __name__ == '__main__': + main() + diff --git a/src/windows/tools/quickstats.py b/src/windows/tools/quickstats.py index 88f8fc2..27e79c3 100644 --- a/src/windows/tools/quickstats.py +++ b/src/windows/tools/quickstats.py @@ -2,48 +2,7 @@ Created on Jun 14, 2013 ''' - -import os -import sys -from optparse import OptionParser - -explevels = ['EXPLOITABLE', 'PROBABLY_EXPLOITABLE', - 'PROBABLY_NOT_EXPLOITABLE', 'UNKNOWN', 'HEISENBUG'] - - -def countcrashers(tld): - totalcrashers = 0 - # Walk the results directory - for root, dirs, files in os.walk(tld): - curdir = os.path.basename(root) - for explevel in explevels: - if curdir == explevel: - explevelcount = len([hash for hash in os.listdir(root) if '0x' in hash]) - totalcrashers = totalcrashers + explevelcount - print '%s: %s' % (explevel, explevelcount) - print 'Total: %s' % totalcrashers - - -def main(): - # If user doesn't specify a directory to crawl, use "results" - usage = "usage: %prog [options]" - parser = OptionParser(usage=usage) - parser.add_option('-d', '--dir', - help='directory to look for results in. Default is "results"', - dest='resultsdir', default='results') - (options, args) = parser.parse_args() - tld = options.resultsdir - if not os.path.isdir(tld): - if os.path.isdir('../results'): - tld = '../results' - elif os.path.isdir('crashers'): - # Probably using FOE 1.0, which defaults to "crashers" for output - tld = 'crashers' - else: - print 'Cannot find resuls directory %s' % tld - sys.exit(0) - - countcrashers(tld) +from certfuzz.tools.windows.quickstats import main if __name__ == '__main__': main() From f1dd6de0690c73fe274eaf83c3f7efc7d09baf3d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:12:13 -0500 Subject: [PATCH 0057/1169] move repro into certfuzz.tools.windows --- src/certfuzz/tools/windows/repro.py | 154 ++++++++++++++++++++++++++++ src/windows/tools/repro.py | 147 +------------------------- 2 files changed, 155 insertions(+), 146 deletions(-) create mode 100644 src/certfuzz/tools/windows/repro.py diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py new file mode 100644 index 0000000..4ecd3b8 --- /dev/null +++ b/src/certfuzz/tools/windows/repro.py @@ -0,0 +1,154 @@ +''' +Created on Feb 4, 2013 + +@organization: cert.org +''' +import logging +import os +import string +import re +from subprocess import Popen + +try: + from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file + from certfuzz import debuggers + from certfuzz.file_handlers.basicfile import BasicFile + from certfuzz.runners.runner_base import get_command_args_list + from certfuzz.campaign.config.foe_config import Config + from certfuzz.debuggers import msec # @UnusedImport +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + import sys + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file + from certfuzz import debuggers + from certfuzz.file_handlers.basicfile import BasicFile + from certfuzz.runners.runner_base import get_command_args_list + from certfuzz.campaign.config.foe_config import Config + from certfuzz.debuggers import msec # @UnusedImport + +logger = logging.getLogger() +logger.setLevel(logging.WARNING) + + +def parseiterpath(commandline): + # Return the path to the iteration's fuzzed file + for part in commandline.split(): + if 'foe-crash-' in part: + return part + + +def getiterpath(msecfile): + # Find the commandline in an msec file + with open(msecfile) as mseclines: + for line in mseclines: + m = re.match('CommandLine: ', line) + if m: + return parseiterpath(line) + + +def main(): + debuggers.verify_supported_platform() + + from optparse import OptionParser + + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + usage = "usage: %prog [options] fuzzedfile" + parser = OptionParser(usage) + parser.add_option('', '--debug', dest='debug', action='store_true', + help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--verbose', dest='verbose', action='store_true', + help='Enable verbose messages') + parser.add_option('-c', '--config', default='configs/foe.yaml', + dest='config', + help='path to the configuration file to use') + parser.add_option('-w', '--windbg', dest='use_windbg', + action='store_true', + help='Use windbg instead of cdb') + parser.add_option('-b', '--break', dest='break_on_start', + action='store_true', + help='Break on start of debugger session') + parser.add_option('-d', '--debugheap', dest='debugheap', + action='store_true', + help='Use debug heap') + parser.add_option('-p', '--debugger', dest='debugger', + help='Use specified debugger') + parser.add_option('-f', '--filepath', dest='filepath', + action='store_true', help='Recreate original file path') + (options, args) = parser.parse_args() + + if options.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + cfg_file = options.config + logger.debug('Config file: %s', cfg_file) + + if len(args) and os.path.exists(args[0]): + fullpath_fuzzed_file = os.path.abspath(args[0]) + fuzzed_file = BasicFile(fullpath_fuzzed_file) + logger.info('Fuzzed file is %s', fuzzed_file) + else: + parser.error('fuzzedfile must be specified') + + config = Config(cfg_file).config + + iterationpath = '' + template = string.Template(config['target']['cmdline_template']) + if options.filepath: + # Recreate same file path as fuzz iteration + resultdir = os.path.dirname(fuzzed_file.path) + for msecfile in all_files(resultdir, '*.msec'): + print '** using msecfile: %s' % msecfile + iterationpath = getiterpath(msecfile) + break + + if iterationpath: + iterationdir = os.path.dirname(iterationpath) + iterationfile = os.path.basename(iterationpath) + mkdir_p(iterationdir) + copy_file(fuzzed_file.path, + os.path.join(iterationdir, iterationfile)) + fuzzed_file.path = iterationpath + + cmd_as_args = get_command_args_list(template, fuzzed_file.path)[1] + targetdir = os.path.dirname(cmd_as_args[0]) + + args = [] + + if options.use_windbg and options.debugger: + parser.error('Options --windbg and --debugger are mutually exclusive.') + + if options.debugger: + debugger_app = options.debugger + elif options.use_windbg: + debugger_app = 'windbg' + else: + debugger_app = 'cdb' + + args.append(debugger_app) + + if not options.debugger: + # Using cdb or windbg + args.append('-amsec.dll') + if options.debugheap: + # do not use hd, xd options if debugheap is set + pass + else: + args.extend(('-hd', '-xd', 'gp')) + if not options.break_on_start: + args.extend(('-xd', 'bpe', '-G')) + args.extend(('-xd', 'wob', '-o',)) + args.extend(cmd_as_args) + logger.info('args %s' % cmd_as_args) + + p = Popen(args, cwd=targetdir, universal_newlines=True) + p.wait() + +if __name__ == '__main__': + main() diff --git a/src/windows/tools/repro.py b/src/windows/tools/repro.py index 4ecd3b8..af03c8b 100644 --- a/src/windows/tools/repro.py +++ b/src/windows/tools/repro.py @@ -3,152 +3,7 @@ @organization: cert.org ''' -import logging -import os -import string -import re -from subprocess import Popen - -try: - from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file - from certfuzz import debuggers - from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.runners.runner_base import get_command_args_list - from certfuzz.campaign.config.foe_config import Config - from certfuzz.debuggers import msec # @UnusedImport -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - import sys - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file - from certfuzz import debuggers - from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.runners.runner_base import get_command_args_list - from certfuzz.campaign.config.foe_config import Config - from certfuzz.debuggers import msec # @UnusedImport - -logger = logging.getLogger() -logger.setLevel(logging.WARNING) - - -def parseiterpath(commandline): - # Return the path to the iteration's fuzzed file - for part in commandline.split(): - if 'foe-crash-' in part: - return part - - -def getiterpath(msecfile): - # Find the commandline in an msec file - with open(msecfile) as mseclines: - for line in mseclines: - m = re.match('CommandLine: ', line) - if m: - return parseiterpath(line) - - -def main(): - debuggers.verify_supported_platform() - - from optparse import OptionParser - - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - usage = "usage: %prog [options] fuzzedfile" - parser = OptionParser(usage) - parser.add_option('', '--debug', dest='debug', action='store_true', - help='Enable debug messages (overrides --verbose)') - parser.add_option('', '--verbose', dest='verbose', action='store_true', - help='Enable verbose messages') - parser.add_option('-c', '--config', default='configs/foe.yaml', - dest='config', - help='path to the configuration file to use') - parser.add_option('-w', '--windbg', dest='use_windbg', - action='store_true', - help='Use windbg instead of cdb') - parser.add_option('-b', '--break', dest='break_on_start', - action='store_true', - help='Break on start of debugger session') - parser.add_option('-d', '--debugheap', dest='debugheap', - action='store_true', - help='Use debug heap') - parser.add_option('-p', '--debugger', dest='debugger', - help='Use specified debugger') - parser.add_option('-f', '--filepath', dest='filepath', - action='store_true', help='Recreate original file path') - (options, args) = parser.parse_args() - - if options.debug: - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - - cfg_file = options.config - logger.debug('Config file: %s', cfg_file) - - if len(args) and os.path.exists(args[0]): - fullpath_fuzzed_file = os.path.abspath(args[0]) - fuzzed_file = BasicFile(fullpath_fuzzed_file) - logger.info('Fuzzed file is %s', fuzzed_file) - else: - parser.error('fuzzedfile must be specified') - - config = Config(cfg_file).config - - iterationpath = '' - template = string.Template(config['target']['cmdline_template']) - if options.filepath: - # Recreate same file path as fuzz iteration - resultdir = os.path.dirname(fuzzed_file.path) - for msecfile in all_files(resultdir, '*.msec'): - print '** using msecfile: %s' % msecfile - iterationpath = getiterpath(msecfile) - break - - if iterationpath: - iterationdir = os.path.dirname(iterationpath) - iterationfile = os.path.basename(iterationpath) - mkdir_p(iterationdir) - copy_file(fuzzed_file.path, - os.path.join(iterationdir, iterationfile)) - fuzzed_file.path = iterationpath - - cmd_as_args = get_command_args_list(template, fuzzed_file.path)[1] - targetdir = os.path.dirname(cmd_as_args[0]) - - args = [] - - if options.use_windbg and options.debugger: - parser.error('Options --windbg and --debugger are mutually exclusive.') - - if options.debugger: - debugger_app = options.debugger - elif options.use_windbg: - debugger_app = 'windbg' - else: - debugger_app = 'cdb' - - args.append(debugger_app) - - if not options.debugger: - # Using cdb or windbg - args.append('-amsec.dll') - if options.debugheap: - # do not use hd, xd options if debugheap is set - pass - else: - args.extend(('-hd', '-xd', 'gp')) - if not options.break_on_start: - args.extend(('-xd', 'bpe', '-G')) - args.extend(('-xd', 'wob', '-o',)) - args.extend(cmd_as_args) - logger.info('args %s' % cmd_as_args) - - p = Popen(args, cwd=targetdir, universal_newlines=True) - p.wait() +from certfuzz.tools.windows.repro import main if __name__ == '__main__': main() From d877c3c731a531bf18f5279a4a3a21d71692db75 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:13:33 -0500 Subject: [PATCH 0058/1169] move zipdiff into certfuzz.tools.windows --- src/certfuzz/tools/windows/zipdiff.py | 74 +++++++++++++++++++++++++++ src/windows/tools/zipdiff.py | 67 +----------------------- 2 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 src/certfuzz/tools/windows/zipdiff.py diff --git a/src/certfuzz/tools/windows/zipdiff.py b/src/certfuzz/tools/windows/zipdiff.py new file mode 100644 index 0000000..345c13c --- /dev/null +++ b/src/certfuzz/tools/windows/zipdiff.py @@ -0,0 +1,74 @@ +''' +Created on Jul 10, 2013 + +@organization: cert.org +''' + +import os +import collections +import zipfile +from optparse import OptionParser + +saved_arcinfo = collections.OrderedDict() + + +def readzip(filepath): + global savedarcinfo + # If the seed is zip-based, fuzz the contents rather than the container + tempzip = zipfile.ZipFile(filepath, 'r') + + ''' + get info on all the archived files and concatentate their contents + into self.input + ''' + unzippedbytes = '' + for i in tempzip.namelist(): + data = tempzip.read(i) + + # save split indices and compression type for archival reconstruction + + saved_arcinfo[i] = (len(unzippedbytes), len(data)) + unzippedbytes += data + tempzip.close() + return unzippedbytes + + +def main(): + global saved_arcinfo + usage = 'usage: %prog zip1 zip2' + parser = OptionParser(usage=usage) + (options, args) = parser.parse_args() + + if len(args) != 2: + parser.error('Incorrect number of arguments') + return + + changedbytes = [] + changedfiles = [] + + zip1 = args[0] + zip2 = args[1] + zip1bytes = readzip(zip1) + zip2bytes = readzip(zip2) + zip1len = len(zip1bytes) + + if zip1len != len(zip2bytes): + print 'Zip contents are not the same size. Aborting.' + + for i in range(0, zip1len): + if zip1bytes[i] != zip2bytes[i]: +# print 'Zip contents differ at offset %s' % i + changedbytes.append(i) + + for changedbyte in changedbytes: + for name, info in saved_arcinfo.iteritems(): + startaddr = info[0] + endaddr = info[0] + info[1] + if startaddr <= changedbyte <= endaddr and name not in changedfiles: + print '%s modified' % name + changedfiles.append(name) + #print '%s: %s-%s' %(name, info[0], info[0]+info[1]) + + +if __name__ == '__main__': + main() diff --git a/src/windows/tools/zipdiff.py b/src/windows/tools/zipdiff.py index 345c13c..a33827e 100644 --- a/src/windows/tools/zipdiff.py +++ b/src/windows/tools/zipdiff.py @@ -3,72 +3,7 @@ @organization: cert.org ''' - -import os -import collections -import zipfile -from optparse import OptionParser - -saved_arcinfo = collections.OrderedDict() - - -def readzip(filepath): - global savedarcinfo - # If the seed is zip-based, fuzz the contents rather than the container - tempzip = zipfile.ZipFile(filepath, 'r') - - ''' - get info on all the archived files and concatentate their contents - into self.input - ''' - unzippedbytes = '' - for i in tempzip.namelist(): - data = tempzip.read(i) - - # save split indices and compression type for archival reconstruction - - saved_arcinfo[i] = (len(unzippedbytes), len(data)) - unzippedbytes += data - tempzip.close() - return unzippedbytes - - -def main(): - global saved_arcinfo - usage = 'usage: %prog zip1 zip2' - parser = OptionParser(usage=usage) - (options, args) = parser.parse_args() - - if len(args) != 2: - parser.error('Incorrect number of arguments') - return - - changedbytes = [] - changedfiles = [] - - zip1 = args[0] - zip2 = args[1] - zip1bytes = readzip(zip1) - zip2bytes = readzip(zip2) - zip1len = len(zip1bytes) - - if zip1len != len(zip2bytes): - print 'Zip contents are not the same size. Aborting.' - - for i in range(0, zip1len): - if zip1bytes[i] != zip2bytes[i]: -# print 'Zip contents differ at offset %s' % i - changedbytes.append(i) - - for changedbyte in changedbytes: - for name, info in saved_arcinfo.iteritems(): - startaddr = info[0] - endaddr = info[0] + info[1] - if startaddr <= changedbyte <= endaddr and name not in changedfiles: - print '%s modified' % name - changedfiles.append(name) - #print '%s: %s-%s' %(name, info[0], info[0]+info[1]) - +from certfuzz.tools.windows.zipdiff import main if __name__ == '__main__': main() From 9e157dce73a83fcfb6dfdf57fbb61ea3e8a59418 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:15:51 -0500 Subject: [PATCH 0059/1169] create certfuzz.tools.common package for things that are identical across platforms --- src/certfuzz/tools/common/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/certfuzz/tools/common/__init__.py diff --git a/src/certfuzz/tools/common/__init__.py b/src/certfuzz/tools/common/__init__.py new file mode 100644 index 0000000..e69de29 From 8939417f5be483a77ec4fceb8c24df90e40e57f3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 09:17:46 -0500 Subject: [PATCH 0060/1169] move mtsp_enum into certfuzz.tools.common --- .../tools/{linux => common}/mtsp_enum.py | 0 src/certfuzz/tools/windows/mtsp_enum.py | 50 ------------------- src/linux/tools/mtsp_enum.py | 2 +- src/windows/tools/mtsp_enum.py | 2 +- 4 files changed, 2 insertions(+), 52 deletions(-) rename src/certfuzz/tools/{linux => common}/mtsp_enum.py (100%) delete mode 100644 src/certfuzz/tools/windows/mtsp_enum.py diff --git a/src/certfuzz/tools/linux/mtsp_enum.py b/src/certfuzz/tools/common/mtsp_enum.py similarity index 100% rename from src/certfuzz/tools/linux/mtsp_enum.py rename to src/certfuzz/tools/common/mtsp_enum.py diff --git a/src/certfuzz/tools/windows/mtsp_enum.py b/src/certfuzz/tools/windows/mtsp_enum.py deleted file mode 100644 index a8e1ccd..0000000 --- a/src/certfuzz/tools/windows/mtsp_enum.py +++ /dev/null @@ -1,50 +0,0 @@ -''' -Created on Mar 8, 2013 - -@organization: cert.org -''' -import logging -import argparse -import os.path - -try: - from certfuzz.fuzztools.text import enumerate_string -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - import sys - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.fuzztools.text import enumerate_string - - -def main(): - logger = logging.getLogger() - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument('--debug', help='', action='store_true') - group.add_argument('-v', '--verbose', help='', action='store_true') - parser.add_argument('searchstring', - type=str, - help='The string to enumerate') - parser.add_argument('fuzzedfile', - help='Path to a fuzzedfile', - type=str) - - args = parser.parse_args() - - if args.debug: - logger.setLevel(logging.DEBUG) - elif args.verbose: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.WARNING) - - enumerate_string(path=args.fuzzedfile, str_to_enum=args.searchstring) - - -if __name__ == '__main__': - main() diff --git a/src/linux/tools/mtsp_enum.py b/src/linux/tools/mtsp_enum.py index 46e5849..f2a1cc1 100755 --- a/src/linux/tools/mtsp_enum.py +++ b/src/linux/tools/mtsp_enum.py @@ -4,7 +4,7 @@ @organization: cert.org ''' -from certfuzz.tools.linux.mtsp_enum import main +from certfuzz.tools.common.mtsp_enum import main if __name__ == '__main__': main() diff --git a/src/windows/tools/mtsp_enum.py b/src/windows/tools/mtsp_enum.py index 01c702f..f2d597a 100644 --- a/src/windows/tools/mtsp_enum.py +++ b/src/windows/tools/mtsp_enum.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from certfuzz.tools.windows.mtsp_enum import main +from certfuzz.tools.common.mtsp_enum import main if __name__ == '__main__': main() From ed0b4e1a4de1ae95bc7291a95576b391d1b0da3c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 10:11:14 -0500 Subject: [PATCH 0061/1169] create empty packages for certfuzz.tools unit tests --- src/certfuzz/test/tools/__init__.py | 0 src/certfuzz/test/tools/common/__init__.py | 0 src/certfuzz/test/tools/linux/__init__.py | 0 src/certfuzz/test/tools/windows/__init__.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/certfuzz/test/tools/__init__.py create mode 100644 src/certfuzz/test/tools/common/__init__.py create mode 100644 src/certfuzz/test/tools/linux/__init__.py create mode 100644 src/certfuzz/test/tools/windows/__init__.py diff --git a/src/certfuzz/test/tools/__init__.py b/src/certfuzz/test/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/test/tools/common/__init__.py b/src/certfuzz/test/tools/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/test/tools/linux/__init__.py b/src/certfuzz/test/tools/linux/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/test/tools/windows/__init__.py b/src/certfuzz/test/tools/windows/__init__.py new file mode 100644 index 0000000..e69de29 From 2366485bf9daeaca12e6e89014d4567ac9a92fca Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 10:19:41 -0500 Subject: [PATCH 0062/1169] move tests into the right location --- .../test/tools/common/test_mtsp_enum.py} | 7 +--- .../test/tools/linux}/test_bff_stats.py | 0 .../test/tools/linux}/test_callsim.py | 0 .../linux}/test_create_crasher_script.py | 0 .../test/tools/linux/test_drillresults.py} | 7 +--- .../test/tools/linux}/test_minimizer_plot.py | 0 src/linux/test/tools/test_tools_misc.py | 39 ------------------- 7 files changed, 2 insertions(+), 51 deletions(-) rename src/{linux/test/tools/test_get_eips.py => certfuzz/test/tools/common/test_mtsp_enum.py} (90%) rename src/{linux/test/tools => certfuzz/test/tools/linux}/test_bff_stats.py (100%) rename src/{linux/test/tools => certfuzz/test/tools/linux}/test_callsim.py (100%) rename src/{linux/test/tools => certfuzz/test/tools/linux}/test_create_crasher_script.py (100%) rename src/{linux/test/tools/test_rangefinder_plot.py => certfuzz/test/tools/linux/test_drillresults.py} (90%) rename src/{linux/test/tools => certfuzz/test/tools/linux}/test_minimizer_plot.py (100%) delete mode 100644 src/linux/test/tools/test_tools_misc.py diff --git a/src/linux/test/tools/test_get_eips.py b/src/certfuzz/test/tools/common/test_mtsp_enum.py similarity index 90% rename from src/linux/test/tools/test_get_eips.py rename to src/certfuzz/test/tools/common/test_mtsp_enum.py index fc47475..1efab39 100644 --- a/src/linux/test/tools/test_get_eips.py +++ b/src/certfuzz/test/tools/common/test_mtsp_enum.py @@ -1,27 +1,22 @@ ''' -Created on Apr 10, 2012 +Created on Jan 23, 2014 @organization: cert.org ''' - import unittest class Test(unittest.TestCase): - def setUp(self): pass - def tearDown(self): pass - def testName(self): pass - if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/linux/test/tools/test_bff_stats.py b/src/certfuzz/test/tools/linux/test_bff_stats.py similarity index 100% rename from src/linux/test/tools/test_bff_stats.py rename to src/certfuzz/test/tools/linux/test_bff_stats.py diff --git a/src/linux/test/tools/test_callsim.py b/src/certfuzz/test/tools/linux/test_callsim.py similarity index 100% rename from src/linux/test/tools/test_callsim.py rename to src/certfuzz/test/tools/linux/test_callsim.py diff --git a/src/linux/test/tools/test_create_crasher_script.py b/src/certfuzz/test/tools/linux/test_create_crasher_script.py similarity index 100% rename from src/linux/test/tools/test_create_crasher_script.py rename to src/certfuzz/test/tools/linux/test_create_crasher_script.py diff --git a/src/linux/test/tools/test_rangefinder_plot.py b/src/certfuzz/test/tools/linux/test_drillresults.py similarity index 90% rename from src/linux/test/tools/test_rangefinder_plot.py rename to src/certfuzz/test/tools/linux/test_drillresults.py index fc47475..1efab39 100644 --- a/src/linux/test/tools/test_rangefinder_plot.py +++ b/src/certfuzz/test/tools/linux/test_drillresults.py @@ -1,27 +1,22 @@ ''' -Created on Apr 10, 2012 +Created on Jan 23, 2014 @organization: cert.org ''' - import unittest class Test(unittest.TestCase): - def setUp(self): pass - def tearDown(self): pass - def testName(self): pass - if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/linux/test/tools/test_minimizer_plot.py b/src/certfuzz/test/tools/linux/test_minimizer_plot.py similarity index 100% rename from src/linux/test/tools/test_minimizer_plot.py rename to src/certfuzz/test/tools/linux/test_minimizer_plot.py diff --git a/src/linux/test/tools/test_tools_misc.py b/src/linux/test/tools/test_tools_misc.py deleted file mode 100644 index 30166b2..0000000 --- a/src/linux/test/tools/test_tools_misc.py +++ /dev/null @@ -1,39 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -import os -import stat - -def is_executable(f): - executable = stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH - st = os.stat(f) - mode = st.st_mode - return mode & executable - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_tools_are_executable(self): - my_dir = os.path.dirname(__file__) - parent_dir = os.path.normpath(os.path.join(my_dir, '..', '..')) - tools_dir = os.path.join(parent_dir, 'tools') - self.assertTrue(os.path.exists(tools_dir), '%s not found' % tools_dir) - tools = [x for x in os.listdir(tools_dir) if x.endswith('.py')] - for tool in tools: - #confirm tool is executable - toolpath = os.path.join(tools_dir, tool) - self.assertTrue(os.path.exists(toolpath), '%s does not exist' % tool) - self.assertTrue(os.path.isfile(toolpath), '%s is not a file' % tool) - self.assertTrue(is_executable(toolpath), '%s is not executable' % tool) - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From ac4b53c415801f38c59ce6fa53e1cb5e35e94f1f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 10:20:10 -0500 Subject: [PATCH 0063/1169] create empty test packages for newly moved modules --- .../test/tools/linux/test_debuggerfile.py | 22 +++++++++++++++++++ .../test/tools/linux/test_minimize.py | 22 +++++++++++++++++++ .../test/tools/linux/test_minimizer_plot.py | 7 +----- src/certfuzz/test/tools/linux/test_repro.py | 22 +++++++++++++++++++ .../test/tools/windows/test_clean_foe.py | 22 +++++++++++++++++++ .../test/tools/windows/test_copycrashers.py | 22 +++++++++++++++++++ .../test/tools/windows/test_drillresults.py | 22 +++++++++++++++++++ .../test/tools/windows/test_minimize.py | 22 +++++++++++++++++++ .../test/tools/windows/test_quickstats.py | 22 +++++++++++++++++++ src/certfuzz/test/tools/windows/test_repro.py | 22 +++++++++++++++++++ .../test/tools/windows/test_zipdiff.py | 22 +++++++++++++++++++ 11 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 src/certfuzz/test/tools/linux/test_debuggerfile.py create mode 100644 src/certfuzz/test/tools/linux/test_minimize.py create mode 100644 src/certfuzz/test/tools/linux/test_repro.py create mode 100644 src/certfuzz/test/tools/windows/test_clean_foe.py create mode 100644 src/certfuzz/test/tools/windows/test_copycrashers.py create mode 100644 src/certfuzz/test/tools/windows/test_drillresults.py create mode 100644 src/certfuzz/test/tools/windows/test_minimize.py create mode 100644 src/certfuzz/test/tools/windows/test_quickstats.py create mode 100644 src/certfuzz/test/tools/windows/test_repro.py create mode 100644 src/certfuzz/test/tools/windows/test_zipdiff.py diff --git a/src/certfuzz/test/tools/linux/test_debuggerfile.py b/src/certfuzz/test/tools/linux/test_debuggerfile.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/linux/test_debuggerfile.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/tools/linux/test_minimize.py b/src/certfuzz/test/tools/linux/test_minimize.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/linux/test_minimize.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/tools/linux/test_minimizer_plot.py b/src/certfuzz/test/tools/linux/test_minimizer_plot.py index fc47475..1efab39 100644 --- a/src/certfuzz/test/tools/linux/test_minimizer_plot.py +++ b/src/certfuzz/test/tools/linux/test_minimizer_plot.py @@ -1,27 +1,22 @@ ''' -Created on Apr 10, 2012 +Created on Jan 23, 2014 @organization: cert.org ''' - import unittest class Test(unittest.TestCase): - def setUp(self): pass - def tearDown(self): pass - def testName(self): pass - if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/tools/linux/test_repro.py b/src/certfuzz/test/tools/linux/test_repro.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/linux/test_repro.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/tools/windows/test_clean_foe.py b/src/certfuzz/test/tools/windows/test_clean_foe.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/windows/test_clean_foe.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/tools/windows/test_copycrashers.py b/src/certfuzz/test/tools/windows/test_copycrashers.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/windows/test_copycrashers.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/tools/windows/test_drillresults.py b/src/certfuzz/test/tools/windows/test_drillresults.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/windows/test_drillresults.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/tools/windows/test_minimize.py b/src/certfuzz/test/tools/windows/test_minimize.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/windows/test_minimize.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/tools/windows/test_quickstats.py b/src/certfuzz/test/tools/windows/test_quickstats.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/windows/test_quickstats.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/tools/windows/test_repro.py b/src/certfuzz/test/tools/windows/test_repro.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/windows/test_repro.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/tools/windows/test_zipdiff.py b/src/certfuzz/test/tools/windows/test_zipdiff.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/certfuzz/test/tools/windows/test_zipdiff.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 3bda115257c46615cacb7fab5eb03fa19f47abfd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 14:29:42 -0500 Subject: [PATCH 0064/1169] fix runner stuff that was lost with android merge pre-git --- src/certfuzz/runners/winrun.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index e29f0fe..9c39aab 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -1,4 +1,5 @@ from ..helpers import check_os_compatibility + check_os_compatibility('Windows') import platform @@ -16,7 +17,10 @@ import sys import wmi import time -from . import RunnerArchitectureError, RunnerRegistryError +from .errors import RunnerArchitectureError, RunnerRegistryError +from .errors import RunnerError +from ..campaign.config.foe_config import get_command_args_list +from ..fuzztools.filetools import find_or_create_dir logger = logging.getLogger(__name__) @@ -57,6 +61,27 @@ def kill(p): class WinRunner(RunnerBase): def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): RunnerBase.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) + + logger.debug('Initialize Runner') + self.runtimeout = None + self.exceptions = [] + + self.watchcpu = options.get('watchcpu', False) + try: + self.exceptions = options['exceptions'] + except KeyError: + raise RunnerError('At least one exception code must be specified in runner config.') + +# self._parse_options() + self.saw_crash = False + + self.workingdir = workingdir_base + + (self.cmd, self.cmdlist) = get_command_args_list(cmd_template, fuzzed_file) + logger.debug('Command: %s', self.cmd) + + find_or_create_dir(self.workingdir) + self.t = None self.returncode = None self.remembered = [] @@ -212,7 +237,7 @@ def run(self): n1, d1 = long(proc.PercentProcessorTime), long(proc.Timestamp_Sys100NS) n0, d0 = process_info.get(id, (0, 0)) try: - percent_processor_time = (float(n1 - n0) / float(d1 - d0)) *100.0 + percent_processor_time = (float(n1 - n0) / float(d1 - d0)) * 100.0 except ZeroDivisionError: percent_processor_time = 0.0 process_info[id] = (n1, d1) From fe5cfbdf367295d8283bb15c5ee05bb62f166472 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 14:30:46 -0500 Subject: [PATCH 0065/1169] update for new runner_base that has _prerun, _run, _postrun methods --- src/certfuzz/runners/winrun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 9c39aab..9523ca6 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -198,7 +198,7 @@ def _verify_architecture(self, expected_bits=None): def kill(self, p): kill(p) - def run(self): + def _run(self): ''' Runs the command in self.cmdlist from self.workingdir with a timer bounded by self.runtimeout From eb7ee6d9633ec4a177aedcd0b3ab68e511929249 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 14:46:07 -0500 Subject: [PATCH 0066/1169] refactor imports in runners package --- src/certfuzz/campaign/iteration.py | 2 +- src/certfuzz/runners/__init__.py | 6 ------ src/certfuzz/runners/android_runner.py | 4 ++-- src/certfuzz/runners/winrun.py | 4 ++-- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/campaign/iteration.py b/src/certfuzz/campaign/iteration.py index 57269ed..10b627c 100644 --- a/src/certfuzz/campaign/iteration.py +++ b/src/certfuzz/campaign/iteration.py @@ -13,7 +13,7 @@ from ..fuzztools import filetools from ..fuzztools.filetools import delete_files_or_dirs from ..minimizer import MinimizerError, WindowsMinimizer as Minimizer -from ..runners import RunnerRegistryError +from ..runners.errors import RunnerRegistryError from .config.foe_config import get_command_args_list from .errors import IterationError from .iteration_base import IterationBase diff --git a/src/certfuzz/runners/__init__.py b/src/certfuzz/runners/__init__.py index 9a80230..e69de29 100644 --- a/src/certfuzz/runners/__init__.py +++ b/src/certfuzz/runners/__init__.py @@ -1,6 +0,0 @@ -from .runner_base import Runner -from .errors import AndroidRunnerError -from .errors import RunnerArchitectureError -from .errors import RunnerError -from .errors import RunnerPlatformVersionError -from .errors import RunnerRegistryError diff --git a/src/certfuzz/runners/android_runner.py b/src/certfuzz/runners/android_runner.py index 69f9a62..c4ecc8f 100644 --- a/src/certfuzz/runners/android_runner.py +++ b/src/certfuzz/runners/android_runner.py @@ -3,8 +3,8 @@ @organization: cert.org ''' -from . import Runner -from . import AndroidRunnerError +from .runner_base import Runner +from .errors import AndroidRunnerError from ..android.api.adb_cmd import AdbCmd from ..android.api.activity_manager import ActivityManager from ..android.api.errors import AdbCmdError diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 9523ca6..03502ff 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -3,7 +3,7 @@ check_os_compatibility('Windows') import platform -from . import RunnerPlatformVersionError +from .errors import RunnerPlatformVersionError if not platform.version().startswith('5.'): raise RunnerPlatformVersionError('Incompatible OS: winrun only works on Windows XP and 2003') @@ -37,7 +37,7 @@ def GetShortPathName(longname): # but don't panic if we can't do that either return longname -from . import Runner as RunnerBase +from .runner_base import Runner as RunnerBase from ..debuggers import jit as dbg From bf8d269ddca6cddafec040a36be26f713a354753 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 14:48:42 -0500 Subject: [PATCH 0067/1169] =?UTF-8?q?remove=20runner=20pkg=20tests=20now?= =?UTF-8?q?=20that=20it=E2=80=99s=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/certfuzz/test/runners/test_runners_pkg.py | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 src/certfuzz/test/runners/test_runners_pkg.py diff --git a/src/certfuzz/test/runners/test_runners_pkg.py b/src/certfuzz/test/runners/test_runners_pkg.py deleted file mode 100644 index 7ca7db4..0000000 --- a/src/certfuzz/test/runners/test_runners_pkg.py +++ /dev/null @@ -1,32 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.runners - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.runners - api_list = ['Runner', - 'RunnerArchitectureError', - 'RunnerError', - 'RunnerPlatformVersionError', - 'RunnerRegistryError', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From b023b8cd3c1f2ffdf1f16b5615dd285074c4e612 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 15:36:10 -0500 Subject: [PATCH 0068/1169] add new iteration package in prep for refactoring campaign package --- src/certfuzz/iteration/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/certfuzz/iteration/__init__.py diff --git a/src/certfuzz/iteration/__init__.py b/src/certfuzz/iteration/__init__.py new file mode 100644 index 0000000..e69de29 From 90daf5b818170f9e3a0778a56dede5431febf601 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 15:38:46 -0500 Subject: [PATCH 0069/1169] move iteration_android --- src/certfuzz/android/worker/worker.py | 2 +- src/certfuzz/campaign/campaign_android.py | 2 +- src/certfuzz/campaign/iteration.py | 1 - src/certfuzz/{campaign => iteration}/iteration_android.py | 0 src/certfuzz/test/iteration/__init__.py | 0 .../test/{campaign => iteration}/test_iteration_android.py | 0 6 files changed, 2 insertions(+), 3 deletions(-) rename src/certfuzz/{campaign => iteration}/iteration_android.py (100%) create mode 100644 src/certfuzz/test/iteration/__init__.py rename src/certfuzz/test/{campaign => iteration}/test_iteration_android.py (100%) diff --git a/src/certfuzz/android/worker/worker.py b/src/certfuzz/android/worker/worker.py index a135091..6b4a7f5 100644 --- a/src/certfuzz/android/worker/worker.py +++ b/src/certfuzz/android/worker/worker.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from ...campaign.iteration_android import emu as iteration_emu +from ...iteration.iteration_android import emu as iteration_emu from ..api import AdbCmd from ..api.log_helper import log_formatter from ..avd_mgr.main import avd_manager diff --git a/src/certfuzz/campaign/campaign_android.py b/src/certfuzz/campaign/campaign_android.py index 29b60d5..c3091d2 100644 --- a/src/certfuzz/campaign/campaign_android.py +++ b/src/certfuzz/campaign/campaign_android.py @@ -7,7 +7,7 @@ import random from . import __version__ from .campaign import Campaign -from .iteration_android import do_iteration +from ..iteration.iteration_android import do_iteration from .config.android_config import AndroidConfig from ..db.couchdb.db import TestCaseDb, put_file from ..db.couchdb.datatypes.campaign_doc import AndroidCampaignDoc diff --git a/src/certfuzz/campaign/iteration.py b/src/certfuzz/campaign/iteration.py index 10b627c..f854c30 100644 --- a/src/certfuzz/campaign/iteration.py +++ b/src/certfuzz/campaign/iteration.py @@ -231,7 +231,6 @@ def record_success(self): # found in this iteration. Others found via minimization # don't count for this r self.r.record_success(crash.signature) - self.sf.record_success(crash.signature) def record_failure(self): diff --git a/src/certfuzz/campaign/iteration_android.py b/src/certfuzz/iteration/iteration_android.py similarity index 100% rename from src/certfuzz/campaign/iteration_android.py rename to src/certfuzz/iteration/iteration_android.py diff --git a/src/certfuzz/test/iteration/__init__.py b/src/certfuzz/test/iteration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/test/campaign/test_iteration_android.py b/src/certfuzz/test/iteration/test_iteration_android.py similarity index 100% rename from src/certfuzz/test/campaign/test_iteration_android.py rename to src/certfuzz/test/iteration/test_iteration_android.py From fbbb9741e7a650ce0d80f3045686c37c5d6f704c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 15:41:11 -0500 Subject: [PATCH 0070/1169] move iteration_base --- src/certfuzz/campaign/iteration.py | 2 +- src/certfuzz/{campaign => iteration}/iteration_base.py | 0 .../test/{campaign => iteration}/test_iteration_base.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/certfuzz/{campaign => iteration}/iteration_base.py (100%) rename src/certfuzz/test/{campaign => iteration}/test_iteration_base.py (100%) diff --git a/src/certfuzz/campaign/iteration.py b/src/certfuzz/campaign/iteration.py index f854c30..1ae562d 100644 --- a/src/certfuzz/campaign/iteration.py +++ b/src/certfuzz/campaign/iteration.py @@ -16,7 +16,7 @@ from ..runners.errors import RunnerRegistryError from .config.foe_config import get_command_args_list from .errors import IterationError -from .iteration_base import IterationBase +from ..iteration.iteration_base import IterationBase import glob import logging import os diff --git a/src/certfuzz/campaign/iteration_base.py b/src/certfuzz/iteration/iteration_base.py similarity index 100% rename from src/certfuzz/campaign/iteration_base.py rename to src/certfuzz/iteration/iteration_base.py diff --git a/src/certfuzz/test/campaign/test_iteration_base.py b/src/certfuzz/test/iteration/test_iteration_base.py similarity index 100% rename from src/certfuzz/test/campaign/test_iteration_base.py rename to src/certfuzz/test/iteration/test_iteration_base.py From 3144218a13eb76f0247e671e6853ce2a2e2be623 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 15:54:08 -0500 Subject: [PATCH 0071/1169] move iteration and related errors to certfuzz.iteration package (+2 squashed commits) Squashed commits: [562bff8] move iteration and related errors to certfuzz.iteration package [e29551b] fix imports --- src/certfuzz/bff/windows.py | 4 +--- src/certfuzz/campaign/campaign.py | 2 +- src/certfuzz/campaign/errors.py | 6 +----- src/certfuzz/iteration/errors.py | 10 ++++++++++ .../iteration.py => iteration/iteration_windows.py} | 4 ++-- src/certfuzz/test/iteration/test_iteration_android.py | 3 ++- .../test_iteration_windows.py} | 3 ++- 7 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 src/certfuzz/iteration/errors.py rename src/certfuzz/{campaign/iteration.py => iteration/iteration_windows.py} (96%) rename src/certfuzz/test/{campaign/test_iteration.py => iteration/test_iteration_windows.py} (88%) diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index fdee7d1..ff718cb 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -77,9 +77,7 @@ def parse_options(): parser.add_option('-l', '--logfile', dest='logfile', help='Path to log file', metavar='FILE') parser.add_option('-r', '--result-dir', dest='resultdir', help='Path to result directory (overrides config)', metavar='DIR') - (options, args) = parser.parse_args() - - return options, args + return parser.parse_args() def setup_debugging(logger): diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index 9c65c54..3b0cb35 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -18,7 +18,7 @@ from . import CampaignBase from .errors import CampaignError from .config.foe_config import Config -from .iteration import Iteration +from ..iteration.iteration_windows import Iteration from ..debuggers import registration from ..fuzztools import filetools diff --git a/src/certfuzz/campaign/errors.py b/src/certfuzz/campaign/errors.py index a90a510..bc425d2 100644 --- a/src/certfuzz/campaign/errors.py +++ b/src/certfuzz/campaign/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from certfuzz.errors import CERTFuzzError +from ..errors import CERTFuzzError class CampaignError(CERTFuzzError): @@ -12,7 +12,3 @@ class CampaignError(CERTFuzzError): class AndroidCampaignError(CampaignError): pass - - -class IterationError(CERTFuzzError): - pass diff --git a/src/certfuzz/iteration/errors.py b/src/certfuzz/iteration/errors.py new file mode 100644 index 0000000..e9a7d63 --- /dev/null +++ b/src/certfuzz/iteration/errors.py @@ -0,0 +1,10 @@ +''' +Created on Jan 23, 2014 + +@author: adh +''' +from ..errors import CERTFuzzError + + +class IterationError(CERTFuzzError): + pass diff --git a/src/certfuzz/campaign/iteration.py b/src/certfuzz/iteration/iteration_windows.py similarity index 96% rename from src/certfuzz/campaign/iteration.py rename to src/certfuzz/iteration/iteration_windows.py index 1ae562d..626e9ee 100644 --- a/src/certfuzz/campaign/iteration.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -14,9 +14,9 @@ from ..fuzztools.filetools import delete_files_or_dirs from ..minimizer import MinimizerError, WindowsMinimizer as Minimizer from ..runners.errors import RunnerRegistryError -from .config.foe_config import get_command_args_list +from ..campaign.config.foe_config import get_command_args_list from .errors import IterationError -from ..iteration.iteration_base import IterationBase +from .iteration_base import IterationBase import glob import logging import os diff --git a/src/certfuzz/test/iteration/test_iteration_android.py b/src/certfuzz/test/iteration/test_iteration_android.py index 63cdcd5..5c13ca6 100644 --- a/src/certfuzz/test/iteration/test_iteration_android.py +++ b/src/certfuzz/test/iteration/test_iteration_android.py @@ -4,7 +4,8 @@ @organization: cert.org ''' import unittest -# from certfuzz.campaign import iteration_android +# from certfuzz.iteration import iteration_android + class Test(unittest.TestCase): diff --git a/src/certfuzz/test/campaign/test_iteration.py b/src/certfuzz/test/iteration/test_iteration_windows.py similarity index 88% rename from src/certfuzz/test/campaign/test_iteration.py rename to src/certfuzz/test/iteration/test_iteration_windows.py index 31d600f..1088abc 100644 --- a/src/certfuzz/test/campaign/test_iteration.py +++ b/src/certfuzz/test/iteration/test_iteration_windows.py @@ -5,7 +5,8 @@ ''' import unittest -from certfuzz.campaign.iteration import Iteration +from certfuzz.iteration.iteration_windows import Iteration + class Test(unittest.TestCase): From b551b8ba5a682aa8e9a667926c2a122e60a41904 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 16:09:48 -0500 Subject: [PATCH 0072/1169] relocate android tools into common dir with other tools --- src/certfuzz/{android/tools => tools/android}/__init__.py | 0 src/certfuzz/{android/tools => tools/android}/apk_dumper.py | 6 +++--- .../{android/tools => tools/android}/config_tools.py | 4 ++-- src/setup.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename src/certfuzz/{android/tools => tools/android}/__init__.py (100%) rename src/certfuzz/{android/tools => tools/android}/apk_dumper.py (92%) rename src/certfuzz/{android/tools => tools/android}/config_tools.py (94%) diff --git a/src/certfuzz/android/tools/__init__.py b/src/certfuzz/tools/android/__init__.py similarity index 100% rename from src/certfuzz/android/tools/__init__.py rename to src/certfuzz/tools/android/__init__.py diff --git a/src/certfuzz/android/tools/apk_dumper.py b/src/certfuzz/tools/android/apk_dumper.py similarity index 92% rename from src/certfuzz/android/tools/apk_dumper.py rename to src/certfuzz/tools/android/apk_dumper.py index a09b9f3..d7abe1f 100644 --- a/src/certfuzz/android/tools/apk_dumper.py +++ b/src/certfuzz/tools/android/apk_dumper.py @@ -5,10 +5,11 @@ ''' import logging import argparse -from ..api.aapt import Aapt -from ..api.android_manifest import AndroidManifest +from ...android.api.aapt import Aapt +from ...android.api.android_manifest import AndroidManifest import os + def main(): logger = logging.getLogger() hdlr = logging.StreamHandler() @@ -37,7 +38,6 @@ def main(): manifest = AndroidManifest(manifest_text) - vstr = '{} {}'.format(os.path.basename(args.apk), manifest.version_info) print '#' * (len(vstr) + 4) print '# {} #'.format(vstr) diff --git a/src/certfuzz/android/tools/config_tools.py b/src/certfuzz/tools/android/config_tools.py similarity index 94% rename from src/certfuzz/android/tools/config_tools.py rename to src/certfuzz/tools/android/config_tools.py index 806eaeb..d3029cc 100644 --- a/src/certfuzz/android/tools/config_tools.py +++ b/src/certfuzz/tools/android/config_tools.py @@ -9,7 +9,7 @@ import shutil import yaml import argparse -from ..api.log_helper import log_formatter +from ...android.api.log_helper import log_formatter logger = logging.getLogger() hdlr = logging.StreamHandler() @@ -20,7 +20,7 @@ def main(): parser = argparse.ArgumentParser(description='Create a configuration file with the default options') parser.add_argument('--force', '-f', - help='Overwrite the config file if it already exists', + help='Overwrite the config file if it already exists', action='store_true', default=False) parser.add_argument('--directory', '--dir', '-d', help='The directory in which to store the config file', diff --git a/src/setup.py b/src/setup.py index b635ff0..46b121c 100644 --- a/src/setup.py +++ b/src/setup.py @@ -28,8 +28,8 @@ 'bff_avd_mgr = certfuzz.android.avd_mgr.main:main', 'bff_android = certfuzz.android.controller.bff_android:main', 'bff_avd_cloner = certfuzz.android.celery.avd_mgr.cloner:main', - 'bff_apk_dumper = certfuzz.android.tools.apk_dumper:main', - 'config_init = certfuzz.android.tools.config_tools:main' + 'bff_apk_dumper = certfuzz.tools.android.apk_dumper:main', + 'config_init = certfuzz.tools.android.config_tools:main' ]}, include_package_data=True, ) From 7d83ebf6606f2b91615716ea1faefed80530d3ce Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 16:13:51 -0500 Subject: [PATCH 0073/1169] move certfuzz.android.controller.bff_android to certfuzz.bff.android --- .../{android/controller/bff_android.py => bff/android.py} | 8 ++++---- src/setup.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/certfuzz/{android/controller/bff_android.py => bff/android.py} (91%) diff --git a/src/certfuzz/android/controller/bff_android.py b/src/certfuzz/bff/android.py similarity index 91% rename from src/certfuzz/android/controller/bff_android.py rename to src/certfuzz/bff/android.py index 9efb14c..e7d13a3 100644 --- a/src/certfuzz/android/controller/bff_android.py +++ b/src/certfuzz/bff/android.py @@ -5,10 +5,10 @@ ''' import logging import os -from ...campaign.campaign_android import AndroidCampaign -from ...fuzztools import filetools -from ...campaign.errors import AndroidCampaignError -from ..api.log_helper import log_formatter +from ..campaign.campaign_android import AndroidCampaign +from ..fuzztools import filetools +from ..campaign.errors import AndroidCampaignError +from ..android.api.log_helper import log_formatter logger = logging.getLogger() diff --git a/src/setup.py b/src/setup.py index 46b121c..e223607 100644 --- a/src/setup.py +++ b/src/setup.py @@ -26,7 +26,7 @@ ], entry_points={'console_scripts': [ 'bff_avd_mgr = certfuzz.android.avd_mgr.main:main', - 'bff_android = certfuzz.android.controller.bff_android:main', + 'bff_android = certfuzz.bff.android:main', 'bff_avd_cloner = certfuzz.android.celery.avd_mgr.cloner:main', 'bff_apk_dumper = certfuzz.tools.android.apk_dumper:main', 'config_init = certfuzz.tools.android.config_tools:main' From 2aa44400caf986e9ccde5f58fdb3a55702212d41 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 16:18:52 -0500 Subject: [PATCH 0074/1169] relocate unit tests to match location of modules now --- .../controller/test_bff_android.py => bff/test_android.py} | 0 src/certfuzz/test/{android/tools => tools/android}/__init__.py | 0 .../test/{android/tools => tools/android}/test_apk_dumper.py | 0 .../test/{android/tools => tools/android}/test_config_tools.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/certfuzz/test/{android/controller/test_bff_android.py => bff/test_android.py} (100%) rename src/certfuzz/test/{android/tools => tools/android}/__init__.py (100%) rename src/certfuzz/test/{android/tools => tools/android}/test_apk_dumper.py (100%) rename src/certfuzz/test/{android/tools => tools/android}/test_config_tools.py (100%) diff --git a/src/certfuzz/test/android/controller/test_bff_android.py b/src/certfuzz/test/bff/test_android.py similarity index 100% rename from src/certfuzz/test/android/controller/test_bff_android.py rename to src/certfuzz/test/bff/test_android.py diff --git a/src/certfuzz/test/android/tools/__init__.py b/src/certfuzz/test/tools/android/__init__.py similarity index 100% rename from src/certfuzz/test/android/tools/__init__.py rename to src/certfuzz/test/tools/android/__init__.py diff --git a/src/certfuzz/test/android/tools/test_apk_dumper.py b/src/certfuzz/test/tools/android/test_apk_dumper.py similarity index 100% rename from src/certfuzz/test/android/tools/test_apk_dumper.py rename to src/certfuzz/test/tools/android/test_apk_dumper.py diff --git a/src/certfuzz/test/android/tools/test_config_tools.py b/src/certfuzz/test/tools/android/test_config_tools.py similarity index 100% rename from src/certfuzz/test/android/tools/test_config_tools.py rename to src/certfuzz/test/tools/android/test_config_tools.py From c808bb2e8f2cca071c0299da2e28564f2416ba30 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 24 Jan 2014 15:15:28 -0500 Subject: [PATCH 0075/1169] ignore eclipse project settings --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 213fa5e..c400c12 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ dev_builds +.settings From 15d840dfcfad2e2d65db78e006f8e79380ac494a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 6 Feb 2014 15:17:22 -0500 Subject: [PATCH 0076/1169] ignore stuff --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c400c12..add7425 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ dev_builds .settings +*.env +*.egg-info +src/dist From 844e5785b0e0e960b6d9be0ace26b0e7339c0043 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 6 Feb 2014 15:28:00 -0500 Subject: [PATCH 0077/1169] remove dead code --- build/dist/build_base.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/build/dist/build_base.py b/build/dist/build_base.py index b192322..06bf629 100644 --- a/build/dist/build_base.py +++ b/build/dist/build_base.py @@ -118,8 +118,6 @@ def process_args(self): self.svn_url = "%s/%s" % (self.branches, self.branch) self.zipfile_template = '%s-%s-r$SVN_REV.zip' % (self.PROJECT, self.branch) elif self.buildtype == 'trunk': -# # TODO remove the self.export line when 2.6 is merged back to trunk -# self.export = self._export_pre_2_6 self.svn_url = self.trunk self.zipfile_template = '%s-trunk-r$SVN_REV.zip' % (self.PROJECT) else: @@ -141,17 +139,6 @@ def build(self): logger.info('Packaging') self.package() - def _export_pre_2_6(self): - ''' - Exports pre-2.6 code from the repository. When complete, the code will be - in the directory specified by self.export_path - ''' - svn_base = "%s/src" % self.svn_url - src = svn_base - dst = self.export_path - svn_export(src, dst) - self.svn_rev = svn_rev(svn_base) - def _export(self): ''' Exports the code from the repository. When complete, the code will be From 2c79a60c55ab2ae9e87d9ef44411282ba8c1cc3c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 08:59:20 -0500 Subject: [PATCH 0078/1169] reorganize imports --- build/dev/build_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/dev/build_base.py b/build/dev/build_base.py index 63c2bc1..4cd030f 100644 --- a/build/dev/build_base.py +++ b/build/dev/build_base.py @@ -6,8 +6,8 @@ import os import logging -from dev.misc import copydir, copyfile, onerror import shutil +from dev.misc import copydir, copyfile, onerror logger = logging.getLogger(__name__) From 8f10d48d99ba7dd5b844d1b3292deb5ef80b871a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 08:59:47 -0500 Subject: [PATCH 0079/1169] logger should be module-specific, not root logger. --- build/dev/misc.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build/dev/misc.py b/build/dev/misc.py index e1b698f..1a49069 100644 --- a/build/dev/misc.py +++ b/build/dev/misc.py @@ -8,10 +8,7 @@ from distutils import dir_util import shutil -logger = logging.getLogger() -logger.setLevel(logging.INFO) -hdlr = logging.StreamHandler() -logger.addHandler(hdlr) +logger = logging.getLogger(__name__) def onerror(func, path, exc_info): From c53351647612972ec3e8748f8a350c11e94df72a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 09:00:48 -0500 Subject: [PATCH 0080/1169] Improve integration of prepend_license --- build/dist/build_base.py | 18 ++--- build/dist/prepend_license.py | 115 ++++++++++++++++------------- src/linux/{COPYING => COPYING.txt} | 0 3 files changed, 70 insertions(+), 63 deletions(-) rename src/linux/{COPYING => COPYING.txt} (100%) diff --git a/build/dist/build_base.py b/build/dist/build_base.py index 06bf629..7627de4 100644 --- a/build/dist/build_base.py +++ b/build/dist/build_base.py @@ -12,6 +12,7 @@ from .svn import svn_export, svn_rev from .misc import mkdir_p import urlparse +from .prepend_license import main as _prepend_license logger = logging.getLogger(__name__) @@ -165,16 +166,13 @@ def prepend_license(self): ''' Adds the license text to the code prior to packaging ''' - # TODO refactor this to call prepend_license in a pythonic way - # rather than shelling it out - args = ['python', '%s/prepend_license.py' % self.PWD, - '--add', - '--dir', self.export_path, - '--license', os.path.join(self.export_path, self.LICENSE_FILE), - '--overwrite', - '--verbose', - ] - subprocess.call(args) + _prepend_license(license_file=os.path.join(self.export_path, self.LICENSE_FILE), + basedir=self.export_path, + remove=False, + add=True, + debug=False, + overwrite=True, + ) def prune(self): ''' diff --git a/build/dist/prepend_license.py b/build/dist/prepend_license.py index 48717d4..85281a9 100644 --- a/build/dist/prepend_license.py +++ b/build/dist/prepend_license.py @@ -2,7 +2,6 @@ Created on Jan 31, 2011 @organization: cert.org - ''' import os @@ -13,11 +12,11 @@ import sys logger = logging.getLogger(__name__) -logger.setLevel(logging.WARNING) LICENSE_PREFIX = '### ' LICENSE_FILE = 'COPYING' + def parse_cmdline_args(): from optparse import OptionParser parser = OptionParser() @@ -46,6 +45,7 @@ def parse_cmdline_args(): return options + # Adapted from Python Cookbook 2nd Ed. p.88 def all_files(root, patterns='*', single_level=False, yield_folders=False): # Expand patterns from semicolon-separated string to list @@ -64,12 +64,14 @@ def all_files(root, patterns='*', single_level=False, yield_folders=False): if single_level: break + def write_to_screen(f, lines): print '*** WOULD BE WRITTEN TO %s ***' % f for l in lines: print l.rstrip() print '*** END OF FILE %s ***' % f + def write_to_file(f, lines, keep_old=True): # write the combined output to a tempfile (fp, fn) = tempfile.mkstemp(suffix='.py', text=True) @@ -82,7 +84,8 @@ def write_to_file(f, lines, keep_old=True): shutil.move(f, '%s.old' % f) shutil.move(fn, f) -def build_license_lines(license_file, prefix=LICENSE_PREFIX): + +def build_license_lines(license_file): with open(license_file, 'r') as f: license_text = f.readlines() @@ -94,6 +97,7 @@ def build_license_lines(license_file, prefix=LICENSE_PREFIX): license_lines.append('\n') return license_lines + def find_extra_blank_lines(lines): f = lambda (x, y): not str(y).strip() blank_lines = filter(f, enumerate(lines)) @@ -114,6 +118,7 @@ def find_extra_blank_lines(lines): return sorted(lines_safe_to_remove, reverse=True) + def remove_excess_blank_lines(lines): # remove extra blank lines for index in find_extra_blank_lines(lines): @@ -127,64 +132,68 @@ def remove_excess_blank_lines(lines): while lines and not lines[-1].strip(): lines.pop() + +def prepend_license_to_file(license_file, remove, add, debug, f, overwrite): + logger.debug('Add license to %s', f) + if add: + license_lines = build_license_lines(license_file) + + with open(f, 'r') as orig_fp: + f_lines = [l for l in orig_fp.readlines() if not l.startswith(LICENSE_PREFIX)] + + # don't do anything with empty files + if not len(f_lines): + logger.info('Skipping empty file: %s', f) + return + + lines = [] # check for shebang and keep it at the top if it exists + skip_first_line = False + if f_lines[0].startswith('#!'): + logger.debug('Handling #! at beginning of %s', f) + lines.append(f_lines[0]) + skip_first_line = True + if remove: + logger.debug('Removing license text') + if add: + logger.info('Adding license text to %s', f) # insert license lines + lines.extend(license_lines) +# logger.debug('Appending the rest of the original file') + + # if we put a shebang at the top of the file, skip it here + if skip_first_line: + lines.extend(f_lines[1:]) + else: + lines.extend(f_lines) + remove_excess_blank_lines(lines) + if debug: + logger.debug('Output to screen only') + write_to_screen(f, lines) + else: + orig_permissions = os.stat(f).st_mode & 0777 + keep_old = not overwrite + write_to_file(f, lines, keep_old) + os.chmod(f, orig_permissions) + + +def main(license_file=None, + basedir=None, + remove=False, + add=False, + debug=False, + overwrite=False): + logger.debug('basedir = %s', basedir) + for f in all_files(basedir, '*.py'): + prepend_license_to_file(license_file, remove, add, debug, f, overwrite) + if __name__ == '__main__': hdlr = logging.StreamHandler() logger.addHandler(hdlr) options = parse_cmdline_args() - if options.add: - license_file = options.license_file - license_lines = build_license_lines(license_file, prefix=options.prefix) - if options.debug: logger.setLevel(logging.DEBUG) elif options.verbose: logger.setLevel(logging.INFO) - for f in all_files(options.basedir, '*.py'): - - orig_fp = open(f, 'r') - file_lines = orig_fp.readlines() - # skip lines that start with three hashes - f_lines = [l for l in file_lines if not l.startswith(options.prefix)] - orig_fp.close() - - # don't do anything with empty files - if not len(f_lines): - logger.info('Skipping empty file: %s', f) - continue - - lines = [] - - # check for shebang and keep it at the top if it exists - skip_first_line = False - if f_lines[0].startswith('#!'): - logger.debug('Handling #! at beginning of %s', f) - lines.append(f_lines[0]) - skip_first_line = True - - if options.remove: - logger.debug('Removing license text') - if options.add: - logger.info('Adding license text to %s', f) - # insert license lines - lines.extend(license_lines) - - logger.debug('Appending the rest of the original file') - # if we put a shebang at the top of the file, skip it here - if skip_first_line: - lines.extend(f_lines[1:]) - else: - lines.extend(f_lines) - - remove_excess_blank_lines(lines) - - if options.debug: - logger.debug('Output to screen only') - write_to_screen(f, lines) - else: - orig_permissions = os.stat(f).st_mode & 0777 - keep_old = not options.overwrite - write_to_file(f, lines, keep_old) - os.chmod(f, orig_permissions) + main(options.license_file, options.remove, options.add, options.debug, options.overwrite) diff --git a/src/linux/COPYING b/src/linux/COPYING.txt similarity index 100% rename from src/linux/COPYING rename to src/linux/COPYING.txt From a976fbd4a4bc3534023521d6907e1e80cedc7d68 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 09:01:52 -0500 Subject: [PATCH 0081/1169] new make_dist2.py script to build from a git checkout. This should work for linux. Additional work is needed for windows and OSX installers. --- build/README.txt | 27 ++++++ build/dist/build2.py | 197 +++++++++++++++++++++++++++++++++++++++++++ build/make_dist2.py | 45 ++++++++++ 3 files changed, 269 insertions(+) create mode 100644 build/README.txt create mode 100644 build/dist/build2.py create mode 100644 build/make_dist2.py diff --git a/build/README.txt b/build/README.txt new file mode 100644 index 0000000..98b20b4 --- /dev/null +++ b/build/README.txt @@ -0,0 +1,27 @@ +Use make_dev.py to copy files in a dev environment to a different dir for use by +your test VM. + +Use make_dist2.py to build a distributable package. + +floyd:build adh$ python make_dist2.py --help +usage: make_dist2.py [-h] [-d] [-v] platform srcpath distpath + +positional arguments: + platform One of ['windows', 'osx', 'linux'] + srcpath path/to/bff/src + distpath Directory to build into + +optional arguments: + -h, --help show this help message and exit + -d, --debug enable debug messages + -v, --verbose enable debug messages + + +**NOTE** +As of 2014-02-11 This only works for "linux". Windows and OSX build capability will +follow soon. + + +make_dist.py is broken. Or rather, it depends on the code being in subversion, +which is no longer true. + diff --git a/build/dist/build2.py b/build/dist/build2.py new file mode 100644 index 0000000..5c3fd52 --- /dev/null +++ b/build/dist/build2.py @@ -0,0 +1,197 @@ +''' +Created on Feb 10, 2014 + +@organization: cert.org +''' +import logging +import os +from dev.misc import copydir, copyfile, onerror +import shutil +import tempfile +from .prepend_license import main as _prepend_license +from .errors import BuildError +import subprocess + + +logger = logging.getLogger(__name__) + + +SUPPORTED_PLATFORMS = {'linux': None, + 'windows': None, + 'osx': None, + } + + +class Build(object): + _blacklist = [] + _common_dirs = ['certfuzz', 'seedfiles'] + _license_file = 'COPYING.txt' + + def __init__(self, platform=None, distpath=None, srcpath=None): + self.platform = platform + + if distpath == None: + self.base_path = os.path.abspath(os.path.dirname(__file__)) + else: + self.base_path = os.path.abspath(distpath) + + if srcpath == None: + self.src_path = os.path.abspath(os.path.join(self.base_path, '../../src')) + else: + self.src_path = os.path.abspath(srcpath) + + self.tmpdir = None +# +# self.dev_builds_path = os.path.abspath(os.path.join(self.src_path, '..', 'dev_builds')) +# self.target_path = os.path.abspath(os.path.join(self.dev_builds_path, self.name)) + self.platform_path = os.path.join(self.src_path, self.platform) + self._in_runtime_context = False + self._filename_pfx = 'BFF-{}'.format(self.platform) + self.zipfile = '{}.zip'.format(self._filename_pfx) + + def __enter__(self): + logger.debug('Entering Build context') + self.tmpdir = tempfile.mkdtemp(prefix='bff_build_{}_'.format(self.platform)) + logger.debug('Temp dir is %s', self.tmpdir) + self._in_runtime_context = True + return self + + def __exit__(self, etype, value, traceback): + logger.debug('Removing temp dir %s', self.tmpdir) + shutil.rmtree(self.tmpdir) + self._in_runtime_context = False + + def build(self): + ''' + Perform a build. Must be invoked using with Build(foo)... syntax + ''' + assert self._in_runtime_context, 'Build() must be invoked using with... syntax' + self.export() + self.prune() + self.refine() + self.prepend_license() + self.package() + + def export(self): + logger.info('Exporting') + logger.info('Copy platform-specific files to tmpdir') + self._copy_platform() + + logger.info('Copy common files to tmpdir') + self._copy_common_dirs() + + def prune(self): + logger.info('Pruning') + logger.debug('nothing really happened here') + + def refine(self): + logger.info('Refining') + logger.info('Set up results dir') + self._create_results_dir() + + logger.info('Clean up build tmpdir') + self._clean_up(self.tmpdir, remove_blacklist=False) + + def prepend_license(self): + ''' + Adds the license text to the code prior to packaging + ''' + logger.info('Prepending License to *.py') + lf = os.path.join(self.tmpdir, self._license_file) + _prepend_license(license_file=lf, + basedir=self.tmpdir, + remove=False, + add=True, + debug=False, + overwrite=True, + ) + + def package(self): + ''' + Creates a zip file containing the code + ''' + logger.info('Packaging') + if self.zipfile: + zipfile_base = self.zipfile + else: + raise BuildError('Unable to determine zipfile name') + + export_dir = self.tmpdir + fd, tmpzip = tempfile.mkstemp(prefix='{}-'.format(self._filename_pfx), + suffix='.zip') + os.close(fd) + + + import zipfile + + def _zipdir(path, zip): + cwd = os.getcwd() + os.chdir(path) + for root, dirs, files in os.walk('.'): + for file in files: + zip.write(os.path.join(root, file)) + os.chdir(cwd) + + with zipfile.ZipFile(tmpzip, 'w') as zipf: + _zipdir(export_dir, zipf) + +# args = ['zip', '-r', '-v', tmpzip, '.'] +# logger.debug('shell: {}'.format(' '.join(args))) +# subprocess.call(args, cwd=export_dir) + + target = os.path.join(self.base_path, zipfile_base) + if os.path.exists(target): + os.remove(target) + + logger.debug('moving {} to {}'.format(tmpzip, target)) + shutil.move(tmpzip, target) + + _perm = 0644 + logger.debug('setting {:04o} permissions on {}'.format(_perm, target)) + os.chmod(target, _perm) + + def _copy_platform(self): + # copy platform-specific content + for f in os.listdir(self.platform_path): + f_src = os.path.join(self.platform_path, f) + + # blacklist files and dirs by name + # these files will not be copied + if f in self._blacklist: + logger.info('Skipping path (blacklisted) %s', f_src) + continue + + f_dst = os.path.join(self.tmpdir, f) + if os.path.isdir(f_src): + copydir(f_src, f_dst) + elif os.path.isfile(f_src): + copyfile(f_src, f_dst) + else: + logger.warning("Not sure what to do with %s", f_src) + + def _copy_common_dirs(self): + # copy other dirs + for d in self._common_dirs: + d_src = os.path.join(self.src_path, d) + d_dst = os.path.join(self.tmpdir, d) + copydir(d_src, d_dst) + + def _create_results_dir(self): + # create result dir if it doesn't exist, otherwise don't touch it + result_path = os.path.join(self.tmpdir, 'results') + if not os.path.exists(result_path): + logger.info('Result path does not exist, creating %s', result_path) + os.makedirs(result_path) + else: + logger.info('Result path %s already exists, proceeding', result_path) + + def _clean_up(self, path, remove_blacklist=True): + logger.debug("Cleaning up %s", path) + for f in os.listdir(path): + fpath = os.path.join(path, f) + if os.path.isdir(fpath): + if f in self._blacklist: + logger.info('Removing %s dir from %s', f, path) + shutil.rmtree(fpath, ignore_errors=False, onerror=onerror) + else: + self._clean_up(fpath, remove_blacklist=True) diff --git a/build/make_dist2.py b/build/make_dist2.py new file mode 100644 index 0000000..3a5eaa3 --- /dev/null +++ b/build/make_dist2.py @@ -0,0 +1,45 @@ +''' +Created on Feb 6, 2014 + +@organization: cert.org +''' +import argparse +import logging +from dist.build2 import Build +from dist.build2 import SUPPORTED_PLATFORMS as builders + +logger = logging.getLogger(__name__) + + +def main(): + logger = logging.getLogger() + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--debug', help='enable debug messages', action="store_true") + parser.add_argument('-v', '--verbose', help='enable debug messages', action="store_true") + parser.add_argument('platform', type=str, help='One of {}'.format(builders.keys())) + parser.add_argument('srcpath', type=str, help='path/to/bff/src') + parser.add_argument('distpath', type=str, help='Directory to build into') + args = parser.parse_args() + + if args.debug: + logger.setLevel(logging.DEBUG) + elif args.verbose: + logger.setLevel(logging.INFO) + else: + logger.setLevel(logging.WARNING) + + if not args.platform in builders: + print 'platform must be one of {}'.format(builders.keys()) + exit(1) + + # assume that we're running in a git checkout? + with Build(platform=args.platform, + distpath=args.distpath, + srcpath=args.srcpath) as b: + b.build() + +if __name__ == '__main__': + main() From fd9cd203c914a60506aa6eeceae63505c4997dc4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 13:00:15 -0500 Subject: [PATCH 0082/1169] ignore osx build artifacts --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index add7425..6c800d3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dev_builds *.env *.egg-info src/dist +build/dist/osx/installer/Readme.txt +build/dist/osx/installer/License.txt From 43769aebb567590b2c502d6430409501074360d5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 13:00:46 -0500 Subject: [PATCH 0083/1169] reconstruct darwin build process --- build/README.txt | 4 +- build/dist/build2.py | 194 ++-------------------------- build/dist/build_base2.py | 188 ++++++++++++++++++++++++++++ build/dist/linux/linux_build2.py | 20 +++ build/dist/osx/darwin_build2.py | 208 +++++++++++++++++++++++++++++++ build/make_dist2.py | 21 ++-- 6 files changed, 445 insertions(+), 190 deletions(-) create mode 100644 build/dist/build_base2.py create mode 100644 build/dist/linux/linux_build2.py create mode 100644 build/dist/osx/darwin_build2.py diff --git a/build/README.txt b/build/README.txt index 98b20b4..cb4af4d 100644 --- a/build/README.txt +++ b/build/README.txt @@ -7,7 +7,7 @@ floyd:build adh$ python make_dist2.py --help usage: make_dist2.py [-h] [-d] [-v] platform srcpath distpath positional arguments: - platform One of ['windows', 'osx', 'linux'] + platform One of ['windows', 'osx', ‘darwin’] srcpath path/to/bff/src distpath Directory to build into @@ -18,7 +18,7 @@ optional arguments: **NOTE** -As of 2014-02-11 This only works for "linux". Windows and OSX build capability will +As of 2014-02-11 This only works for "linux" and “darwinâ€. Windows build capability will follow soon. diff --git a/build/dist/build2.py b/build/dist/build2.py index 5c3fd52..a3c6af9 100644 --- a/build/dist/build2.py +++ b/build/dist/build2.py @@ -4,194 +4,26 @@ @organization: cert.org ''' import logging -import os -from dev.misc import copydir, copyfile, onerror -import shutil -import tempfile -from .prepend_license import main as _prepend_license +from .linux.linux_build2 import LinuxBuild +from .osx.darwin_build2 import DarwinBuild from .errors import BuildError -import subprocess logger = logging.getLogger(__name__) -SUPPORTED_PLATFORMS = {'linux': None, +SUPPORTED_PLATFORMS = {'linux': LinuxBuild, 'windows': None, - 'osx': None, + 'darwin': DarwinBuild, } -class Build(object): - _blacklist = [] - _common_dirs = ['certfuzz', 'seedfiles'] - _license_file = 'COPYING.txt' - - def __init__(self, platform=None, distpath=None, srcpath=None): - self.platform = platform - - if distpath == None: - self.base_path = os.path.abspath(os.path.dirname(__file__)) - else: - self.base_path = os.path.abspath(distpath) - - if srcpath == None: - self.src_path = os.path.abspath(os.path.join(self.base_path, '../../src')) - else: - self.src_path = os.path.abspath(srcpath) - - self.tmpdir = None -# -# self.dev_builds_path = os.path.abspath(os.path.join(self.src_path, '..', 'dev_builds')) -# self.target_path = os.path.abspath(os.path.join(self.dev_builds_path, self.name)) - self.platform_path = os.path.join(self.src_path, self.platform) - self._in_runtime_context = False - self._filename_pfx = 'BFF-{}'.format(self.platform) - self.zipfile = '{}.zip'.format(self._filename_pfx) - - def __enter__(self): - logger.debug('Entering Build context') - self.tmpdir = tempfile.mkdtemp(prefix='bff_build_{}_'.format(self.platform)) - logger.debug('Temp dir is %s', self.tmpdir) - self._in_runtime_context = True - return self - - def __exit__(self, etype, value, traceback): - logger.debug('Removing temp dir %s', self.tmpdir) - shutil.rmtree(self.tmpdir) - self._in_runtime_context = False - - def build(self): - ''' - Perform a build. Must be invoked using with Build(foo)... syntax - ''' - assert self._in_runtime_context, 'Build() must be invoked using with... syntax' - self.export() - self.prune() - self.refine() - self.prepend_license() - self.package() - - def export(self): - logger.info('Exporting') - logger.info('Copy platform-specific files to tmpdir') - self._copy_platform() - - logger.info('Copy common files to tmpdir') - self._copy_common_dirs() - - def prune(self): - logger.info('Pruning') - logger.debug('nothing really happened here') - - def refine(self): - logger.info('Refining') - logger.info('Set up results dir') - self._create_results_dir() - - logger.info('Clean up build tmpdir') - self._clean_up(self.tmpdir, remove_blacklist=False) - - def prepend_license(self): - ''' - Adds the license text to the code prior to packaging - ''' - logger.info('Prepending License to *.py') - lf = os.path.join(self.tmpdir, self._license_file) - _prepend_license(license_file=lf, - basedir=self.tmpdir, - remove=False, - add=True, - debug=False, - overwrite=True, - ) - - def package(self): - ''' - Creates a zip file containing the code - ''' - logger.info('Packaging') - if self.zipfile: - zipfile_base = self.zipfile - else: - raise BuildError('Unable to determine zipfile name') - - export_dir = self.tmpdir - fd, tmpzip = tempfile.mkstemp(prefix='{}-'.format(self._filename_pfx), - suffix='.zip') - os.close(fd) - - - import zipfile - - def _zipdir(path, zip): - cwd = os.getcwd() - os.chdir(path) - for root, dirs, files in os.walk('.'): - for file in files: - zip.write(os.path.join(root, file)) - os.chdir(cwd) - - with zipfile.ZipFile(tmpzip, 'w') as zipf: - _zipdir(export_dir, zipf) - -# args = ['zip', '-r', '-v', tmpzip, '.'] -# logger.debug('shell: {}'.format(' '.join(args))) -# subprocess.call(args, cwd=export_dir) - - target = os.path.join(self.base_path, zipfile_base) - if os.path.exists(target): - os.remove(target) - - logger.debug('moving {} to {}'.format(tmpzip, target)) - shutil.move(tmpzip, target) - - _perm = 0644 - logger.debug('setting {:04o} permissions on {}'.format(_perm, target)) - os.chmod(target, _perm) - - def _copy_platform(self): - # copy platform-specific content - for f in os.listdir(self.platform_path): - f_src = os.path.join(self.platform_path, f) - - # blacklist files and dirs by name - # these files will not be copied - if f in self._blacklist: - logger.info('Skipping path (blacklisted) %s', f_src) - continue - - f_dst = os.path.join(self.tmpdir, f) - if os.path.isdir(f_src): - copydir(f_src, f_dst) - elif os.path.isfile(f_src): - copyfile(f_src, f_dst) - else: - logger.warning("Not sure what to do with %s", f_src) - - def _copy_common_dirs(self): - # copy other dirs - for d in self._common_dirs: - d_src = os.path.join(self.src_path, d) - d_dst = os.path.join(self.tmpdir, d) - copydir(d_src, d_dst) - - def _create_results_dir(self): - # create result dir if it doesn't exist, otherwise don't touch it - result_path = os.path.join(self.tmpdir, 'results') - if not os.path.exists(result_path): - logger.info('Result path does not exist, creating %s', result_path) - os.makedirs(result_path) - else: - logger.info('Result path %s already exists, proceeding', result_path) - - def _clean_up(self, path, remove_blacklist=True): - logger.debug("Cleaning up %s", path) - for f in os.listdir(path): - fpath = os.path.join(path, f) - if os.path.isdir(fpath): - if f in self._blacklist: - logger.info('Removing %s dir from %s', f, path) - shutil.rmtree(fpath, ignore_errors=False, onerror=onerror) - else: - self._clean_up(fpath, remove_blacklist=True) +def builder_for(platform): + ''' + Factory method that returns the appropriate Build class for the platform requested + :param platform: + ''' + try: + return SUPPORTED_PLATFORMS[platform] + except KeyError: + raise BuildError('Unsupported platform: {}'.format(platform)) diff --git a/build/dist/build_base2.py b/build/dist/build_base2.py new file mode 100644 index 0000000..13658ed --- /dev/null +++ b/build/dist/build_base2.py @@ -0,0 +1,188 @@ +''' +Created on Feb 11, 2014 + +@author: adh +''' +import os +import shutil +import tempfile +import logging +import zipfile + +from dev.misc import copydir, copyfile, onerror + +from .prepend_license import main as _prepend_license + +logger = logging.getLogger(__name__) + + +def _zipdir(path, zip_): + cwd = os.getcwd() + os.chdir(path) + for root, _dirs, files in os.walk('.'): + for f in files: + zip_.write(os.path.join(root, f)) + os.chdir(cwd) + + +class Build(object): + _blacklist = [] + _common_dirs = ['certfuzz', 'seedfiles'] + _license_file = 'COPYING.txt' + + def __init__(self, platform=None, distpath=None, srcpath=None): + self.platform = platform + + if distpath == None: + self.base_path = os.path.abspath(os.path.dirname(__file__)) + else: + self.base_path = os.path.abspath(distpath) + + if srcpath == None: + self.src_path = os.path.abspath(os.path.join(self.base_path, '../../src')) + else: + self.src_path = os.path.abspath(srcpath) + + self.tmp_dir = None + self.build_dir = None + self.platform_path = os.path.join(self.src_path, self.platform) + self._in_runtime_context = False + self._filename_pfx = 'BFF-{}'.format(self.platform) + self.zipfile = '{}.zip'.format(self._filename_pfx) + self.target = os.path.join(self.base_path, self.zipfile) + + def __enter__(self): + logger.debug('Entering Build context') + self.tmp_dir = tempfile.mkdtemp(prefix='bff_build_{}_'.format(self.platform)) + logger.debug('Temp dir is %s', self.tmp_dir) + self.build_dir = os.path.join(self.tmp_dir, 'bff') + os.mkdir(self.build_dir) + self._in_runtime_context = True + return self + + def __exit__(self, etype, value, traceback): + handled = False + logger.debug('Removing temp dir %s', self.tmp_dir) + shutil.rmtree(self.tmp_dir) + self._in_runtime_context = False + + return handled + + def build(self): + ''' + Perform a build. Must be invoked using with Build(foo)... syntax + ''' + assert self._in_runtime_context, 'Build() must be invoked using with... syntax' + self.export() + self.prune() + self.refine() + self.prepend_license() + self.package() + + def export(self): + logger.info('Exporting') + logger.info('Copy platform-specific files to tmp_dir') + self._copy_platform() + + logger.info('Copy common files to tmp_dir') + self._copy_common_dirs() + + def prune(self): + logger.info('Pruning') + logger.debug('nothing really happened here') + + def refine(self): + logger.info('Refining') + logger.info('Set up results dir') + self._create_results_dir() + + logger.info('Clean up build tmp_dir') + self._clean_up(self.build_dir, remove_blacklist=False) + + def prepend_license(self): + ''' + Adds the license text to the code prior to packaging + ''' + logger.info('Prepending License to *.py') + lf = os.path.join(self.build_dir, self._license_file) + _prepend_license(license_file=lf, + basedir=self.build_dir, + remove=False, + add=True, + debug=False, + overwrite=True, + ) + + def package(self): + logger.info('Packaging') + + def _move_to_target(self, tmpzip): + if os.path.exists(self.target): + os.remove(self.target) + logger.debug('moving {} to {}'.format(tmpzip, self.target)) + shutil.move(tmpzip, self.target) + _perm = 0644 + logger.debug('setting {:04o} permissions on {}'.format(_perm, self.target)) + os.chmod(self.target, _perm) + + def _create_zip(self): + fd, tmpzip = tempfile.mkstemp(prefix='{}-'.format(self._filename_pfx), + suffix='.zip', + dir=self.tmp_dir) + os.close(fd) + + with zipfile.ZipFile(tmpzip, 'w') as zipf: + _zipdir(self.build_dir, zipf) + return tmpzip + + def _copy_platform(self): + if os.path.isdir(self.platform_path): + platform_path = self.platform_path + else: + logger.info('No platform-specific info found at %s', self.platform_path) + platform_path = os.path.join(self.src_path, 'linux') + logger.info('Defaulting to %s', platform_path) + + # copy platform-specific content + for f in os.listdir(platform_path): + f_src = os.path.join(platform_path, f) + + # blacklist files and dirs by name + # these files will not be copied + if f in self._blacklist: + logger.info('Skipping path (blacklisted) %s', f_src) + continue + + f_dst = os.path.join(self.build_dir, f) + if os.path.isdir(f_src): + copydir(f_src, f_dst) + elif os.path.isfile(f_src): + copyfile(f_src, f_dst) + else: + logger.warning("Not sure what to do with %s", f_src) + + def _copy_common_dirs(self): + # copy other dirs + for d in self._common_dirs: + d_src = os.path.join(self.src_path, d) + d_dst = os.path.join(self.build_dir, d) + copydir(d_src, d_dst) + + def _create_results_dir(self): + # create result dir if it doesn't exist, otherwise don't touch it + result_path = os.path.join(self.build_dir, 'results') + if not os.path.exists(result_path): + logger.info('Result path does not exist, creating %s', result_path) + os.makedirs(result_path) + else: + logger.info('Result path %s already exists, proceeding', result_path) + + def _clean_up(self, path, remove_blacklist=True): + for f in os.listdir(path): + fpath = os.path.join(path, f) + if os.path.isdir(fpath): + if f in self._blacklist: + logger.info('Removing %s dir from %s', f, path) + shutil.rmtree(fpath, ignore_errors=False, onerror=onerror) + else: + self._clean_up(fpath, remove_blacklist=True) diff --git a/build/dist/linux/linux_build2.py b/build/dist/linux/linux_build2.py new file mode 100644 index 0000000..2057922 --- /dev/null +++ b/build/dist/linux/linux_build2.py @@ -0,0 +1,20 @@ +''' +Created on Feb 11, 2014 + +@author: adh +''' +import logging +from ..build_base2 import Build + +logger = logging.getLogger(__name__) + + +class LinuxBuild(Build): + def package(self): + ''' + Creates a zip file containing the code + ''' + Build.package(self) + + tmpzip = self._create_zip() + self._move_to_target(tmpzip) diff --git a/build/dist/osx/darwin_build2.py b/build/dist/osx/darwin_build2.py new file mode 100644 index 0000000..1da6438 --- /dev/null +++ b/build/dist/osx/darwin_build2.py @@ -0,0 +1,208 @@ +''' +Created on Dec 9, 2013 + +@author: adh +''' +import os +import shutil +import subprocess +#import string +import re + +import logging + +from ..build_base2 import Build +from ..errors import BuildError + +logger = logging.getLogger(__name__) + + +# mac-specific +def hdiutil(command, *parameters): + args = ['hdiutil', command] + args.extend(parameters) + logger.debug(args) + subprocess.call(args) + + +# mac-specific +def packagemaker(working_dir='.', *parameters): + args = ['/Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker'] + args.extend(parameters) + # pushd + oldcwd = os.getcwd() + os.chdir(working_dir) + + logger.debug('cd %s && %s', working_dir, args) + subprocess.call(args) + + # popd + os.chdir(oldcwd) + +# remember where this file is so we can find the installer code +_CWD = os.path.abspath(os.path.dirname(__file__)) + + +class DarwinBuild(Build): + # this platform string is used to decide which code dir to export from svn + # OSX and linux use the same code. + PLATFORM = 'linux' + LICENSE_FILE = 'COPYING.txt' + INSTALLER_BASE = os.path.join(_CWD, 'installer') + SHARED_DEPS = '/Volumes/xcat/build/bff/osx/' + LOCAL_DEPS = os.path.expanduser('~/bff_deps/') + + def __init__(self, *args, **kwargs): + Build.__init__(self, *args, **kwargs) + + self.dmg_file = '{}.dmg'.format(self._filename_pfx) + self.sparse_image = None + self.final_dmg = None + self.dmg_file_template = None + + def __exit__(self, etype, value, traceback): + Build.__exit__(self, etype, value, traceback) + + # only clean up if we are exiting normally + if not etype: + try: + os.remove(self.sparse_image) + except: + print "Failed to remove %s" % self.sparse_image + +# def process_args(self): +# ''' +# Process the other arguments passed to the build object +# ''' +# super(self.__class__, self).process_args() +# +# if self.buildtype == 'tag': +# self.dmg_file = '%s-%s.dmg' % (self.PROJECT, self.tag) +# elif self.buildtype == 'branch': +# self.dmg_file_template = '%s-%s-r$SVN_REV.dmg' % (self.PROJECT, self.branch) +# elif self.buildtype == 'trunk': +# self.dmg_file_template = '%s-trunk-r$SVN_REV.dmg' % (self.PROJECT) +# else: +# raise BuildError('Unknown buildtype: %s' % self.buildtype) + +# def export(self): +# # export the linux code +# super(self.__class__, self).export() + + def refine(self): + Build.refine(self) + + # pushd + oldcwd = os.getcwd() + os.chdir(self.build_dir) + + # now move the whole export over to installer + target = os.path.join(self.INSTALLER_BASE, 'bff') + if os.path.exists(target): + logger.debug('Deleting old target %s', target) + shutil.rmtree(target) + logger.debug('Copying %s -> %s', self.build_dir, target) + shutil.copytree(self.build_dir, target) + + # Copy Readme and License files to installer directory + logger.debug('Copying Readme and License files...') + logger.debug('cd %s', target) + os.chdir(target) + logger.debug('cwd = %s', os.getcwd()) + shutil.copy('README', '../Readme.txt') + shutil.copy(self.LICENSE_FILE, '../License.txt') + os.chdir(oldcwd) + + def _build_sparseimage(self): + #${SPARSE_DMG}: clean_sparseimage convert_template mount_sparseimage package copy_pkg unmount_sparseimage + + #DMG_TEMPLATE=${INSTALLER_BASE}/BFF-template.dmg + self.dmg_template = os.path.join(self.INSTALLER_BASE, 'BFF-template.dmg') + + #SPARSE_DMG=${base_path}/BFF-sparse.sparseimage + self.sparse_image = os.path.join(self.base_path, 'BFF-sparse.sparseimage') + + #clean_sparseimage: + if os.path.exists(self.sparse_image): + # ${RM} ${SPARSE_DMG} + logger.debug('Deleting old sparseimage', self.sparse_image) + os.remove(self.sparse_image) + + #convert_template: + # hdiutil convert ${DMG_TEMPLATE} -format UDSP -o ${SPARSE_DMG} + hdiutil('convert', self.dmg_template, '-format', 'UDSP', '-o', self.sparse_image) + + #unmount_old_dmg: + # ls -1d /Volumes/CERT\ BFF* | tr '\n' '\0' | xargs -0 -n1 -Ixxx hdiutil detach "xxx" + for d in os.listdir('/Volumes'): + if d.startswith('CERT BFF'): + volume_to_detach = os.path.join('/Volumes', d) + hdiutil('detach', volume_to_detach) + + #mount_sparseimage: unmount_old_dmg + # hdiutil mount ${SPARSE_DMG} + hdiutil('mount', self.sparse_image) + + #package: + # cd ${INSTALLER_BASE} && /Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker \ + # -d BFF_installer.pmdoc -v -o "/Volumes/CERT BFF/Install CERT BFF.pkg" + packagemaker(self.INSTALLER_BASE, + '-d', 'BFF_installer.pmdoc', + '-v', + '-o', '/Volumes/CERT BFF/Install CERT BFF.pkg' + ) + + #copy_pkg: + # cp -a ${INSTALLER_BASE}/build/pkgs/* /Volumes/CERT\ BFF/pkgs/ + srcdir = os.path.join(self.INSTALLER_BASE, 'build', 'pkgs') + dstdir = '/Volumes/CERT BFF/pkgs' + + # TODO: replace this with a native api call + # note however that shutil.copytree() is not sufficient + # because it requires that the target dir not already exist + logger.debug('Copy %s -> %s', srcdir, dstdir) + subprocess.call('cp -a %s %s' % (os.path.join(srcdir, '*'), re.escape(dstdir)), shell=True) + + #unmount_sparseimage: + # hdiutil detach "/Volumes/CERT BFF" + hdiutil('detach', '/Volumes/CERT BFF') + + def _convert_sparseimage_to_dmg(self): + #FINAL_DMG=${base_path}/BFF.dmg + self.final_dmg = os.path.join(self.base_path, 'BFF.dmg') + + #clean_dmg: + # ${RM} ${FINAL_DMG} + if os.path.exists(self.final_dmg): + logger.debug('Deleting old dmg %s', self.final_dmg) + os.remove(self.final_dmg) + + #${FINAL_DMG}: clean_dmg ${SPARSE_DMG} + # hdiutil convert ${SPARSE_DMG} -format UDBZ -o ${FINAL_DMG} + + hdiutil('convert', self.sparse_image, '-format', 'UDBZ', '-o', self.final_dmg) + + #rename_dmg: ${FINAL_DMG} + # SVN_REV=`cd ${LINUX_DIST_BASE} && ${SVN} info | grep Revision | cut -d' ' -f2`; \ + # VERSION=`cd ${BUILD_BASE} && grep __version__ bff.py | cut -d'=' -f2 | sed -e "s/ //g" -e "s/\'//g"`; \ + if self.dmg_file: + dmg_file_base = self.dmg_file + else: + raise BuildError('Unable to determine dmg file name') + + # ${MV} ${FINAL_DMG} ${base_path}/BFF-$$VERSION-$$SVN_REV.dmg + dmg_file = os.path.join(self.base_path, dmg_file_base) + logger.debug('Move %s -> %s', self.final_dmg, dmg_file) + os.rename(self.final_dmg, dmg_file) + + def _sync_dependencies(self): + # Retrieve binary dependecies for building OSX installer + # rsync -EaxSv /Volumes/xcat/build/bff/osx/ installer/ + # TODO: What if rsync fails? + subprocess.call(['rsync', '-EaxSv', self.SHARED_DEPS, self.LOCAL_DEPS]) + subprocess.call(['rsync', '-EaxSv', self.LOCAL_DEPS, self.INSTALLER_BASE]) + + def package(self): + self._sync_dependencies() + self._build_sparseimage() + self._convert_sparseimage_to_dmg() diff --git a/build/make_dist2.py b/build/make_dist2.py index 3a5eaa3..a4d9054 100644 --- a/build/make_dist2.py +++ b/build/make_dist2.py @@ -5,17 +5,14 @@ ''' import argparse import logging -from dist.build2 import Build +from dist.build2 import builder_for from dist.build2 import SUPPORTED_PLATFORMS as builders +from dist.errors import BuildError logger = logging.getLogger(__name__) def main(): - logger = logging.getLogger() - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - parser = argparse.ArgumentParser() parser.add_argument('-d', '--debug', help='enable debug messages', action="store_true") parser.add_argument('-v', '--verbose', help='enable debug messages', action="store_true") @@ -36,10 +33,20 @@ def main(): exit(1) # assume that we're running in a git checkout? - with Build(platform=args.platform, + try: + builder = builder_for(args.platform) + except BuildError as e: + logger.error('Build Error: %s', e) + return + + with builder(platform=args.platform, distpath=args.distpath, srcpath=args.srcpath) as b: - b.build() + b.build() if __name__ == '__main__': + logger = logging.getLogger() + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + main() From b537aae06bd499fb40a29af57f70466b51941f41 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 15:09:52 -0500 Subject: [PATCH 0084/1169] get rid of __init__.py contents --- build/dist/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/build/dist/__init__.py b/build/dist/__init__.py index 0e67bc3..e69de29 100644 --- a/build/dist/__init__.py +++ b/build/dist/__init__.py @@ -1 +0,0 @@ -from build_base import Build From dd5e36f90b198d58c2c95a9781540d569c6276cd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 15:11:58 -0500 Subject: [PATCH 0085/1169] reconstruct windows build process --- build/dist/build2.py | 4 ++- build/dist/windows/nsis/buildnsi.py | 41 ++++++++-------------------- build/dist/windows/windows_build2.py | 37 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 31 deletions(-) create mode 100644 build/dist/windows/windows_build2.py diff --git a/build/dist/build2.py b/build/dist/build2.py index a3c6af9..4b3b7e6 100644 --- a/build/dist/build2.py +++ b/build/dist/build2.py @@ -4,8 +4,10 @@ @organization: cert.org ''' import logging + from .linux.linux_build2 import LinuxBuild from .osx.darwin_build2 import DarwinBuild +from .windows.windows_build2 import WindowsBuild from .errors import BuildError @@ -13,7 +15,7 @@ SUPPORTED_PLATFORMS = {'linux': LinuxBuild, - 'windows': None, + 'windows': WindowsBuild, 'darwin': DarwinBuild, } diff --git a/build/dist/windows/nsis/buildnsi.py b/build/dist/windows/nsis/buildnsi.py index d0d84b2..6ccc4b8 100644 --- a/build/dist/windows/nsis/buildnsi.py +++ b/build/dist/windows/nsis/buildnsi.py @@ -1,30 +1,14 @@ +''' +Created on Feb 10, 2014 + +@organization: cert.org +''' import sys import os import string -#import subprocess - -# TODO remove if no longer needed -#def split_and_strip(line, delim=':'): -# ''' -# Return the second half of the line after the delimiter, stripped of -# whitespace -# @param line: -# @param delim: defaults to ":" -# ''' -# return line.split(delim)[1].strip() - - -# TODO remove if no longer needed -#def get_svn_rev(): -# svninfo = subprocess.Popen(['svn', 'info'], stdout=subprocess.PIPE).communicate()[0] -# svninfolines = svninfo.splitlines() -# for line in svninfolines: -# if line.startswith('Revision: '): -# svn_revision = split_and_strip(line) -# -# return svn_revision - -def main(svn_rev=None, outfile=None, build_dir=None): + + +def main(version_string='', outfile=None, build_dir=None): distpath = '' @@ -47,14 +31,11 @@ def main(svn_rev=None, outfile=None, build_dir=None): fp.write(toptext) -# svn_revision = get_svn_rev() - svn_revision = svn_rev - - fp.write('!define VERSION "02.01.00.%s"\n' % svn_revision) + fp.write('!define VERSION "%s"\n' % version_string) fp.write('!define COPYRIGHT "CERT 2013"\n') - fp.write('!define DESCRIPTION "FOE 2.1"\n') + fp.write('!define DESCRIPTION "FOE %s"\n' % version_string) fp.write('!define LICENSE_TXT "%s\COPYING.txt"\n' % distpath) - fp.write('!define INSTALLER_NAME "%s\..\..\FOE-2.1-r%s-setup.exe"\n' % (distpath, svn_revision)) + fp.write('!define INSTALLER_NAME "%s\..\..\FOE-%s-setup.exe"\n' % (distpath, version_string)) headerfile = open("nsis_header.txt", "r") headertext = headerfile.read() diff --git a/build/dist/windows/windows_build2.py b/build/dist/windows/windows_build2.py new file mode 100644 index 0000000..ae1b875 --- /dev/null +++ b/build/dist/windows/windows_build2.py @@ -0,0 +1,37 @@ +''' +Created on Dec 9, 2013 + +@author: adh +''' +import os +import shutil + +import subprocess + +from ..build_base2 import Build + +basedir = os.path.dirname(__file__) + + +class WindowsBuild(Build): + PLATFORM = 'windows' + LICENSE_FILE = 'COPYING.txt' + + def package(self): + ''' + Builds a Windows Installer + ''' + from .nsis import buildnsi + + # Copy files required by nsis + for f in ['cert.ico', 'EnvVarUpdate.nsh', 'vmwarning.txt']: + src = os.path.join(basedir, 'nsis', f) + shutil.copy(src, self.build_dir) + + nsifile = os.path.join(self.build_dir, 'foe2.nsi') + + # generate the nsi file + buildnsi.main(version_string="02.01.00.99", outfile=nsifile, build_dir=self.build_dir) + + # invoke makensis on the file we just made + subprocess.call(['makensis', nsifile]) From 45559413b8fc3598a2d1528edf27ba1aea43354f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 15:13:38 -0500 Subject: [PATCH 0086/1169] get rid of old code --- build/dist/build_base.py | 190 ------------------------- build/dist/linux/linux_build.py | 42 ------ build/dist/osx/Makefile | 98 ------------- build/dist/osx/darwin_build.py | 210 ---------------------------- build/dist/svn.py | 22 --- build/dist/windows/windows_build.py | 73 ---------- build/make_dist.py | 110 --------------- 7 files changed, 745 deletions(-) delete mode 100644 build/dist/build_base.py delete mode 100644 build/dist/linux/linux_build.py delete mode 100644 build/dist/osx/Makefile delete mode 100644 build/dist/osx/darwin_build.py delete mode 100644 build/dist/svn.py delete mode 100644 build/dist/windows/windows_build.py delete mode 100755 build/make_dist.py diff --git a/build/dist/build_base.py b/build/dist/build_base.py deleted file mode 100644 index 7627de4..0000000 --- a/build/dist/build_base.py +++ /dev/null @@ -1,190 +0,0 @@ -''' -Created on Dec 9, 2013 - -@author: adh -''' -import os -import logging -import shutil -import subprocess - -from .errors import BuildError -from .svn import svn_export, svn_rev -from .misc import mkdir_p -import urlparse -from .prepend_license import main as _prepend_license - -logger = logging.getLogger(__name__) - - -def _add_trailing_slash(url): - if url.endswith('/'): - return url - return url + '/' - - -class Build(object): - ''' - classdocs - ''' - PROJECT = 'BFF' - - PLATFORM = None - LICENSE_FILE = 'COPYING' - PWD = os.path.abspath(os.path.dirname(__file__)) - BUILD_BASE = PWD - DIST_BASE = os.path.abspath(os.path.join(PWD, '../../dist_builds')) - - def __init__(self, buildtype, args, url=None, platform=None): - ''' - Constructor - ''' - self.buildtype = buildtype - self.args = args - if platform: - self.platform = platform - else: - self.platform = self.PLATFORM - - _url = _add_trailing_slash(url) - - # urlparse.urljoin doesn't work right with svn:// URIs. But it should. - # https://mail.python.org/pipermail/python-bugs-list/2011-August/145058.html - self.trunk = '%s/trunk' % _url - self.branches = '%s/branches' % _url - self.tags = '%s/tags' % _url - - self.build_dir = None - self.export_path = None - self.svn_rev = None - self.branch = None - self.svn_url = None - self.zipfile_template = None - self.tag = None - - # set the default export function - # note: buildtype=trunk will override this in process_args - self.export = self._export - - self.export_base = '%s-%s-export' % (self.PROJECT, self.platform) - self.zipfile = None - self.in_runtime_context = False - - def __enter__(self): - ''' - Set up the runtime context for with... syntax - ''' - self.process_args() - - if not os.path.exists(self.DIST_BASE): - mkdir_p(self.DIST_BASE) - - import tempfile - pfx = '%s-%s-' % (self.PROJECT, self.platform) - self.build_dir = tempfile.mkdtemp(prefix=pfx, dir=self.DIST_BASE) - self.export_path = os.path.join(self.build_dir, self.export_base) - self.in_runtime_context = True - logger.info('Starting build') - return self - - def __exit__(self, etype, value, traceback): - ''' - Teardown the runtime context - :param etype: - :param value: - :param traceback: - ''' - self.in_runtime_context = False - - # only clean up if we're exiting normally - if not etype: - logger.info('Build complete, cleaning up') - shutil.rmtree(self.build_dir) - logger.info('Exiting build') - - def process_args(self): - ''' - Process the other arguments passed to the build object - ''' - if self.buildtype == 'tag': - if not len(self.args): - raise BuildError('Build type %s requires a tag' % self.buildtype) - self.tag = self.args.pop(0) - self.svn_url = "%s/%s" % (self.tags, self.tag) - self.zipfile = '%s-%s.zip' % (self.PROJECT, self.tag) - elif self.buildtype == 'branch': - if not len(self.args): - raise BuildError('Build type %s requires a tag' % self.buildtype) - self.branch = self.args.pop(0) - self.svn_url = "%s/%s" % (self.branches, self.branch) - self.zipfile_template = '%s-%s-r$SVN_REV.zip' % (self.PROJECT, self.branch) - elif self.buildtype == 'trunk': - self.svn_url = self.trunk - self.zipfile_template = '%s-trunk-r$SVN_REV.zip' % (self.PROJECT) - else: - raise BuildError('Unknown buildtype: %s' % self.buildtype) - - def build(self): - ''' - Perform a build. Must be invoked using with Build(foo)... syntax - ''' - assert self.in_runtime_context, 'Build() must be invoked using with... syntax' - logger.info('Exporting') - self.export() - logger.info('Pruning') - self.prune() - logger.info('Refining') - self.refine() - logger.info('Prepending license') - self.prepend_license() - logger.info('Packaging') - self.package() - - def _export(self): - ''' - Exports the code from the repository. When complete, the code will be - in the directory specified by self.export_path - ''' - svn_base = "%s/src" % self.svn_url - for d in [self.platform, 'certfuzz', 'seedfiles']: - src = "%s/%s" % (svn_base, d) - if d == self.platform: - dst = self.export_path - else: - dst = os.path.join(self.export_path, d) - svn_export(src, dst) - self.svn_rev = svn_rev(svn_base) - - def refine(self): - ''' - Post-export refinement of the code prior to packaging. - ''' - result_dir = os.path.join(self.export_path, 'results') - mkdir_p(result_dir) - - def prepend_license(self): - ''' - Adds the license text to the code prior to packaging - ''' - _prepend_license(license_file=os.path.join(self.export_path, self.LICENSE_FILE), - basedir=self.export_path, - remove=False, - add=True, - debug=False, - overwrite=True, - ) - - def prune(self): - ''' - Prunes unneeded code from the export prior to packaging - ''' - for dirname, _dirnames, _filenames in os.walk(self.export_path): - if os.path.basename(dirname) == 'obsolete': - logger.info("Removing %s" % dirname) - shutil.rmtree(dirname) - - def package(self): - ''' - Packages the code for distribution - ''' - raise NotImplementedError diff --git a/build/dist/linux/linux_build.py b/build/dist/linux/linux_build.py deleted file mode 100644 index e512e3e..0000000 --- a/build/dist/linux/linux_build.py +++ /dev/null @@ -1,42 +0,0 @@ -''' -Created on Dec 9, 2013 - -@author: adh -''' -import string -import subprocess -import os -import shutil - -from ..build_base import Build -from ..svn import svn_rev -from ..errors import BuildError - - -class LinuxBuild(Build): - PLATFORM = 'linux' - LICENSE_FILE = 'COPYING' - - def package(self): - ''' - Creates a zip file containing the code - ''' - if self.zipfile: - zipfile_base = self.zipfile - elif self.zipfile_template: - rev = svn_rev(self.svn_url) - zipfile_template = string.Template(self.zipfile_template) - zipfile_base = zipfile_template.substitute(SVN_REV=rev) - else: - raise BuildError('Unable to determine zipfile name') - - export_dir = os.path.join(self.build_dir, self.export_base) - parent_zipfile = os.path.join('..', zipfile_base) - args = ['zip', '-r', '-v', parent_zipfile, '.'] - subprocess.call(args, cwd=export_dir) - - source = os.path.join(self.build_dir, zipfile_base) - target = os.path.join(self.DIST_BASE, zipfile_base) - if os.path.exists(target): - os.remove(target) - shutil.move(source, target) diff --git a/build/dist/osx/Makefile b/build/dist/osx/Makefile deleted file mode 100644 index e7cc7a2..0000000 --- a/build/dist/osx/Makefile +++ /dev/null @@ -1,98 +0,0 @@ -SHELL := /bin/bash - -RM=rm -rfv -CP=cp -Rv -MV=mv -v -SVN=svn - -LINUX_DIST_BASE=../linux -# base dir where we'll create the build -DIST_BASE=../../../dist_builds - -PROJECT=BFF -PLATFORM=osx - -BUILD_BASE=${INSTALLER_BASE}/bff - -INSTALLER_BASE=installer -DMG_TEMPLATE=${INSTALLER_BASE}/BFF-template.dmg - -SPARSE_DMG=${DIST_BASE}/BFF-sparse.sparseimage -FINAL_DMG=${DIST_BASE}/BFF.dmg - -clean: clean_dmg clean_sparseimage - ${RM} ${BUILD_BASE} - -linux_dist: - # Build the linux trunk dist - cd ${LINUX_DIST_BASE} && ${MAKE} trunk EXPORT_BASE=bff - ${MV} ${DIST_BASE}/bff ${INSTALLER_BASE} - # get the svn revision from the repo - -linux_tag_dist: - # Build the linux dist - # be sure to specify TAG - cd ${LINUX_DIST_BASE} && ${MAKE} tag TAG=${TAG} EXPORT_BASE=bff - ${MV} ${DIST_BASE}/bff ${INSTALLER_BASE} - # get the svn revision from the repo - -linux_branch_dist: - # Build the linux dist - # be sure to specify BRANCH - cd ${LINUX_DIST_BASE} && ${MAKE} branch BRANCH=${BRANCH} EXPORT_BASE=bff - ${MV} ${DIST_BASE}/bff ${INSTALLER_BASE} - # get the svn revision from the repo - -rename_files: - ${CP} ${BUILD_BASE}/README ${INSTALLER_BASE}/Readme.txt - ${CP} ${BUILD_BASE}/COPYING ${INSTALLER_BASE}/License.txt - -osx_dist: clean linux_dist rename_files - -osx_tag_dist: clean linux_tag_dist rename_files - -osx_branch_dist: clean linux_branch_dist rename_files - -unmount_old_dmg: - ls -1d /Volumes/CERT\ BFF* | tr '\n' '\0' | xargs -0 -n1 -Ixxx hdiutil detach "xxx" - -${SPARSE_DMG}: clean_sparseimage convert_template mount_sparseimage package copy_pkg unmount_sparseimage - -clean_sparseimage: - ${RM} ${SPARSE_DMG} - -clean_sparseimage_again: - ${RM} ${SPARSE_DMG} - -convert_template: - hdiutil convert ${DMG_TEMPLATE} -format UDSP -o ${SPARSE_DMG} - -mount_sparseimage: unmount_old_dmg - hdiutil mount ${SPARSE_DMG} - -package: - cd ${INSTALLER_BASE} && /Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker \ - -d BFF_installer.pmdoc -v -o "/Volumes/CERT BFF/Install CERT BFF.pkg" - -copy_pkg: - cp -a ${INSTALLER_BASE}/build/pkgs/* /Volumes/CERT\ BFF/pkgs/ - -unmount_sparseimage: - hdiutil detach "/Volumes/CERT BFF" - -clean_dmg: - ${RM} ${FINAL_DMG} - -${FINAL_DMG}: clean_dmg ${SPARSE_DMG} - hdiutil convert ${SPARSE_DMG} -format UDBZ -o ${FINAL_DMG} - -rename_dmg: ${FINAL_DMG} - SVN_REV=`cd ${LINUX_DIST_BASE} && ${SVN} info | grep Revision | cut -d' ' -f2`; \ - VERSION=`cd ${BUILD_BASE} && grep __version__ bff.py | cut -d'=' -f2 | sed -e "s/ //g" -e "s/\'//g"`; \ - ${MV} ${FINAL_DMG} ${DIST_BASE}/BFF-$$VERSION-$$SVN_REV.dmg - -dist: osx_dist ${SPARSE_DMG} ${FINAL_DMG} clean_sparseimage_again rename_dmg - -tag_dist: osx_tag_dist ${SPARSE_DMG} ${FINAL_DMG} clean_sparseimage_again rename_dmg - -branch_dist: osx_branch_dist ${SPARSE_DMG} ${FINAL_DMG} clean_sparseimage_again rename_dmg diff --git a/build/dist/osx/darwin_build.py b/build/dist/osx/darwin_build.py deleted file mode 100644 index 6ae283f..0000000 --- a/build/dist/osx/darwin_build.py +++ /dev/null @@ -1,210 +0,0 @@ -''' -Created on Dec 9, 2013 - -@author: adh -''' -import os -import shutil -import subprocess -#import string -import re - -import logging - -from ..build_base import Build -from ..svn import svn_rev -from ..errors import BuildError - -from string import Template - -logger = logging.getLogger(__name__) - - -# mac-specific -def hdiutil(command, *parameters): - args = ['hdiutil', command] - args.extend(parameters) - logger.debug(args) - subprocess.call(args) - - -# mac-specific -def packagemaker(working_dir='.', *parameters): - args = ['/Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker'] - args.extend(parameters) - # pushd - oldcwd = os.getcwd() - os.chdir(working_dir) - - logger.debug('cd %s && %s', working_dir, args) - subprocess.call(args) - - # popd - os.chdir(oldcwd) - - -class DarwinBuild(Build): - # this platform string is used to decide which code dir to export from svn - # OSX and linux use the same code. - PLATFORM = 'linux' - LICENSE_FILE = 'COPYING' - INSTALLER_BASE = os.path.join(Build.BUILD_BASE, 'osx', 'installer') - SHARED_DEPS = '/Volumes/xcat/build/bff/osx/' - LOCAL_DEPS = os.path.expanduser('~/bff_deps/') - dmg_file = '' - - def __init__(self, *args, **kwargs): - Build.__init__(self, *args, **kwargs) - - self.dmg_template = None - self.sparse_image = None - self.final_dmg = None - self.dmg_file_template = None - - def __exit__(self, etype, value, traceback): - Build.__exit__(self, etype, value, traceback) - - # only clean up if we are exiting normally - if not etype: - try: - os.remove(self.sparse_image) - except: - print "Failed to remove %s" % self.sparse_image - - def process_args(self): - ''' - Process the other arguments passed to the build object - ''' - super(self.__class__, self).process_args() - - if self.buildtype == 'tag': - self.dmg_file = '%s-%s.dmg' % (self.PROJECT, self.tag) - elif self.buildtype == 'branch': - self.dmg_file_template = '%s-%s-r$SVN_REV.dmg' % (self.PROJECT, self.branch) - elif self.buildtype == 'trunk': - self.dmg_file_template = '%s-trunk-r$SVN_REV.dmg' % (self.PROJECT) - else: - raise BuildError('Unknown buildtype: %s' % self.buildtype) - -# def export(self): -# # export the linux code -# super(self.__class__, self).export() - - def refine(self): - Build.refine(self) - - # pushd - oldcwd = os.getcwd() - os.chdir(self.export_path) - - # now move the whole export over to installer - target = os.path.join(self.INSTALLER_BASE, 'bff') - if os.path.exists(target): - logger.debug('Deleting old target %s', target) - shutil.rmtree(target) - logger.debug('Copying %s -> %s', self.export_path, target) - shutil.copytree(self.export_path, target) - - # Copy Readme and License files to installer directory - logger.debug('Copying Readme and License files...') - os.chdir(target) - shutil.copy('README', '../Readme.txt') - shutil.copy('COPYING', '../License.txt') - - def _build_sparseimage(self): - #${SPARSE_DMG}: clean_sparseimage convert_template mount_sparseimage package copy_pkg unmount_sparseimage - - #DMG_TEMPLATE=${INSTALLER_BASE}/BFF-template.dmg - self.dmg_template = os.path.join(self.INSTALLER_BASE, 'BFF-template.dmg') - - #SPARSE_DMG=${DIST_BASE}/BFF-sparse.sparseimage - self.sparse_image = os.path.join(self.DIST_BASE, 'BFF-sparse.sparseimage') - - #clean_sparseimage: - if os.path.exists(self.sparse_image): - # ${RM} ${SPARSE_DMG} - logger.debug('Deleting old sparseimage', self.sparse_image) - os.remove(self.sparse_image) - - #convert_template: - # hdiutil convert ${DMG_TEMPLATE} -format UDSP -o ${SPARSE_DMG} - hdiutil('convert', self.dmg_template, '-format', 'UDSP', '-o', self.sparse_image) - - #unmount_old_dmg: - # ls -1d /Volumes/CERT\ BFF* | tr '\n' '\0' | xargs -0 -n1 -Ixxx hdiutil detach "xxx" - for d in os.listdir('/Volumes'): - if d.startswith('CERT BFF'): - volume_to_detach = os.path.join('/Volumes', d) - hdiutil('detach', volume_to_detach) - - #mount_sparseimage: unmount_old_dmg - # hdiutil mount ${SPARSE_DMG} - hdiutil('mount', self.sparse_image) - - #package: - # cd ${INSTALLER_BASE} && /Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker \ - # -d BFF_installer.pmdoc -v -o "/Volumes/CERT BFF/Install CERT BFF.pkg" - packagemaker(self.INSTALLER_BASE, - '-d', 'BFF_installer.pmdoc', - '-v', - '-o', '/Volumes/CERT BFF/Install CERT BFF.pkg' - ) - - #copy_pkg: - # cp -a ${INSTALLER_BASE}/build/pkgs/* /Volumes/CERT\ BFF/pkgs/ - srcdir = os.path.join(self.INSTALLER_BASE, 'build', 'pkgs') - dstdir = '/Volumes/CERT BFF/pkgs' - - # TODO: replace this with a native api call - # note however that shutil.copytree() is not sufficient - # because it requires that the target dir not already exist - logger.debug('Copy %s -> %s', srcdir, dstdir) - subprocess.call('cp -a %s %s' % (os.path.join(srcdir, '*'), re.escape(dstdir)), shell=True) - - #unmount_sparseimage: - # hdiutil detach "/Volumes/CERT BFF" - hdiutil('detach', '/Volumes/CERT BFF') - - def _convert_sparseimage_to_dmg(self): - #FINAL_DMG=${DIST_BASE}/BFF.dmg - self.final_dmg = os.path.join(self.DIST_BASE, 'BFF.dmg') - - #clean_dmg: - # ${RM} ${FINAL_DMG} - if os.path.exists(self.final_dmg): - logger.debug('Deleting old dmg %s', self.final_dmg) - os.remove(self.final_dmg) - - #${FINAL_DMG}: clean_dmg ${SPARSE_DMG} - # hdiutil convert ${SPARSE_DMG} -format UDBZ -o ${FINAL_DMG} - - hdiutil('convert', self.sparse_image, '-format', 'UDBZ', '-o', self.final_dmg) - - #rename_dmg: ${FINAL_DMG} - # SVN_REV=`cd ${LINUX_DIST_BASE} && ${SVN} info | grep Revision | cut -d' ' -f2`; \ - # VERSION=`cd ${BUILD_BASE} && grep __version__ bff.py | cut -d'=' -f2 | sed -e "s/ //g" -e "s/\'//g"`; \ - if self.dmg_file: - dmg_file_base = self.dmg_file - elif self.dmg_file_template: - rev = svn_rev(self.svn_url) - dmg_file_template = Template(self.dmg_file_template) - dmg_file_base = dmg_file_template.substitute(SVN_REV=rev) - else: - raise BuildError('Unable to determine dmg file name') - - # ${MV} ${FINAL_DMG} ${DIST_BASE}/BFF-$$VERSION-$$SVN_REV.dmg - dmg_file = os.path.join(self.DIST_BASE, dmg_file_base) - logger.debug('Move %s -> %s', self.final_dmg, dmg_file) - os.rename(self.final_dmg, dmg_file) - - def _sync_dependencies(self): - # Retrieve binary dependecies for building OSX installer - # rsync -EaxSv /Volumes/xcat/build/bff/osx/ installer/ - # TODO: What if rsync fails? - subprocess.call(['rsync', '-EaxSv', self.SHARED_DEPS, self.LOCAL_DEPS]) - subprocess.call(['rsync', '-EaxSv', self.LOCAL_DEPS, self.INSTALLER_BASE]) - - def package(self): - self._sync_dependencies() - self._build_sparseimage() - self._convert_sparseimage_to_dmg() diff --git a/build/dist/svn.py b/build/dist/svn.py deleted file mode 100644 index 50d3a77..0000000 --- a/build/dist/svn.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Dec 9, 2013 - -@author: adh -''' -import subprocess - - -def svn_export(src, dst): - args = ['svn', 'export', src, dst] - subprocess.call(args) - - -def svn_rev(url): - args = ['svn', 'info', url] - p = subprocess.Popen(args, stdout=subprocess.PIPE) - for l in p.communicate()[0].splitlines(): - if not l: - continue - k, v = [x.strip() for x in l.split(':', 1)] - if k == "Revision": - return v diff --git a/build/dist/windows/windows_build.py b/build/dist/windows/windows_build.py deleted file mode 100644 index f46532a..0000000 --- a/build/dist/windows/windows_build.py +++ /dev/null @@ -1,73 +0,0 @@ -''' -Created on Dec 9, 2013 - -@author: adh -''' -import os -import shutil - -import subprocess - -from ..build_base import Build -from ..errors import BuildError - -basedir = os.path.dirname(__file__) - - -class WindowsBuild(Build): - PLATFORM = 'windows' - LICENSE_FILE = 'COPYING.txt' - - def prune(self): - super(self.__class__, self).prune() - - # prune everything in certfuzz/analysis except drillresults - cfadir = os.path.join(self.export_path, 'certfuzz', 'analysis') - p_to_del = [] - if os.path.exists(cfadir): - p_to_del.extend([os.path.join(cfadir, x) for x in os.listdir(cfadir) if x != "drillresults"]) - - # prune these dirs too - for x in ['certfuzz/analyzers', - 'certfuzz/campaign/config/bff_config.py', - 'certfuzz/debuggers/crashwrangler.py', - 'certfuzz/debuggers/gdb.py', - 'certfuzz/debuggers/mr_crash_hash.py', - 'certfuzz/debuggers/nulldebugger.py', - 'certfuzz/debuggers/templates', - 'build', - 'installer', - 'test', - ]: - p_to_del.append(os.path.join(self.export_path, x)) - - for p in p_to_del: - if os.path.isfile(p): - os.remove(p) - elif os.path.isdir(p): - shutil.rmtree(p) - - if os.path.exists(p): - raise BuildError("Unable to remove %s" % p) - - def package(self): - ''' - Builds a Windows Installer - ''' - from .nsis import buildnsi - - # Copy files required by nsis - for f in ['cert.ico', 'EnvVarUpdate.nsh', 'vmwarning.txt']: - src = os.path.join(basedir, 'nsis', f) - shutil.copy(src, self.build_dir) -# shutil.copy('dist/windows/nsis/cert.ico', self.build_dir) -# shutil.copy('dist/windows/nsis/EnvVarUpdate.nsh', self.build_dir) - - nsifile = os.path.join(self.build_dir, 'foe2.nsi') - - # generate the nsi file - buildnsi.main(svn_rev=self.svn_rev, outfile=nsifile, build_dir=self.build_dir) -# subprocess.call(args, stdout=open(nsifile, 'w')) - - # invoke makensis on the file we just made - subprocess.call(['makensis', nsifile]) diff --git a/build/make_dist.py b/build/make_dist.py deleted file mode 100755 index ff5fdc8..0000000 --- a/build/make_dist.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python -''' -Created on Jun 29, 2012 - -@organization: cert.org -''' -from optparse import OptionParser - -from dist.linux.linux_build import LinuxBuild -from dist.osx.darwin_build import DarwinBuild -from dist.windows.windows_build import WindowsBuild -from dist.errors import BuildError - -import subprocess -import logging - -logger = logging.getLogger() -logger.addHandler(logging.StreamHandler()) -logger.setLevel(logging.WARNING) - -build_class = { - 'linux': LinuxBuild, - 'windows': WindowsBuild, - 'osx': DarwinBuild, - } - -commands = ['branch', 'tag', 'trunk'] -cmd_args = { - 'branch': '', - 'tag': '' - } - - -def usage(): - print 'usage: $0 ' - print 'where' - print ' is one of: %s' % build_class.keys() - print ' is one of: tag, branch, trunk' - print 'tag args: tag-name' - exit() - - -def fail(msg): - print 'Build failed: %s' % msg - exit(1) - - -def ls_dist_base(): - print - print - print - print - header = "*** Listing build dir %s ***" % builder.DIST_BASE - print "*" * len(header) - print header - print "*" * len(header) - print - subprocess.call(['ls', '-l', builder.DIST_BASE]) - -if __name__ == '__main__': - usage = 'usage: %prog ' - desc_parts = ['platforms: %s' % build_class.keys(), - 'commands: %s' % commands, - 'command_args: %s' % ['%s: %s' % (k, v) for (k, v) in cmd_args.iteritems()] - ] - description = '\n'.join(desc_parts) - - parser = OptionParser(usage=usage, description=description) - parser.add_option('', '--verbose', dest='verbose', action='store_true', default=False, help='Enable verbose logging') - parser.add_option('', '--debug', dest='debug', action='store_true', default=False, help='Enable debug logging (overrides --verbose)') - parser.add_option('', '--url', dest='url', default=None, help='Enable verbose logging') - - (options, args) = parser.parse_args() - - if options.debug: - logger.setLevel(logging.DEBUG) - elif options.verbose: - logger.setLevel(logging.INFO) - - if options.url is None: - fail('Specify svn repo location using --url option') - - if not len(args): - parser.print_usage() - exit() - - _url = options.url - - platform = args.pop(0) - - try: - builder = build_class[platform] - except KeyError: - fail('Platform must be one of %s' % build_class.keys()) - - buildtype = args.pop(0) - - if not buildtype in commands: - fail('Command must be one of %s' % commands) - - logfile_hdlr = logging.FileHandler('make_dist_%s_%s.log' % (platform, buildtype), mode='w') - logger.addHandler(logfile_hdlr) - - try: - with builder(buildtype, args, url=_url) as build: - build.build() - except BuildError, e: - fail(e) - - ls_dist_base() From 137a36fad8d37f9f5c4ebdcdc13545810458f8a3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Feb 2014 15:18:48 -0500 Subject: [PATCH 0087/1169] update read me --- build/README.txt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/build/README.txt b/build/README.txt index cb4af4d..c09507b 100644 --- a/build/README.txt +++ b/build/README.txt @@ -3,25 +3,14 @@ your test VM. Use make_dist2.py to build a distributable package. -floyd:build adh$ python make_dist2.py --help usage: make_dist2.py [-h] [-d] [-v] platform srcpath distpath positional arguments: - platform One of ['windows', 'osx', ‘darwin’] + platform One of ['windows', 'darwin', 'linux'] srcpath path/to/bff/src distpath Directory to build into optional arguments: -h, --help show this help message and exit -d, --debug enable debug messages - -v, --verbose enable debug messages - - -**NOTE** -As of 2014-02-11 This only works for "linux" and “darwinâ€. Windows build capability will -follow soon. - - -make_dist.py is broken. Or rather, it depends on the code being in subversion, -which is no longer true. - + -v, --verbose enable debug messages \ No newline at end of file From 177363efbdf1e744972dab73250857f97f5ac644 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 24 Jan 2014 16:36:41 -0500 Subject: [PATCH 0088/1169] started refactoring certfuzz.bff.linux to look more like FOE. Committing for the weekend, the file is currently broken. --- src/certfuzz/bff/linux.py | 540 ++++++++++++++++++++++---------------- 1 file changed, 318 insertions(+), 222 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 7e6d7cd..cfbddb5 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -42,8 +42,8 @@ SEED_INTERVAL = 500 -SEED_TS = performance.TimeStamper() -START_SEED = 0 +#SEED_TS = performance.TimeStamper() +#START_SEED = 0 STATE_TIMER = StateTimer() @@ -52,10 +52,10 @@ logger.setLevel(0) -def get_rate(current_seed): - seeds = current_seed - START_SEED - rate = seeds / SEED_TS.since_start() - return rate +#def get_rate(current_seed): +# seeds = current_seed - START_SEED +# rate = seeds / SEED_TS.since_start() +# return rate def get_uniq_logger(logfile): @@ -290,20 +290,20 @@ def setup_logging_to_console(log_obj, level): add_log_handler(log_obj, level, hdlr, formatter) -def setup_logfile(logdir, log_basename='bff.log', level=logging.DEBUG, - max_bytes=1e8, backup_count=5): - ''' - Creates a log file in / at level - @param logdir: the directory where the log file should reside - @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') - @param level: the logging level (defaults to logging.DEBUG) - ''' - filetools.make_directories(logdir) - logfile = os.path.join(logdir, log_basename) - handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) - formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") - add_log_handler(logger, level, handler, formatter) - logger.info('Logging %s at %s', logging.getLevelName(level), logfile) +#def setup_logfile(logdir, log_basename='bff.log', level=logging.DEBUG, +# max_bytes=1e8, backup_count=5): +# ''' +# Creates a log file in / at level +# @param logdir: the directory where the log file should reside +# @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') +# @param level: the logging level (defaults to logging.DEBUG) +# ''' +# filetools.make_directories(logdir) +# logfile = os.path.join(logdir, log_basename) +# handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) +# formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") +# add_log_handler(logger, level, handler, formatter) +# logger.info('Logging %s at %s', logging.getLevelName(level), logfile) def get_config_file(basedir): @@ -322,9 +322,302 @@ def get_config_file(basedir): return config_file +class Intervals(object): + def __init__(self, min=0, max=1e10, interval=1): + self.min = min + self.max = max + self.interval = interval + self.curr_pos = self.min + + def __iter__(self): + start = self.curr_pos + end = self.curr_pos + self.interval + self.curr_pos += self.interval + return xrange(start, end) + + +class CampaignScriptError(Exception): + pass + + +class Iteration(object): + def __init__(self, cfg=None, seednum=None, seedfile=None, r=None): + self.cfg = cfg + self.seednum = seednum + self.seedfile = seedfile + self.r = r + + # convenience aliases + self.s1 = self.seednum + self.s2 = self.s1 + self.sf = self.seedfile + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + pass + + def _fuzz_and_run(self, quiet_flag): + if self.first_chunk: + # disable the --quiet option in zzuf + # on the first chunk only + quiet_flag = False + self.first_chunk = False + else: + quiet_flag = True + + # do the fuzz + cmdline = self.cfg.get_command(self.sf.path) + + STATE_TIMER.enter_state('fuzzing') + zzuf = Zzuf(self.cfg.local_dir, self.s1, + self.s2, + cmdline, + self.sf.path, + self.cfg.zzuf_log_file, + self.cfg.copymode, + self.r.min, + self.r.max, + self.cfg.progtimeout, + quiet_flag) + saw_crash = zzuf.go() + return saw_crash, zzuf + + def _log(self): +# # emit a log entry + crashcount = z.get_crashcount(self.cfg.crashers_dir) +# rate = get_rate(self.s1) +# seed_str = "seeds=%d-%d" % (self.s1, self.s2) +# range_str = "range=%.6f-%.6f" % (self.r.min, self.r.max) +# rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) +# expected_density = self.seedfile_set.expected_crash_density +# xd_str = "expected=%.9f" % expected_density +# xr_str = 'expected_rate=%.6f uniq/day' % (expected_density * rate * 86400) +# logger.info('Fuzzing %s %s %s %s %s %s crash_count=%d', +# self.sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) + logger.info('Fuzzing...crash_count=%d', crashcount) + + def go2(self): + self._fuzz() + self._run() + for testcase in self.candidates: + self.verify(testcase) + + for testcase in self.verified: + self.analyze(testcase) + + for testcase in self.analyzed: + self.construct_report(testcase) + + def go(self): + # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot + touch_watchdog_file(self.cfg) + self._check_ppid() + + saw_crash, zzuf = self._fuzz_and_run(quiet_flag) + STATE_TIMER.enter_state('checking_results') + + if not saw_crash: + # we must have made it through this chunk without a crash + # so go to next chunk + self.sf.record_tries(tries=1) + self.r.record_tries(tries=1) + self._log() + return + + # we must have seen a crash + # get the results + zzuf_log = ZzufLog(self.cfg.zzuf_log_file, self.cfg.zzuf_log_out(self.sf.output_dir)) + + # Don't generate cases for killed process or out-of-memory + # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will + # report the exit code in its output log. The exit code is 128 + the signal number. + crash_status = zzuf_log.crash_logged(self.cfg.copymode) + sr.bookmark_s1() + self.s1 = zzuf_log.seed + + # record the fact that we've made it this far + try_count = self.s1_delta() + self.sf.record_tries(tries=try_count) + self.r.record_tries(tries=try_count) + + new_uniq_crash = False + if crash_status: + logger.info('Generating testcase for %s', zzuf_log.line) + # a true crash + zzuf_range = zzuf_log.range + # create the temp dir for the results + self.cfg.create_tmpdir() + outfile = self.cfg.get_testcase_outfile(sf.path, sr.s1) + logger.debug('Output file is %s', outfile) + testcase = zzuf.generate_test_case(sf.path, sr.s1, zzuf_range, outfile) + + # Do internal verification using GDB / Valgrind / Stderr + fuzzedfile = file_handlers.basicfile.BasicFile(outfile) + + with BffCrash(self.cfg, sf, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, + self.cfg.killprocname, self.cfg.backtracelevels, + self.cfg.crashers_dir, sr.s1, r) as c: + if c.is_crash: + new_uniq_crash = verify_crasher(c, hashes, self.cfg, seedfile_set) + + # record the zzuf log line for this crash + if not c.logger: + c.get_logger() + c.logger.debug("zzuflog: %s", zzuf_log.line) + c.logger.info('Command: %s', testcase.cmdline) + + self.cfg.clean_tmpdir() + + sr.increment_seed() + +# # cache objects in case of reboot +# cache_state(self.cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) +# pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) +# cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) +# cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) + +# if new_uniq_crash: +# # we had a hit, so break the inner while() loop +# # so we can pick a new range. This is to avoid +# # having a crash-rich range run away with the +# # probability before other ranges have been tried +# break + + +class Campaign(object): + def __init__(self, cfg_path=None, scriptpath=None): + # Read the cfg file + self.cfg_path = cfg_path + logger.info('Reading config from %s', cfg_path) + self.cfg = cfg_helper.read_config_options(cfg_path) + self.scriptpath = scriptpath + self.seedfile_set = None + self._last_ppid = None + + def __enter__(self): + # set up local logging + self._setup_logfile(self.cfg.local_dir, + log_basename='bff.log', + level=logging.DEBUG, + max_bytes=1e8, + backup_count=3) + + # set up remote logging + self._setup_logfile(self.cfg.output_dir, + log_basename='bff.log', + level=logging.INFO, + max_bytes=1e7, + backup_count=5) + + self._check_for_script() + z.setup_dirs_and_files(self.cfg_path, self.cfg) + start_process_killer(self.scriptpath, self.cfg) + z.set_unbuffered_stdout() + self._create_seedfile_set() + if self.cfg.watchdogtimeout: + self._setup_watchdog() + + # flag to indicate whether this is a fresh script start up or not + self.first_chunk = True + # remember our parent process id so we can tell if it changes later + self._last_ppid = os.getppid() + + return self + + def __exit__(self, etype, value, mytraceback): + handled = False + if etype is CampaignScriptError: + logger.warning("Please configure BFF to fuzz a binary. Exiting...") + handled = True + + return handled + + def _cache_prg(self): + sf = self.seedfile_set.next_item() + # Run the program once to cache it into memory + z.cache_program_once(self.cfg, sf.path) + # Give target time to die + time.sleep(1) + + + def _setup_watchdog(self): + # set up the watchdog timeout within the VM and restart the daemon + watchdog = WatchDog(self.cfg.watchdogfile, + self.cfg.watchdogtimeout) + touch_watchdog_file(self.cfg) + watchdog.go() + + def _create_seedfile_set(self): + logger.info('Building seedfile set') + sfs_logfile = os.path.join(self.cfg.seedfile_output_dir, 'seedfile_set.log') + with SeedfileSet(campaign_id=self.cfg.campaign_id, + originpath=self.cfg.seedfile_origin_dir, + localpath=self.cfg.seedfile_local_dir, + outputpath=self.cfg.seedfile_output_dir, + logfile=sfs_logfile, + ) as sfset: + self.seedfile_set = sfset + + + def _setup_logfile(self, logdir, log_basename='bff.log', level=logging.DEBUG, + max_bytes=1e8, backup_count=5): + ''' + Creates a log file in / at level + @param logdir: the directory where the log file should reside + @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') + @param level: the logging level (defaults to logging.DEBUG) + ''' + filetools.make_directories(logdir) + logfile = os.path.join(logdir, log_basename) + handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) + formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") + add_log_handler(logger, level, handler, formatter) + logger.info('Logging %s at %s', logging.getLevelName(level), logfile) + + def _check_for_script(self): + if self.cfg.program_is_script(): + logger.warning("Target application is a shell script.") + raise CampaignScriptError() + #cfg.disable_verification() + #time.sleep(10) + + def _check_ppid(self): + # check parent process id + _ppid_now = os.getppid() + if not _ppid_now == self._last_ppid: + logger.warning('Parent process ID changed from %d to %d', self._last_ppid, _ppid_now) + self._last_ppid = _ppid_now + + def _do_interval(self, s1, s2): + # interval.go + logger.debug('Starting interval %d-%d', s1, s2) + # wipe the tmp dir clean to try to avoid filling the VM disk + TmpReaper().clean_tmp() + + sf = self.seedfile_set.next_item() + r = sf.rangefinder.next_item() + + logger.info(STATE_TIMER) + + for s in xrange(s1, s2): + with Iteration(seednum=s, seedfile=sf, r=r) as iteration: + iteration.go() + + def go(self): + # campaign.go + cfg = self.cfg + seedfile_set = self.seedfile_set + + for (s1, s2) in Intervals(min=cfg.start_seed, + max=cfg.max_seed, + interval=cfg.seed_interval): + self._do_interval(s1, s2) + def main(): - global START_SEED +# global START_SEED hashes = [] # give up if we don't have a debugger @@ -356,206 +649,9 @@ def main(): local_cfg_file = os.path.expanduser('~/bff.cfg') filetools.copy_file(remote_cfg_file, local_cfg_file) - # Read the cfg file - logger.info('Reading config from %s', local_cfg_file) - cfg = cfg_helper.read_config_options(local_cfg_file) - - # set up local logging - setup_logfile(cfg.local_dir, log_basename='bff.log', level=logging.DEBUG, - max_bytes=1e8, backup_count=3) - - # set up remote logging - setup_logfile(cfg.output_dir, log_basename='bff.log', level=logging.INFO, - max_bytes=1e7, backup_count=5) + with Campaign(cfg_path=local_cfg_file) as campaign: + campaign.go() - try: - check_for_script(cfg) - except: - logger.warning("Please configure BFF to fuzz a binary. Exiting...") - sys.exit() - - z.setup_dirs_and_files(local_cfg_file, cfg) - - # make sure we cache it for the next run -# cache_state(cfg.campaign_id, 'cfg', cfg, cfg.cached_config_file) - - sr = get_cached_state('seedrange', cfg.campaign_id, cfg.cached_seedrange_file) - if not sr: - sr = SeedRange(cfg.start_seed, cfg.seed_interval, cfg.max_seed) - - # set START_SEED global for timestamping - START_SEED = sr.s1 - - start_process_killer(scriptpath, cfg) - - z.set_unbuffered_stdout() - - # set up the seedfile set so we can pick seedfiles for everything else... - seedfile_set = get_cached_state('seedfile_set', cfg.campaign_id, cfg.cached_seedfile_set) - if not seedfile_set: - logger.info('Building seedfile set') - sfs_logfile = os.path.join(cfg.seedfile_output_dir, 'seedfile_set.log') - with SeedfileSet(campaign_id=cfg.campaign_id, - originpath=cfg.seedfile_origin_dir, - localpath=cfg.seedfile_local_dir, - outputpath=cfg.seedfile_output_dir, - logfile=sfs_logfile, - ) as sfset: - seedfile_set = sfset - - # set up the watchdog timeout within the VM and restart the daemon - if cfg.watchdogtimeout: - watchdog = WatchDog(cfg.watchdogfile, cfg.watchdogtimeout) - touch_watchdog_file(cfg) - watchdog.go() - - cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) - - sf = seedfile_set.next_item() - - # Run the program once to cache it into memory - z.cache_program_once(cfg, sf.path) - - # Give target time to die - time.sleep(1) - - # flag to indicate whether this is a fresh script start up or not - first_chunk = True - - # remember our parent process id so we can tell if it changes later - _last_ppid = os.getppid() - - # campaign.go - while sr.in_max_range(): - - # wipe the tmp dir clean to try to avoid filling the VM disk - TmpReaper().clean_tmp() - - sf = seedfile_set.next_item() - - r = sf.rangefinder.next_item() - sr.set_s2() - logger.info(STATE_TIMER) - while sr.in_range(): - # interval.go - logger.debug('Starting interval %d-%d', sr.s1, sr.s2) - - # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot - touch_watchdog_file(cfg) - - # check parent process id - _ppid_now = os.getppid() - if not _ppid_now == _last_ppid: - logger.warning('Parent process ID changed from %d to %d', _last_ppid, _ppid_now) - _last_ppid = _ppid_now - - # do the fuzz - cmdline = cfg.get_command(sf.path) - - if first_chunk: - # disable the --quiet option in zzuf - # on the first chunk only - quiet_flag = False - first_chunk = False - else: - quiet_flag = True - - STATE_TIMER.enter_state('fuzzing') - zzuf = Zzuf(cfg.local_dir, - sr.s1, - sr.s2, - cmdline, - sf.path, - cfg.zzuf_log_file, - cfg.copymode, - r.min, - r.max, - cfg.progtimeout, - quiet_flag, - ) - saw_crash = zzuf.go() - STATE_TIMER.enter_state('checking_results') - - if not saw_crash: - # we must have made it through this chunk without a crash - # so go to next chunk - try_count = sr.s1_s2_delta() - sf.record_tries(tries=try_count) - r.record_tries(tries=try_count) - - # emit a log entry - crashcount = z.get_crashcount(cfg.crashers_dir) - rate = get_rate(sr.s1) - seed_str = "seeds=%d-%d" % (sr.s1, sr.s2) - range_str = "range=%.6f-%.6f" % (r.min, r.max) - rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) - expected_density = seedfile_set.expected_crash_density - xd_str = "expected=%.9f" % expected_density - xr_str = 'expected_rate=%.6f uniq/day' % (expected_density * rate * 86400) - logger.info('Fuzzing %s %s %s %s %s %s crash_count=%d', - sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) - - # set s1 to s2 so that as soon as we continue we'll break out of the sr.in_range() loop - sr.set_s1_to_s2() - continue - - # we must have seen a crash - - # get the results - zzuf_log = ZzufLog(cfg.zzuf_log_file, cfg.zzuf_log_out(sf.output_dir)) - - # Don't generate cases for killed process or out-of-memory - # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will - # report the exit code in its output log. The exit code is 128 + the signal number. - crash_status = zzuf_log.crash_logged(cfg.copymode) - sr.bookmark_s1() - sr.s1 = zzuf_log.seed - - # record the fact that we've made it this far - try_count = sr.s1_delta() - sf.record_tries(tries=try_count) - r.record_tries(tries=try_count) - - new_uniq_crash = False - if crash_status: - logger.info('Generating testcase for %s', zzuf_log.line) - # a true crash - zzuf_range = zzuf_log.range - # create the temp dir for the results - cfg.create_tmpdir() - outfile = cfg.get_testcase_outfile(sf.path, sr.s1) - logger.debug('Output file is %s', outfile) - testcase = zzuf.generate_test_case(sf.path, sr.s1, zzuf_range, outfile) - - # Do internal verification using GDB / Valgrind / Stderr - fuzzedfile = file_handlers.basicfile.BasicFile(outfile) - - with BffCrash(cfg, sf, fuzzedfile, cfg.program, cfg.debugger_timeout, - cfg.killprocname, cfg.backtracelevels, - cfg.crashers_dir, sr.s1, r) as c: - if c.is_crash: - new_uniq_crash = verify_crasher(c, hashes, cfg, seedfile_set) - - # record the zzuf log line for this crash - if not c.logger: - c.get_logger() - c.logger.debug("zzuflog: %s", zzuf_log.line) - c.logger.info('Command: %s', testcase.cmdline) - - cfg.clean_tmpdir() - - sr.increment_seed() - - # cache objects in case of reboot - cache_state(cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) - pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) - cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) - cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) - - if new_uniq_crash: - # we had a hit, so break the inner while() loop - # so we can pick a new range. This is to avoid - # having a crash-rich range run away with the - # probability before other ranges have been tried - break +if __name__ == '__main__': + main() From 1e19c44a2ef5055eca8ae99ff68c30af462c6e5d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 15:25:51 -0500 Subject: [PATCH 0089/1169] new watchdog file toucher class --- src/certfuzz/file_handlers/watchdog_file.py | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/certfuzz/file_handlers/watchdog_file.py diff --git a/src/certfuzz/file_handlers/watchdog_file.py b/src/certfuzz/file_handlers/watchdog_file.py new file mode 100644 index 0000000..1a3bced --- /dev/null +++ b/src/certfuzz/file_handlers/watchdog_file.py @@ -0,0 +1,31 @@ +''' +Created on Feb 12, 2014 + +@author: adh +''' +import os + + +class Twdf(object): + + def __init__(self): + self.func = None + self.wdf = None + self.remote_d = None + + def enable(self): + self.func = self._twdf + + def disable(self): + self.func = self._noop + + def _noop(self, *_args, **_kwargs): + pass + + def _twdf(self): + if os.access(self.remote_d, os.W_OK): + open(self.wdf, 'w').close() + +TWDF = Twdf() + +touch_watchdog_file = TWDF.func From 7591c55fda44de6ad28791876a0e795df2a7e911 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 15:26:15 -0500 Subject: [PATCH 0090/1169] refactor iteration into its own class --- src/certfuzz/iteration/linux.py | 371 ++++++++++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 src/certfuzz/iteration/linux.py diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py new file mode 100644 index 0000000..2cf6a00 --- /dev/null +++ b/src/certfuzz/iteration/linux.py @@ -0,0 +1,371 @@ +''' +Created on Feb 12, 2014 + +@author: adh +''' +import logging + +from .. import file_handlers +from ..analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind +from ..analyzers.callgrind import callgrind +from ..analyzers.callgrind.annotate import annotate_callgrind +from ..analyzers.callgrind.annotate import annotate_callgrind_tree +from ..analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError +from ..analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError +from ..analyzers.errors import AnalyzerEmptyOutputError +from ..crash.bff_crash import BffCrash +from ..debuggers import crashwrangler # @UnusedImport +from ..debuggers import gdb # @UnusedImport +from ..fuzztools import bff_helper as z, filetools, performance +from ..fuzztools.state_timer import STATE_TIMER +from ..fuzztools.zzuf import Zzuf +from ..fuzztools.zzuflog import ZzufLog +from ..minimizer import MinimizerError, UnixMinimizer as Minimizer +import os +from certfuzz.file_handlers.watchdog_file import touch_watchdog_file + +logger = logging.getLogger(__name__) + + +def determine_uniqueness(crash, hashes): + ''' + Gets the crash signature, then compares it against known crashes. + Sets crash.is_unique = True if it is new + ''' + + # short-circuit on crashes with no signature + if not crash.signature: + logger.warning('Crash has no signature, cleaning up') + crash.delete_files() + return + + if crash.signature in hashes: + crash.is_unique = False + return + + # fall back to checking if the crash directory exists + crash_dir_found = filetools.find_or_create_dir(crash.result_dir) + + crash.is_unique = not crash_dir_found + + +def analyze_crasher(cfg, crash): + ''' + Runs multiple analyses and collects data about a crash. Returns a list of other crashes + encountered during the process of analyzing the current crash. + @param cfg: A BFF config object + @param crash: A crash object + @return: a list of Crasher objects + ''' + other_crashers_found = [] + + dbg_out_file_orig = crash.dbg.file + logger.debug('Original debugger file: %s', dbg_out_file_orig) + + if cfg.minimizecrashers: + STATE_TIMER.enter_state('minimize_testcase') + # try to reduce the Hamming Distance between the crasher file and the known good seedfile + # crash.fuzzedfile will be replaced with the minimized result + try: + with Minimizer(cfg=cfg, crash=crash, bitwise=False, + seedfile_as_target=True, confidence=0.999, + tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout + ) as minimizer: + minimizer.go() + other_crashers_found.extend(minimizer.other_crashes.values()) + except MinimizerError, e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) + minimizer = None + + touch_watchdog_file(cfg) + # calculate the hamming distances for this crash + # between the original seedfile and the minimized fuzzed file + crash.calculate_hamming_distances() + + if cfg.minimize_to_string: + STATE_TIMER.enter_state('minimize_testcase_to_string') + # Minimize to a string of 'x's + # crash.fuzzedfile will be replaced with the minimized result + try: + with Minimizer(cfg=cfg, crash=crash, bitwise=False, + seedfile_as_target=False, confidence=0.9, + tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout + ) as min2string: + min2string.go() + other_crashers_found.extend(min2string.other_crashes.values()) + except MinimizerError, e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) + min2string = None + touch_watchdog_file(cfg) + + STATE_TIMER.enter_state('analyze_testcase') + + # get one last debugger output for the newly minimized file + if crash.pc_in_function: + # change the debugger template + crash.set_debugger_template('complete') + else: + # use a debugger template that specifies fixed offsets from $pc for disassembly + crash.set_debugger_template('complete_nofunction') + logger.info('Getting complete debugger output for crash: %s', crash.fuzzedfile.path) + crash.get_debug_output(crash.fuzzedfile.path) + + if dbg_out_file_orig != crash.dbg.file: + # we have a new debugger output + # remove the old one + filetools.delete_files(dbg_out_file_orig) + if os.path.exists(dbg_out_file_orig): + logger.warning('Failed to remove old debugger file %s', dbg_out_file_orig) + else: + logger.debug('Removed old debug file %s', dbg_out_file_orig) + + # use the minimized file for the rest of the analyses + analyzers = [ + stderr.StdErr, + cw_gmalloc.CrashWranglerGmalloc, + ] + if cfg.use_valgrind: + analyzers.extend([ + valgrind.Valgrind, + callgrind.Callgrind, + ]) + if cfg.use_pin_calltrace: + analyzers.extend([ + pin_calltrace.Pin_calltrace, + ]) + + for analyzer in analyzers: + touch_watchdog_file(cfg) + + analyzer_instance = analyzer(cfg, crash) + if analyzer_instance: + try: + analyzer_instance.go() + except AnalyzerEmptyOutputError: + logger.warning('Unexpected empty output from analyzer. Continuing') + + logger.info('Annotating callgrind output') + try: + annotate_callgrind(crash) + annotate_callgrind_tree(crash) + except CallgrindAnnotateEmptyOutputFileError: + logger.warning('Unexpected empty output from annotate_callgrind. Continuing') + except CallgrindAnnotateMissingInputFileError: + logger.warning('Missing callgrind output. Continuing') + + return other_crashers_found + + +def verify_crasher(c, hashes, cfg, seedfile_set): + logger.debug('verifying crash') + found_new_crash = False + + crashes = [] + crashes.append(c) + + for crash in crashes: + # loop until we're out of crashes to verify + logger.debug('crashes to verify: %d', len(crashes)) + STATE_TIMER.enter_state('verify_testcase') + + # crashes may be added as a result of minimization + crash.is_unique = False + determine_uniqueness(crash, hashes) + crash.get_logger() + if crash.is_unique: + hashes.append(crash) + # only toggle it once + if not found_new_crash: + found_new_crash = True + + logger.debug("%s did not exist in cache, crash is unique", crash.signature) + more_crashes = analyze_crasher(cfg, crash) + + if cfg.recycle_crashers: + logger.debug('Recycling crash as seedfile') + iterstring = crash.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + crash.seedfile.md5 + '-' + iterstring + crash.seedfile.ext + crasherseed_path = os.path.join(cfg.seedfile_origin_dir, crasherseedname) + filetools.copy_file(crash.fuzzedfile.path, crasherseed_path) + seedfile_set.add_file(crasherseed_path) + # add new crashes to the queue + crashes.extend(more_crashes) + crash.copy_files() + + uniqlogger = get_uniq_logger(cfg.uniq_log) + uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', crash.seedfile.basename, crash.signature, crash.seednum, crash.range, crash.hd_bits, crash.hd_bytes) + logger.info('%s first seen at %d', crash.signature, crash.seednum) + else: + logger.debug('%s was found, not unique', crash.signature) + # always clean up after yourself + crash.clean_tmpdir() + + # clean up + crash.delete_files() + # whether it was unique or not, record some details for posterity + # record the details of this crash so we can regenerate it later if needed + crash.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', crash.seedfile.basename, crash.seednum, crash.range, crash.fuzzedfile.path) + crash.logger.info('PC=%s', crash.pc) + + # score this crash for the seedfile + crash.seedfile.record_success(crash.signature, tries=0) + if crash.range: + # ...and for the range + crash.range.record_success(crash.signature, tries=0) + + return found_new_crash + + +class Iteration(object): + def __init__(self, cfg=None, seednum=None, seedfile=None, r=None): + self.cfg = cfg + self.seednum = seednum + self.seedfile = seedfile + self.r = r + + # convenience aliases + self.s1 = self.seednum + self.s2 = self.s1 + self.sf = self.seedfile + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + pass + + def _fuzz_and_run(self, quiet_flag): + if self.first_chunk: + # disable the --quiet option in zzuf + # on the first chunk only + quiet_flag = False + self.first_chunk = False + else: + quiet_flag = True + + # do the fuzz + cmdline = self.cfg.get_command(self.sf.path) + + STATE_TIMER.enter_state('fuzzing') + zzuf = Zzuf(self.cfg.local_dir, self.s1, + self.s2, + cmdline, + self.sf.path, + self.cfg.zzuf_log_file, + self.cfg.copymode, + self.r.min, + self.r.max, + self.cfg.progtimeout, + quiet_flag) + saw_crash = zzuf.go() + return saw_crash, zzuf + + def _log(self): +# # emit a log entry + crashcount = z.get_crashcount(self.cfg.crashers_dir) +# rate = get_rate(self.s1) +# seed_str = "seeds=%d-%d" % (self.s1, self.s2) +# range_str = "range=%.6f-%.6f" % (self.r.min, self.r.max) +# rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) +# expected_density = self.seedfile_set.expected_crash_density +# xd_str = "expected=%.9f" % expected_density +# xr_str = 'expected_rate=%.6f uniq/day' % (expected_density * rate * 86400) +# logger.info('Fuzzing %s %s %s %s %s %s crash_count=%d', +# self.sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) + logger.info('Fuzzing...crash_count=%d', crashcount) + + def go2(self): + self._fuzz() + self._run() + for testcase in self.candidates: + self.verify(testcase) + + for testcase in self.verified: + self.analyze(testcase) + + for testcase in self.analyzed: + self.construct_report(testcase) + + def _process_crash(self): + pass + + + + def go(self): + sf = self.seedfile + + self._check_ppid() + + saw_crash, zzuf = self._fuzz_and_run(quiet_flag) + STATE_TIMER.enter_state('checking_results') + + if not saw_crash: + # we must have made it through this chunk without a crash + # so go to next chunk + self.sf.record_tries(tries=1) + self.r.record_tries(tries=1) + self._log() + return + + # we must have seen a crash + # get the results + zzuf_log = ZzufLog(self.cfg.zzuf_log_file, self.cfg.zzuf_log_out(self.sf.output_dir)) + + # Don't generate cases for killed process or out-of-memory + # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will + # report the exit code in its output log. The exit code is 128 + the signal number. + crash_status = zzuf_log.crash_logged(self.cfg.copymode) + + # sr.bookmark_s1() + self.s1_old = self.s1 + + self.s1 = zzuf_log.seed + + # record the fact that we've made it this far + try_count = self.s1_delta() + self.sf.record_tries(tries=try_count) + self.r.record_tries(tries=try_count) + + new_uniq_crash = False + if crash_status: + logger.info('Generating testcase for %s', zzuf_log.line) + # a true crash + zzuf_range = zzuf_log.range + # create the temp dir for the results + self.cfg.create_tmpdir() + outfile = self.cfg.get_testcase_outfile(sf.path, self.s1) + logger.debug('Output file is %s', outfile) + testcase = zzuf.generate_test_case(sf.path, self.s1, zzuf_range, outfile) + + # Do internal verification using GDB / Valgrind / Stderr + fuzzedfile = file_handlers.basicfile.BasicFile(outfile) + + with BffCrash(self.cfg, sf, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, + self.cfg.killprocname, self.cfg.backtracelevels, + self.cfg.crashers_dir, self.s1, self.r) as c: + if c.is_crash: + new_uniq_crash = verify_crasher(c, hashes, self.cfg, seedfile_set) + + # record the zzuf log line for this crash + if not c.logger: + c.get_logger() + c.logger.debug("zzuflog: %s", zzuf_log.line) + c.logger.info('Command: %s', testcase.cmdline) + + self.cfg.clean_tmpdir() + + # incrementing seed number is the campaign's job, not ours +# sr.increment_seed() + +# # cache objects in case of reboot +# cache_state(self.cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) +# pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) +# cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) +# cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) + +# if new_uniq_crash: +# # we had a hit, so break the inner while() loop +# # so we can pick a new range. This is to avoid +# # having a crash-rich range run away with the +# # probability before other ranges have been tried +# break From 5e9d6939641461b5f36aa9ecf10ea3bfc929d4db Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 15:26:38 -0500 Subject: [PATCH 0091/1169] refactor campaign into its own module --- src/certfuzz/campaign/errors.py | 4 + src/certfuzz/campaign/linux.py | 180 ++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 src/certfuzz/campaign/linux.py diff --git a/src/certfuzz/campaign/errors.py b/src/certfuzz/campaign/errors.py index bc425d2..a621ff1 100644 --- a/src/certfuzz/campaign/errors.py +++ b/src/certfuzz/campaign/errors.py @@ -12,3 +12,7 @@ class CampaignError(CERTFuzzError): class AndroidCampaignError(CampaignError): pass + + +class CampaignScriptError(CampaignError): + pass diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py new file mode 100644 index 0000000..7737daa --- /dev/null +++ b/src/certfuzz/campaign/linux.py @@ -0,0 +1,180 @@ +''' +Created on Feb 12, 2014 + +@author: adh +''' + +import logging +from logging.handlers import RotatingFileHandler +from optparse import OptionParser +import os +import platform +import sys +import time + +from .. import debuggers, file_handlers +from ..analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind +from ..analyzers.callgrind import callgrind +from ..analyzers.callgrind.annotate import annotate_callgrind +from ..analyzers.callgrind.annotate import annotate_callgrind_tree +from ..analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError +from ..analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError +from ..analyzers.errors import AnalyzerEmptyOutputError +from ..campaign.config import bff_config as cfg_helper +from ..crash.bff_crash import BffCrash +from ..debuggers import crashwrangler # @UnusedImport +from ..debuggers import gdb # @UnusedImport +from ..file_handlers.seedfile_set import SeedfileSet +from ..file_handlers.tmp_reaper import TmpReaper +from ..fuzztools import bff_helper as z, filetools, performance +from ..fuzztools.object_caching import cache_state, get_cached_state +from ..fuzztools.process_killer import ProcessKiller +from ..fuzztools.seedrange import SeedRange +from ..fuzztools.state_timer import STATE_TIMER +from ..fuzztools.watchdog import WatchDog +from ..fuzztools.zzuf import Zzuf +from ..fuzztools.zzuflog import ZzufLog +from ..minimizer import MinimizerError, UnixMinimizer as Minimizer +from certfuzz.iteration.linux import Iteration +from certfuzz.campaign.errors import CampaignScriptError +from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file + + +logger = logging.getLogger(__name__) + + +class Campaign(object): + def __init__(self, cfg_path=None, scriptpath=None): + # Read the cfg file + self.cfg_path = cfg_path + logger.info('Reading config from %s', cfg_path) + self.cfg = cfg_helper.read_config_options(cfg_path) + self.scriptpath = scriptpath + self.seedfile_set = None + self._last_ppid = None + self.hashes = [] + + def __enter__(self): + # set up local logging + self._setup_logfile(self.cfg.local_dir, + log_basename='bff.log', + level=logging.DEBUG, + max_bytes=1e8, + backup_count=3) + + # set up remote logging + self._setup_logfile(self.cfg.output_dir, + log_basename='bff.log', + level=logging.INFO, + max_bytes=1e7, + backup_count=5) + + self._check_for_script() + z.setup_dirs_and_files(self.cfg_path, self.cfg) + self.start_process_killer(self.scriptpath, self.cfg) + z.set_unbuffered_stdout() + self._create_seedfile_set() + if self.cfg.watchdogtimeout: + self._setup_watchdog() + + # flag to indicate whether this is a fresh script start up or not + self.first_chunk = True + # remember our parent process id so we can tell if it changes later + self._last_ppid = os.getppid() + + return self + + def __exit__(self, etype, value, mytraceback): + handled = False + if etype is CampaignScriptError: + logger.warning("Please configure BFF to fuzz a binary. Exiting...") + handled = True + + return handled + + def start_process_killer(self): + cfg = self.cfg + # set up and spawn the process killer + killscript = cfg.get_killscript_path(self.scriptpath) + ProcessKiller(killscript, cfg.killprocname, cfg.killproctimeout).go() + logger.debug("Process killer started: %s %s %d", killscript, cfg.killprocname, cfg.killproctimeout) + + def _cache_prg(self): + sf = self.seedfile_set.next_item() + # Run the program once to cache it into memory + z.cache_program_once(self.cfg, sf.path) + # Give target time to die + time.sleep(1) + + def touch_watchdog_file(self): + if self.cfg.watchdogtimeout: + # this one just checks the permission + if os.access(self.cfg.remote_dir, os.W_OK): + # equivalent to 'touch cfg.watchdogfile' + open(self.cfg.watchdogfile, 'w').close() + + def _setup_watchdog(self): + # set up the watchdog timeout within the VM and restart the daemon + watchdog = WatchDog(self.cfg.watchdogfile, + self.cfg.watchdogtimeout) + if self.cfg.watchdogtimeout: + TWDF.remote_d = self.cfg.remote_dir + TWDF.wdf = self.cfg.watchdogfile + TWDF.enable() + + touch_watchdog_file() + watchdog.go() + + def _create_seedfile_set(self): + logger.info('Building seedfile set') + sfs_logfile = os.path.join(self.cfg.seedfile_output_dir, 'seedfile_set.log') + with SeedfileSet(campaign_id=self.cfg.campaign_id, + originpath=self.cfg.seedfile_origin_dir, + localpath=self.cfg.seedfile_local_dir, + outputpath=self.cfg.seedfile_output_dir, + logfile=sfs_logfile, + ) as sfset: + self.seedfile_set = sfset + + + def _check_for_script(self): + if self.cfg.program_is_script(): + logger.warning("Target application is a shell script.") + raise CampaignScriptError() + #cfg.disable_verification() + #time.sleep(10) + + def _check_ppid(self): + # check parent process id + _ppid_now = os.getppid() + if not _ppid_now == self._last_ppid: + logger.warning('Parent process ID changed from %d to %d', self._last_ppid, _ppid_now) + self._last_ppid = _ppid_now + + def _do_interval(self): + s1 = self.s1 + s2 = self.s2 + # interval.go + logger.debug('Starting interval %d-%d', s1, s2) + # wipe the tmp dir clean to try to avoid filling the VM disk + TmpReaper().clean_tmp() + + sf = self.seedfile_set.next_item() + r = sf.rangefinder.next_item() + + logger.info(STATE_TIMER) + + for s in xrange(s1, s2): + # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot + self.touch_watchdog_file() + with Iteration(seednum=s, seedfile=sf, r=r) as iteration: + iteration.go() + + def go(self): + # campaign.go + cfg = self.cfg + + for s in xrange(cfg.start_seed, cfg.max_seed, cfg.seed_interval): + self.s1 = s + self.s2 = s + cfg.seed_interval + self._do_interval() From 885a2722b531aa84438dd9c91ae903ea8699c4d3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 15:27:00 -0500 Subject: [PATCH 0092/1169] make state timer a singleton --- src/certfuzz/fuzztools/state_timer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/fuzztools/state_timer.py b/src/certfuzz/fuzztools/state_timer.py index babdfef..31f13df 100644 --- a/src/certfuzz/fuzztools/state_timer.py +++ b/src/certfuzz/fuzztools/state_timer.py @@ -54,3 +54,5 @@ def time_in(self, state): return self.timers[state] else: return 0.0 + +STATE_TIMER = StateTimer() From 2a2fb70bef82d88b2b130c10dd822eb5684dff30 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 15:27:16 -0500 Subject: [PATCH 0093/1169] refactor a bunch of stuff into separate modules --- src/certfuzz/bff/linux.py | 573 ++------------------------------------ 1 file changed, 25 insertions(+), 548 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index cfbddb5..cd84b85 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -11,31 +11,13 @@ import os import platform import sys -import time - -from .. import debuggers, file_handlers -from ..analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind -from ..analyzers.callgrind import callgrind -from ..analyzers.callgrind.annotate import annotate_callgrind -from ..analyzers.callgrind.annotate import annotate_callgrind_tree -from ..analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError -from ..analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError -from ..analyzers.errors import AnalyzerEmptyOutputError -from ..campaign.config import bff_config as cfg_helper -from ..crash.bff_crash import BffCrash + from ..debuggers import crashwrangler # @UnusedImport from ..debuggers import gdb # @UnusedImport from ..file_handlers.seedfile_set import SeedfileSet from ..file_handlers.tmp_reaper import TmpReaper from ..fuzztools import bff_helper as z, filetools, performance -from ..fuzztools.object_caching import cache_state, get_cached_state -from ..fuzztools.process_killer import ProcessKiller -from ..fuzztools.seedrange import SeedRange -from ..fuzztools.state_timer import StateTimer -from ..fuzztools.watchdog import WatchDog -from ..fuzztools.zzuf import Zzuf -from ..fuzztools.zzuflog import ZzufLog -from ..minimizer import MinimizerError, UnixMinimizer as Minimizer +from certfuzz.campaign.linux import Campaign DEBUG = True @@ -45,8 +27,6 @@ #SEED_TS = performance.TimeStamper() #START_SEED = 0 -STATE_TIMER = StateTimer() - logger = logging.getLogger() logger.name = 'bff' logger.setLevel(0) @@ -66,223 +46,6 @@ def get_uniq_logger(logfile): return l -def determine_uniqueness(crash, hashes): - ''' - Gets the crash signature, then compares it against known crashes. - Sets crash.is_unique = True if it is new - ''' - - # short-circuit on crashes with no signature - if not crash.signature: - logger.warning('Crash has no signature, cleaning up') - crash.delete_files() - return - - if crash.signature in hashes: - crash.is_unique = False - return - - # fall back to checking if the crash directory exists - crash_dir_found = filetools.find_or_create_dir(crash.result_dir) - - crash.is_unique = not crash_dir_found - - -def analyze_crasher(cfg, crash): - ''' - Runs multiple analyses and collects data about a crash. Returns a list of other crashes - encountered during the process of analyzing the current crash. - @param cfg: A BFF config object - @param crash: A crash object - @return: a list of Crasher objects - ''' - other_crashers_found = [] - - dbg_out_file_orig = crash.dbg.file - logger.debug('Original debugger file: %s', dbg_out_file_orig) - - if cfg.minimizecrashers: - STATE_TIMER.enter_state('minimize_testcase') - # try to reduce the Hamming Distance between the crasher file and the known good seedfile - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=cfg, crash=crash, bitwise=False, - seedfile_as_target=True, confidence=0.999, - tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout - ) as minimizer: - minimizer.go() - other_crashers_found.extend(minimizer.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) - minimizer = None - - touch_watchdog_file(cfg) - # calculate the hamming distances for this crash - # between the original seedfile and the minimized fuzzed file - crash.calculate_hamming_distances() - - if cfg.minimize_to_string: - STATE_TIMER.enter_state('minimize_testcase_to_string') - # Minimize to a string of 'x's - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=cfg, crash=crash, bitwise=False, - seedfile_as_target=False, confidence=0.9, - tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout - ) as min2string: - min2string.go() - other_crashers_found.extend(min2string.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) - min2string = None - touch_watchdog_file(cfg) - - STATE_TIMER.enter_state('analyze_testcase') - - # get one last debugger output for the newly minimized file - if crash.pc_in_function: - # change the debugger template - crash.set_debugger_template('complete') - else: - # use a debugger template that specifies fixed offsets from $pc for disassembly - crash.set_debugger_template('complete_nofunction') - logger.info('Getting complete debugger output for crash: %s', crash.fuzzedfile.path) - crash.get_debug_output(crash.fuzzedfile.path) - - if dbg_out_file_orig != crash.dbg.file: - # we have a new debugger output - # remove the old one - filetools.delete_files(dbg_out_file_orig) - if os.path.exists(dbg_out_file_orig): - logger.warning('Failed to remove old debugger file %s', dbg_out_file_orig) - else: - logger.debug('Removed old debug file %s', dbg_out_file_orig) - - # use the minimized file for the rest of the analyses - analyzers = [ - stderr.StdErr, - cw_gmalloc.CrashWranglerGmalloc, - ] - if cfg.use_valgrind: - analyzers.extend([ - valgrind.Valgrind, - callgrind.Callgrind, - ]) - if cfg.use_pin_calltrace: - analyzers.extend([ - pin_calltrace.Pin_calltrace, - ]) - - for analyzer in analyzers: - touch_watchdog_file(cfg) - - analyzer_instance = analyzer(cfg, crash) - if analyzer_instance: - try: - analyzer_instance.go() - except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from analyzer. Continuing') - - logger.info('Annotating callgrind output') - try: - annotate_callgrind(crash) - annotate_callgrind_tree(crash) - except CallgrindAnnotateEmptyOutputFileError: - logger.warning('Unexpected empty output from annotate_callgrind. Continuing') - except CallgrindAnnotateMissingInputFileError: - logger.warning('Missing callgrind output. Continuing') - - return other_crashers_found - - -def verify_crasher(c, hashes, cfg, seedfile_set): - logger.debug('verifying crash') - found_new_crash = False - - crashes = [] - crashes.append(c) - - for crash in crashes: - # loop until we're out of crashes to verify - logger.debug('crashes to verify: %d', len(crashes)) - STATE_TIMER.enter_state('verify_testcase') - - # crashes may be added as a result of minimization - crash.is_unique = False - determine_uniqueness(crash, hashes) - crash.get_logger() - if crash.is_unique: - hashes.append(crash) - # only toggle it once - if not found_new_crash: - found_new_crash = True - - logger.debug("%s did not exist in cache, crash is unique", crash.signature) - more_crashes = analyze_crasher(cfg, crash) - - if cfg.recycle_crashers: - logger.debug('Recycling crash as seedfile') - iterstring = crash.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + crash.seedfile.md5 + '-' + iterstring + crash.seedfile.ext - crasherseed_path = os.path.join(cfg.seedfile_origin_dir, crasherseedname) - filetools.copy_file(crash.fuzzedfile.path, crasherseed_path) - seedfile_set.add_file(crasherseed_path) - # add new crashes to the queue - crashes.extend(more_crashes) - crash.copy_files() - - uniqlogger = get_uniq_logger(cfg.uniq_log) - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', crash.seedfile.basename, crash.signature, crash.seednum, crash.range, crash.hd_bits, crash.hd_bytes) - logger.info('%s first seen at %d', crash.signature, crash.seednum) - else: - logger.debug('%s was found, not unique', crash.signature) - # always clean up after yourself - crash.clean_tmpdir() - - # clean up - crash.delete_files() - # whether it was unique or not, record some details for posterity - # record the details of this crash so we can regenerate it later if needed - crash.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', crash.seedfile.basename, crash.seednum, crash.range, crash.fuzzedfile.path) - crash.logger.info('PC=%s', crash.pc) - - # score this crash for the seedfile - crash.seedfile.record_success(crash.signature, tries=0) - if crash.range: - # ...and for the range - crash.range.record_success(crash.signature, tries=0) - - return found_new_crash - - -def check_for_script(cfg): - if cfg.program_is_script(): - logger.warning("Target application is a shell script.") - raise - #cfg.disable_verification() - #time.sleep(10) - - -def touch_watchdog_file(cfg): - if cfg.watchdogtimeout: - # this one just checks the permission - if os.access(cfg.remote_dir, os.W_OK): - # equivalent to 'touch cfg.watchdogfile' - open(cfg.watchdogfile, 'w').close() - - -def start_process_killer(scriptpath, cfg): - # set up and spawn the process killer - killscript = cfg.get_killscript_path(scriptpath) - ProcessKiller(killscript, cfg.killprocname, cfg.killproctimeout).go() - logger.debug("Process killer started: %s %s %d", killscript, cfg.killprocname, cfg.killproctimeout) - - -def add_log_handler(log_obj, level, hdlr, formatter): - hdlr.setLevel(level) - hdlr.setFormatter(formatter) - log_obj.addHandler(hdlr) - def setup_logging_to_console(log_obj, level): hdlr = logging.StreamHandler() @@ -290,20 +53,20 @@ def setup_logging_to_console(log_obj, level): add_log_handler(log_obj, level, hdlr, formatter) -#def setup_logfile(logdir, log_basename='bff.log', level=logging.DEBUG, -# max_bytes=1e8, backup_count=5): -# ''' -# Creates a log file in / at level -# @param logdir: the directory where the log file should reside -# @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') -# @param level: the logging level (defaults to logging.DEBUG) -# ''' -# filetools.make_directories(logdir) -# logfile = os.path.join(logdir, log_basename) -# handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) -# formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") -# add_log_handler(logger, level, handler, formatter) -# logger.info('Logging %s at %s', logging.getLevelName(level), logfile) +def setup_logfile(logdir, log_basename='bff.log', level=logging.DEBUG, + max_bytes=1e8, backup_count=5): + ''' + Creates a log file in / at level + @param logdir: the directory where the log file should reside + @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') + @param level: the logging level (defaults to logging.DEBUG) + ''' + filetools.make_directories(logdir) + logfile = os.path.join(logdir, log_basename) + handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) + formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") + add_log_handler(logger, level, handler, formatter) + logger.info('Logging %s at %s', logging.getLevelName(level), logfile) def get_config_file(basedir): @@ -322,308 +85,22 @@ def get_config_file(basedir): return config_file -class Intervals(object): - def __init__(self, min=0, max=1e10, interval=1): - self.min = min - self.max = max - self.interval = interval - self.curr_pos = self.min - - def __iter__(self): - start = self.curr_pos - end = self.curr_pos + self.interval - self.curr_pos += self.interval - return xrange(start, end) - - -class CampaignScriptError(Exception): - pass - - -class Iteration(object): - def __init__(self, cfg=None, seednum=None, seedfile=None, r=None): - self.cfg = cfg - self.seednum = seednum - self.seedfile = seedfile - self.r = r - - # convenience aliases - self.s1 = self.seednum - self.s2 = self.s1 - self.sf = self.seedfile - - def __enter__(self): - return self - - def __exit__(self, etype, value, traceback): - pass - - def _fuzz_and_run(self, quiet_flag): - if self.first_chunk: - # disable the --quiet option in zzuf - # on the first chunk only - quiet_flag = False - self.first_chunk = False - else: - quiet_flag = True - - # do the fuzz - cmdline = self.cfg.get_command(self.sf.path) - - STATE_TIMER.enter_state('fuzzing') - zzuf = Zzuf(self.cfg.local_dir, self.s1, - self.s2, - cmdline, - self.sf.path, - self.cfg.zzuf_log_file, - self.cfg.copymode, - self.r.min, - self.r.max, - self.cfg.progtimeout, - quiet_flag) - saw_crash = zzuf.go() - return saw_crash, zzuf - - def _log(self): -# # emit a log entry - crashcount = z.get_crashcount(self.cfg.crashers_dir) -# rate = get_rate(self.s1) -# seed_str = "seeds=%d-%d" % (self.s1, self.s2) -# range_str = "range=%.6f-%.6f" % (self.r.min, self.r.max) -# rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) -# expected_density = self.seedfile_set.expected_crash_density -# xd_str = "expected=%.9f" % expected_density -# xr_str = 'expected_rate=%.6f uniq/day' % (expected_density * rate * 86400) -# logger.info('Fuzzing %s %s %s %s %s %s crash_count=%d', -# self.sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) - logger.info('Fuzzing...crash_count=%d', crashcount) - - def go2(self): - self._fuzz() - self._run() - for testcase in self.candidates: - self.verify(testcase) - - for testcase in self.verified: - self.analyze(testcase) - - for testcase in self.analyzed: - self.construct_report(testcase) - - def go(self): - # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot - touch_watchdog_file(self.cfg) - self._check_ppid() - - saw_crash, zzuf = self._fuzz_and_run(quiet_flag) - STATE_TIMER.enter_state('checking_results') - - if not saw_crash: - # we must have made it through this chunk without a crash - # so go to next chunk - self.sf.record_tries(tries=1) - self.r.record_tries(tries=1) - self._log() - return - - # we must have seen a crash - # get the results - zzuf_log = ZzufLog(self.cfg.zzuf_log_file, self.cfg.zzuf_log_out(self.sf.output_dir)) - - # Don't generate cases for killed process or out-of-memory - # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will - # report the exit code in its output log. The exit code is 128 + the signal number. - crash_status = zzuf_log.crash_logged(self.cfg.copymode) - sr.bookmark_s1() - self.s1 = zzuf_log.seed - - # record the fact that we've made it this far - try_count = self.s1_delta() - self.sf.record_tries(tries=try_count) - self.r.record_tries(tries=try_count) - - new_uniq_crash = False - if crash_status: - logger.info('Generating testcase for %s', zzuf_log.line) - # a true crash - zzuf_range = zzuf_log.range - # create the temp dir for the results - self.cfg.create_tmpdir() - outfile = self.cfg.get_testcase_outfile(sf.path, sr.s1) - logger.debug('Output file is %s', outfile) - testcase = zzuf.generate_test_case(sf.path, sr.s1, zzuf_range, outfile) - - # Do internal verification using GDB / Valgrind / Stderr - fuzzedfile = file_handlers.basicfile.BasicFile(outfile) - - with BffCrash(self.cfg, sf, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, - self.cfg.killprocname, self.cfg.backtracelevels, - self.cfg.crashers_dir, sr.s1, r) as c: - if c.is_crash: - new_uniq_crash = verify_crasher(c, hashes, self.cfg, seedfile_set) - - # record the zzuf log line for this crash - if not c.logger: - c.get_logger() - c.logger.debug("zzuflog: %s", zzuf_log.line) - c.logger.info('Command: %s', testcase.cmdline) - - self.cfg.clean_tmpdir() - - sr.increment_seed() - -# # cache objects in case of reboot -# cache_state(self.cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) -# pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) -# cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) -# cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) - -# if new_uniq_crash: -# # we had a hit, so break the inner while() loop -# # so we can pick a new range. This is to avoid -# # having a crash-rich range run away with the -# # probability before other ranges have been tried -# break - - -class Campaign(object): - def __init__(self, cfg_path=None, scriptpath=None): - # Read the cfg file - self.cfg_path = cfg_path - logger.info('Reading config from %s', cfg_path) - self.cfg = cfg_helper.read_config_options(cfg_path) - self.scriptpath = scriptpath - self.seedfile_set = None - self._last_ppid = None - - def __enter__(self): - # set up local logging - self._setup_logfile(self.cfg.local_dir, - log_basename='bff.log', - level=logging.DEBUG, - max_bytes=1e8, - backup_count=3) - - # set up remote logging - self._setup_logfile(self.cfg.output_dir, - log_basename='bff.log', - level=logging.INFO, - max_bytes=1e7, - backup_count=5) - - self._check_for_script() - z.setup_dirs_and_files(self.cfg_path, self.cfg) - start_process_killer(self.scriptpath, self.cfg) - z.set_unbuffered_stdout() - self._create_seedfile_set() - if self.cfg.watchdogtimeout: - self._setup_watchdog() - - # flag to indicate whether this is a fresh script start up or not - self.first_chunk = True - # remember our parent process id so we can tell if it changes later - self._last_ppid = os.getppid() - - return self - - def __exit__(self, etype, value, mytraceback): - handled = False - if etype is CampaignScriptError: - logger.warning("Please configure BFF to fuzz a binary. Exiting...") - handled = True - - return handled - - def _cache_prg(self): - sf = self.seedfile_set.next_item() - # Run the program once to cache it into memory - z.cache_program_once(self.cfg, sf.path) - # Give target time to die - time.sleep(1) - - - def _setup_watchdog(self): - # set up the watchdog timeout within the VM and restart the daemon - watchdog = WatchDog(self.cfg.watchdogfile, - self.cfg.watchdogtimeout) - touch_watchdog_file(self.cfg) - watchdog.go() - - def _create_seedfile_set(self): - logger.info('Building seedfile set') - sfs_logfile = os.path.join(self.cfg.seedfile_output_dir, 'seedfile_set.log') - with SeedfileSet(campaign_id=self.cfg.campaign_id, - originpath=self.cfg.seedfile_origin_dir, - localpath=self.cfg.seedfile_local_dir, - outputpath=self.cfg.seedfile_output_dir, - logfile=sfs_logfile, - ) as sfset: - self.seedfile_set = sfset - - - def _setup_logfile(self, logdir, log_basename='bff.log', level=logging.DEBUG, - max_bytes=1e8, backup_count=5): - ''' - Creates a log file in / at level - @param logdir: the directory where the log file should reside - @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') - @param level: the logging level (defaults to logging.DEBUG) - ''' - filetools.make_directories(logdir) - logfile = os.path.join(logdir, log_basename) - handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) - formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") - add_log_handler(logger, level, handler, formatter) - logger.info('Logging %s at %s', logging.getLevelName(level), logfile) - - def _check_for_script(self): - if self.cfg.program_is_script(): - logger.warning("Target application is a shell script.") - raise CampaignScriptError() - #cfg.disable_verification() - #time.sleep(10) - - def _check_ppid(self): - # check parent process id - _ppid_now = os.getppid() - if not _ppid_now == self._last_ppid: - logger.warning('Parent process ID changed from %d to %d', self._last_ppid, _ppid_now) - self._last_ppid = _ppid_now - - def _do_interval(self, s1, s2): - # interval.go - logger.debug('Starting interval %d-%d', s1, s2) - # wipe the tmp dir clean to try to avoid filling the VM disk - TmpReaper().clean_tmp() - - sf = self.seedfile_set.next_item() - r = sf.rangefinder.next_item() - - logger.info(STATE_TIMER) - - for s in xrange(s1, s2): - with Iteration(seednum=s, seedfile=sf, r=r) as iteration: - iteration.go() - - def go(self): - # campaign.go - cfg = self.cfg - seedfile_set = self.seedfile_set - - for (s1, s2) in Intervals(min=cfg.start_seed, - max=cfg.max_seed, - interval=cfg.seed_interval): - self._do_interval(s1, s2) + +def add_log_handler(log_obj, level, hdlr, formatter): + hdlr.setLevel(level) + hdlr.setFormatter(formatter) + log_obj.addHandler(hdlr) def main(): # global START_SEED - hashes = [] +# hashes = [] - # give up if we don't have a debugger - debuggers.verify_supported_platform() +# # give up if we don't have a debugger +# debuggers.verify_supported_platform() setup_logging_to_console(logger, logging.INFO) + setup_logfile() logger.info("Welcome to BFF!") scriptpath = os.path.dirname(sys.argv[0]) From a5534356d60dc00040bb8db477da46622b739776 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 15:30:28 -0500 Subject: [PATCH 0094/1169] clean up imports & comment --- src/certfuzz/bff/linux.py | 5 +---- src/certfuzz/campaign/linux.py | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index cd84b85..890c5b3 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -14,9 +14,7 @@ from ..debuggers import crashwrangler # @UnusedImport from ..debuggers import gdb # @UnusedImport -from ..file_handlers.seedfile_set import SeedfileSet -from ..file_handlers.tmp_reaper import TmpReaper -from ..fuzztools import bff_helper as z, filetools, performance +from ..fuzztools import filetools from certfuzz.campaign.linux import Campaign @@ -46,7 +44,6 @@ def get_uniq_logger(logfile): return l - def setup_logging_to_console(log_obj, level): hdlr = logging.StreamHandler() formatter = logging.Formatter('%(name)s %(message)s') diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 7737daa..bba09d8 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -118,6 +118,7 @@ def _setup_watchdog(self): watchdog = WatchDog(self.cfg.watchdogfile, self.cfg.watchdogtimeout) if self.cfg.watchdogtimeout: + # setup our watchdog file toucher TWDF.remote_d = self.cfg.remote_dir TWDF.wdf = self.cfg.watchdogfile TWDF.enable() From 5f4c637cab98e4cbfef9056997159a3f2a07fab3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 15:31:42 -0500 Subject: [PATCH 0095/1169] clean up dead code. --- src/certfuzz/campaign/linux.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index bba09d8..9f93c1d 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -106,22 +106,14 @@ def _cache_prg(self): # Give target time to die time.sleep(1) - def touch_watchdog_file(self): - if self.cfg.watchdogtimeout: - # this one just checks the permission - if os.access(self.cfg.remote_dir, os.W_OK): - # equivalent to 'touch cfg.watchdogfile' - open(self.cfg.watchdogfile, 'w').close() - def _setup_watchdog(self): # set up the watchdog timeout within the VM and restart the daemon watchdog = WatchDog(self.cfg.watchdogfile, self.cfg.watchdogtimeout) - if self.cfg.watchdogtimeout: - # setup our watchdog file toucher - TWDF.remote_d = self.cfg.remote_dir - TWDF.wdf = self.cfg.watchdogfile - TWDF.enable() + # setup our watchdog file toucher + TWDF.remote_d = self.cfg.remote_dir + TWDF.wdf = self.cfg.watchdogfile + TWDF.enable() touch_watchdog_file() watchdog.go() From 1dc06f46211d25480559fc8e1e94214fe392463b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 16:00:02 -0500 Subject: [PATCH 0096/1169] continued refactoring --- src/certfuzz/iteration/linux.py | 192 ++++++++++++++++++++------------ 1 file changed, 119 insertions(+), 73 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 2cf6a00..798c251 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -16,7 +16,7 @@ from ..crash.bff_crash import BffCrash from ..debuggers import crashwrangler # @UnusedImport from ..debuggers import gdb # @UnusedImport -from ..fuzztools import bff_helper as z, filetools, performance +from ..fuzztools import bff_helper as z, filetools from ..fuzztools.state_timer import STATE_TIMER from ..fuzztools.zzuf import Zzuf from ..fuzztools.zzuflog import ZzufLog @@ -77,7 +77,7 @@ def analyze_crasher(cfg, crash): logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) minimizer = None - touch_watchdog_file(cfg) + touch_watchdog_file() # calculate the hamming distances for this crash # between the original seedfile and the minimized fuzzed file crash.calculate_hamming_distances() @@ -96,7 +96,7 @@ def analyze_crasher(cfg, crash): except MinimizerError, e: logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) min2string = None - touch_watchdog_file(cfg) + touch_watchdog_file() STATE_TIMER.enter_state('analyze_testcase') @@ -135,7 +135,7 @@ def analyze_crasher(cfg, crash): ]) for analyzer in analyzers: - touch_watchdog_file(cfg) + touch_watchdog_file() analyzer_instance = analyzer(cfg, crash) if analyzer_instance: @@ -229,12 +229,76 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None): self.sf = self.seedfile def __enter__(self): + self._check_ppid() + return self def __exit__(self, etype, value, traceback): + self.cfg.clean_tmpdir() + + def _log(self): +# # emit a log entry + crashcount = z.get_crashcount(self.cfg.crashers_dir) +# rate = get_rate(self.s1) +# seed_str = "seeds=%d-%d" % (self.s1, self.s2) +# range_str = "range=%.6f-%.6f" % (self.r.min, self.r.max) +# rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) +# expected_density = self.seedfile_set.expected_crash_density +# xd_str = "expected=%.9f" % expected_density +# xr_str = 'expected_rate=%.6f uniq/day' % (expected_density * rate * 86400) +# logger.info('Fuzzing %s %s %s %s %s %s crash_count=%d', +# self.sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) + logger.info('Fuzzing...crash_count=%d', crashcount) + + def analyze(self, testcase): + ''' + Loops through all known analyzers for a given testcase + :param testcase: + ''' + for analyzer in self.analyzers: + analyzer(testcase) + + def verify(self, testcase): + ''' + Confirms that a test case is interesting enough to pursue further analysis + :param testcase: + ''' + with testcase as c: + if c.is_crash: + new_uniq_crash = verify_crasher(c, hashes, self.cfg, seedfile_set) + + # record the zzuf log line for this crash + if not c.logger: + c.get_logger() + c.logger.debug("zzuflog: %s", zzuf_log.line) + c.logger.info('Command: %s', testcase.cmdline) + + + + def construct_report(self, testcase): + ''' + Constructs a report package for the test case + :param testcase: + ''' + + def _prefuzz(self): pass - def _fuzz_and_run(self, quiet_flag): + def _fuzz(self): + pass + + def _postfuzz(self): + pass + + def fuzz(self): + self._prefuzz() + self._fuzz() + self._postfuzz() + + def _prerun(self): + pass + + def _run(self): if self.first_chunk: # disable the --quiet option in zzuf # on the first chunk only @@ -257,49 +321,11 @@ def _fuzz_and_run(self, quiet_flag): self.r.max, self.cfg.progtimeout, quiet_flag) - saw_crash = zzuf.go() - return saw_crash, zzuf - - def _log(self): -# # emit a log entry - crashcount = z.get_crashcount(self.cfg.crashers_dir) -# rate = get_rate(self.s1) -# seed_str = "seeds=%d-%d" % (self.s1, self.s2) -# range_str = "range=%.6f-%.6f" % (self.r.min, self.r.max) -# rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) -# expected_density = self.seedfile_set.expected_crash_density -# xd_str = "expected=%.9f" % expected_density -# xr_str = 'expected_rate=%.6f uniq/day' % (expected_density * rate * 86400) -# logger.info('Fuzzing %s %s %s %s %s %s crash_count=%d', -# self.sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) - logger.info('Fuzzing...crash_count=%d', crashcount) - - def go2(self): - self._fuzz() - self._run() - for testcase in self.candidates: - self.verify(testcase) - - for testcase in self.verified: - self.analyze(testcase) - - for testcase in self.analyzed: - self.construct_report(testcase) - - def _process_crash(self): - pass + self.saw_crash = zzuf.go() - - - def go(self): - sf = self.seedfile - - self._check_ppid() - - saw_crash, zzuf = self._fuzz_and_run(quiet_flag) + def _postrun(self): STATE_TIMER.enter_state('checking_results') - - if not saw_crash: + if not self.saw_crash: # we must have made it through this chunk without a crash # so go to next chunk self.sf.record_tries(tries=1) @@ -326,34 +352,54 @@ def go(self): self.sf.record_tries(tries=try_count) self.r.record_tries(tries=try_count) - new_uniq_crash = False - if crash_status: - logger.info('Generating testcase for %s', zzuf_log.line) - # a true crash - zzuf_range = zzuf_log.range - # create the temp dir for the results - self.cfg.create_tmpdir() - outfile = self.cfg.get_testcase_outfile(sf.path, self.s1) - logger.debug('Output file is %s', outfile) - testcase = zzuf.generate_test_case(sf.path, self.s1, zzuf_range, outfile) - - # Do internal verification using GDB / Valgrind / Stderr - fuzzedfile = file_handlers.basicfile.BasicFile(outfile) - - with BffCrash(self.cfg, sf, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, - self.cfg.killprocname, self.cfg.backtracelevels, - self.cfg.crashers_dir, self.s1, self.r) as c: - if c.is_crash: - new_uniq_crash = verify_crasher(c, hashes, self.cfg, seedfile_set) - - # record the zzuf log line for this crash - if not c.logger: - c.get_logger() - c.logger.debug("zzuflog: %s", zzuf_log.line) - c.logger.info('Command: %s', testcase.cmdline) - - self.cfg.clean_tmpdir() + if not crash_status: + return + + logger.info('Generating testcase for %s', zzuf_log.line) + # a true crash + zzuf_range = zzuf_log.range + # create the temp dir for the results + self.cfg.create_tmpdir() + outfile = self.cfg.get_testcase_outfile(self.seedfile.path, self.s1) + logger.debug('Output file is %s', outfile) + testcase = zzuf.generate_test_case(self.seedfile.path, self.s1, zzuf_range, outfile) + + # Do internal verification using GDB / Valgrind / Stderr + fuzzedfile = file_handlers.basicfile.BasicFile(outfile) + crasher = BffCrash(self.cfg, self.seedfile, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, + self.cfg.killprocname, self.cfg.backtracelevels, + self.cfg.crashers_dir, self.s1, self.r) + + self.candidates.append(crasher) + + def run(self): + self._prerun() + self._run() + self._postrun() + + def go2(self): + self.fuzz() + self.run() + + # every test case is a candidate until verified + for testcase in self.candidates: + self.verify(testcase) + + # analyze each verified crash + for testcase in self.verified: + self.analyze(testcase) + + # construct output bundle for each analyzed test case + for testcase in self.analyzed: + self.construct_report(testcase) + + def _process_crash(self): + pass + + def go(self): + + new_uniq_crash = False # incrementing seed number is the campaign's job, not ours # sr.increment_seed() From 1dc0798da344a00c78240424df54d118fd1608b0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 16:08:48 -0500 Subject: [PATCH 0097/1169] breakout a base class for iterations This will eventually merge with the other iteration classes in certfuzz.iteration but for now it's just continuing the bff evolution towards foe --- src/certfuzz/iteration/linux.py | 116 +++++++++++++++----------------- 1 file changed, 56 insertions(+), 60 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 798c251..b576645 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -216,7 +216,62 @@ def verify_crasher(c, hashes, cfg, seedfile_set): return found_new_crash -class Iteration(object): +class IterBase(object): + def _prefuzz(self): + pass + + def _fuzz(self): + pass + + def _postfuzz(self): + pass + + def fuzz(self): + self._prefuzz() + self._fuzz() + self._postfuzz() + + def _prerun(self): + pass + + def _run(self): + pass + + def _postrun(self): + pass + + def run(self): + self._prerun() + self._run() + self._postrun() + + def verify(self, testcase): + pass + + def analyze(self, testcase): + pass + + def construct_report(self, testcase): + pass + + def go(self): + self.fuzz() + self.run() + + # every test case is a candidate until verified + for testcase in self.candidates: + self.verify(testcase) + + # analyze each verified crash + for testcase in self.verified: + self.analyze(testcase) + + # construct output bundle for each analyzed test case + for testcase in self.analyzed: + self.construct_report(testcase) + + +class Iteration(IterBase): def __init__(self, cfg=None, seednum=None, seedfile=None, r=None): self.cfg = cfg self.seednum = seednum @@ -281,23 +336,6 @@ def construct_report(self, testcase): :param testcase: ''' - def _prefuzz(self): - pass - - def _fuzz(self): - pass - - def _postfuzz(self): - pass - - def fuzz(self): - self._prefuzz() - self._fuzz() - self._postfuzz() - - def _prerun(self): - pass - def _run(self): if self.first_chunk: # disable the --quiet option in zzuf @@ -373,45 +411,3 @@ def _postrun(self): self.candidates.append(crasher) - def run(self): - self._prerun() - self._run() - self._postrun() - - def go2(self): - self.fuzz() - self.run() - - # every test case is a candidate until verified - for testcase in self.candidates: - self.verify(testcase) - - # analyze each verified crash - for testcase in self.verified: - self.analyze(testcase) - - # construct output bundle for each analyzed test case - for testcase in self.analyzed: - self.construct_report(testcase) - - def _process_crash(self): - pass - - def go(self): - - new_uniq_crash = False - # incrementing seed number is the campaign's job, not ours -# sr.increment_seed() - -# # cache objects in case of reboot -# cache_state(self.cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) -# pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) -# cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) -# cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) - -# if new_uniq_crash: -# # we had a hit, so break the inner while() loop -# # so we can pick a new range. This is to avoid -# # having a crash-rich range run away with the -# # probability before other ranges have been tried -# break From f61c916baf69704cdbde23ebdcd1c5d13544ca3b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 16:21:07 -0500 Subject: [PATCH 0098/1169] rename IterationBase to IterationBase2 (since we're constructing a new IterationBase3 in certfuzz.iteration.linux --- src/certfuzz/iteration/iteration_android.py | 4 ++-- src/certfuzz/iteration/iteration_base.py | 2 +- src/certfuzz/iteration/iteration_windows.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/iteration/iteration_android.py b/src/certfuzz/iteration/iteration_android.py index c77ee56..2dde6b1 100644 --- a/src/certfuzz/iteration/iteration_android.py +++ b/src/certfuzz/iteration/iteration_android.py @@ -16,7 +16,7 @@ from ..fuzztools.filetools import find_or_create_dir from ..runners.android_runner import AndroidRunner from ..runners import RunnerError -from .iteration_base import IterationBase +from .iteration_base import IterationBase2 from ..fuzzers import FuzzerExhaustedError import logging @@ -75,7 +75,7 @@ def do_iteration(iter_args): return True -class AndroidIteration(IterationBase): +class AndroidIteration(IterationBase2): def __init__(self, campaign_id=None, db_config=None, num=0, fuzzopts=None, runopts=None, sf=None, emu_handle=None, sf_dir=None, intent=None): self.campaign_id = campaign_id diff --git a/src/certfuzz/iteration/iteration_base.py b/src/certfuzz/iteration/iteration_base.py index f4f6203..b3b5c21 100644 --- a/src/certfuzz/iteration/iteration_base.py +++ b/src/certfuzz/iteration/iteration_base.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -class IterationBase(object): +class IterationBase2(object): def __init__(self): pass diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 626e9ee..5da1dad 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -16,7 +16,7 @@ from ..runners.errors import RunnerRegistryError from ..campaign.config.foe_config import get_command_args_list from .errors import IterationError -from .iteration_base import IterationBase +from .iteration_base import IterationBase2 import glob import logging import os @@ -31,7 +31,7 @@ MAX_IOERRORS = 5 -class Iteration(IterationBase): +class Iteration(IterationBase2): def __init__(self, sf, rng_seed, current_seed, config, fuzzer, runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, cmd_template, uniq_func, working_dir_base, outdir, debug): From b11460757b43ee7a21756ec1a06af03cc9d18eeb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Feb 2014 16:22:18 -0500 Subject: [PATCH 0099/1169] rename IterBase to IterationBase3 to align with prior commit --- src/certfuzz/iteration/linux.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index b576645..31d511a 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -23,6 +23,8 @@ from ..minimizer import MinimizerError, UnixMinimizer as Minimizer import os from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +import tempfile +import shutil logger = logging.getLogger(__name__) @@ -216,7 +218,18 @@ def verify_crasher(c, hashes, cfg, seedfile_set): return found_new_crash -class IterBase(object): +class IterationBase3(object): + def __init__(self, workdirbase): + self.workdirbase = workdirbase + self.workdir = None + + def __enter__(self): + self.workdir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) + return self + + def __exit__(self, etype, value, traceback): + shutil.rmtree(self.workdir) + def _prefuzz(self): pass @@ -271,8 +284,9 @@ def go(self): self.construct_report(testcase) -class Iteration(IterBase): +class Iteration(IterationBase3): def __init__(self, cfg=None, seednum=None, seedfile=None, r=None): + IterationBase3.__init__(self, workdirbase) self.cfg = cfg self.seednum = seednum self.seedfile = seedfile @@ -283,12 +297,14 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None): self.s2 = self.s1 self.sf = self.seedfile + def __enter__(self): + IterationBase3.__enter__(self) self._check_ppid() - return self def __exit__(self, etype, value, traceback): + IterationBase3.__exit__(self, etype, value, traceback) self.cfg.clean_tmpdir() def _log(self): @@ -329,7 +345,6 @@ def verify(self, testcase): c.logger.info('Command: %s', testcase.cmdline) - def construct_report(self, testcase): ''' Constructs a report package for the test case From a66ba1302609a1873712af4c20b34dd4f77bc5c2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 09:56:49 -0500 Subject: [PATCH 0100/1169] fix make_dev builder and logging --- .externalToolBuilders/dev_builder.launch | 4 ++-- build/make_dev.py | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.externalToolBuilders/dev_builder.launch b/.externalToolBuilders/dev_builder.launch index 8ee4ffa..238cf9c 100644 --- a/.externalToolBuilders/dev_builder.launch +++ b/.externalToolBuilders/dev_builder.launch @@ -3,7 +3,7 @@ - + - + diff --git a/build/make_dev.py b/build/make_dev.py index c6cb591..b9b15b5 100644 --- a/build/make_dev.py +++ b/build/make_dev.py @@ -3,11 +3,13 @@ @organization: cert.org ''' - +import logging from dev.linux.linux_build import LinuxBuild from dev.windows.windows_build import WindowsBuild #from dev.osx import DarwinBuild +logger = logging.getLogger(__name__) + builders = { 'linux': LinuxBuild, 'windows': WindowsBuild, @@ -26,6 +28,10 @@ def build(platform): if __name__ == '__main__': + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) from optparse import OptionParser From 729f81f5946465ead477c51aa5a4f1d8b51c7cda Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 09:59:58 -0500 Subject: [PATCH 0101/1169] remove setup_logfile (it's moving into certfuzz.campaign.linux) --- src/certfuzz/bff/linux.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 890c5b3..2124a0e 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -50,22 +50,6 @@ def setup_logging_to_console(log_obj, level): add_log_handler(log_obj, level, hdlr, formatter) -def setup_logfile(logdir, log_basename='bff.log', level=logging.DEBUG, - max_bytes=1e8, backup_count=5): - ''' - Creates a log file in / at level - @param logdir: the directory where the log file should reside - @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') - @param level: the logging level (defaults to logging.DEBUG) - ''' - filetools.make_directories(logdir) - logfile = os.path.join(logdir, log_basename) - handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) - formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") - add_log_handler(logger, level, handler, formatter) - logger.info('Logging %s at %s', logging.getLevelName(level), logfile) - - def get_config_file(basedir): config_dir = os.path.join(basedir, 'conf.d') @@ -97,7 +81,7 @@ def main(): # debuggers.verify_supported_platform() setup_logging_to_console(logger, logging.INFO) - setup_logfile() +# setup_logfile() logger.info("Welcome to BFF!") scriptpath = os.path.dirname(sys.argv[0]) @@ -105,6 +89,8 @@ def main(): # parse command line options logger.info('Parsing command line options') + + #TODO: replace OptionParser with argparse parser = OptionParser() parser.add_option('', '--debug', dest='debug', help='Turn on debugging output', action='store_true') parser.add_option('-c', '--config', dest='cfg', help='Config file location') From 351506f9730db2dc3ba8781a31533cbe0f527055 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 10:01:48 -0500 Subject: [PATCH 0102/1169] relocate bff_helper functions into campaign and iteration classes. Also start building out the temp dir hierarchy similar to FOE. --- src/certfuzz/campaign/config/bff_config.py | 11 --- src/certfuzz/campaign/linux.py | 105 ++++++++++++++++----- src/certfuzz/fuzztools/bff_helper.py | 27 ------ src/certfuzz/iteration/linux.py | 11 +-- 4 files changed, 89 insertions(+), 65 deletions(-) diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index 2b2cf3e..45f302e 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -123,18 +123,7 @@ def __init__(self, cfg): self.watchdogfile = os.path.expanduser(self.cfg.get('directories', 'watchdog_file')) # derived properties -# self.program_basename = os.path.basename(self.program) self.program_basename = os.path.basename(self.program).replace('"', '') -# self.program_basename = string.replace(self.program_basename, '"', '') - - self.dirs_to_create = [self.local_dir, - self.cached_objects_dir, - self.seedfile_local_dir, - self.output_dir, - self.seedfile_output_dir, - self.crashers_dir, - self.testscase_tmp_dir, - ] self.uniq_log = os.path.join(self.output_dir, UNIQ_LOG) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 9f93c1d..020b4c0 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -38,6 +38,10 @@ from certfuzz.iteration.linux import Iteration from certfuzz.campaign.errors import CampaignScriptError from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file +import shutil +import tempfile +from certfuzz.fuzztools.filetools import mkdir_p +from ..fuzztools import subprocess_helper as subp logger = logging.getLogger(__name__) @@ -53,26 +57,25 @@ def __init__(self, cfg_path=None, scriptpath=None): self.seedfile_set = None self._last_ppid = None self.hashes = [] + self.working_dir = None def __enter__(self): + self._setup_dirs() + + # setup working dir + self.working_dir = tempfile.mkdtemp(prefix='campaign_', dir=self.cfg.local_dir) + logger.debug('workdir=%s', self.working_dir) + # set up local logging - self._setup_logfile(self.cfg.local_dir, - log_basename='bff.log', - level=logging.DEBUG, - max_bytes=1e8, - backup_count=3) + self._setup_logfile(logdir=self.cfg.local_dir, backup_count=3) # set up remote logging - self._setup_logfile(self.cfg.output_dir, - log_basename='bff.log', - level=logging.INFO, - max_bytes=1e7, - backup_count=5) + self._setup_logfile(logdir=self.cfg.output_dir, level=logging.INFO, max_bytes=1e7) self._check_for_script() - z.setup_dirs_and_files(self.cfg_path, self.cfg) - self.start_process_killer(self.scriptpath, self.cfg) - z.set_unbuffered_stdout() + self._copy_config() + self._start_process_killer() + self._set_unbuffered_stdout() self._create_seedfile_set() if self.cfg.watchdogtimeout: self._setup_watchdog() @@ -90,23 +93,83 @@ def __exit__(self, etype, value, mytraceback): logger.warning("Please configure BFF to fuzz a binary. Exiting...") handled = True + # if etype not set or if we handled it + if not etype or handled: + shutil.rmtree(self.working_dir) + return handled - def start_process_killer(self): - cfg = self.cfg + def _setup_dirs(self): + logger.debug('setup dirs') + paths = [self.cfg.local_dir, + self.cfg.cached_objects_dir, + self.cfg.seedfile_local_dir, + self.cfg.output_dir, + self.cfg.seedfile_output_dir, + self.cfg.crashers_dir, + self.cfg.testscase_tmp_dir, + ] + + for d in paths: + if not os.path.exists(d): + logger.debug('Creating dir %s', d) + mkdir_p(d) + + def _setup_logfile(self, logdir, log_basename='bff.log', level=logging.DEBUG, + max_bytes=1e8, backup_count=5): + ''' + Creates a log file in / at level + @param logdir: the directory where the log file should reside + @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') + @param level: the logging level (defaults to logging.DEBUG) + ''' + filetools.make_directories(logdir) + logfile = os.path.join(logdir, log_basename) + hdlr = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) + formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") + + hdlr.setLevel(level) + hdlr.setFormatter(formatter) + logger.addHandler(hdlr) + + logger.info('Logging %s at %s', logging.getLevelName(level), logfile) + + def _copy_config(self): + logger.debug('copy config') + + filetools.copy_file(self.cfg_path, self.cfg.output_dir) + + def _set_unbuffered_stdout(self): + ''' + Reopens stdout with a buffersize of 0 (unbuffered) + @rtype: none + ''' + logger.debug('set unbuffered stdout') + # reopen stdout file descriptor with write mode + # and 0 as the buffer size (unbuffered) + sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) + + def _start_process_killer(self): + logger.debug('start process killer') # set up and spawn the process killer - killscript = cfg.get_killscript_path(self.scriptpath) - ProcessKiller(killscript, cfg.killprocname, cfg.killproctimeout).go() - logger.debug("Process killer started: %s %s %d", killscript, cfg.killprocname, cfg.killproctimeout) + killscript = self.cfg.get_killscript_path(self.scriptpath) + ProcessKiller(killscript, self.cfg.killprocname, self.cfg.killproctimeout).go() + logger.debug("Process killer started: %s %s %d", killscript, self.cfg.killprocname, self.cfg.killproctimeout) def _cache_prg(self): + logger.debug('cache program') sf = self.seedfile_set.next_item() + # Run the program once to cache it into memory - z.cache_program_once(self.cfg, sf.path) + fullpathorig = self.cfg.full_path_original(sf.path) + cmdargs = self.cfg.get_command_list(fullpathorig) + subp.run_with_timer(cmdargs, self.cfg.progtimeout * 8, self.cfg.killprocname, use_shell=True) + # Give target time to die time.sleep(1) def _setup_watchdog(self): + logger.debug('setup watchdog') # set up the watchdog timeout within the VM and restart the daemon watchdog = WatchDog(self.cfg.watchdogfile, self.cfg.watchdogtimeout) @@ -129,8 +192,8 @@ def _create_seedfile_set(self): ) as sfset: self.seedfile_set = sfset - def _check_for_script(self): + logger.debug('check for script') if self.cfg.program_is_script(): logger.warning("Target application is a shell script.") raise CampaignScriptError() @@ -160,7 +223,7 @@ def _do_interval(self): for s in xrange(s1, s2): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot self.touch_watchdog_file() - with Iteration(seednum=s, seedfile=sf, r=r) as iteration: + with Iteration(seednum=s, seedfile=sf, r=r, workdirbase=self.working_dir) as iteration: iteration.go() def go(self): diff --git a/src/certfuzz/fuzztools/bff_helper.py b/src/certfuzz/fuzztools/bff_helper.py index 7898143..8f5eb20 100644 --- a/src/certfuzz/fuzztools/bff_helper.py +++ b/src/certfuzz/fuzztools/bff_helper.py @@ -6,18 +6,6 @@ @organization: cert.org ''' import os -import sys -from ..fuzztools import subprocess_helper as subp -from ..fuzztools import filetools - -def set_unbuffered_stdout(): - ''' - Reopens stdout with a buffersize of 0 (unbuffered) - @rtype: none - ''' - # reopen stdout file descriptor with write mode - # and 0 as the buffer size (unbuffered) - sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) # analyze results @@ -29,18 +17,3 @@ def get_crashcount(uniquedir): ''' dirs = [d for d in os.listdir(uniquedir) if os.path.isdir(os.path.join(uniquedir, d))] return len(dirs) - -def cache_program_once(cfg, seedfile): - fullpathorig = cfg.full_path_original(seedfile) - cmdargs = cfg.get_command_list(fullpathorig) - subp.run_with_timer(cmdargs, cfg.progtimeout * 8, cfg.killprocname, use_shell=True) - -def setup_dirs_and_files(cfg_file, cfg): - # Set up a local fuzzing directory. HGFS or CIFS involves too much overhead, so - # fuzz locally and then copy over interesting cases as they're encountered - filetools.make_directories(*cfg.dirs_to_create) - - # Copy seed file and cfg to local fuzzing directory as well as fuzz run output directory - # TODO: don't think we need this given Seedfile Dir Manager -# filetools.copy_file(cfg.fullpathseedfile, cfg.fullpathlocalfuzzdir, cfg.output_dir) - filetools.copy_file(cfg_file, cfg.output_dir) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 31d511a..aeb4f00 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -221,14 +221,15 @@ def verify_crasher(c, hashes, cfg, seedfile_set): class IterationBase3(object): def __init__(self, workdirbase): self.workdirbase = workdirbase - self.workdir = None + self.working_dir = None def __enter__(self): - self.workdir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) + self.working_dir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) + logger.debug('workdir=%s', self.working_dir) return self def __exit__(self, etype, value, traceback): - shutil.rmtree(self.workdir) + shutil.rmtree(self.working_dir) def _prefuzz(self): pass @@ -285,7 +286,7 @@ def go(self): class Iteration(IterationBase3): - def __init__(self, cfg=None, seednum=None, seedfile=None, r=None): + def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None): IterationBase3.__init__(self, workdirbase) self.cfg = cfg self.seednum = seednum @@ -297,7 +298,6 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None): self.s2 = self.s1 self.sf = self.seedfile - def __enter__(self): IterationBase3.__enter__(self) self._check_ppid() @@ -344,7 +344,6 @@ def verify(self, testcase): c.logger.debug("zzuflog: %s", zzuf_log.line) c.logger.info('Command: %s', testcase.cmdline) - def construct_report(self, testcase): ''' Constructs a report package for the test case From c12409b363aa86180603bfa47bb66dfa3c1c6a97 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 10:11:14 -0500 Subject: [PATCH 0103/1169] mkdir_p fix Based on http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python?rq=1 --- src/certfuzz/fuzztools/filetools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 51373f6..67efde4 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -47,9 +47,10 @@ def mkdir_p(path): os.makedirs(path) except OSError as exc: # if the dir already exists, just move along - if exc.errno == errno.EEXIST: + if exc.errno == errno.EEXIST and os.path.isdir(path): pass - else: raise + else: + raise # file system helpers def make_directories(*paths): From 776f485827e16145d6d3b0206e62dfa30c07e627 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 11:04:19 -0500 Subject: [PATCH 0104/1169] Make touch_watchdog_file choose what it runs at runtime versus having it be fixed at load time. --- src/certfuzz/campaign/linux.py | 2 +- src/certfuzz/file_handlers/watchdog_file.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 020b4c0..b8c8f4d 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -222,7 +222,7 @@ def _do_interval(self): for s in xrange(s1, s2): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot - self.touch_watchdog_file() + touch_watchdog_file() with Iteration(seednum=s, seedfile=sf, r=r, workdirbase=self.working_dir) as iteration: iteration.go() diff --git a/src/certfuzz/file_handlers/watchdog_file.py b/src/certfuzz/file_handlers/watchdog_file.py index 1a3bced..3683905 100644 --- a/src/certfuzz/file_handlers/watchdog_file.py +++ b/src/certfuzz/file_handlers/watchdog_file.py @@ -28,4 +28,6 @@ def _twdf(self): TWDF = Twdf() -touch_watchdog_file = TWDF.func + +def touch_watchdog_file(): + TWDF.func() From 9890af4e03b064362c63239b1b9e5c8aa882e652 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 11:06:00 -0500 Subject: [PATCH 0105/1169] hardcode killscript for now --- src/certfuzz/campaign/config/bff_config.py | 9 --------- src/certfuzz/campaign/linux.py | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index 45f302e..a7aef22 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -21,7 +21,6 @@ UNIQ_LOG = "uniquelog.txt" LAST_SEEDFILE = 'lastseed' -KILL_SCRIPT = "killproc.sh" MINIMIZED_EXT = "minimal" ZZUF_LOG_FILE = 'zzuf_log.txt' @@ -130,7 +129,6 @@ def __init__(self, cfg): self.crashexitcodesfile = os.path.join(self.local_dir, CRASH_EXIT_CODE_FILE) self.zzuf_log_file = os.path.join(self.local_dir, ZZUF_LOG_FILE) - self.killscript = KILL_SCRIPT self.tmpdir = None self.cached_config_file = os.path.join(self.cached_objects_dir, CACHED_CONFIG_OBJECT_FILE) @@ -253,10 +251,3 @@ def get_testcase_outfile(self, seedfile, s1): new_basename = '%s%s' % (new_root, ext) self.create_tmpdir() return os.path.join(self.tmpdir, new_basename) - - def get_killscript_path(self, scriptpath): - ''' - @rtype: string - @return: the path to the killscript: / - ''' - return os.path.join(scriptpath, self.killscript) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index b8c8f4d..79effd5 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -152,7 +152,7 @@ def _set_unbuffered_stdout(self): def _start_process_killer(self): logger.debug('start process killer') # set up and spawn the process killer - killscript = self.cfg.get_killscript_path(self.scriptpath) + killscript = os.path.abspath(os.path.expanduser('~/bff/killproc.sh')) ProcessKiller(killscript, self.cfg.killprocname, self.cfg.killproctimeout).go() logger.debug("Process killer started: %s %s %d", killscript, self.cfg.killprocname, self.cfg.killproctimeout) From 21eb73aec517c586690ac6b7f520ff37ff48eac5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 11:06:39 -0500 Subject: [PATCH 0106/1169] get rid of max_seed idea, just let it run forever if nothing breaks. --- src/certfuzz/campaign/config/bff_config.py | 1 - src/certfuzz/campaign/linux.py | 3 ++- src/linux/conf.d/bff.cfg | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index a7aef22..4c20a15 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -81,7 +81,6 @@ def __init__(self, cfg): self.copymode = self.cfg.getint('zzuf', 'copymode') self.start_seed = self.cfg.getint('zzuf', 'start_seed') self.seed_interval = self.cfg.getint('zzuf', 'seed_interval') - self.max_seed = self.cfg.getint('zzuf', 'max_seed') # [verifier] self.backtracelevels = self.cfg.getint('verifier', 'backtracelevels') diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 79effd5..d07ed55 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -42,6 +42,7 @@ import tempfile from certfuzz.fuzztools.filetools import mkdir_p from ..fuzztools import subprocess_helper as subp +import itertools logger = logging.getLogger(__name__) @@ -230,7 +231,7 @@ def go(self): # campaign.go cfg = self.cfg - for s in xrange(cfg.start_seed, cfg.max_seed, cfg.seed_interval): + for s in itertools.count(start=cfg.start_seed, step=cfg.seed_interval): self.s1 = s self.s2 = s + cfg.seed_interval self._do_interval() diff --git a/src/linux/conf.d/bff.cfg b/src/linux/conf.d/bff.cfg index 4ccf549..f3a4883 100644 --- a/src/linux/conf.d/bff.cfg +++ b/src/linux/conf.d/bff.cfg @@ -95,9 +95,6 @@ start_seed=0 # in a reasonable amount of time. seed_interval=20 -# The maximum zzuf seed (iteration) to use -max_seed=10000000000 - ################################################################ # VERIFIER PARAMETERS From 090aa282c80b2846222dd69da1be74592cfda3be Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 11:17:12 -0500 Subject: [PATCH 0107/1169] refactor ppid observer into separate module --- src/certfuzz/campaign/linux.py | 13 +++---------- src/certfuzz/fuzztools/ppid_observer.py | 21 +++++++++++++++++++++ src/certfuzz/iteration/linux.py | 3 ++- 3 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 src/certfuzz/fuzztools/ppid_observer.py diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index d07ed55..a82df84 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -43,6 +43,7 @@ from certfuzz.fuzztools.filetools import mkdir_p from ..fuzztools import subprocess_helper as subp import itertools +from certfuzz.fuzztools.ppid_observer import check_ppid logger = logging.getLogger(__name__) @@ -56,7 +57,6 @@ def __init__(self, cfg_path=None, scriptpath=None): self.cfg = cfg_helper.read_config_options(cfg_path) self.scriptpath = scriptpath self.seedfile_set = None - self._last_ppid = None self.hashes = [] self.working_dir = None @@ -83,8 +83,8 @@ def __enter__(self): # flag to indicate whether this is a fresh script start up or not self.first_chunk = True - # remember our parent process id so we can tell if it changes later - self._last_ppid = os.getppid() + + check_ppid() return self @@ -201,13 +201,6 @@ def _check_for_script(self): #cfg.disable_verification() #time.sleep(10) - def _check_ppid(self): - # check parent process id - _ppid_now = os.getppid() - if not _ppid_now == self._last_ppid: - logger.warning('Parent process ID changed from %d to %d', self._last_ppid, _ppid_now) - self._last_ppid = _ppid_now - def _do_interval(self): s1 = self.s1 s2 = self.s2 diff --git a/src/certfuzz/fuzztools/ppid_observer.py b/src/certfuzz/fuzztools/ppid_observer.py new file mode 100644 index 0000000..ee23736 --- /dev/null +++ b/src/certfuzz/fuzztools/ppid_observer.py @@ -0,0 +1,21 @@ +''' +Created on Feb 13, 2014 + +@author: adh +''' +import os +import logging + +logger = logging.getLogger(__name__) + +# remember our parent process id at startup +PPID = os.getppid() + + +def check_ppid(): + global PPID + current_ppid = os.getppid() + + if current_ppid != PPID: + logger.warning('Parent process ID changed from %d to %d', PPID, current_ppid) + PPID = current_ppid diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index aeb4f00..67b64d1 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -25,6 +25,7 @@ from certfuzz.file_handlers.watchdog_file import touch_watchdog_file import tempfile import shutil +from certfuzz.fuzztools.ppid_observer import check_ppid logger = logging.getLogger(__name__) @@ -300,7 +301,7 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No def __enter__(self): IterationBase3.__enter__(self) - self._check_ppid() + check_ppid() return self def __exit__(self, etype, value, traceback): From 751095485c1d5e2b2089196a5e51f6d8913192e6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 11:34:10 -0500 Subject: [PATCH 0108/1169] use args for interval start and stop --- src/certfuzz/campaign/linux.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index a82df84..c2eb95e 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -201,9 +201,7 @@ def _check_for_script(self): #cfg.disable_verification() #time.sleep(10) - def _do_interval(self): - s1 = self.s1 - s2 = self.s2 + def _do_interval(self, s1, s2): # interval.go logger.debug('Starting interval %d-%d', s1, s2) # wipe the tmp dir clean to try to avoid filling the VM disk @@ -225,6 +223,6 @@ def go(self): cfg = self.cfg for s in itertools.count(start=cfg.start_seed, step=cfg.seed_interval): - self.s1 = s - self.s2 = s + cfg.seed_interval - self._do_interval() + s1 = s + s2 = s + cfg.seed_interval + self._do_interval(s1, s2) From 5384578fa06c03a4b11c47f91bc6e1bfc4e534c6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 11:34:36 -0500 Subject: [PATCH 0109/1169] iteration needs a config --- src/certfuzz/campaign/linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index c2eb95e..427047c 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -215,7 +215,7 @@ def _do_interval(self, s1, s2): for s in xrange(s1, s2): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() - with Iteration(seednum=s, seedfile=sf, r=r, workdirbase=self.working_dir) as iteration: + with Iteration(cfg=self.cfg, seednum=s, seedfile=sf, r=r, workdirbase=self.working_dir) as iteration: iteration.go() def go(self): From abe43d703083a113d5527740522feb0cf808d524 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 11:34:56 -0500 Subject: [PATCH 0110/1169] don't crash if cfg.tmpdir is not set --- src/certfuzz/campaign/config/bff_config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index 4c20a15..7f94405 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -232,6 +232,9 @@ def create_tmpdir(self): assert os.path.isdir(self.tmpdir) def clean_tmpdir(self): + if self.tmpdir is None: + return + if os.path.exists(self.tmpdir): shutil.rmtree(self.tmpdir) logger.debug("Removed temp dir %s", self.tmpdir) From 0524f98c6fbfce5e59cc80a65fad5df4e8834bbc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 11:57:41 -0500 Subject: [PATCH 0111/1169] pop first chunk stuff up to the campaign level --- src/certfuzz/campaign/linux.py | 9 ++++++--- src/certfuzz/iteration/linux.py | 13 +++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 427047c..7108ac5 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -201,7 +201,7 @@ def _check_for_script(self): #cfg.disable_verification() #time.sleep(10) - def _do_interval(self, s1, s2): + def _do_interval(self, s1, s2, first_chunk=False): # interval.go logger.debug('Starting interval %d-%d', s1, s2) # wipe the tmp dir clean to try to avoid filling the VM disk @@ -209,20 +209,23 @@ def _do_interval(self, s1, s2): sf = self.seedfile_set.next_item() r = sf.rangefinder.next_item() + qf = not first_chunk logger.info(STATE_TIMER) for s in xrange(s1, s2): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() - with Iteration(cfg=self.cfg, seednum=s, seedfile=sf, r=r, workdirbase=self.working_dir) as iteration: + with Iteration(cfg=self.cfg, seednum=s, seedfile=sf, r=r, workdirbase=self.working_dir, quiet=qf) as iteration: iteration.go() def go(self): # campaign.go cfg = self.cfg + first_chunk = True for s in itertools.count(start=cfg.start_seed, step=cfg.seed_interval): s1 = s s2 = s + cfg.seed_interval - self._do_interval(s1, s2) + self._do_interval(s1, s2, first_chunk) + first_chunk = False diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 67b64d1..8850387 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -287,12 +287,13 @@ def go(self): class Iteration(IterationBase3): - def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None): + def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True): IterationBase3.__init__(self, workdirbase) self.cfg = cfg self.seednum = seednum self.seedfile = seedfile self.r = r + self.quiet_flag = quiet # convenience aliases self.s1 = self.seednum @@ -352,14 +353,6 @@ def construct_report(self, testcase): ''' def _run(self): - if self.first_chunk: - # disable the --quiet option in zzuf - # on the first chunk only - quiet_flag = False - self.first_chunk = False - else: - quiet_flag = True - # do the fuzz cmdline = self.cfg.get_command(self.sf.path) @@ -373,7 +366,7 @@ def _run(self): self.r.min, self.r.max, self.cfg.progtimeout, - quiet_flag) + self.quiet_flag) self.saw_crash = zzuf.go() def _postrun(self): From de2f8ca26e469b178d5b03aecb61f371e4ba9ae5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 13:09:17 -0500 Subject: [PATCH 0112/1169] reorganize methods --- src/certfuzz/iteration/linux.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 8850387..00406ac 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -241,11 +241,6 @@ def _fuzz(self): def _postfuzz(self): pass - def fuzz(self): - self._prefuzz() - self._fuzz() - self._postfuzz() - def _prerun(self): pass @@ -255,6 +250,11 @@ def _run(self): def _postrun(self): pass + def fuzz(self): + self._prefuzz() + self._fuzz() + self._postfuzz() + def run(self): self._prerun() self._run() From 2b78968e95708b593aa5348c7f9a3b9fe07674db Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 13:09:41 -0500 Subject: [PATCH 0113/1169] add placeholder lists for test cases --- src/certfuzz/iteration/linux.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 00406ac..a774bab 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -223,6 +223,9 @@ class IterationBase3(object): def __init__(self, workdirbase): self.workdirbase = workdirbase self.working_dir = None + self.candidates = [] + self.verified = [] + self.analyzed = [] def __enter__(self): self.working_dir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) From 9ab2398ea1cfe884e2022405e802ce30266a4ce8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 14:09:49 -0500 Subject: [PATCH 0114/1169] add KeyboardInterrupt handling (ctrl-c works) --- src/certfuzz/campaign/linux.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 7108ac5..684608e 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -44,6 +44,7 @@ from ..fuzztools import subprocess_helper as subp import itertools from certfuzz.fuzztools.ppid_observer import check_ppid +import traceback logger = logging.getLogger(__name__) @@ -59,6 +60,7 @@ def __init__(self, cfg_path=None, scriptpath=None): self.seedfile_set = None self.hashes = [] self.working_dir = None + self.debug = True def __enter__(self): self._setup_dirs() @@ -90,6 +92,9 @@ def __enter__(self): def __exit__(self, etype, value, mytraceback): handled = False + if etype is KeyboardInterrupt: + logger.warning('Keyboard interrupt - exiting') + handled = True if etype is CampaignScriptError: logger.warning("Please configure BFF to fuzz a binary. Exiting...") handled = True @@ -97,9 +102,34 @@ def __exit__(self, etype, value, mytraceback): # if etype not set or if we handled it if not etype or handled: shutil.rmtree(self.working_dir) + elif etype: + logger.debug('Unhandled exception:') + logger.debug(' type: %s', etype) + logger.debug(' value: %s', value) + for l in traceback.format_exception(etype, value, mytraceback): + logger.debug(l.rstrip()) + + if self.debug and etype and not handled: + # leave it behind if we're in debug mode + # and there's a problem + logger.debug('Skipping cleanup since we are in debug mode.') + else: + self._cleanup_workdir() return handled + def _cleanup_workdir(self): + try: + shutil.rmtree(self.working_dir) + except: + pass + + if os.path.exists(self.working_dir): + logger.warning("Unable to remove campaign working dir: %s", self.working_dir) + else: + logger.debug('Removed campaign working dir: %s', self.working_dir) + + def _setup_dirs(self): logger.debug('setup dirs') paths = [self.cfg.local_dir, From f080bae2ef686ed0bb0156eef440a65eb2098476 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 14:10:27 -0500 Subject: [PATCH 0115/1169] zzuf seed arg changes if s1==s2 --- src/certfuzz/fuzztools/zzuf.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index 0f462a6..457a96f 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -16,7 +16,7 @@ def __init__(self, seedfile, seed, range, outfile): ''' @param seedfile: The original seed file to use @param seed: The zzuf seed number to use - @param range: + @param range: @param outfile: ''' self.seedfile = seedfile @@ -35,14 +35,14 @@ def generate(self): class Zzuf: def __init__(self, dir, s1, s2, cmd, seedfile, file, copymode, ratiomin, ratiomax, timeout, quiet=True): ''' - + @param dir: @param s1: The starting seed @param s2: The ending seed @param cmd: The command to run @param file: The zzuf output file - @param copymode: - @param ratiomin: + @param copymode: + @param ratiomin: @param ratiomax: @param timeout: A float timeout ''' @@ -75,7 +75,7 @@ def _get_go_fuzz_cmdline(self): def go(self): ''' - Changes directory to then starts a zzuf run with the + Changes directory to then starts a zzuf run with the given parameters. ''' command = self._get_go_fuzz_cmdline() @@ -86,7 +86,7 @@ def go(self): def generate_test_case(self, seedfile, seed, range, outfile): ''' - Generates the test case for the given , , + Generates the test case for the given , , and , storing the result in ''' @@ -113,12 +113,15 @@ def _get_zzuf_args(self): # will be added to the zzuf command line to enable this mode. # Some applications do not behave properly with zzuf loaded via LD_PRELOAD. # Those applications should be fuzzed in copy mode, which also specifies the option - # to look at the process exit code to indicate failures. This works well for + # to look at the process exit code to indicate failures. This works well for # programs that are launched by a shell script. if (self.copymode): parts.append("opmode=copy") - parts.append("seed=%d:%d" % (self.s1, self.s2)) + if self.s1 == self.s2: + parts.append("seed=%d" % self.s1) + else: + parts.append("seed=%d:%d" % (self.s1, self.s2)) # prefix everything with a "--" then build the string return " ".join(["--%s" % p for p in parts]) From 482817fa1fb6e4b4413d6375f48140fb5089f223 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 14:12:03 -0500 Subject: [PATCH 0116/1169] don't iterate over lists that can change in the middle of the loop. use while() construct instead --- src/certfuzz/iteration/linux.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index a774bab..3ae6a29 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -276,16 +276,25 @@ def go(self): self.fuzz() self.run() + # short circuit if nothing found + if not self.candidates: + return + # every test case is a candidate until verified - for testcase in self.candidates: + # use a while loop so we have the option of adding + # candidates during the loop + while len(self.candidates) > 0: + testcase = self.candidates.pop(0) self.verify(testcase) # analyze each verified crash - for testcase in self.verified: + while len(self.verified) > 0: + testcase = self.verified.pop(0) self.analyze(testcase) # construct output bundle for each analyzed test case - for testcase in self.analyzed: + while len(self.analyzed) > 0: + testcase = self.analyzed.pop(0) self.construct_report(testcase) From 51ddbdad9ade2bc2e56342eec4abc11066a7e85c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 14:13:21 -0500 Subject: [PATCH 0117/1169] fix up our zzuf invocation --- src/certfuzz/iteration/linux.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 3ae6a29..bd581fa 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -364,13 +364,14 @@ def construct_report(self, testcase): :param testcase: ''' - def _run(self): + def _prerun(self): + IterationBase3._prerun(self) # do the fuzz cmdline = self.cfg.get_command(self.sf.path) STATE_TIMER.enter_state('fuzzing') - zzuf = Zzuf(self.cfg.local_dir, self.s1, - self.s2, + self.zzuf = Zzuf(self.cfg.local_dir, self.s1, + self.s1, cmdline, self.sf.path, self.cfg.zzuf_log_file, @@ -379,15 +380,20 @@ def _run(self): self.r.max, self.cfg.progtimeout, self.quiet_flag) - self.saw_crash = zzuf.go() + + def _run(self): + IterationBase3._run(self) + self.zzuf.go() def _postrun(self): + IterationBase3._postrun(self) + STATE_TIMER.enter_state('checking_results') - if not self.saw_crash: # we must have made it through this chunk without a crash # so go to next chunk - self.sf.record_tries(tries=1) - self.r.record_tries(tries=1) + self.sf.record_tries(tries=1) + self.r.record_tries(tries=1) + if not self.zzuf.saw_crash: self._log() return @@ -400,16 +406,6 @@ def _postrun(self): # report the exit code in its output log. The exit code is 128 + the signal number. crash_status = zzuf_log.crash_logged(self.cfg.copymode) - # sr.bookmark_s1() - self.s1_old = self.s1 - - self.s1 = zzuf_log.seed - - # record the fact that we've made it this far - try_count = self.s1_delta() - self.sf.record_tries(tries=try_count) - self.r.record_tries(tries=try_count) - if not crash_status: return @@ -420,7 +416,7 @@ def _postrun(self): self.cfg.create_tmpdir() outfile = self.cfg.get_testcase_outfile(self.seedfile.path, self.s1) logger.debug('Output file is %s', outfile) - testcase = zzuf.generate_test_case(self.seedfile.path, self.s1, zzuf_range, outfile) + testcase = self.zzuf.generate_test_case(self.seedfile.path, self.s1, zzuf_range, outfile) # Do internal verification using GDB / Valgrind / Stderr fuzzedfile = file_handlers.basicfile.BasicFile(outfile) From c0a72f76ae9b342e7cc7832e223cac779e8339b9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 14:36:26 -0500 Subject: [PATCH 0118/1169] put verify_supported_platform call back. See BFF-513 for followup tasks --- src/certfuzz/campaign/linux.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 684608e..ee37e17 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -62,7 +62,11 @@ def __init__(self, cfg_path=None, scriptpath=None): self.working_dir = None self.debug = True + # give up if we don't have a debugger + debuggers.verify_supported_platform() + def __enter__(self): + self._setup_dirs() # setup working dir From 1c9d9ae30a91865f2b468da91fc1da041de21787 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 15:39:45 -0500 Subject: [PATCH 0119/1169] start integrating crash verification --- src/certfuzz/campaign/linux.py | 21 +++- src/certfuzz/iteration/linux.py | 197 ++++++++++++++++---------------- 2 files changed, 118 insertions(+), 100 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index ee37e17..d1114e3 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -61,6 +61,7 @@ def __init__(self, cfg_path=None, scriptpath=None): self.hashes = [] self.working_dir = None self.debug = True + self.crashes_seen = set() # give up if we don't have a debugger debuggers.verify_supported_platform() @@ -235,6 +236,22 @@ def _check_for_script(self): #cfg.disable_verification() #time.sleep(10) + def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): + ''' + If crash_id represents a new crash, add the crash_id to crashes_seen + and return True. Otherwise return False. + + @param crash_id: the crash_id to look up + @param exploitability: not used at this time + ''' + if not crash_id in self.crashes_seen: + self.crashes_seen.add(crash_id) + logger.debug("%s did not exist in cache, crash is unique", crash_id) + return True + + logger.debug('%s was found, not unique', crash_id) + return False + def _do_interval(self, s1, s2, first_chunk=False): # interval.go logger.debug('Starting interval %d-%d', s1, s2) @@ -250,7 +267,9 @@ def _do_interval(self, s1, s2, first_chunk=False): for s in xrange(s1, s2): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() - with Iteration(cfg=self.cfg, seednum=s, seedfile=sf, r=r, workdirbase=self.working_dir, quiet=qf) as iteration: + with Iteration(cfg=self.cfg, seednum=s, seedfile=sf, r=r, + workdirbase=self.working_dir, quiet=qf, + uniq_func=self._crash_is_unique,) as iteration: iteration.go() def go(self): diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index bd581fa..5b46245 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -30,26 +30,26 @@ logger = logging.getLogger(__name__) -def determine_uniqueness(crash, hashes): - ''' - Gets the crash signature, then compares it against known crashes. - Sets crash.is_unique = True if it is new - ''' - - # short-circuit on crashes with no signature - if not crash.signature: - logger.warning('Crash has no signature, cleaning up') - crash.delete_files() - return - - if crash.signature in hashes: - crash.is_unique = False - return - - # fall back to checking if the crash directory exists - crash_dir_found = filetools.find_or_create_dir(crash.result_dir) - - crash.is_unique = not crash_dir_found +#def determine_uniqueness(crash, hashes): +# ''' +# Gets the crash signature, then compares it against known crashes. +# Sets crash.is_unique = True if it is new +# ''' +# +# # short-circuit on crashes with no signature +# if not crash.signature: +# logger.warning('Crash has no signature, cleaning up') +# crash.delete_files() +# return +# +# if crash.signature in hashes: +# crash.is_unique = False +# return +# +# # fall back to checking if the crash directory exists +# crash_dir_found = filetools.find_or_create_dir(crash.result_dir) +# +# crash.is_unique = not crash_dir_found def analyze_crasher(cfg, crash): @@ -158,71 +158,11 @@ def analyze_crasher(cfg, crash): return other_crashers_found - -def verify_crasher(c, hashes, cfg, seedfile_set): - logger.debug('verifying crash') - found_new_crash = False - - crashes = [] - crashes.append(c) - - for crash in crashes: - # loop until we're out of crashes to verify - logger.debug('crashes to verify: %d', len(crashes)) - STATE_TIMER.enter_state('verify_testcase') - - # crashes may be added as a result of minimization - crash.is_unique = False - determine_uniqueness(crash, hashes) - crash.get_logger() - if crash.is_unique: - hashes.append(crash) - # only toggle it once - if not found_new_crash: - found_new_crash = True - - logger.debug("%s did not exist in cache, crash is unique", crash.signature) - more_crashes = analyze_crasher(cfg, crash) - - if cfg.recycle_crashers: - logger.debug('Recycling crash as seedfile') - iterstring = crash.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + crash.seedfile.md5 + '-' + iterstring + crash.seedfile.ext - crasherseed_path = os.path.join(cfg.seedfile_origin_dir, crasherseedname) - filetools.copy_file(crash.fuzzedfile.path, crasherseed_path) - seedfile_set.add_file(crasherseed_path) - # add new crashes to the queue - crashes.extend(more_crashes) - crash.copy_files() - - uniqlogger = get_uniq_logger(cfg.uniq_log) - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', crash.seedfile.basename, crash.signature, crash.seednum, crash.range, crash.hd_bits, crash.hd_bytes) - logger.info('%s first seen at %d', crash.signature, crash.seednum) - else: - logger.debug('%s was found, not unique', crash.signature) - # always clean up after yourself - crash.clean_tmpdir() - - # clean up - crash.delete_files() - # whether it was unique or not, record some details for posterity - # record the details of this crash so we can regenerate it later if needed - crash.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', crash.seedfile.basename, crash.seednum, crash.range, crash.fuzzedfile.path) - crash.logger.info('PC=%s', crash.pc) - - # score this crash for the seedfile - crash.seedfile.record_success(crash.signature, tries=0) - if crash.range: - # ...and for the range - crash.range.record_success(crash.signature, tries=0) - - return found_new_crash - - class IterationBase3(object): def __init__(self, workdirbase): self.workdirbase = workdirbase self.working_dir = None + self.analyzers = None self.candidates = [] self.verified = [] self.analyzed = [] @@ -264,10 +204,14 @@ def run(self): self._postrun() def verify(self, testcase): - pass + self._preverify(testcase) + self._verify(testcase) + self._postverify(testcase) def analyze(self, testcase): - pass + self._preanalyze(testcase) + self._analyze(testcase) + self._postanalyze(testcase) def construct_report(self, testcase): pass @@ -299,7 +243,7 @@ def go(self): class Iteration(IterationBase3): - def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True): + def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None): IterationBase3.__init__(self, workdirbase) self.cfg = cfg self.seednum = seednum @@ -307,6 +251,11 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No self.r = r self.quiet_flag = quiet + if uniq_func is None: + self.uniq_func = lambda _tc_id: True + else: + self.uniq_func = uniq_func + # convenience aliases self.s1 = self.seednum self.s2 = self.s1 @@ -335,28 +284,78 @@ def _log(self): # self.sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) logger.info('Fuzzing...crash_count=%d', crashcount) - def analyze(self, testcase): - ''' - Loops through all known analyzers for a given testcase - :param testcase: - ''' - for analyzer in self.analyzers: - analyzer(testcase) - def verify(self, testcase): + def _verify(self, testcase): ''' Confirms that a test case is interesting enough to pursue further analysis :param testcase: ''' - with testcase as c: - if c.is_crash: - new_uniq_crash = verify_crasher(c, hashes, self.cfg, seedfile_set) + IterationBase3.verify(self, testcase) + + # if you find more testcases, append them to self.candidates + # verified crashes append to self.verified + + logger.debug('verifying crash') + with testcase as tc: + if tc.is_crash: + + found_new_crash = False + + # loop until we're out of crashes to verify + logger.debug('crashes to verify: %d', len(self.candidates)) + STATE_TIMER.enter_state('verify_testcase') + + tc.is_unique = self.uniq_func(tc.signature) + + tc.get_logger() + + if tc.is_unique: + self.verified.append(tc) +# # only toggle it once +# if not found_new_crash: +# found_new_crash = True + + more_crashes = analyze_crasher(self.cfg, tc) + + if self.cfg.recycle_crashers: + logger.debug('Recycling crash as seedfile') + iterstring = tc.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + tc.seedfile.md5 + '-' + iterstring + tc.seedfile.ext + crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) + filetools.copy_file(tc.fuzzedfile.path, crasherseed_path) + seedfile_set.add_file(crasherseed_path) + # add new crashes to the queue + self.candidates.extend(more_crashes) + tc.copy_files() + + uniqlogger = get_uniq_logger(self.cfg.uniq_log) + uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', tc.seedfile.basename, tc.signature, tc.seednum, tc.range, tc.hd_bits, tc.hd_bytes) + logger.info('%s first seen at %d', tc.signature, tc.seednum) + else: + logger.debug('%s was found, not unique', tc.signature) + # always clean up after yourself + tc.clean_tmpdir() + + # clean up + tc.delete_files() + # whether it was unique or not, record some details for posterity + # record the details of this crash so we can regenerate it later if needed + tc.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', tc.seedfile.basename, tc.seednum, tc.range, tc.fuzzedfile.path) + tc.logger.info('PC=%s', tc.pc) + + # score this crash for the seedfile + tc.seedfile.record_success(tc.signature, tries=0) + if tc.range: + # ...and for the range + tc.range.record_success(tc.signature, tries=0) + + new_uniq_crash = found_new_crash # record the zzuf log line for this crash - if not c.logger: - c.get_logger() - c.logger.debug("zzuflog: %s", zzuf_log.line) - c.logger.info('Command: %s', testcase.cmdline) + if not tc.logger: + tc.get_logger() + tc.logger.debug("zzuflog: %s", zzuf_log.line) + tc.logger.info('Command: %s', testcase.cmdline) def construct_report(self, testcase): ''' From ac3e626f4ef52ed21981b5a9e2f4904f4ad7e36e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 15:40:28 -0500 Subject: [PATCH 0120/1169] update verify and analyze architectures --- src/certfuzz/iteration/linux.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 5b46245..2649bd0 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -193,6 +193,29 @@ def _run(self): def _postrun(self): pass + def _preanalyze(self, testcase): + pass + + def _analyze(self, testcase): + ''' + Loops through all known analyzers for a given testcase + :param testcase: + ''' + for analyzer in self.analyzers: + analyzer(testcase) + + def _postanalyze(self, testcase): + pass + + def _preverify(self, testcase): + pass + + def _verify(self, testcase): + pass + + def _postverify(self, testcase): + pass + def fuzz(self): self._prefuzz() self._fuzz() From c6d41ad9a10d33a052d2be9a4dd83e1c0ab3c945 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Feb 2014 15:42:07 -0500 Subject: [PATCH 0121/1169] move IterationBase3 to its own module --- src/certfuzz/iteration/iteration_base3.py | 112 ++++++++++++++++++++++ src/certfuzz/iteration/linux.py | 107 +-------------------- 2 files changed, 113 insertions(+), 106 deletions(-) create mode 100644 src/certfuzz/iteration/iteration_base3.py diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py new file mode 100644 index 0000000..e5fda81 --- /dev/null +++ b/src/certfuzz/iteration/iteration_base3.py @@ -0,0 +1,112 @@ +''' +Created on Feb 13, 2014 + +@author: adh +''' +class IterationBase3(object): + def __init__(self, workdirbase): + self.workdirbase = workdirbase + self.working_dir = None + self.analyzers = None + self.candidates = [] + self.verified = [] + self.analyzed = [] + + def __enter__(self): + self.working_dir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) + logger.debug('workdir=%s', self.working_dir) + return self + + def __exit__(self, etype, value, traceback): + shutil.rmtree(self.working_dir) + + def _prefuzz(self): + pass + + def _fuzz(self): + pass + + def _postfuzz(self): + pass + + def _prerun(self): + pass + + def _run(self): + pass + + def _postrun(self): + pass + + def _preanalyze(self, testcase): + pass + + def _analyze(self, testcase): + ''' + Loops through all known analyzers for a given testcase + :param testcase: + ''' + for analyzer in self.analyzers: + analyzer(testcase) + + def _postanalyze(self, testcase): + pass + + def _preverify(self, testcase): + pass + + def _verify(self, testcase): + pass + + def _postverify(self, testcase): + pass + + def fuzz(self): + self._prefuzz() + self._fuzz() + self._postfuzz() + + def run(self): + self._prerun() + self._run() + self._postrun() + + def verify(self, testcase): + self._preverify(testcase) + self._verify(testcase) + self._postverify(testcase) + + def analyze(self, testcase): + self._preanalyze(testcase) + self._analyze(testcase) + self._postanalyze(testcase) + + def construct_report(self, testcase): + pass + + def go(self): + self.fuzz() + self.run() + + # short circuit if nothing found + if not self.candidates: + return + + # every test case is a candidate until verified + # use a while loop so we have the option of adding + # candidates during the loop + while len(self.candidates) > 0: + testcase = self.candidates.pop(0) + self.verify(testcase) + + # analyze each verified crash + while len(self.verified) > 0: + testcase = self.verified.pop(0) + self.analyze(testcase) + + # construct output bundle for each analyzed test case + while len(self.analyzed) > 0: + testcase = self.analyzed.pop(0) + self.construct_report(testcase) + + diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 2649bd0..7d9551e 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -26,6 +26,7 @@ import tempfile import shutil from certfuzz.fuzztools.ppid_observer import check_ppid +from certfuzz.iteration.iteration_base3 import IterationBase3 logger = logging.getLogger(__name__) @@ -158,112 +159,6 @@ def analyze_crasher(cfg, crash): return other_crashers_found -class IterationBase3(object): - def __init__(self, workdirbase): - self.workdirbase = workdirbase - self.working_dir = None - self.analyzers = None - self.candidates = [] - self.verified = [] - self.analyzed = [] - - def __enter__(self): - self.working_dir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) - logger.debug('workdir=%s', self.working_dir) - return self - - def __exit__(self, etype, value, traceback): - shutil.rmtree(self.working_dir) - - def _prefuzz(self): - pass - - def _fuzz(self): - pass - - def _postfuzz(self): - pass - - def _prerun(self): - pass - - def _run(self): - pass - - def _postrun(self): - pass - - def _preanalyze(self, testcase): - pass - - def _analyze(self, testcase): - ''' - Loops through all known analyzers for a given testcase - :param testcase: - ''' - for analyzer in self.analyzers: - analyzer(testcase) - - def _postanalyze(self, testcase): - pass - - def _preverify(self, testcase): - pass - - def _verify(self, testcase): - pass - - def _postverify(self, testcase): - pass - - def fuzz(self): - self._prefuzz() - self._fuzz() - self._postfuzz() - - def run(self): - self._prerun() - self._run() - self._postrun() - - def verify(self, testcase): - self._preverify(testcase) - self._verify(testcase) - self._postverify(testcase) - - def analyze(self, testcase): - self._preanalyze(testcase) - self._analyze(testcase) - self._postanalyze(testcase) - - def construct_report(self, testcase): - pass - - def go(self): - self.fuzz() - self.run() - - # short circuit if nothing found - if not self.candidates: - return - - # every test case is a candidate until verified - # use a while loop so we have the option of adding - # candidates during the loop - while len(self.candidates) > 0: - testcase = self.candidates.pop(0) - self.verify(testcase) - - # analyze each verified crash - while len(self.verified) > 0: - testcase = self.verified.pop(0) - self.analyze(testcase) - - # construct output bundle for each analyzed test case - while len(self.analyzed) > 0: - testcase = self.analyzed.pop(0) - self.construct_report(testcase) - class Iteration(IterationBase3): def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None): From 74257cacbaae003e948cf1a3da664d3ef3b67aa6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 09:26:39 -0500 Subject: [PATCH 0122/1169] uniq logger goes in campaign not in main bff script --- src/certfuzz/bff/linux.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 2124a0e..800e2e7 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -36,12 +36,6 @@ # return rate -def get_uniq_logger(logfile): - l = logging.getLogger('uniq_crash') - if len(l.handlers) == 0: - hdlr = logging.FileHandler(logfile) - l.addHandler(hdlr) - return l def setup_logging_to_console(log_obj, level): From 9420ea7be0b564ff560f1af0f44a9bdb3d3b57d0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 09:27:50 -0500 Subject: [PATCH 0123/1169] refactor analyze crasher into chunks --- src/certfuzz/iteration/iteration_base3.py | 79 ++++-- src/certfuzz/iteration/linux.py | 298 +++++++++++----------- 2 files changed, 201 insertions(+), 176 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index e5fda81..fd6b32c 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -3,11 +3,21 @@ @author: adh ''' +import tempfile +import logging +import shutil +from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +from certfuzz.analyzers.errors import AnalyzerEmptyOutputError + +logger = logging.getLogger(__name__) + + class IterationBase3(object): def __init__(self, workdirbase): self.workdirbase = workdirbase self.working_dir = None - self.analyzers = None + self.analyzer_classes = [] + self.candidates = [] self.verified = [] self.analyzed = [] @@ -20,69 +30,87 @@ def __enter__(self): def __exit__(self, etype, value, traceback): shutil.rmtree(self.working_dir) - def _prefuzz(self): + def _pre_fuzz(self): pass def _fuzz(self): pass - def _postfuzz(self): + def _post_fuzz(self): pass - def _prerun(self): + def _pre_run(self): pass def _run(self): pass - def _postrun(self): + def _post_run(self): pass - def _preanalyze(self, testcase): + def _pre_analyze(self, testcase): pass def _analyze(self, testcase): ''' - Loops through all known analyzers for a given testcase + Loops through all known analyzer_classes for a given testcase :param testcase: ''' - for analyzer in self.analyzers: - analyzer(testcase) + for analyzer_class in self.analyzer_classes: + touch_watchdog_file() - def _postanalyze(self, testcase): + analyzer_instance = analyzer_class(self.cfg, testcase) + if analyzer_instance: + try: + analyzer_instance.go() + except AnalyzerEmptyOutputError: + logger.warning('Unexpected empty output from analyzer_class. Continuing') + + def _post_analyze(self, testcase): pass - def _preverify(self, testcase): + def _pre_verify(self, testcase): pass def _verify(self, testcase): pass - def _postverify(self, testcase): + def _post_verify(self, testcase): + pass + + def _pre_report(self, testcase): + pass + + def _report(self, testcase): + pass + + def _post_report(self, testcase): pass def fuzz(self): - self._prefuzz() + self._pre_fuzz() self._fuzz() - self._postfuzz() + self._post_fuzz() def run(self): - self._prerun() + self._pre_run() self._run() - self._postrun() + self._post_run() def verify(self, testcase): - self._preverify(testcase) + self._pre_verify(testcase) self._verify(testcase) - self._postverify(testcase) + self._post_verify(testcase) def analyze(self, testcase): - self._preanalyze(testcase) + self._pre_analyze(testcase) self._analyze(testcase) - self._postanalyze(testcase) + self._post_analyze(testcase) - def construct_report(self, testcase): - pass + def report(self, testcase): + self._pre_report(testcase) + self._report(testcase) + self._post_report(testcase) def go(self): self.fuzz() @@ -99,6 +127,11 @@ def go(self): testcase = self.candidates.pop(0) self.verify(testcase) + # minimize verified testcases + while len(self.verified) > 0: + testcase = self.verified.pop(0) + self.minimize(testcase) + # analyze each verified crash while len(self.verified) > 0: testcase = self.verified.pop(0) @@ -107,6 +140,6 @@ def go(self): # construct output bundle for each analyzed test case while len(self.analyzed) > 0: testcase = self.analyzed.pop(0) - self.construct_report(testcase) + self.report(testcase) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 7d9551e..a17cda1 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -53,111 +53,13 @@ # crash.is_unique = not crash_dir_found -def analyze_crasher(cfg, crash): - ''' - Runs multiple analyses and collects data about a crash. Returns a list of other crashes - encountered during the process of analyzing the current crash. - @param cfg: A BFF config object - @param crash: A crash object - @return: a list of Crasher objects - ''' - other_crashers_found = [] - - dbg_out_file_orig = crash.dbg.file - logger.debug('Original debugger file: %s', dbg_out_file_orig) - - if cfg.minimizecrashers: - STATE_TIMER.enter_state('minimize_testcase') - # try to reduce the Hamming Distance between the crasher file and the known good seedfile - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=cfg, crash=crash, bitwise=False, - seedfile_as_target=True, confidence=0.999, - tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout - ) as minimizer: - minimizer.go() - other_crashers_found.extend(minimizer.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) - minimizer = None - - touch_watchdog_file() - # calculate the hamming distances for this crash - # between the original seedfile and the minimized fuzzed file - crash.calculate_hamming_distances() - - if cfg.minimize_to_string: - STATE_TIMER.enter_state('minimize_testcase_to_string') - # Minimize to a string of 'x's - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=cfg, crash=crash, bitwise=False, - seedfile_as_target=False, confidence=0.9, - tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout - ) as min2string: - min2string.go() - other_crashers_found.extend(min2string.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) - min2string = None - touch_watchdog_file() - - STATE_TIMER.enter_state('analyze_testcase') - - # get one last debugger output for the newly minimized file - if crash.pc_in_function: - # change the debugger template - crash.set_debugger_template('complete') - else: - # use a debugger template that specifies fixed offsets from $pc for disassembly - crash.set_debugger_template('complete_nofunction') - logger.info('Getting complete debugger output for crash: %s', crash.fuzzedfile.path) - crash.get_debug_output(crash.fuzzedfile.path) - - if dbg_out_file_orig != crash.dbg.file: - # we have a new debugger output - # remove the old one - filetools.delete_files(dbg_out_file_orig) - if os.path.exists(dbg_out_file_orig): - logger.warning('Failed to remove old debugger file %s', dbg_out_file_orig) - else: - logger.debug('Removed old debug file %s', dbg_out_file_orig) - - # use the minimized file for the rest of the analyses - analyzers = [ - stderr.StdErr, - cw_gmalloc.CrashWranglerGmalloc, - ] - if cfg.use_valgrind: - analyzers.extend([ - valgrind.Valgrind, - callgrind.Callgrind, - ]) - if cfg.use_pin_calltrace: - analyzers.extend([ - pin_calltrace.Pin_calltrace, - ]) - - for analyzer in analyzers: - touch_watchdog_file() - analyzer_instance = analyzer(cfg, crash) - if analyzer_instance: - try: - analyzer_instance.go() - except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from analyzer. Continuing') - - logger.info('Annotating callgrind output') - try: - annotate_callgrind(crash) - annotate_callgrind_tree(crash) - except CallgrindAnnotateEmptyOutputFileError: - logger.warning('Unexpected empty output from annotate_callgrind. Continuing') - except CallgrindAnnotateMissingInputFileError: - logger.warning('Missing callgrind output. Continuing') - - return other_crashers_found +def get_uniq_logger(logfile): + l = logging.getLogger('uniq_crash') + if len(l.handlers) == 0: + hdlr = logging.FileHandler(logfile) + l.addHandler(hdlr) + return l class Iteration(IterationBase3): @@ -174,6 +76,8 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No else: self.uniq_func = uniq_func + self._setup_analyzers() + # convenience aliases self.s1 = self.seednum self.s2 = self.s1 @@ -202,13 +106,136 @@ def _log(self): # self.sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) logger.info('Fuzzing...crash_count=%d', crashcount) + def _setup_analyzers(self): + self.analyzer_classes.append(stderr.StdErr) + self.analyzer_classes.append(cw_gmalloc.CrashWranglerGmalloc) + + if self.cfg.use_valgrind: + self.analyzer_classes.append(valgrind.Valgrind) + self.analyzer_classes.append(callgrind.Callgrind) + + if self.cfg.use_pin_calltrace: + self.analyzer_classes.append(pin_calltrace.Pin_calltrace) + + def _pre_analyze(self, testcase): + IterationBase3._pre_analyze(self, testcase) + + other_crashers_found = [] + + dbg_out_file_orig = testcase.dbg.file + logger.debug('Original debugger file: %s', dbg_out_file_orig) + + if self.cfg.minimizecrashers: + STATE_TIMER.enter_state('minimize_testcase') + # try to reduce the Hamming Distance between the crasher file and the known good seedfile + # crash.fuzzedfile will be replaced with the minimized result + try: + with Minimizer(cfg=self.cfg, crash=testcase, bitwise=False, + seedfile_as_target=True, confidence=0.999, + tempdir=self.cfg.local_dir, maxtime=self.cfg.minimizertimeout + ) as minimizer: + minimizer.go() + other_crashers_found.extend(minimizer.other_crashes.values()) + except MinimizerError, e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) + minimizer = None + + touch_watchdog_file() + # calculate the hamming distances for this crash + # between the original seedfile and the minimized fuzzed file + testcase.calculate_hamming_distances() + + if self.cfg.minimize_to_string: + STATE_TIMER.enter_state('minimize_testcase_to_string') + # Minimize to a string of 'x's + # crash.fuzzedfile will be replaced with the minimized result + try: + with Minimizer(cfg=self.cfg, crash=testcase, bitwise=False, + seedfile_as_target=False, confidence=0.9, + tempdir=self.cfg.local_dir, maxtime=self.cfg.minimizertimeout + ) as min2string: + min2string.go() + other_crashers_found.extend(min2string.other_crashes.values()) + except MinimizerError, e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) + min2string = None + touch_watchdog_file() + + # add new crashes to the queue + self.candidates.extend(other_crashers_found) + + STATE_TIMER.enter_state('analyze_testcase') + + # get one last debugger output for the newly minimized file + if testcase.pc_in_function: + # change the debugger template + testcase.set_debugger_template('complete') + else: + # use a debugger template that specifies fixed offsets from $pc for disassembly + testcase.set_debugger_template('complete_nofunction') + logger.info('Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) + testcase.get_debug_output(testcase.fuzzedfile.path) + + if dbg_out_file_orig != testcase.dbg.file: + # we have a new debugger output + # remove the old one + filetools.delete_files(dbg_out_file_orig) + if os.path.exists(dbg_out_file_orig): + logger.warning('Failed to remove old debugger file %s', dbg_out_file_orig) + else: + logger.debug('Removed old debug file %s', dbg_out_file_orig) + + def _post_analyze(self, testcase): + IterationBase3._post_analyze(self, testcase) + + logger.info('Annotating callgrind output') + try: + annotate_callgrind(testcase) + annotate_callgrind_tree(testcase) + except CallgrindAnnotateEmptyOutputFileError: + logger.warning('Unexpected empty output from annotate_callgrind. Continuing') + except CallgrindAnnotateMissingInputFileError: + logger.warning('Missing callgrind output. Continuing') + + if self.cfg.recycle_crashers: + logger.debug('Recycling crash as seedfile') + iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext + crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) + filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) + seedfile_set.add_file(crasherseed_path) + + # score this crash for the seedfile + testcase.seedfile.record_success(testcase.signature, tries=0) + if testcase.range: + # ...and for the range + testcase.range.record_success(testcase.signature, tries=0) + + def _pre_report(self, testcase): + uniqlogger = get_uniq_logger(self.cfg.uniq_log) + uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) + logger.info('%s first seen at %d', testcase.signature, testcase.seednum) + + # whether it was unique or not, record some details for posterity + # record the details of this crash so we can regenerate it later if needed + testcase.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', testcase.seedfile.basename, testcase.seednum, testcase.range, testcase.fuzzedfile.path) + testcase.logger.info('PC=%s', testcase.pc) + + def _report(self, testcase): + testcase.copy_files() + + def _post_report(self, testcase): + # always clean up after yourself + testcase.clean_tmpdir() + # clean up + testcase.delete_files() def _verify(self, testcase): ''' Confirms that a test case is interesting enough to pursue further analysis :param testcase: ''' - IterationBase3.verify(self, testcase) + IterationBase3._verify(self, testcase) # if you find more testcases, append them to self.candidates # verified crashes append to self.verified @@ -219,70 +246,31 @@ def _verify(self, testcase): found_new_crash = False - # loop until we're out of crashes to verify logger.debug('crashes to verify: %d', len(self.candidates)) STATE_TIMER.enter_state('verify_testcase') tc.is_unique = self.uniq_func(tc.signature) - tc.get_logger() if tc.is_unique: - self.verified.append(tc) -# # only toggle it once -# if not found_new_crash: -# found_new_crash = True - - more_crashes = analyze_crasher(self.cfg, tc) - - if self.cfg.recycle_crashers: - logger.debug('Recycling crash as seedfile') - iterstring = tc.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + tc.seedfile.md5 + '-' + iterstring + tc.seedfile.ext - crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) - filetools.copy_file(tc.fuzzedfile.path, crasherseed_path) - seedfile_set.add_file(crasherseed_path) - # add new crashes to the queue - self.candidates.extend(more_crashes) - tc.copy_files() - - uniqlogger = get_uniq_logger(self.cfg.uniq_log) - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', tc.seedfile.basename, tc.signature, tc.seednum, tc.range, tc.hd_bits, tc.hd_bytes) logger.info('%s first seen at %d', tc.signature, tc.seednum) + self.verified.append(tc) else: logger.debug('%s was found, not unique', tc.signature) - # always clean up after yourself - tc.clean_tmpdir() - # clean up - tc.delete_files() - # whether it was unique or not, record some details for posterity - # record the details of this crash so we can regenerate it later if needed - tc.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', tc.seedfile.basename, tc.seednum, tc.range, tc.fuzzedfile.path) - tc.logger.info('PC=%s', tc.pc) - - # score this crash for the seedfile - tc.seedfile.record_success(tc.signature, tries=0) - if tc.range: - # ...and for the range - tc.range.record_success(tc.signature, tries=0) - - new_uniq_crash = found_new_crash # record the zzuf log line for this crash if not tc.logger: tc.get_logger() - tc.logger.debug("zzuflog: %s", zzuf_log.line) - tc.logger.info('Command: %s', testcase.cmdline) - def construct_report(self, testcase): + def report(self, testcase): ''' Constructs a report package for the test case :param testcase: ''' - def _prerun(self): - IterationBase3._prerun(self) + def _pre_run(self): + IterationBase3._pre_run(self) # do the fuzz cmdline = self.cfg.get_command(self.sf.path) @@ -302,8 +290,8 @@ def _run(self): IterationBase3._run(self) self.zzuf.go() - def _postrun(self): - IterationBase3._postrun(self) + def _post_run(self): + IterationBase3._post_run(self) STATE_TIMER.enter_state('checking_results') # we must have made it through this chunk without a crash @@ -333,14 +321,18 @@ def _postrun(self): self.cfg.create_tmpdir() outfile = self.cfg.get_testcase_outfile(self.seedfile.path, self.s1) logger.debug('Output file is %s', outfile) - testcase = self.zzuf.generate_test_case(self.seedfile.path, self.s1, zzuf_range, outfile) + self.zzuf.generate_test_case(self.seedfile.path, self.s1, zzuf_range, outfile) # Do internal verification using GDB / Valgrind / Stderr fuzzedfile = file_handlers.basicfile.BasicFile(outfile) - crasher = BffCrash(self.cfg, self.seedfile, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, + testcase = BffCrash(self.cfg, self.seedfile, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, self.cfg.killprocname, self.cfg.backtracelevels, self.cfg.crashers_dir, self.s1, self.r) - self.candidates.append(crasher) + testcase.get_logger() + testcase.logger.debug("zzuflog: %s", zzuf_log.line) +# testcase.logger.info('Command: %s', testcase.cmdline) + + self.candidates.append(testcase) From 8b910bc376dc26d7d948ce7f4a2638bb8dd36f51 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 09:30:28 -0500 Subject: [PATCH 0124/1169] reorder methods to reflect actual program flow --- src/certfuzz/iteration/linux.py | 205 +++++++++++++++----------------- 1 file changed, 98 insertions(+), 107 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index a17cda1..26bfb25 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -117,6 +117,104 @@ def _setup_analyzers(self): if self.cfg.use_pin_calltrace: self.analyzer_classes.append(pin_calltrace.Pin_calltrace) + def _pre_run(self): + IterationBase3._pre_run(self) + # do the fuzz + cmdline = self.cfg.get_command(self.sf.path) + + STATE_TIMER.enter_state('fuzzing') + self.zzuf = Zzuf(self.cfg.local_dir, self.s1, + self.s1, + cmdline, + self.sf.path, + self.cfg.zzuf_log_file, + self.cfg.copymode, + self.r.min, + self.r.max, + self.cfg.progtimeout, + self.quiet_flag) + + def _run(self): + IterationBase3._run(self) + self.zzuf.go() + + def _post_run(self): + IterationBase3._post_run(self) + + STATE_TIMER.enter_state('checking_results') + # we must have made it through this chunk without a crash + # so go to next chunk + self.sf.record_tries(tries=1) + self.r.record_tries(tries=1) + if not self.zzuf.saw_crash: + self._log() + return + + # we must have seen a crash + # get the results + zzuf_log = ZzufLog(self.cfg.zzuf_log_file, self.cfg.zzuf_log_out(self.sf.output_dir)) + + # Don't generate cases for killed process or out-of-memory + # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will + # report the exit code in its output log. The exit code is 128 + the signal number. + crash_status = zzuf_log.crash_logged(self.cfg.copymode) + + if not crash_status: + return + + logger.info('Generating testcase for %s', zzuf_log.line) + # a true crash + zzuf_range = zzuf_log.range + # create the temp dir for the results + self.cfg.create_tmpdir() + outfile = self.cfg.get_testcase_outfile(self.seedfile.path, self.s1) + logger.debug('Output file is %s', outfile) + self.zzuf.generate_test_case(self.seedfile.path, self.s1, zzuf_range, outfile) + + # Do internal verification using GDB / Valgrind / Stderr + fuzzedfile = file_handlers.basicfile.BasicFile(outfile) + + testcase = BffCrash(self.cfg, self.seedfile, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, + self.cfg.killprocname, self.cfg.backtracelevels, + self.cfg.crashers_dir, self.s1, self.r) + + # record the zzuf log line for this crash + if not testcase.logger: + testcase.get_logger() + + testcase.logger.debug("zzuflog: %s", zzuf_log.line) +# testcase.logger.info('Command: %s', testcase.cmdline) + + self.candidates.append(testcase) + + def _verify(self, testcase): + ''' + Confirms that a test case is interesting enough to pursue further analysis + :param testcase: + ''' + IterationBase3._verify(self, testcase) + + # if you find more testcases, append them to self.candidates + # verified crashes append to self.verified + + logger.debug('verifying crash') + with testcase as tc: + if tc.is_crash: + + found_new_crash = False + + logger.debug('crashes to verify: %d', len(self.candidates)) + STATE_TIMER.enter_state('verify_testcase') + + tc.is_unique = self.uniq_func(tc.signature) + + + if tc.is_unique: + logger.info('%s first seen at %d', tc.signature, tc.seednum) + self.verified.append(tc) + else: + logger.debug('%s was found, not unique', tc.signature) + def _pre_analyze(self, testcase): IterationBase3._pre_analyze(self, testcase) @@ -229,110 +327,3 @@ def _post_report(self, testcase): testcase.clean_tmpdir() # clean up testcase.delete_files() - - def _verify(self, testcase): - ''' - Confirms that a test case is interesting enough to pursue further analysis - :param testcase: - ''' - IterationBase3._verify(self, testcase) - - # if you find more testcases, append them to self.candidates - # verified crashes append to self.verified - - logger.debug('verifying crash') - with testcase as tc: - if tc.is_crash: - - found_new_crash = False - - logger.debug('crashes to verify: %d', len(self.candidates)) - STATE_TIMER.enter_state('verify_testcase') - - tc.is_unique = self.uniq_func(tc.signature) - - - if tc.is_unique: - logger.info('%s first seen at %d', tc.signature, tc.seednum) - self.verified.append(tc) - else: - logger.debug('%s was found, not unique', tc.signature) - - - # record the zzuf log line for this crash - if not tc.logger: - tc.get_logger() - - def report(self, testcase): - ''' - Constructs a report package for the test case - :param testcase: - ''' - - def _pre_run(self): - IterationBase3._pre_run(self) - # do the fuzz - cmdline = self.cfg.get_command(self.sf.path) - - STATE_TIMER.enter_state('fuzzing') - self.zzuf = Zzuf(self.cfg.local_dir, self.s1, - self.s1, - cmdline, - self.sf.path, - self.cfg.zzuf_log_file, - self.cfg.copymode, - self.r.min, - self.r.max, - self.cfg.progtimeout, - self.quiet_flag) - - def _run(self): - IterationBase3._run(self) - self.zzuf.go() - - def _post_run(self): - IterationBase3._post_run(self) - - STATE_TIMER.enter_state('checking_results') - # we must have made it through this chunk without a crash - # so go to next chunk - self.sf.record_tries(tries=1) - self.r.record_tries(tries=1) - if not self.zzuf.saw_crash: - self._log() - return - - # we must have seen a crash - # get the results - zzuf_log = ZzufLog(self.cfg.zzuf_log_file, self.cfg.zzuf_log_out(self.sf.output_dir)) - - # Don't generate cases for killed process or out-of-memory - # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will - # report the exit code in its output log. The exit code is 128 + the signal number. - crash_status = zzuf_log.crash_logged(self.cfg.copymode) - - if not crash_status: - return - - logger.info('Generating testcase for %s', zzuf_log.line) - # a true crash - zzuf_range = zzuf_log.range - # create the temp dir for the results - self.cfg.create_tmpdir() - outfile = self.cfg.get_testcase_outfile(self.seedfile.path, self.s1) - logger.debug('Output file is %s', outfile) - self.zzuf.generate_test_case(self.seedfile.path, self.s1, zzuf_range, outfile) - - # Do internal verification using GDB / Valgrind / Stderr - fuzzedfile = file_handlers.basicfile.BasicFile(outfile) - - testcase = BffCrash(self.cfg, self.seedfile, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, - self.cfg.killprocname, self.cfg.backtracelevels, - self.cfg.crashers_dir, self.s1, self.r) - - testcase.get_logger() - testcase.logger.debug("zzuflog: %s", zzuf_log.line) -# testcase.logger.info('Command: %s', testcase.cmdline) - - self.candidates.append(testcase) - From 3076a3d353b6763da1a0ac9c13e99f2c3a8de02b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 11:31:07 -0500 Subject: [PATCH 0125/1169] call _minimize from _post_verify --- src/certfuzz/iteration/iteration_base3.py | 5 ----- src/certfuzz/iteration/linux.py | 25 +++++++++++------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index fd6b32c..860b69c 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -127,11 +127,6 @@ def go(self): testcase = self.candidates.pop(0) self.verify(testcase) - # minimize verified testcases - while len(self.verified) > 0: - testcase = self.verified.pop(0) - self.minimize(testcase) - # analyze each verified crash while len(self.verified) > 0: testcase = self.verified.pop(0) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 26bfb25..a98b306 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -52,8 +52,6 @@ # # crash.is_unique = not crash_dir_found - - def get_uniq_logger(logfile): l = logging.getLogger('uniq_crash') if len(l.handlers) == 0: @@ -192,6 +190,7 @@ def _verify(self, testcase): Confirms that a test case is interesting enough to pursue further analysis :param testcase: ''' + STATE_TIMER.enter_state('verify_testcase') IterationBase3._verify(self, testcase) # if you find more testcases, append them to self.candidates @@ -201,23 +200,21 @@ def _verify(self, testcase): with testcase as tc: if tc.is_crash: - found_new_crash = False - - logger.debug('crashes to verify: %d', len(self.candidates)) - STATE_TIMER.enter_state('verify_testcase') - tc.is_unique = self.uniq_func(tc.signature) - if tc.is_unique: logger.info('%s first seen at %d', tc.signature, tc.seednum) + new_testcases = self._minimize(tc) + # add any new candidates for verification to the candidates list + self.candidates.extend(new_testcases) + + # we're ready to proceed with this testcase + # so add it to the verified list self.verified.append(tc) else: logger.debug('%s was found, not unique', tc.signature) - def _pre_analyze(self, testcase): - IterationBase3._pre_analyze(self, testcase) - + def _minimize(self, testcase): other_crashers_found = [] dbg_out_file_orig = testcase.dbg.file @@ -259,8 +256,10 @@ def _pre_analyze(self, testcase): min2string = None touch_watchdog_file() - # add new crashes to the queue - self.candidates.extend(other_crashers_found) + return other_crashers_found + + def _pre_analyze(self, testcase): + IterationBase3._pre_analyze(self, testcase) STATE_TIMER.enter_state('analyze_testcase') From f4eed5bf4067250043022785e21eb68f12c902ed Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 13:21:28 -0500 Subject: [PATCH 0126/1169] pass in working dir from above --- src/certfuzz/campaign/linux.py | 3 ++- src/certfuzz/crash/bff_crash.py | 7 +++---- src/certfuzz/iteration/linux.py | 14 +++++++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index d1114e3..000f559 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -59,6 +59,7 @@ def __init__(self, cfg_path=None, scriptpath=None): self.scriptpath = scriptpath self.seedfile_set = None self.hashes = [] + self.workdirbase = self.cfg.testscase_tmp_dir self.working_dir = None self.debug = True self.crashes_seen = set() @@ -71,7 +72,7 @@ def __enter__(self): self._setup_dirs() # setup working dir - self.working_dir = tempfile.mkdtemp(prefix='campaign_', dir=self.cfg.local_dir) + self.working_dir = tempfile.mkdtemp(prefix='campaign_', dir=self.workdirbase) logger.debug('workdir=%s', self.working_dir) # set up local logging diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index b230396..2014f67 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -32,14 +32,13 @@ class BffCrash(Crash): def __init__(self, cfg, seedfile, fuzzedfile, program, debugger_timeout, killprocname, backtrace_lines, - crashers_dir, seednum=None, range=None, keep_faddr=False): + crashers_dir, workdir_base, seednum=None, range=None, keep_faddr=False): ''' Constructor ''' - - super(self.__class__, self).__init__(seedfile, fuzzedfile, debugger_timeout) + Crash.__init__(self, seedfile, fuzzedfile, debugger_timeout) self.cfg = cfg - self.workdir_base = self.cfg.testscase_tmp_dir + self.workdir_base = workdir_base self.program = program self.killprocname = killprocname self.backtrace_lines = backtrace_lines diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index a98b306..8b9d9a4 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -172,9 +172,17 @@ def _post_run(self): # Do internal verification using GDB / Valgrind / Stderr fuzzedfile = file_handlers.basicfile.BasicFile(outfile) - testcase = BffCrash(self.cfg, self.seedfile, fuzzedfile, self.cfg.program, self.cfg.debugger_timeout, - self.cfg.killprocname, self.cfg.backtracelevels, - self.cfg.crashers_dir, self.s1, self.r) + testcase = BffCrash(cfg=self.cfg, + seedfile=self.seedfile, + fuzzedfile=fuzzedfile, + program=self.cfg.program, + debugger_timeout=self.cfg.debugger_timeout, + killprocname=self.cfg.killprocname, + backtrace_lines=self.cfg.backtracelevels, + crashers_dir=self.cfg.crashers_dir, + workdir_base=self.cfg.testscase_tmp_dir, + seednum=self.s1, + range=self.r) # record the zzuf log line for this crash if not testcase.logger: From 69dc73e15f1d876bb4a01d37ca1ba2605d1946a6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 13:21:50 -0500 Subject: [PATCH 0127/1169] clean up error handling in __exit__ method --- src/certfuzz/campaign/linux.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 000f559..cb7a535 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -97,7 +97,8 @@ def __enter__(self): return self def __exit__(self, etype, value, mytraceback): - handled = False + handled = not etype + if etype is KeyboardInterrupt: logger.warning('Keyboard interrupt - exiting') handled = True @@ -105,22 +106,22 @@ def __exit__(self, etype, value, mytraceback): logger.warning("Please configure BFF to fuzz a binary. Exiting...") handled = True - # if etype not set or if we handled it - if not etype or handled: - shutil.rmtree(self.working_dir) - elif etype: - logger.debug('Unhandled exception:') - logger.debug(' type: %s', etype) - logger.debug(' value: %s', value) - for l in traceback.format_exception(etype, value, mytraceback): - logger.debug(l.rstrip()) + if handled: + self._cleanup_workdir() + elif self.debug: + # Not handled, debug set - if self.debug and etype and not handled: # leave it behind if we're in debug mode # and there's a problem logger.debug('Skipping cleanup since we are in debug mode.') else: - self._cleanup_workdir() + # Not handled, debug not set + + logger.debug('Unhandled exception:') + logger.debug(' type: %s', etype) + logger.debug(' value: %s', value) + for l in traceback.format_exception(etype, value, mytraceback): + logger.debug(l.rstrip()) return handled From 241ad39edaadc7ca39afc56ede8e45083fc855b5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 14:14:35 -0500 Subject: [PATCH 0128/1169] got it running, but it's not copying crashers out of their temp location yet --- src/certfuzz/crash/bff_crash.py | 3 ++- src/certfuzz/iteration/iteration_base3.py | 13 ++++++++++++- src/certfuzz/iteration/linux.py | 18 +++++++++--------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index 2014f67..f25d4d5 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -58,7 +58,8 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, self.result_dir = None def __exit__(self, etype, value, traceback): - self.clean_tmpdir() + pass +# self.clean_tmpdir() def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 860b69c..fb1a585 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -22,13 +22,24 @@ def __init__(self, workdirbase): self.verified = [] self.analyzed = [] + self.debug = True + def __enter__(self): self.working_dir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) logger.debug('workdir=%s', self.working_dir) return self def __exit__(self, etype, value, traceback): - shutil.rmtree(self.working_dir) + handled = False + + if etype and self.debug: + # leave it behind if we're in debug mode + # and there's a problem + logger.debug('Skipping cleanup since we are in debug mode.') + else: + shutil.rmtree(self.working_dir) + + return handled def _pre_fuzz(self): pass diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 8b9d9a4..32542d7 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -180,7 +180,7 @@ def _post_run(self): killprocname=self.cfg.killprocname, backtrace_lines=self.cfg.backtracelevels, crashers_dir=self.cfg.crashers_dir, - workdir_base=self.cfg.testscase_tmp_dir, + workdir_base=self.working_dir, seednum=self.s1, range=self.r) @@ -212,6 +212,9 @@ def _verify(self, testcase): if tc.is_unique: logger.info('%s first seen at %d', tc.signature, tc.seednum) + self.dbg_out_file_orig = testcase.dbg.file + logger.debug('Original debugger file: %s', self.dbg_out_file_orig) + new_testcases = self._minimize(tc) # add any new candidates for verification to the candidates list self.candidates.extend(new_testcases) @@ -225,9 +228,6 @@ def _verify(self, testcase): def _minimize(self, testcase): other_crashers_found = [] - dbg_out_file_orig = testcase.dbg.file - logger.debug('Original debugger file: %s', dbg_out_file_orig) - if self.cfg.minimizecrashers: STATE_TIMER.enter_state('minimize_testcase') # try to reduce the Hamming Distance between the crasher file and the known good seedfile @@ -281,14 +281,14 @@ def _pre_analyze(self, testcase): logger.info('Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) testcase.get_debug_output(testcase.fuzzedfile.path) - if dbg_out_file_orig != testcase.dbg.file: + if self.dbg_out_file_orig != testcase.dbg.file: # we have a new debugger output # remove the old one - filetools.delete_files(dbg_out_file_orig) - if os.path.exists(dbg_out_file_orig): - logger.warning('Failed to remove old debugger file %s', dbg_out_file_orig) + filetools.delete_files(self.dbg_out_file_orig) + if os.path.exists(self.dbg_out_file_orig): + logger.warning('Failed to remove old debugger file %s', self.dbg_out_file_orig) else: - logger.debug('Removed old debug file %s', dbg_out_file_orig) + logger.debug('Removed old debug file %s', self.dbg_out_file_orig) def _post_analyze(self, testcase): IterationBase3._post_analyze(self, testcase) From e3a06fd89a8a5c6832db572695f713b03a1c403e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 14:36:06 -0500 Subject: [PATCH 0129/1169] clean up dead code --- src/certfuzz/iteration/linux.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 32542d7..505c0ab 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -12,7 +12,6 @@ from ..analyzers.callgrind.annotate import annotate_callgrind_tree from ..analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError from ..analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError -from ..analyzers.errors import AnalyzerEmptyOutputError from ..crash.bff_crash import BffCrash from ..debuggers import crashwrangler # @UnusedImport from ..debuggers import gdb # @UnusedImport @@ -23,35 +22,12 @@ from ..minimizer import MinimizerError, UnixMinimizer as Minimizer import os from certfuzz.file_handlers.watchdog_file import touch_watchdog_file -import tempfile -import shutil from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.iteration_base3 import IterationBase3 logger = logging.getLogger(__name__) -#def determine_uniqueness(crash, hashes): -# ''' -# Gets the crash signature, then compares it against known crashes. -# Sets crash.is_unique = True if it is new -# ''' -# -# # short-circuit on crashes with no signature -# if not crash.signature: -# logger.warning('Crash has no signature, cleaning up') -# crash.delete_files() -# return -# -# if crash.signature in hashes: -# crash.is_unique = False -# return -# -# # fall back to checking if the crash directory exists -# crash_dir_found = filetools.find_or_create_dir(crash.result_dir) -# -# crash.is_unique = not crash_dir_found - def get_uniq_logger(logfile): l = logging.getLogger('uniq_crash') if len(l.handlers) == 0: From 1c65a7b3a79e10771648346e2a11155b00fe62ac Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 15:18:19 -0500 Subject: [PATCH 0130/1169] DRY up minimizer call --- src/certfuzz/iteration/linux.py | 61 +++++++++++++++------------------ 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 505c0ab..bbb8289 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -191,9 +191,7 @@ def _verify(self, testcase): self.dbg_out_file_orig = testcase.dbg.file logger.debug('Original debugger file: %s', self.dbg_out_file_orig) - new_testcases = self._minimize(tc) - # add any new candidates for verification to the candidates list - self.candidates.extend(new_testcases) + self._minimize(tc) # we're ready to proceed with this testcase # so add it to the verified list @@ -202,45 +200,40 @@ def _verify(self, testcase): logger.debug('%s was found, not unique', tc.signature) def _minimize(self, testcase): - other_crashers_found = [] - if self.cfg.minimizecrashers: - STATE_TIMER.enter_state('minimize_testcase') - # try to reduce the Hamming Distance between the crasher file and the known good seedfile - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=self.cfg, crash=testcase, bitwise=False, - seedfile_as_target=True, confidence=0.999, - tempdir=self.cfg.local_dir, maxtime=self.cfg.minimizertimeout - ) as minimizer: - minimizer.go() - other_crashers_found.extend(minimizer.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) - minimizer = None + self._mininimize_to_seedfile(testcase) + if self.cfg.minimize_to_string: + self._minimize_to_string(testcase) - touch_watchdog_file() + def _mininimize_to_seedfile(self, testcase): + self._minimize_generic(testcase, sftarget=True, confidence=0.999) # calculate the hamming distances for this crash # between the original seedfile and the minimized fuzzed file testcase.calculate_hamming_distances() - if self.cfg.minimize_to_string: - STATE_TIMER.enter_state('minimize_testcase_to_string') - # Minimize to a string of 'x's - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=self.cfg, crash=testcase, bitwise=False, - seedfile_as_target=False, confidence=0.9, - tempdir=self.cfg.local_dir, maxtime=self.cfg.minimizertimeout - ) as min2string: - min2string.go() - other_crashers_found.extend(min2string.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) - min2string = None + def _minimize_to_string(self, testcase): + self._minimize_generic(testcase, sftarget=False, confidence=0.9) + + def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): touch_watchdog_file() - return other_crashers_found + STATE_TIMER.enter_state('minimize_testcase') + # try to reduce the Hamming Distance between the crasher file and the known good seedfile + # crash.fuzzedfile will be replaced with the minimized result + try: + with Minimizer(cfg=self.cfg, + crash=testcase, + bitwise=False, + seedfile_as_target=sftarget, + confidence=confidence, + tempdir=self.cfg.local_dir, + maxtime=self.cfg.minimizertimeout + ) as m: + m.go() + self.candidates.extend(m.other_crashes.values()) + except MinimizerError as e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) + m = None def _pre_analyze(self, testcase): IterationBase3._pre_analyze(self, testcase) From 1751f6d046ab65a6bf52ab2c98ae3a7352c0dbe0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 15:18:49 -0500 Subject: [PATCH 0131/1169] add analyzed test cases to list for handling downstream --- src/certfuzz/iteration/iteration_base3.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index fb1a585..4f51dc6 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -77,6 +77,8 @@ def _analyze(self, testcase): except AnalyzerEmptyOutputError: logger.warning('Unexpected empty output from analyzer_class. Continuing') + self.analyzed.append(testcase) + def _post_analyze(self, testcase): pass From 2f2703c55de995b4940c642d95e864f4f0063220 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 15:46:11 -0500 Subject: [PATCH 0132/1169] don't set logging level this far down in the code --- src/certfuzz/crash/bff_crash.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index f25d4d5..d304b6f 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -18,7 +18,6 @@ pass logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) debugger = None host_info = hostinfo.HostInfo() From 38a11eeb250502069cf3296c71d09de13d08355d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 15:46:36 -0500 Subject: [PATCH 0133/1169] always get the logger --- src/certfuzz/iteration/linux.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index bbb8289..a9dc96c 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -160,9 +160,8 @@ def _post_run(self): seednum=self.s1, range=self.r) - # record the zzuf log line for this crash - if not testcase.logger: - testcase.get_logger() + # record the zzuf log line for this crash + testcase.get_logger() testcase.logger.debug("zzuflog: %s", zzuf_log.line) # testcase.logger.info('Command: %s', testcase.cmdline) From 779cf7ea05f7404b656d4a522eba7ba1ba4571c5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 16:18:25 -0500 Subject: [PATCH 0134/1169] collapse find_or_create_dir into a substitute for mkdir_p --- src/certfuzz/fuzztools/filetools.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 67efde4..0637946 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -42,15 +42,24 @@ def wrapper(*args, **kwargs): return wrapper + def mkdir_p(path): + ''' + If directory exists, just return True + Otherwise create it and return False + :param path: + ''' try: os.makedirs(path) except OSError as exc: # if the dir already exists, just move along if exc.errno == errno.EEXIST and os.path.isdir(path): - pass + return True else: raise + return False + +find_or_create_dir = mkdir_p # file system helpers def make_directories(*paths): @@ -62,15 +71,6 @@ def make_directories(*paths): if not os.path.exists(d): mkdir_p(d) -def find_or_create_dir(dir): - if not os.path.exists(dir): - make_directories(dir) - logger.debug("Created dir %s", dir) - dir_found = False - else: - dir_found = True - return dir_found - def delete_files(*files): delete_files2(files) From 97b336f4a51902fa74ce5269652e4d9383b94458 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 16:20:34 -0500 Subject: [PATCH 0135/1169] check for the existence of the test case output dir and create it if needed --- src/certfuzz/iteration/linux.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index a9dc96c..04a2309 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -183,7 +183,13 @@ def _verify(self, testcase): with testcase as tc: if tc.is_crash: - tc.is_unique = self.uniq_func(tc.signature) + is_new_to_campaign = self.uniq_func(tc.signature) + + # fall back to checking if the crash directory exists + # + crash_dir_found = filetools.find_or_create_dir(tc.result_dir) + + tc.is_unique = is_new_to_campaign and not crash_dir_found if tc.is_unique: logger.info('%s first seen at %d', tc.signature, tc.seednum) From a7e628a53830a641a902613a23d88576c711bbef Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 16:25:21 -0500 Subject: [PATCH 0136/1169] add empty unit test files for new modules --- src/certfuzz/test/campaign/test_linux.py | 22 +++++++++++++++++++ .../test/file_handlers/test_watchdog_file.py | 22 +++++++++++++++++++ .../test/fuzztools/test_ppid_observer.py | 22 +++++++++++++++++++ .../test/iteration/test_iteration_base3.py | 22 +++++++++++++++++++ src/certfuzz/test/iteration/test_linux.py | 22 +++++++++++++++++++ 5 files changed, 110 insertions(+) create mode 100644 src/certfuzz/test/campaign/test_linux.py create mode 100644 src/certfuzz/test/file_handlers/test_watchdog_file.py create mode 100644 src/certfuzz/test/fuzztools/test_ppid_observer.py create mode 100644 src/certfuzz/test/iteration/test_iteration_base3.py create mode 100644 src/certfuzz/test/iteration/test_linux.py diff --git a/src/certfuzz/test/campaign/test_linux.py b/src/certfuzz/test/campaign/test_linux.py new file mode 100644 index 0000000..fba8510 --- /dev/null +++ b/src/certfuzz/test/campaign/test_linux.py @@ -0,0 +1,22 @@ +''' +Created on Feb 14, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/file_handlers/test_watchdog_file.py b/src/certfuzz/test/file_handlers/test_watchdog_file.py new file mode 100644 index 0000000..fba8510 --- /dev/null +++ b/src/certfuzz/test/file_handlers/test_watchdog_file.py @@ -0,0 +1,22 @@ +''' +Created on Feb 14, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/fuzztools/test_ppid_observer.py b/src/certfuzz/test/fuzztools/test_ppid_observer.py new file mode 100644 index 0000000..fba8510 --- /dev/null +++ b/src/certfuzz/test/fuzztools/test_ppid_observer.py @@ -0,0 +1,22 @@ +''' +Created on Feb 14, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/iteration/test_iteration_base3.py b/src/certfuzz/test/iteration/test_iteration_base3.py new file mode 100644 index 0000000..fba8510 --- /dev/null +++ b/src/certfuzz/test/iteration/test_iteration_base3.py @@ -0,0 +1,22 @@ +''' +Created on Feb 14, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/iteration/test_linux.py b/src/certfuzz/test/iteration/test_linux.py new file mode 100644 index 0000000..fba8510 --- /dev/null +++ b/src/certfuzz/test/iteration/test_linux.py @@ -0,0 +1,22 @@ +''' +Created on Feb 14, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 85e22a7adf9690a257dfd0d5c98587202e6cc04c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 14 Feb 2014 16:27:59 -0500 Subject: [PATCH 0137/1169] drop obsolete unit test --- src/certfuzz/test/campaign/config/test_bff_config.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/certfuzz/test/campaign/config/test_bff_config.py b/src/certfuzz/test/campaign/config/test_bff_config.py index 5788d3c..c11a62e 100644 --- a/src/certfuzz/test/campaign/config/test_bff_config.py +++ b/src/certfuzz/test/campaign/config/test_bff_config.py @@ -8,7 +8,6 @@ import ConfigParser from certfuzz.campaign.config.bff_config import ConfigHelper from certfuzz.campaign.config.bff_config import MINIMIZED_EXT -from certfuzz.campaign.config.bff_config import KILL_SCRIPT import tempfile from certfuzz.campaign.config.bff_config import read_config_options @@ -102,9 +101,6 @@ def test_check_program_file_type(self): def test_get_minimized_file(self): self.assertEqual(self.cfg.get_minimized_file('foo.txt'), 'foo-%s.txt' % MINIMIZED_EXT) - def test_get_killscript_path(self): - self.assertEqual(self.cfg.get_killscript_path('foo'), os.path.join('foo', '%s') % KILL_SCRIPT) - def test_uniquelog(self): self.assertEqual(self.cfg.uniq_log, os.path.join('output_dir', 'uniquelog.txt')) From 54024a14495398e32c0e05025328166c2932cf7e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 17 Feb 2014 15:53:10 -0500 Subject: [PATCH 0138/1169] fix for BFF-487 --- .../output_parsers/debugger_file_base.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py index 415e69c..dd60dc9 100644 --- a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py +++ b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py @@ -162,24 +162,24 @@ def _hashable_backtrace(self): logger.debug('bt=%s', bt) frame_address = 0 bt_frame = None - - # Get the address of the current backtrace frame + + # Get the address of the current backtrace frame n = re.match(regex['bt_addr'], bt) if n: # Get the frame address from the backtrace line bt_frame = n.group(1) frame_address = int(bt_frame, 16) - + elif self.registers_hex.get(self.pc_name) and not self.used_pc: # Backtrace entry #0 doesn't have an address listed, so use EIP instead # But set a flag not to use EIP again, as inline frames behave the same way self.used_pc = True - frame_address = int(self.registers_hex[self.pc_name], 16) + frame_address = int(self.registers_hex[self.pc_name], 16) if self.libc_start_addr < frame_address < self.libc_end_addr: # Don't include any backtrace frames that are in libc continue - + if self.libgcc_start_addr < frame_address < self.libgcc_end_addr: # Don't include any backtrace frames that are in libgcc continue @@ -188,7 +188,7 @@ def _hashable_backtrace(self): x = re.match(regex['bt_function'], bt) if x and x.group(1) in blacklist: continue - + # If debug symbols are available, the backtrace will include the line number m = re.search(regex['bt_at'], bt) if m: @@ -199,7 +199,7 @@ def _hashable_backtrace(self): if '/sysdeps/' in bt_frame: logger.debug('Found sysdeps, skipping') continue - + # Append either the frame address or source code line number if bt_frame: hashable.append(bt_frame) @@ -250,7 +250,7 @@ def _backtrace_without_questionmarks(self): def backtrace_line(self, idx, l): m = re.match(regex['bt_line'], l) if m: - item = m.group(1) + item = m.group(1) # sometimes gdb splits across lines # so get the next one if it looks like ' at ' or ' from ' next_idx = idx + 1 @@ -409,7 +409,7 @@ def _look_for_crash(self, line): if not self.is_crash: return - logger.debug('_look_for_crash') +# logger.debug('_look_for_crash') if 'SIGKILL' in line: self.is_crash = False elif 'SIGHUP' in line: @@ -457,7 +457,7 @@ def _look_for_debug_build(self, line): if ' at ' in line: logger.debug('Debug build = True') self.is_debugbuild = True - + def _look_for_64bit(self, line): ''' Check for 64-bit process by looking at address of bt frame addresses @@ -473,7 +473,7 @@ def _look_for_64bit(self, line): logger.debug('Target process is 64-bit') self.pc_name = 'rip' self.registers_sought = list(registers64) - + def _look_for_libc_location(self, line): ''' Get start and end address of libc library, for blacklisting purposes From 500f4f4dbeac85fae8407a2967ccecbf06d652b4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 17 Feb 2014 15:55:11 -0500 Subject: [PATCH 0139/1169] fix for BFF-469 --- src/linux/conf.d/bff.cfg | 358 +++++++++++++++++++-------------------- 1 file changed, 179 insertions(+), 179 deletions(-) diff --git a/src/linux/conf.d/bff.cfg b/src/linux/conf.d/bff.cfg index f3a4883..e047894 100644 --- a/src/linux/conf.d/bff.cfg +++ b/src/linux/conf.d/bff.cfg @@ -1,179 +1,179 @@ -################################################################ -# -# This file specifies the options that BFF will use to fuzz -# Comments are specified by the "#" character -# -################################################################ -# FUZZ CAMPAIGN SETTINGS -################################################################ -[campaign] -# A string that uniquely identifies this campaign -id=Default BFF Campaign - - -################################################################ -# TARGET APPLICATION INVOCATION OPTIONS: -################################################################ -[target] -# -# Command-line arguments will be split using python shlex.split() so -# be sure to add quotes where needed -# $SEEDFILE will be replaced at runtime with the appropriate -# seed file name. -# Use quotes if the target application has spaces in the path -# examples: -cmdline=~/convert $SEEDFILE /dev/null - -# Name of process to monitor for hangs. This is done by greping the -# process list, so choose carefully! Usually the same name as "program" -# will suffice, but in cases where the program is started from a script -# you may want to list the actual process name. -# This process name is also used to kill the target process when BFF -# launches the target application from an analyzer, such as gdb or valgrind -# Use quotes if the target application has spaces in its name -killprocname=convert - - -################################################################ -# LOCATIONS FOR FUZZ RUN FILES: -# Output files are placed in [outputdir]/[seedfile] -################################################################ -[directories] -# Location of the fuzzing scripts -# (VMware shared directory if using UbuFuzz VM) -remote_dir=~/bff - -# The location of the seed files -seedfile_origin_dir=%(remote_dir)s/seedfiles/examples - -# location of debugger templates -debugger_template_dir=%(remote_dir)s/certfuzz/debuggers/templates - -# location of results: -# If results are stored in a shared location, -# this directory needs to be unique for each fuzzing machine -output_dir=~/results -crashers_dir=%(output_dir)s/crashers -seedfile_output_dir=%(output_dir)s/seeds - -# dirs local to the fuzzing machine -# Local directory for fuzzing run mutated files. -local_dir=~/fuzzing -seedfile_local_dir=%(local_dir)s/seeds - -# BFF stores cached objects to assist in recovering -# from fuzzing machine reboots -cached_objects_dir=%(local_dir)s -temp_working_dir=%(local_dir)s/tmp - -# Location of file used checked by Linux watchdog to determine -# if fuzzer is still running -watchdog_file=/tmp/bff_watchdog - - -################################################################ -# ZZUF FUZZER PARAMETERS -################################################################ -[zzuf] -# Use zzuf's "copy" mode, which creates a temporary fuzzed file -# Default is 1, where zzuf will determine the file to fuzz, mangle it as -# a randomly-named file in $TMPDIR and then open that with the target app. -# This mode is compatible with more applications, but it can be slower in -# some cases, due to the extra file i/o required. -# If set to 0, zzuf will use LD_PRELOAD to mangle input in memory, rather -# than creating a temporary file. Note that this mode can interfere with -# some target applications. -# OSX Cocoa applications require copymode=1 -copymode=1 - -# The zzuf seed number to start with -start_seed=0 - -# How many iterations you want zzuf to try per seedfile/range selection -# If you have a large number of seed files and/or the target application -# is slow, you may wish to make this value smaller to get better coverage -# in a reasonable amount of time. -seed_interval=20 - - -################################################################ -# VERIFIER PARAMETERS -################################################################ -[verifier] -# -# number of backtrace levels to hash for uniqueness. -# Increase this number for more crash uniqueness granularity. -# Decrease this number if you think that you are getting too many -# duplicate crashes. -backtracelevels=5 - -# Include backtrace frames that aren't part of a loaded library? -# Set this value to False if you wish to consider unmapped stack -# frames in the crash hashes. This can be useful for target application -# that perform JIT compilation -exclude_unmapped_frames=True - -# Save cases that cause failed ASSERTs? If set to 1, then __assert_fail termination, -# e.g. via assert(), it will be considered a crash. -savefailedasserts=False - -# Use valgrind (and callgrind) -# Note that valgrind can be slow. Disabling this option can improve throughput -use_valgrind=True - -# Use PIN to get call traces for every crash, as opposed to just those -# that result in total stack corruption. -# PIN is even slower than valgrind -use_pin_calltrace=False - -# Obtain minimally-different testcase for each unique crash -minimizecrashers=True - -# Minimize to a metasploit string -# Note: this is in addition to minimize to seedfile if minimize_crashers=True -# Also, if minimize_to_string is true, then its minimized result will be used -# for all subsequent analyses (i.e., valgrind, callgrind, etc.) -# Disabled by default due to amount of time that the minimization takes. -# Stand-alone string minimization can be done using tools/minimize.py. -minimize_to_string = False - -# Recycle crashing testcases as seed files for further fuzzing. -# This can improve the number of unique crash hashes found, however this may -# just demonstrate weaknesses in backtrace-based uniqueness determination -# rather than finding new underlying vulnerabilities. -recycle_crashers = False - - -################################################################ -# APPLICATION TIMEOUTS -################################################################ -[timeouts] -# -# maximum program execution time (seconds) that BFF will allow: -progtimeout=5 - -# maximum time (seconds) that process specified by "killprocname" option -# can run before it's killed via killproc.sh, -# which is used to kill stray processes. Normally, zzuf will kill -# the target process if the above timeout is reached. -killproctimeout=130 - -# maximum time (seconds) to let the program run to capture debugger and -# CERT Triage Tools exploitable output. -debugger_timeout=60 - -# maximum time (seconds) to let the program run to capture valgrind output: -valgrindtimeout=120 - -# Set value for the Linux watchdog timer. If watchdog_file is not touched -# for a period longer than this value (seconds), then the vm is rebooted -# by the watchdog. -# Set to 0 to disable watchdog functionality. -watchdogtimeout=3600 - -# Minimization can sometimes take a long time, and time spent minimizing -# is time not spent fuzzing. If a minimization run exceeds this time -# (in seconds) the minimization will terminate (keeping whatever progress -# it has made at that point) and return to fuzzing. -minimizertimeout=3600 - +################################################################ +# +# This file specifies the options that BFF will use to fuzz +# Comments are specified by the "#" character +# +################################################################ +# FUZZ CAMPAIGN SETTINGS +################################################################ +[campaign] +# A string that uniquely identifies this campaign +id=Default BFF Campaign + + +################################################################ +# TARGET APPLICATION INVOCATION OPTIONS: +################################################################ +[target] +# +# Command-line arguments will be split using python shlex.split() so +# be sure to add quotes where needed +# $SEEDFILE will be replaced at runtime with the appropriate +# seed file name. +# Use quotes if the target application has spaces in the path +# examples: +cmdline=~/convert $SEEDFILE /dev/null + +# Name of process to monitor for hangs. This is done by greping the +# process list, so choose carefully! Usually the same name as "program" +# will suffice, but in cases where the program is started from a script +# you may want to list the actual process name. +# This process name is also used to kill the target process when BFF +# launches the target application from an analyzer, such as gdb or valgrind +# Use quotes if the target application has spaces in its name +killprocname=convert + + +################################################################ +# LOCATIONS FOR FUZZ RUN FILES: +# Output files are placed in [outputdir]/[seedfile] +################################################################ +[directories] +# Location of the fuzzing scripts +# (VMware shared directory if using UbuFuzz VM) +remote_dir=~/bff + +# The location of the seed files +seedfile_origin_dir=%(remote_dir)s/seedfiles/examples + +# location of debugger templates +debugger_template_dir=%(remote_dir)s/certfuzz/debuggers/templates + +# location of results: +# If results are stored in a shared location, +# this directory needs to be unique for each fuzzing machine +output_dir=~/results +crashers_dir=%(output_dir)s/crashers +seedfile_output_dir=%(output_dir)s/seeds + +# dirs local to the fuzzing machine +# Local directory for fuzzing run mutated files. +local_dir=~/fuzzing +seedfile_local_dir=%(local_dir)s/seeds + +# BFF stores cached objects to assist in recovering +# from fuzzing machine reboots +cached_objects_dir=%(local_dir)s +temp_working_dir=%(local_dir)s/tmp + +# Location of file used checked by Linux watchdog to determine +# if fuzzer is still running +watchdog_file=/tmp/bff_watchdog + + +################################################################ +# ZZUF FUZZER PARAMETERS +################################################################ +[zzuf] +# Use zzuf's "copy" mode, which creates a temporary fuzzed file +# Default is 1, where zzuf will determine the file to fuzz, mangle it as +# a randomly-named file in $TMPDIR and then open that with the target app. +# This mode is compatible with more applications, but it can be slower in +# some cases, due to the extra file i/o required. +# If set to 0, zzuf will use LD_PRELOAD to mangle input in memory, rather +# than creating a temporary file. Note that this mode can interfere with +# some target applications. +# OSX Cocoa applications require copymode=1 +copymode=1 + +# The zzuf seed number to start with +start_seed=0 + +# How many iterations you want zzuf to try per seedfile/range selection +# If you have a large number of seed files and/or the target application +# is slow, you may wish to make this value smaller to get better coverage +# in a reasonable amount of time. +seed_interval=20 + + +################################################################ +# VERIFIER PARAMETERS +################################################################ +[verifier] +# +# number of backtrace levels to hash for uniqueness. +# Increase this number for more crash uniqueness granularity. +# Decrease this number if you think that you are getting too many +# duplicate crashes. +backtracelevels=5 + +# Include backtrace frames that aren't part of a loaded library? +# Set this value to False if you wish to consider unmapped stack +# frames in the crash hashes. This can be useful for target application +# that perform JIT compilation +exclude_unmapped_frames=True + +# Save cases that cause failed ASSERTs? If set to 1, then __assert_fail termination, +# e.g. via assert(), it will be considered a crash. +savefailedasserts=False + +# Use valgrind (and callgrind) +# Note that valgrind can be slow. Disabling this option can improve throughput +use_valgrind=True + +# Use PIN to get call traces for every crash, as opposed to just those +# that result in total stack corruption. +# PIN is even slower than valgrind +use_pin_calltrace=False + +# Obtain minimally-different testcase for each unique crash +minimizecrashers=True + +# Minimize to a metasploit string +# Note: this is in addition to minimize to seedfile if minimize_crashers=True +# Also, if minimize_to_string is true, then its minimized result will be used +# for all subsequent analyses (i.e., valgrind, callgrind, etc.) +# Disabled by default due to amount of time that the minimization takes. +# Stand-alone string minimization can be done using tools/minimize.py. +minimize_to_string = False + +# Recycle crashing testcases as seed files for further fuzzing. +# This can improve the number of unique crash hashes found, however this may +# just demonstrate weaknesses in backtrace-based uniqueness determination +# rather than finding new underlying vulnerabilities. +recycle_crashers = False + + +################################################################ +# APPLICATION TIMEOUTS +################################################################ +[timeouts] +# +# maximum program execution time (seconds) that BFF will allow: +progtimeout=5 + +# maximum time (seconds) that process specified by "killprocname" option +# can run before it's killed via killproc.sh, +# which is used to kill stray processes. Normally, zzuf will kill +# the target process if the above timeout is reached. +killproctimeout=130 + +# maximum time (seconds) to let the program run to capture debugger and +# CERT Triage Tools exploitable output. +debugger_timeout=60 + +# maximum time (seconds) to let the program run to capture valgrind output: +valgrindtimeout=120 + +# Set value for the Linux watchdog timer. If watchdog_file is not touched +# for a period longer than this value (seconds), then the vm is rebooted +# by the watchdog. +# Set to 0 to disable watchdog functionality. +watchdogtimeout=3600 + +# Minimization can sometimes take a long time, and time spent minimizing +# is time not spent fuzzing. If a minimization run exceeds this time +# (in seconds) the minimization will terminate (keeping whatever progress +# it has made at that point) and return to fuzzing. +minimizertimeout=3600 + From 13131d60f03760a37898cb29e8a0e6de9e694106 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 17 Feb 2014 16:00:33 -0500 Subject: [PATCH 0140/1169] Removed obsolete experimental package --- src/experimental/__init__.py | 0 src/experimental/android/forking_emu.py | 47 ------------------ src/experimental/android/manager.py | 34 ------------- src/experimental/android/runner.py | 63 ------------------------- 4 files changed, 144 deletions(-) delete mode 100644 src/experimental/__init__.py delete mode 100644 src/experimental/android/forking_emu.py delete mode 100644 src/experimental/android/manager.py delete mode 100644 src/experimental/android/runner.py diff --git a/src/experimental/__init__.py b/src/experimental/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/experimental/android/forking_emu.py b/src/experimental/android/forking_emu.py deleted file mode 100644 index 6c334aa..0000000 --- a/src/experimental/android/forking_emu.py +++ /dev/null @@ -1,47 +0,0 @@ -''' -Created on Dec 13, 2012 - -@organization: cert.org -''' -import multiprocessing -import logging -from emulator_manager import drone - -logger = logging.getLogger(__name__) -logger.setLevel(logging.WARNING) - -def _drone_pool(num_procs): - pool = multiprocessing.Pool(processes=num_procs) - pool.map(drone, range(num_procs)) - pool.terminate() - -def _drone_loop(num_procs): -# lock = multiprocessing.Lock() - for i in range(num_procs): - multiprocessing.Process(target=drone, args=(i,)).start() - -def hive_queen(num_procs): - if not num_procs: - # procs_per_cpu > 1.0 => oversubscribe - # procs_per_cpu < 1.0 => undersubscribe - procs_per_cpu = 1.0 - num_procs = int(multiprocessing.cpu_count() * procs_per_cpu) - -# _drone_pool(num_procs) - _drone_loop(num_procs) - -if __name__ == '__main__': - from argparse import ArgumentParser - - logger.setLevel(logging.DEBUG) - logger.addHandler(logging.StreamHandler()) - hdlr = logging.FileHandler('emu.log') - logger.addHandler(hdlr) - - parser = ArgumentParser(description='Spawn Android Emulators') - parser.add_argument('--numprocs', dest='numprocs', type=int) - args = parser.parse_args() - - logger.info('Parsed args: %s', args) - - hive_queen(num_procs=args.numprocs) diff --git a/src/experimental/android/manager.py b/src/experimental/android/manager.py deleted file mode 100644 index 3adfa70..0000000 --- a/src/experimental/android/manager.py +++ /dev/null @@ -1,34 +0,0 @@ -''' -Created on Dec 12, 2012 - -@organization: cert.org -''' -import time -import os -import logging -from emu_cloner import EmulatorClone - -logger = logging.getLogger() - -boot_time_offset = 30 -naptime = lambda x: x * boot_time_offset - -def main(): - with EmulatorClone(from_avd='new_demo') as emu: - emu.run() - -def drone(drone_id): - hdlr = logging.FileHandler('emu-%d.log' % os.getpid()) - logger.addHandler(hdlr) - - timer = naptime(drone_id) - msg = "[DRONE-{:d} PID={:d} sleep={:d}]".format(drone_id, os.getpid(), timer) - logger.info(msg) - # wait for a bit before continuing - # this is intended to help avoid cpu saturation on startup - time.sleep(timer) - - main() - -if __name__ == '__main__': - main() diff --git a/src/experimental/android/runner.py b/src/experimental/android/runner.py deleted file mode 100644 index 5345c3c..0000000 --- a/src/experimental/android/runner.py +++ /dev/null @@ -1,63 +0,0 @@ -''' -Created on Jan 4, 2013 - -@organization: cert.org -''' -import logging -import os - -from api.android_emulator import AndroidEmulator -from api.adb_cmd import AdbCmd - -logger = logging.getLogger(__name__) - -class EmulatorRunnerError(Exception): - pass - - -class EmulatorRunner(object): - def __init__(self, avd): - self.avd = avd - self.hide = False - self.wait_time = 120 - self.handle = None - - def __enter__(self): - logger.debug('Enter context %s, avd=%s', self, self.avd) - self._start_emu() - logger.info("got device! %s" , self.handle) - return self - - def __exit__(self, etype, evalue, traceback): - logger.debug('Exiting context %s', self) -# self._stop_emu() - pass - - def _start_emu(self): - with AndroidEmulator(self.avd, no_window=self.hide) as emu: - emu.start() - self.handle = emu.handle - - def _stop_emu(self): - if self.handle: - AdbCmd(self.handle).emu_kill() - - def run(self): - if not self.handle: - raise EmulatorRunnerError('emulator handle not found') - - shell_cmd = "date; uptime" - - with AdbCmd(self.handle) as adb: - adb.shell([shell_cmd]) - if adb.stdout: - outfile = 'emu-%d.out' % os.getpid() - with open(outfile, 'w') as f: - f.writelines(adb.stdout) - if adb.stderr: - outfile = 'emu-%d.err' % os.getpid() - with open(outfile, 'w') as f: - f.writelines(adb.stderr) - - - From 63908c30fa7b7c7987b638c014a539bed46f6309 Mon Sep 17 00:00:00 2001 From: Todd Lewellen Date: Tue, 4 Mar 2014 15:12:38 -0500 Subject: [PATCH 0141/1169] nsis couldn't find COPYING.txt. --- build/dist/windows/nsis/buildnsi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/dist/windows/nsis/buildnsi.py b/build/dist/windows/nsis/buildnsi.py index 6ccc4b8..2a06d28 100644 --- a/build/dist/windows/nsis/buildnsi.py +++ b/build/dist/windows/nsis/buildnsi.py @@ -34,7 +34,7 @@ def main(version_string='', outfile=None, build_dir=None): fp.write('!define VERSION "%s"\n' % version_string) fp.write('!define COPYRIGHT "CERT 2013"\n') fp.write('!define DESCRIPTION "FOE %s"\n' % version_string) - fp.write('!define LICENSE_TXT "%s\COPYING.txt"\n' % distpath) + fp.write('!define LICENSE_TXT "%s\..\COPYING.txt"\n' % distpath) fp.write('!define INSTALLER_NAME "%s\..\..\FOE-%s-setup.exe"\n' % (distpath, version_string)) headerfile = open("nsis_header.txt", "r") From 0cd5b8072ee33b2febb7be6ff9d0a4c5d3167d06 Mon Sep 17 00:00:00 2001 From: Todd Lewellen Date: Tue, 4 Mar 2014 15:41:10 -0500 Subject: [PATCH 0142/1169] the windows build script wasn't copying the built windows installer out of the temp directory. It was just creating it and then deleting it. --- build/dist/windows/windows_build2.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/dist/windows/windows_build2.py b/build/dist/windows/windows_build2.py index ae1b875..d368c2b 100644 --- a/build/dist/windows/windows_build2.py +++ b/build/dist/windows/windows_build2.py @@ -35,3 +35,6 @@ def package(self): # invoke makensis on the file we just made subprocess.call(['makensis', nsifile]) + + tmpzip = self._create_zip() + self._move_to_target(tmpzip) From 0bc6e4a061630a9a55e508d8502ca8b83811fe4b Mon Sep 17 00:00:00 2001 From: Todd Lewellen Date: Tue, 4 Mar 2014 16:31:37 -0500 Subject: [PATCH 0143/1169] Second try: The exe installer needs to be copied out of the temp directory so it is not deleted at the end of the build. --- build/dist/windows/windows_build2.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/build/dist/windows/windows_build2.py b/build/dist/windows/windows_build2.py index d368c2b..677c40b 100644 --- a/build/dist/windows/windows_build2.py +++ b/build/dist/windows/windows_build2.py @@ -30,11 +30,16 @@ def package(self): nsifile = os.path.join(self.build_dir, 'foe2.nsi') + version_string = "02.01.00.99" # generate the nsi file - buildnsi.main(version_string="02.01.00.99", outfile=nsifile, build_dir=self.build_dir) + buildnsi.main(version_string=version_string, outfile=nsifile, build_dir=self.build_dir) # invoke makensis on the file we just made subprocess.call(['makensis', nsifile]) - tmpzip = self._create_zip() - self._move_to_target(tmpzip) + distpath = 'BFF-windows-export' + if self.build_dir: + distpath = os.path.join(self.build_dir, distpath) + + exefile = '%s\..\..\FOE-%s-setup.exe' % (distpath, version_string) + self._move_to_target(exefile) From 3bfd947eb72718e44ba04c2c5c7c7917c28e54ad Mon Sep 17 00:00:00 2001 From: Todd Lewellen Date: Tue, 4 Mar 2014 16:51:30 -0500 Subject: [PATCH 0144/1169] third try: trying to get the exe file to be copied out of the temp directory --- build/dist/windows/windows_build2.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build/dist/windows/windows_build2.py b/build/dist/windows/windows_build2.py index 677c40b..1e5e1fe 100644 --- a/build/dist/windows/windows_build2.py +++ b/build/dist/windows/windows_build2.py @@ -41,5 +41,7 @@ def package(self): if self.build_dir: distpath = os.path.join(self.build_dir, distpath) - exefile = '%s\..\..\FOE-%s-setup.exe' % (distpath, version_string) - self._move_to_target(exefile) + exename = 'FOE-%s-setup.exe' % version_string + exefile = '%s\..\..\%s' % (distpath, exename) + self.target = os.path.join(os.path.dirname(self.target), exename) + self._move_to_target(exefile) \ No newline at end of file From 1267c6ea1e12c5b099d51f319144b53f67af1bf6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 5 Mar 2014 09:26:10 -0500 Subject: [PATCH 0145/1169] add debug logging --- build/dist/windows/windows_build2.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build/dist/windows/windows_build2.py b/build/dist/windows/windows_build2.py index ae1b875..3a7865d 100644 --- a/build/dist/windows/windows_build2.py +++ b/build/dist/windows/windows_build2.py @@ -5,11 +5,13 @@ ''' import os import shutil - +import logging import subprocess from ..build_base2 import Build +logger = logging.getLogger(__name__) + basedir = os.path.dirname(__file__) @@ -21,17 +23,21 @@ def package(self): ''' Builds a Windows Installer ''' + logger.debug('Import buildnsi') from .nsis import buildnsi # Copy files required by nsis for f in ['cert.ico', 'EnvVarUpdate.nsh', 'vmwarning.txt']: src = os.path.join(basedir, 'nsis', f) + logger.debug('Copy %s -> %s', src, self.build_dir) shutil.copy(src, self.build_dir) nsifile = os.path.join(self.build_dir, 'foe2.nsi') + logger.debug('nsi file is %s', nsifile) # generate the nsi file buildnsi.main(version_string="02.01.00.99", outfile=nsifile, build_dir=self.build_dir) # invoke makensis on the file we just made + logger.debug('invoking makensis on %s', nsifile) subprocess.call(['makensis', nsifile]) From 82ee971525f8dfb1eb9f72e74f4553f56474038f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 5 Mar 2014 13:55:10 -0500 Subject: [PATCH 0146/1169] drop obsolete tests --- src/linux/test/test_bff.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/linux/test/test_bff.py b/src/linux/test/test_bff.py index 163719d..66e2055 100644 --- a/src/linux/test/test_bff.py +++ b/src/linux/test/test_bff.py @@ -35,23 +35,23 @@ def setUp(self): pass def tearDown(self): pass - def test_get_rate(self): - bff.SEED_TS = Mock() - bff.SEED_TS.since_start = lambda: 1.0 - for i in range(100): - self.assertEqual(float(i / 1.0), bff.get_rate(i)) - - def test_get_uniq_logger(self): - logfile = tempfile.mktemp() - ulog = bff.get_uniq_logger(logfile) - self.assertEqual('Logger', ulog.__class__.__name__) - self.assertEqual(0, os.path.getsize(logfile)) - msg = 'foo' - ulog.warning(msg) - # length is msg + a carriage return - self.assertEqual(len(msg) + 1, os.path.getsize(logfile)) - os.remove(logfile) - self.assertFalse(os.path.exists(logfile)) +# def test_get_rate(self): +# bff.SEED_TS = Mock() +# bff.SEED_TS.since_start = lambda: 1.0 +# for i in range(100): +# self.assertEqual(float(i / 1.0), bff.get_rate(i)) +# +# def test_get_uniq_logger(self): +# logfile = tempfile.mktemp() +# ulog = bff.get_uniq_logger(logfile) +# self.assertEqual('Logger', ulog.__class__.__name__) +# self.assertEqual(0, os.path.getsize(logfile)) +# msg = 'foo' +# ulog.warning(msg) +# # length is msg + a carriage return +# self.assertEqual(len(msg) + 1, os.path.getsize(logfile)) +# os.remove(logfile) +# self.assertFalse(os.path.exists(logfile)) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.test_load_obj_from_file'] From 7a805472213c9e9170c870801a48212b056543d3 Mon Sep 17 00:00:00 2001 From: Todd Lewellen Date: Wed, 5 Mar 2014 15:20:21 -0500 Subject: [PATCH 0147/1169] Fixing path resolution issue which prevents source files from being included in the windows installer. --- build/dist/windows/nsis/buildnsi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/dist/windows/nsis/buildnsi.py b/build/dist/windows/nsis/buildnsi.py index 2a06d28..e69cd02 100644 --- a/build/dist/windows/nsis/buildnsi.py +++ b/build/dist/windows/nsis/buildnsi.py @@ -43,8 +43,8 @@ def main(version_string='', outfile=None, build_dir=None): fp.write(headertext) - for path, dirs, files in os.walk(distpath): - realpath = string.replace(path, distpath, "") + for path, dirs, files in os.walk(build_dir): + realpath = string.replace(path, build_dir, "") fp.write('SetOutPath "$INSTDIR%s"\n' % realpath) for bfffile in files: filepath = os.path.join(os.path.abspath(path), bfffile) From b2935fbbb0be6e4f875dc04491878c9db80fcaa3 Mon Sep 17 00:00:00 2001 From: Todd Lewellen Date: Wed, 5 Mar 2014 16:24:22 -0500 Subject: [PATCH 0148/1169] Fixed uninstaller section in buildnsi.py to point to the correct directory paths. --- build/dist/windows/nsis/buildnsi.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/build/dist/windows/nsis/buildnsi.py b/build/dist/windows/nsis/buildnsi.py index e69cd02..2f90b4b 100644 --- a/build/dist/windows/nsis/buildnsi.py +++ b/build/dist/windows/nsis/buildnsi.py @@ -10,12 +10,9 @@ def main(version_string='', outfile=None, build_dir=None): - distpath = '' - + distpath = 'BFF-windows-export' if build_dir: - distpath = '%s\BFF-windows-export' % build_dir - else: - distpath = 'BFF-windows-export' + distpath = os.path.join(build_dir, distpath) # either open a file for writing, or just dump to stdout if outfile: @@ -41,6 +38,7 @@ def main(version_string='', outfile=None, build_dir=None): headertext = headerfile.read() headerfile.close() + # Write installer section fp.write(headertext) for path, dirs, files in os.walk(build_dir): @@ -54,11 +52,14 @@ def main(version_string='', outfile=None, build_dir=None): midtext = midfile.read() midfile.close() + # End of installer section + + # Write uninstaller section fp.write(midtext) dirlist = [] - for path, dirs, files in os.walk(distpath): - realpath = string.replace(path, distpath, "") + for path, dirs, files in os.walk(build_dir): + realpath = string.replace(path, build_dir, "") for bfffile in files: fp.write('Delete "$INSTDIR%s\%s"\n' % (realpath, bfffile)) # Remove .pyc files as well. From d3edca1da4d2078b472fcaef0859cb4cfe319b8e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 10 Mar 2014 10:41:53 -0400 Subject: [PATCH 0149/1169] fix unit tests (+1 squashed commit) Squashed commits: [9f3cdcf] move version string to certfuzz.version module --- src/certfuzz/__init__.py | 4 ++-- src/certfuzz/defaults.py | 6 ------ src/certfuzz/test/{test_defaults.py => test_version.py} | 7 ++++--- src/certfuzz/version.py | 6 ++++++ 4 files changed, 12 insertions(+), 11 deletions(-) delete mode 100644 src/certfuzz/defaults.py rename src/certfuzz/test/{test_defaults.py => test_version.py} (62%) create mode 100644 src/certfuzz/version.py diff --git a/src/certfuzz/__init__.py b/src/certfuzz/__init__.py index a8deaad..7fc59e8 100644 --- a/src/certfuzz/__init__.py +++ b/src/certfuzz/__init__.py @@ -2,5 +2,5 @@ The certfuzz package forms the basis of the CERT Failure Observation Engine (FOE) and Basic Fuzzing Framework (BFF). ''' -from .errors import CERTFuzzError -from .defaults import __version__ +from certfuzz.errors import CERTFuzzError +from certfuzz.version import __version__ diff --git a/src/certfuzz/defaults.py b/src/certfuzz/defaults.py deleted file mode 100644 index 1098baa..0000000 --- a/src/certfuzz/defaults.py +++ /dev/null @@ -1,6 +0,0 @@ -''' -Created on Feb 11, 2013 - -@organization: cert.org -''' -__version__ = '2.8' diff --git a/src/certfuzz/test/test_defaults.py b/src/certfuzz/test/test_version.py similarity index 62% rename from src/certfuzz/test/test_defaults.py rename to src/certfuzz/test/test_version.py index 50eab28..24b1f77 100644 --- a/src/certfuzz/test/test_defaults.py +++ b/src/certfuzz/test/test_version.py @@ -4,10 +4,10 @@ @organization: cert.org ''' import unittest -from certfuzz import defaults +from certfuzz import version -class Test(unittest.TestCase): +class Test(unittest.TestCase): def setUp(self): pass @@ -15,7 +15,8 @@ def tearDown(self): pass def testName(self): - pass + self.assertTrue(hasattr(version, '__version__'), 'version has no attribute __version__') + self.assertEqual(str, type(version.__version__)) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/certfuzz/version.py b/src/certfuzz/version.py new file mode 100644 index 0000000..16b0af8 --- /dev/null +++ b/src/certfuzz/version.py @@ -0,0 +1,6 @@ +''' +Created on Mar 10, 2014 + +@author: adh +''' +__version__ = '2.8' From b5815b91a0b7e5f89b2db5336aeb2fa2eebace37 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 10 Mar 2014 11:31:55 -0400 Subject: [PATCH 0150/1169] replace relative imports with absolute imports --- src/certfuzz/analyzers/__init__.py | 1 - src/certfuzz/analyzers/analyzer_base.py | 4 +-- src/certfuzz/analyzers/callgrind/__init__.py | 4 --- src/certfuzz/analyzers/callgrind/annotate.py | 9 +++--- .../analyzers/callgrind/annotation_file.py | 2 +- src/certfuzz/analyzers/callgrind/callgrind.py | 2 +- .../analyzers/callgrind/calltree_file.py | 2 +- src/certfuzz/analyzers/callgrind/errors.py | 2 +- src/certfuzz/analyzers/cw_gmalloc.py | 3 +- src/certfuzz/analyzers/errors.py | 2 +- src/certfuzz/analyzers/pin_calltrace.py | 2 +- src/certfuzz/analyzers/stderr.py | 2 +- src/certfuzz/analyzers/valgrind.py | 2 +- .../analyzers/callgrind/test_callgrind_pkg.py | 31 ------------------- .../test/analyzers/test_analyzer_base.py | 2 +- .../test/analyzers/test_analyzers_pkg.py | 28 ----------------- 16 files changed, 16 insertions(+), 82 deletions(-) delete mode 100644 src/certfuzz/test/analyzers/callgrind/test_callgrind_pkg.py delete mode 100644 src/certfuzz/test/analyzers/test_analyzers_pkg.py diff --git a/src/certfuzz/analyzers/__init__.py b/src/certfuzz/analyzers/__init__.py index a46b468..e69de29 100644 --- a/src/certfuzz/analyzers/__init__.py +++ b/src/certfuzz/analyzers/__init__.py @@ -1 +0,0 @@ -from .analyzer_base import Analyzer diff --git a/src/certfuzz/analyzers/analyzer_base.py b/src/certfuzz/analyzers/analyzer_base.py index 6240f76..fcb9d50 100644 --- a/src/certfuzz/analyzers/analyzer_base.py +++ b/src/certfuzz/analyzers/analyzer_base.py @@ -6,8 +6,8 @@ import logging import os -from ..fuzztools import subprocess_helper as subp -from .errors import AnalyzerOutputMissingError, AnalyzerEmptyOutputError +from certfuzz.fuzztools import subprocess_helper as subp +from certfuzz.analyzers.errors import AnalyzerOutputMissingError, AnalyzerEmptyOutputError logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) diff --git a/src/certfuzz/analyzers/callgrind/__init__.py b/src/certfuzz/analyzers/callgrind/__init__.py index ecbcaf8..e69de29 100644 --- a/src/certfuzz/analyzers/callgrind/__init__.py +++ b/src/certfuzz/analyzers/callgrind/__init__.py @@ -1,4 +0,0 @@ -from .errors import CallgrindAnnotateEmptyOutputFileError -from .errors import CallgrindAnnotateError -from .errors import CallgrindAnnotateMissingInputFileError -from .errors import CallgrindAnnotateNoOutputFileError diff --git a/src/certfuzz/analyzers/callgrind/annotate.py b/src/certfuzz/analyzers/callgrind/annotate.py index 027c1fb..5fa8206 100644 --- a/src/certfuzz/analyzers/callgrind/annotate.py +++ b/src/certfuzz/analyzers/callgrind/annotate.py @@ -9,12 +9,11 @@ from subprocess import Popen import logging from optparse import OptionParser +from certfuzz.analyzers.callgrind.annotation_file import AnnotationFile +from certfuzz.analyzers.callgrind import callgrind +from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError, \ + CallgrindAnnotateNoOutputFileError, CallgrindAnnotateEmptyOutputFileError -from . import callgrind -from .errors import CallgrindAnnotateMissingInputFileError -from .errors import CallgrindAnnotateEmptyOutputFileError -from .errors import CallgrindAnnotateNoOutputFileError -from .annotation_file import AnnotationFile logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) diff --git a/src/certfuzz/analyzers/callgrind/annotation_file.py b/src/certfuzz/analyzers/callgrind/annotation_file.py index 684fd90..11ef831 100644 --- a/src/certfuzz/analyzers/callgrind/annotation_file.py +++ b/src/certfuzz/analyzers/callgrind/annotation_file.py @@ -5,8 +5,8 @@ ''' import re import logging +from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateNoOutputFileError -from . import CallgrindAnnotateNoOutputFileError logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) diff --git a/src/certfuzz/analyzers/callgrind/callgrind.py b/src/certfuzz/analyzers/callgrind/callgrind.py index 5d24f73..0e60848 100644 --- a/src/certfuzz/analyzers/callgrind/callgrind.py +++ b/src/certfuzz/analyzers/callgrind/callgrind.py @@ -7,7 +7,7 @@ ''' import logging -from .. import Analyzer +from certfuzz.analyzers.analyzer_base import Analyzer logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) diff --git a/src/certfuzz/analyzers/callgrind/calltree_file.py b/src/certfuzz/analyzers/callgrind/calltree_file.py index 8a8df4d..3d40394 100644 --- a/src/certfuzz/analyzers/callgrind/calltree_file.py +++ b/src/certfuzz/analyzers/callgrind/calltree_file.py @@ -6,7 +6,7 @@ import re import logging -from . import CallgrindAnnotateNoOutputFileError +from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateNoOutputFileError logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) diff --git a/src/certfuzz/analyzers/callgrind/errors.py b/src/certfuzz/analyzers/callgrind/errors.py index 97de912..098296a 100644 --- a/src/certfuzz/analyzers/callgrind/errors.py +++ b/src/certfuzz/analyzers/callgrind/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from ..errors import AnalyzerError +from certfuzz.analyzers.errors import AnalyzerError class CallgrindAnnotateError(AnalyzerError): diff --git a/src/certfuzz/analyzers/cw_gmalloc.py b/src/certfuzz/analyzers/cw_gmalloc.py index cf90333..6f0074b 100644 --- a/src/certfuzz/analyzers/cw_gmalloc.py +++ b/src/certfuzz/analyzers/cw_gmalloc.py @@ -5,12 +5,11 @@ ''' import platform import os.path +from certfuzz.analyzers.analyzer_base import Analyzer _platforms = ['Darwin'] _platform_is_supported = platform.system() in _platforms -from . import Analyzer - OUTFILE_EXT = "gmalloc" get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) diff --git a/src/certfuzz/analyzers/errors.py b/src/certfuzz/analyzers/errors.py index ec6315a..203a906 100644 --- a/src/certfuzz/analyzers/errors.py +++ b/src/certfuzz/analyzers/errors.py @@ -3,7 +3,7 @@ @author: adh ''' -from ..errors import CERTFuzzError +from certfuzz.errors import CERTFuzzError class AnalyzerError(CERTFuzzError): diff --git a/src/certfuzz/analyzers/pin_calltrace.py b/src/certfuzz/analyzers/pin_calltrace.py index 27c3b47..9556671 100644 --- a/src/certfuzz/analyzers/pin_calltrace.py +++ b/src/certfuzz/analyzers/pin_calltrace.py @@ -8,7 +8,7 @@ import logging import os -from . import Analyzer +from certfuzz.analyzers.analyzer_base import Analyzer logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) diff --git a/src/certfuzz/analyzers/stderr.py b/src/certfuzz/analyzers/stderr.py index 6c6fb4b..17f1093 100644 --- a/src/certfuzz/analyzers/stderr.py +++ b/src/certfuzz/analyzers/stderr.py @@ -6,7 +6,7 @@ @organization: cert.org ''' import logging -from . import Analyzer +from certfuzz.analyzers.analyzer_base import Analyzer logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) diff --git a/src/certfuzz/analyzers/valgrind.py b/src/certfuzz/analyzers/valgrind.py index 5e9c682..77f32ad 100644 --- a/src/certfuzz/analyzers/valgrind.py +++ b/src/certfuzz/analyzers/valgrind.py @@ -7,7 +7,7 @@ ''' import logging -from . import Analyzer +from certfuzz.analyzers.analyzer_base import Analyzer logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) diff --git a/src/certfuzz/test/analyzers/callgrind/test_callgrind_pkg.py b/src/certfuzz/test/analyzers/callgrind/test_callgrind_pkg.py deleted file mode 100644 index f986655..0000000 --- a/src/certfuzz/test/analyzers/callgrind/test_callgrind_pkg.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.analyzers.callgrind - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.analyzers.callgrind - api_list = ['CallgrindAnnotateEmptyOutputFileError', - 'CallgrindAnnotateError', - 'CallgrindAnnotateMissingInputFileError', - 'CallgrindAnnotateNoOutputFileError', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/analyzers/test_analyzer_base.py b/src/certfuzz/test/analyzers/test_analyzer_base.py index 3984785..7d8e13d 100644 --- a/src/certfuzz/test/analyzers/test_analyzer_base.py +++ b/src/certfuzz/test/analyzers/test_analyzer_base.py @@ -5,7 +5,7 @@ ''' import unittest -from certfuzz.analyzers import Analyzer +from certfuzz.analyzers.analyzer_base import Analyzer class MockObj(object): def __init__(self, **kwargs): diff --git a/src/certfuzz/test/analyzers/test_analyzers_pkg.py b/src/certfuzz/test/analyzers/test_analyzers_pkg.py deleted file mode 100644 index d93670d..0000000 --- a/src/certfuzz/test/analyzers/test_analyzers_pkg.py +++ /dev/null @@ -1,28 +0,0 @@ -''' -Created on Apr 10, 2012 - -@organization: cert.org -''' -import unittest -import certfuzz.analyzers -from certfuzz.test import misc - - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.analyzers - api_list = ['Analyzer'] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From e80024164b62275b6b6698c5b8cc1e8af90bf5ea Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 10 Mar 2014 12:30:08 -0400 Subject: [PATCH 0151/1169] replace relative imports with absolute imports --- src/certfuzz/campaign/linux.py | 55 ++++++++-------------- src/certfuzz/tools/android/apk_dumper.py | 4 +- src/certfuzz/tools/android/config_tools.py | 2 +- 3 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index cb7a535..1a26e98 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -4,47 +4,32 @@ @author: adh ''' +import itertools import logging from logging.handlers import RotatingFileHandler -from optparse import OptionParser import os -import platform +import shutil import sys +import tempfile import time +import traceback -from .. import debuggers, file_handlers -from ..analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind -from ..analyzers.callgrind import callgrind -from ..analyzers.callgrind.annotate import annotate_callgrind -from ..analyzers.callgrind.annotate import annotate_callgrind_tree -from ..analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError -from ..analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError -from ..analyzers.errors import AnalyzerEmptyOutputError -from ..campaign.config import bff_config as cfg_helper -from ..crash.bff_crash import BffCrash -from ..debuggers import crashwrangler # @UnusedImport -from ..debuggers import gdb # @UnusedImport -from ..file_handlers.seedfile_set import SeedfileSet -from ..file_handlers.tmp_reaper import TmpReaper -from ..fuzztools import bff_helper as z, filetools, performance -from ..fuzztools.object_caching import cache_state, get_cached_state -from ..fuzztools.process_killer import ProcessKiller -from ..fuzztools.seedrange import SeedRange -from ..fuzztools.state_timer import STATE_TIMER -from ..fuzztools.watchdog import WatchDog -from ..fuzztools.zzuf import Zzuf -from ..fuzztools.zzuflog import ZzufLog -from ..minimizer import MinimizerError, UnixMinimizer as Minimizer -from certfuzz.iteration.linux import Iteration +from certfuzz.campaign.config import bff_config as cfg_helper from certfuzz.campaign.errors import CampaignScriptError +from certfuzz.debuggers import crashwrangler # @UnusedImport +from certfuzz.debuggers import gdb # @UnusedImport +from certfuzz.debuggers.registration import verify_supported_platform +from certfuzz.file_handlers.seedfile_set import SeedfileSet +from certfuzz.file_handlers.tmp_reaper import TmpReaper +from certfuzz.fuzztools import subprocess_helper as subp +from certfuzz.fuzztools.filetools import mkdir_p, make_directories, copy_file +from certfuzz.fuzztools.process_killer import ProcessKiller +from certfuzz.fuzztools.state_timer import STATE_TIMER +from certfuzz.fuzztools.watchdog import WatchDog + from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file -import shutil -import tempfile -from certfuzz.fuzztools.filetools import mkdir_p -from ..fuzztools import subprocess_helper as subp -import itertools from certfuzz.fuzztools.ppid_observer import check_ppid -import traceback +from certfuzz.iteration.linux import Iteration logger = logging.getLogger(__name__) @@ -65,7 +50,7 @@ def __init__(self, cfg_path=None, scriptpath=None): self.crashes_seen = set() # give up if we don't have a debugger - debuggers.verify_supported_platform() + verify_supported_platform() def __enter__(self): @@ -161,7 +146,7 @@ def _setup_logfile(self, logdir, log_basename='bff.log', level=logging.DEBUG, @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') @param level: the logging level (defaults to logging.DEBUG) ''' - filetools.make_directories(logdir) + make_directories(logdir) logfile = os.path.join(logdir, log_basename) hdlr = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") @@ -175,7 +160,7 @@ def _setup_logfile(self, logdir, log_basename='bff.log', level=logging.DEBUG, def _copy_config(self): logger.debug('copy config') - filetools.copy_file(self.cfg_path, self.cfg.output_dir) + copy_file(self.cfg_path, self.cfg.output_dir) def _set_unbuffered_stdout(self): ''' diff --git a/src/certfuzz/tools/android/apk_dumper.py b/src/certfuzz/tools/android/apk_dumper.py index d7abe1f..6f9245b 100644 --- a/src/certfuzz/tools/android/apk_dumper.py +++ b/src/certfuzz/tools/android/apk_dumper.py @@ -5,8 +5,8 @@ ''' import logging import argparse -from ...android.api.aapt import Aapt -from ...android.api.android_manifest import AndroidManifest +from certfuzz.android.api.aapt import Aapt +from certfuzz.android.api.android_manifest import AndroidManifest import os diff --git a/src/certfuzz/tools/android/config_tools.py b/src/certfuzz/tools/android/config_tools.py index d3029cc..d1d196e 100644 --- a/src/certfuzz/tools/android/config_tools.py +++ b/src/certfuzz/tools/android/config_tools.py @@ -9,7 +9,7 @@ import shutil import yaml import argparse -from ...android.api.log_helper import log_formatter +from certfuzz.android.api.log_helper import log_formatter logger = logging.getLogger() hdlr = logging.StreamHandler() From 42a1da6832aec563c1206c865cb49facffe10f9a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Mar 2014 15:38:26 -0400 Subject: [PATCH 0152/1169] remove and ignore .project and .pydevproject files (they should be local to individual developer machines) --- .gitignore | 2 ++ .project | 27 --------------------------- .pydevproject | 9 --------- 3 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 .project delete mode 100644 .pydevproject diff --git a/.gitignore b/.gitignore index 6c800d3..340847d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.project +.pydevproject dev_builds .settings *.env diff --git a/.project b/.project deleted file mode 100644 index 5cd433d..0000000 --- a/.project +++ /dev/null @@ -1,27 +0,0 @@ - - - bff_git - - - - - - org.python.pydev.PyDevBuilder - - - - - org.eclipse.ui.externaltools.ExternalToolBuilder - full,incremental, - - - LaunchConfigHandle - <project>/.externalToolBuilders/dev_builder.launch - - - - - - org.python.pydev.pythonNature - - diff --git a/.pydevproject b/.pydevproject deleted file mode 100644 index b6456b6..0000000 --- a/.pydevproject +++ /dev/null @@ -1,9 +0,0 @@ - - -Default -python 2.7 - -/${PROJECT_DIR_NAME}/build -/${PROJECT_DIR_NAME}/src - - From 584b2668878575e6b0745b0d3c4af9e253916aab Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 09:58:51 -0400 Subject: [PATCH 0153/1169] replace relative imports with absolute imports --- src/certfuzz/android/api/aapt.py | 10 +++++----- src/certfuzz/android/api/activity_manager.py | 6 ++++-- src/certfuzz/android/api/adb_cmd.py | 16 ++++++++++------ src/certfuzz/android/api/android_cmd.py | 9 +++++---- src/certfuzz/android/api/android_emulator.py | 15 +++++++-------- src/certfuzz/android/api/defaults.py | 2 +- src/certfuzz/android/api/errors.py | 2 +- 7 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/certfuzz/android/api/aapt.py b/src/certfuzz/android/api/aapt.py index e1937b2..442456f 100644 --- a/src/certfuzz/android/api/aapt.py +++ b/src/certfuzz/android/api/aapt.py @@ -3,12 +3,12 @@ @organization: cert.org ''' -import os -from functools import partial -from .defaults import sdk_platform_tool -from .errors import AaptError import logging -from ...fuzztools.command_line_callable import CommandLineCallable + +from certfuzz.android.api.defaults import sdk_platform_tool +from certfuzz.android.api.errors import AaptError +from certfuzz.fuzztools.command_line_callable import CommandLineCallable + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/android/api/activity_manager.py b/src/certfuzz/android/api/activity_manager.py index 46c2d9e..92b62fa 100644 --- a/src/certfuzz/android/api/activity_manager.py +++ b/src/certfuzz/android/api/activity_manager.py @@ -3,12 +3,14 @@ @organization: cert.org ''' -from .adb_cmd import AdbCmd -from .errors import ActivityManagerError +from certfuzz.android.api.adb_cmd import AdbCmd +from certfuzz.android.api.errors import ActivityManagerError + def am(handle, args): ActivityManager(handle).go(args) + class ActivityManager(object): ''' classdocs diff --git a/src/certfuzz/android/api/adb_cmd.py b/src/certfuzz/android/api/adb_cmd.py index 90f56ae..865c51b 100644 --- a/src/certfuzz/android/api/adb_cmd.py +++ b/src/certfuzz/android/api/adb_cmd.py @@ -3,19 +3,22 @@ @organization: cert.org ''' -import subprocess -import logging -from .defaults import sdk_platform_tool -from .log_helper import pfunc -from .errors import AdbCmdError import functools +import logging import signal -from ...fuzztools.command_line_callable import CommandLineCallable +import subprocess + +from certfuzz.android.api.defaults import sdk_platform_tool +from certfuzz.android.api.errors import AdbCmdError +from certfuzz.android.api.log_helper import pfunc +from certfuzz.fuzztools.command_line_callable import CommandLineCallable + adb = sdk_platform_tool('adb') logger = logging.getLogger(__name__) + def _terminate_and_raise(p, signum, frame): ''' signal handler for use when a process p times out @@ -26,6 +29,7 @@ def _terminate_and_raise(p, signum, frame): p.terminate() raise AdbCmdError() + class AdbCmd(CommandLineCallable): @pfunc(logger=logger) def __init__(self, handle=None): diff --git a/src/certfuzz/android/api/android_cmd.py b/src/certfuzz/android/api/android_cmd.py index deafe27..7887559 100644 --- a/src/certfuzz/android/api/android_cmd.py +++ b/src/certfuzz/android/api/android_cmd.py @@ -4,15 +4,16 @@ @organization: cert.org ''' import logging -from . import sdk_tool -from .log_helper import pfunc -from .errors import AndroidCmdError -from ...fuzztools.command_line_callable import CommandLineCallable +from certfuzz.android.api.log_helper import pfunc +from certfuzz.android.api.errors import AndroidCmdError +from certfuzz.fuzztools.command_line_callable import CommandLineCallable +from certfuzz.android.api.defaults import sdk_tool android = sdk_tool('android') logger = logging.getLogger(__name__) + class AndroidCmd(CommandLineCallable): @pfunc(logger=logger) def __init__(self): diff --git a/src/certfuzz/android/api/android_emulator.py b/src/certfuzz/android/api/android_emulator.py index 0da038d..595d469 100644 --- a/src/certfuzz/android/api/android_emulator.py +++ b/src/certfuzz/android/api/android_emulator.py @@ -4,20 +4,19 @@ @organization: cert.org ''' import logging +import os import socket import subprocess import time -import os -from . import sdk_tool -from .adb_cmd import AdbCmd, AdbCmdError -from .log_helper import pfunc -from .android_cmd import AndroidCmd -from .defaults import inifile, avddir -# from .defaults import TIMERS +from certfuzz.android.api.adb_cmd import AdbCmd, AdbCmdError +from certfuzz.android.api.android_cmd import AndroidCmd +from certfuzz.android.api.defaults import inifile, avddir, sdk_tool +from certfuzz.android.api.errors import AndroidEmulatorError +from certfuzz.android.api.log_helper import pfunc -from .errors import AndroidEmulatorError +# from .defaults import TIMERS emulator = sdk_tool('emulator') # string formatters diff --git a/src/certfuzz/android/api/defaults.py b/src/certfuzz/android/api/defaults.py index 66553ee..ac8fe0a 100644 --- a/src/certfuzz/android/api/defaults.py +++ b/src/certfuzz/android/api/defaults.py @@ -5,7 +5,7 @@ ''' import os from functools import partial -from .errors import Android_API_Error +from certfuzz.android.api.errors import Android_API_Error _defaults = {'sdk_home': os.path.abspath(os.path.expanduser('~/android-sdk')), diff --git a/src/certfuzz/android/api/errors.py b/src/certfuzz/android/api/errors.py index c70d0f5..cfc4af2 100644 --- a/src/certfuzz/android/api/errors.py +++ b/src/certfuzz/android/api/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from .. import AndroidError +from certfuzz.android import AndroidError class Android_API_Error(AndroidError): pass From 0c0e6c3435c3a864ccf9cc9c2faaa0e9f3a60fc8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 10:44:30 -0400 Subject: [PATCH 0154/1169] replace relative imports with absolute imports --- src/certfuzz/android/avd_mgr/cloner.py | 12 +++++++----- src/certfuzz/android/avd_mgr/errors.py | 6 +++++- src/certfuzz/android/avd_mgr/main.py | 7 +++---- src/certfuzz/android/avd_mgr/orphan_catcher.py | 9 ++++----- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/certfuzz/android/avd_mgr/cloner.py b/src/certfuzz/android/avd_mgr/cloner.py index 2fa6b1a..2aa8c5d 100644 --- a/src/certfuzz/android/avd_mgr/cloner.py +++ b/src/certfuzz/android/avd_mgr/cloner.py @@ -5,19 +5,20 @@ ''' import logging import os -import tempfile import shutil +import tempfile import uuid -#from certfuzz.helpers import random_str - -from ..api.defaults import inifile, avddir -from .errors import AvdClonerError from certfuzz.android.api.android_cmd import AndroidCmd +from certfuzz.android.api.defaults import inifile, avddir +from certfuzz.android.avd_mgr.errors import AvdClonerError + +#from certfuzz.helpers import random_str logger = logging.getLogger(__name__) clone_name = '{}-clone-{}'.format + def clone_avd(src=None, dst=None, remove=False): ''' Given the name of an Android Virtual Device, clone it and return the @@ -31,6 +32,7 @@ def clone_avd(src=None, dst=None, remove=False): cloner.clone() return cloner.dst + class AvdCloner(object): ''' Provides ability to clone an Android Virtual Device. diff --git a/src/certfuzz/android/avd_mgr/errors.py b/src/certfuzz/android/avd_mgr/errors.py index 7eb307d..8a395a5 100644 --- a/src/certfuzz/android/avd_mgr/errors.py +++ b/src/certfuzz/android/avd_mgr/errors.py @@ -3,16 +3,20 @@ @organization: cert.org ''' -from .. import AndroidError +from certfuzz.android.errors import AndroidError + class AndroidEmulatorManagerError(AndroidError): pass + class AvdMgrError(AndroidEmulatorManagerError): pass + class AvdClonerError(AndroidEmulatorManagerError): pass + class OrphanedProcessError(AndroidEmulatorManagerError): pass diff --git a/src/certfuzz/android/avd_mgr/main.py b/src/certfuzz/android/avd_mgr/main.py index 2a6ef26..2673b74 100755 --- a/src/certfuzz/android/avd_mgr/main.py +++ b/src/certfuzz/android/avd_mgr/main.py @@ -8,10 +8,9 @@ import os import atexit -from . import AndroidEmulator -from . import AvdCloner -from . import OrphanCatcher -from . import AndroidEmulatorError +from certfuzz.android.avd_mgr import AndroidEmulator +from certfuzz.android.avd_mgr import AvdCloner +from certfuzz.android.avd_mgr import OrphanCatcher logger = logging.getLogger(__name__) diff --git a/src/certfuzz/android/avd_mgr/orphan_catcher.py b/src/certfuzz/android/avd_mgr/orphan_catcher.py index a7ccf0c..b24b9db 100644 --- a/src/certfuzz/android/avd_mgr/orphan_catcher.py +++ b/src/certfuzz/android/avd_mgr/orphan_catcher.py @@ -3,18 +3,17 @@ @organization: cert.org ''' -import os import logging +import os import time - from traceback import format_tb -# from .defaults import POLL_INTERVAL -from .errors import OrphanedProcessError +from certfuzz.android.avd_mgr.errors import OrphanedProcessError + +# from .defaults import POLL_INTERVAL # store pid and ppid at start up, we'll check them later # to see if we are an orphan - logger = logging.getLogger(__name__) def _ping_process(pid): From 7c5955caee2be9eaea744e040d6db55f49e90aef Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 10:47:07 -0400 Subject: [PATCH 0155/1169] replace relative imports with absolute imports --- src/certfuzz/android/controller/errors.py | 3 ++- src/certfuzz/android/worker/errors.py | 3 ++- src/certfuzz/android/worker/worker.py | 11 +++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/android/controller/errors.py b/src/certfuzz/android/controller/errors.py index 7a810f6..04e07b5 100644 --- a/src/certfuzz/android/controller/errors.py +++ b/src/certfuzz/android/controller/errors.py @@ -3,7 +3,8 @@ @organization: cert.org ''' -from .. import AndroidError +from certfuzz.android.errors import AndroidError + class ControllerError(AndroidError): pass diff --git a/src/certfuzz/android/worker/errors.py b/src/certfuzz/android/worker/errors.py index 3cd6905..960b834 100644 --- a/src/certfuzz/android/worker/errors.py +++ b/src/certfuzz/android/worker/errors.py @@ -3,7 +3,8 @@ @organization: cert.org ''' -from .. import AndroidError +from certfuzz.android.errors import AndroidError + class WorkerError(AndroidError): pass diff --git a/src/certfuzz/android/worker/worker.py b/src/certfuzz/android/worker/worker.py index 6b4a7f5..8be4e3f 100644 --- a/src/certfuzz/android/worker/worker.py +++ b/src/certfuzz/android/worker/worker.py @@ -3,10 +3,6 @@ @organization: cert.org ''' -from ...iteration.iteration_android import emu as iteration_emu -from ..api import AdbCmd -from ..api.log_helper import log_formatter -from ..avd_mgr.main import avd_manager import atexit import logging import os @@ -15,6 +11,13 @@ import sys import time +from certfuzz.android.api import AdbCmd +from certfuzz.android.api.log_helper import log_formatter +from certfuzz.android.avd_mgr.main import avd_manager + +from certfuzz.iteration.iteration_android import emu as iteration_emu + + logger = logging.getLogger(__name__) From 003a00b5e99d6c1ea7ac7f7fb6490618892cc781 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 10:47:59 -0400 Subject: [PATCH 0156/1169] remove dead code --- src/certfuzz/android/worker/defaults.py | 16 --- src/certfuzz/android/worker/tasks.py | 147 +----------------------- 2 files changed, 5 insertions(+), 158 deletions(-) delete mode 100644 src/certfuzz/android/worker/defaults.py diff --git a/src/certfuzz/android/worker/defaults.py b/src/certfuzz/android/worker/defaults.py deleted file mode 100644 index 2d9cce9..0000000 --- a/src/certfuzz/android/worker/defaults.py +++ /dev/null @@ -1,16 +0,0 @@ -# ''' -# Created on Jan 17, 2013 -# -# @organization: cert.org -# ''' -# from pkg_resources import resource_string -# import yaml -# import os -# -# _yaml = resource_string(__name__, 'config.yaml') -# _defaults = yaml.safe_load(_yaml) -# HANDLE_FILE_TIMEOUT = 300 -# TOMBSTONE_TIMEOUT = 5 -# DBCFG = _defaults['db'] -# -# SF_CACHE_DIR = os.path.abspath(os.path.expanduser(_defaults['directories']['seedfile_cache'])) diff --git a/src/certfuzz/android/worker/tasks.py b/src/certfuzz/android/worker/tasks.py index 7ea40d5..f476d80 100644 --- a/src/certfuzz/android/worker/tasks.py +++ b/src/certfuzz/android/worker/tasks.py @@ -1,148 +1,11 @@ -# ''' -# Created on Jan 17, 2013 -# -# @organization: cert.org -# ''' -# from .defaults import TOMBSTONE_TIMEOUT, DBCFG, SF_CACHE_DIR -# from ..api.activity_manager import ActivityManagerError -# from ..api.errors import AdbCmdError -# from ..api.log_helper import pfunc -# from ..celery import celery -# from ...android.worker.errors import WorkerError -# from ...crash.android_testcase import AndroidTestCase -# from ...db.couchdb.datatypes import FileDoc -# from ...db.couchdb.db import TestCaseDb -# from ...db.couchdb.db import put_file -# from ...file_handlers.fuzzedfile import FuzzedFile -# # from ...file_handlers.basicfile import BasicFile -# from ...file_handlers.seedfile import SeedFile -# from ...file_handlers.tempdir import TempDir -# from ...fuzztools.filetools import find_or_create_dir -# from ...runners import AndroidRunner, RunnerError -# import logging -# import os +''' +Created on Jan 17, 2013 + +@organization: cert.org +''' class EmuHandle(object): handle = None emu = EmuHandle() -# -# logger = logging.getLogger(__name__) -# -# # use default dbcfg from yaml file -# dbcfg = DBCFG -# sf_dir = SF_CACHE_DIR -# -# def _get_seedfile_by_id(tcdb, sfid): -# # find the doc record by id -# doc = FileDoc.load(tcdb.db, sfid) -# if doc is None: -# raise WorkerError('Seedfile not found in db, sfid=%s', sfid) -# -# sfpath = os.path.join(sf_dir, doc.filename) -# if not os.path.exists(sfpath): -# logger.info('Retrieving %s from db', doc.filename) -# # pull content from db, write to disk -# f = tcdb.db.get_attachment(doc, doc.filename) -# if f is None: -# raise WorkerError('Seedfile content not found in db, sfid=%s', sfid) -# -# logger.debug('...writing content to %s', sfpath) -# # make sure we have a dir to drop it in -# find_or_create_dir(sf_dir) -# with open(sfpath, 'wb') as out: -# out.write(f.read()) -# else: -# logger.debug('Found cached seedfile at %s', sfpath) -# -# sf = SeedFile(sf_dir, sfpath) -# return sf -# -# @celery.task -# def fuzz_and_run(campaign_id, seedfile_id, iteration_num, rng_seed, fuzzopts, runopts): -# logger.info('New task for campaign %s ', campaign_id) -# logger.debug('seedfile_id = %s', seedfile_id) -# logger.debug('iteration_num = %d', iteration_num) -# logger.debug('rng_seed = %s', rng_seed) -# logger.debug('fuzzopts = %s', fuzzopts) -# logger.debug('runopts = %s', runopts) -# -# tcdb = TestCaseDb(**dbcfg) -# -# # get the seedfile, caching if needed -# sfobj = _get_seedfile_by_id(tcdb, seedfile_id) -# -# with TempDir(prefix='bff-fuzz-and-run-') as tmpdir: -# # # FUZZ -# logger.debug('...fuzz') -# src_file = fuzz(sfobj, tmpdir, rng_seed, -# iteration_num, fuzzopts) -# -# dst_basename = '%s-fuzzed%s' % (sfobj.root, sfobj.ext) -# dst_file = os.path.join('/', 'sdcard', dst_basename) -# -# # # RUN -# logger.debug('...run') -# if not runopts.get('runtimeout'): -# runopts['runtimeout'] = TOMBSTONE_TIMEOUT -# -# try: -# with AndroidRunner(handle=emu.handle, -# src_file=src_file, -# dst_file=dst_file, -# campaign_id=campaign_id, -# intent=runopts['intent'], -# workingdir_base=tmpdir, -# options=runopts, -# ) as runner: -# runner.run() -# except (AdbCmdError, RunnerError, ActivityManagerError) as e: -# # this is fatal to this task, so requeue it for somebody else to try -# logger.warning('Runner failed, requeuing task: %s', e) -# raise fuzz_and_run.retry(exc=e) -# -# # # CHECK FOR CRASH -# logger.debug('...check for crash') -# if runner.saw_crash: -# # put fuzzed file in db -# logger.info('...saw crash') -# fuzzedfile = FuzzedFile(path=src_file, derived_from=sfobj) -# put_file(fuzzedfile, tcdb.db) -# -# # create testcase record -# logger.info('...create test case object') -# with AndroidTestCase(seedfile=sfobj, -# fuzzedfile=fuzzedfile, -# workdir_base=tmpdir, -# handle=emu.handle, -# input_dir=runner.result_dir, -# campaign_id=campaign_id, -# ) as testcase: -# logger.info('...store object to db') -# testcase.store(tcdb.db) -# -# -# @pfunc(logger=logger) -# def fuzz(sfobj, outdir_base, rng_seed, iteration, options): -# # TODO: this should move to certfuzz.fuzzers -# from certfuzz.fuzzers.bytemut import ByteMutFuzzer -# -# if options is None: -# options = {} -# -# with ByteMutFuzzer(seedfile_obj=sfobj, -# outdir_base=outdir_base, -# rng_seed=rng_seed, -# iteration=iteration, -# options=options,) as fuzzer: -# # TODO: assuming this works with a SeedFile object you don't need the fake RF -# # class RFdummy(object): -# # def next_item(self): -# # class Rdummy(object): -# # min = 0.0 -# # max = 1.0 -# # return Rdummy() -# # fuzzer.sf.rangefinder = RFdummy() -# fuzzer.fuzz() -# return fuzzer.output_file_path From 3bd23558df07d473f4753d9e23ade6d80cc22968 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 10:49:55 -0400 Subject: [PATCH 0157/1169] replace relative imports with absolute imports --- src/certfuzz/bff/android.py | 9 +++++---- src/certfuzz/bff/linux.py | 9 ++++----- src/certfuzz/bff/windows.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/bff/android.py b/src/certfuzz/bff/android.py index e7d13a3..8af0699 100644 --- a/src/certfuzz/bff/android.py +++ b/src/certfuzz/bff/android.py @@ -5,10 +5,11 @@ ''' import logging import os -from ..campaign.campaign_android import AndroidCampaign -from ..fuzztools import filetools -from ..campaign.errors import AndroidCampaignError -from ..android.api.log_helper import log_formatter + +from certfuzz.android.api.log_helper import log_formatter +from certfuzz.campaign.campaign_android import AndroidCampaign +from certfuzz.campaign.errors import AndroidCampaignError +from certfuzz.fuzztools import filetools logger = logging.getLogger() diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 800e2e7..c2a94eb 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -12,9 +12,10 @@ import platform import sys -from ..debuggers import crashwrangler # @UnusedImport -from ..debuggers import gdb # @UnusedImport -from ..fuzztools import filetools +from certfuzz.debuggers import crashwrangler # @UnusedImport +from certfuzz.debuggers import gdb # @UnusedImport +from certfuzz.fuzztools import filetools + from certfuzz.campaign.linux import Campaign @@ -36,8 +37,6 @@ # return rate - - def setup_logging_to_console(log_obj, level): hdlr = logging.StreamHandler() formatter = logging.Formatter('%(name)s %(message)s') diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index ff718cb..51defec 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -12,7 +12,7 @@ import os import sys -from ..campaign.campaign_windows import WindowsCampaign +from certfuzz.campaign.campaign_windows import WindowsCampaign def _setup_logging_to_screen(options, logger, fmt): From b34cc762b643ad7bd7413032428914923d0e9103 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 10:51:51 -0400 Subject: [PATCH 0158/1169] replace relative imports with absolute imports --- src/certfuzz/campaign/config/android_config.py | 2 +- src/certfuzz/campaign/config/bff_config.py | 12 +++++++----- src/certfuzz/campaign/config/config_base.py | 6 ++++-- src/certfuzz/campaign/config/errors.py | 2 +- src/certfuzz/campaign/config/foe_config.py | 9 +++++---- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/certfuzz/campaign/config/android_config.py b/src/certfuzz/campaign/config/android_config.py index 58c84b8..fc2b57b 100644 --- a/src/certfuzz/campaign/config/android_config.py +++ b/src/certfuzz/campaign/config/android_config.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from .config_base import Config as ConfigBase +from certfuzz.campaign.config.config_base import Config as ConfigBase #from ...android.controller.defaults import CONFIG as DEFAULT_CONFIG import logging diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index 7f94405..db46431 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -7,14 +7,16 @@ This module provides the ConfigHelper class to assist with converting a bff.cfg file into a collection of attributes and methods to facilitate a fuzzing run. ''' -import shutil -import os import ConfigParser -import subprocess +import logging +import os import re import shlex -import logging -from ...fuzztools import filetools +import shutil +import subprocess + +from certfuzz.fuzztools import filetools + logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) diff --git a/src/certfuzz/campaign/config/config_base.py b/src/certfuzz/campaign/config/config_base.py index 534421d..f8b19ea 100644 --- a/src/certfuzz/campaign/config/config_base.py +++ b/src/certfuzz/campaign/config/config_base.py @@ -3,9 +3,11 @@ @organization: cert.org ''' -import yaml -import os.path import logging +import os.path + +import yaml + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/campaign/config/errors.py b/src/certfuzz/campaign/config/errors.py index 5f778c1..2f978f0 100644 --- a/src/certfuzz/campaign/config/errors.py +++ b/src/certfuzz/campaign/config/errors.py @@ -3,7 +3,7 @@ @author: adh ''' -from ..errors import CERTFuzzError +from certfuzz.errors import CERTFuzzError class ConfigError(CERTFuzzError): diff --git a/src/certfuzz/campaign/config/foe_config.py b/src/certfuzz/campaign/config/foe_config.py index c4803fa..6f9884e 100644 --- a/src/certfuzz/campaign/config/foe_config.py +++ b/src/certfuzz/campaign/config/foe_config.py @@ -4,12 +4,13 @@ @organization: cert.org ''' import logging +import shlex from string import Template -from . import Config as ConfigBase -from .errors import ConfigError -from ...helpers import quoted -import shlex +from certfuzz.campaign.config.config_base import Config as ConfigBase +from certfuzz.campaign.config.errors import ConfigError +from certfuzz.helpers import quoted + logger = logging.getLogger(__name__) From 48ef7874f78c0597deabf786d8faebcd36db1d53 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 10:55:31 -0400 Subject: [PATCH 0159/1169] replace relative imports with absolute imports --- src/certfuzz/campaign/__init__.py | 6 ++--- src/certfuzz/campaign/campaign.py | 33 +++++++++++------------ src/certfuzz/campaign/campaign_android.py | 25 +++++++++-------- src/certfuzz/campaign/campaign_base.py | 4 +-- src/certfuzz/campaign/campaign_windows.py | 4 +-- src/certfuzz/campaign/errors.py | 2 +- 6 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/certfuzz/campaign/__init__.py b/src/certfuzz/campaign/__init__.py index ae27afd..d07d5d5 100644 --- a/src/certfuzz/campaign/__init__.py +++ b/src/certfuzz/campaign/__init__.py @@ -1,3 +1,3 @@ -from .. import __version__ -from .campaign_base import import_module_by_name -from .campaign_base import CampaignBase +from certfuzz import __version__ +from certfuzz.campaign.campaign_base import import_module_by_name +from certfuzz.campaign.campaign_base import CampaignBase diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index 3b0cb35..9eb7496 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -4,30 +4,29 @@ @organization: cert.org ''' +import gc import logging import os +import re import shutil import tempfile import traceback -import re + +from certfuzz.campaign import CampaignBase, __version__, import_module_by_name +from certfuzz.campaign.config.foe_config import Config +from certfuzz.campaign.errors import CampaignError +from certfuzz.debuggers import registration +from certfuzz.file_handlers.seedfile_set import SeedfileSet +from certfuzz.fuzzers.errors import FuzzerExhaustedError +from certfuzz.fuzztools import filetools +from certfuzz.fuzztools.object_caching import dump_obj_to_file +from certfuzz.runners.errors import RunnerArchitectureError, \ + RunnerPlatformVersionError +from certfuzz.scoring.errors import EmptySetError + import cPickle as pickle -import gc +from certfuzz.iteration.iteration_windows import Iteration -from . import __version__ -from . import import_module_by_name -from . import CampaignBase -from .errors import CampaignError -from .config.foe_config import Config -from ..iteration.iteration_windows import Iteration - -from ..debuggers import registration -from ..fuzztools import filetools -from ..file_handlers.seedfile_set import SeedfileSet -from ..fuzzers.errors import FuzzerExhaustedError -from ..scoring.errors import EmptySetError -from ..runners.errors import RunnerArchitectureError -from ..runners.errors import RunnerPlatformVersionError -from ..fuzztools.object_caching import dump_obj_to_file logger = logging.getLogger(__name__) diff --git a/src/certfuzz/campaign/campaign_android.py b/src/certfuzz/campaign/campaign_android.py index c3091d2..6f904e4 100644 --- a/src/certfuzz/campaign/campaign_android.py +++ b/src/certfuzz/campaign/campaign_android.py @@ -3,20 +3,23 @@ @organization: cert.org ''' +import errno import logging import random -from . import __version__ -from .campaign import Campaign -from ..iteration.iteration_android import do_iteration -from .config.android_config import AndroidConfig -from ..db.couchdb.db import TestCaseDb, put_file -from ..db.couchdb.datatypes.campaign_doc import AndroidCampaignDoc -from ..file_handlers.directory import Directory -from ..android.api.intent import Intent -from ..android.worker import worker -from .errors import AndroidCampaignError from socket import error as socket_error -import errno + +from certfuzz.android.api.intent import Intent +from certfuzz.android.worker import worker +from certfuzz.campaign import __version__ +from certfuzz.campaign.campaign import Campaign +from certfuzz.campaign.config.android_config import AndroidConfig +from certfuzz.campaign.errors import AndroidCampaignError +from certfuzz.db.couchdb.datatypes.campaign_doc import AndroidCampaignDoc +from certfuzz.db.couchdb.db import TestCaseDb, put_file +from certfuzz.file_handlers.directory import Directory + +from certfuzz.iteration.iteration_android import do_iteration + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index de30500..ec006be 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -10,8 +10,8 @@ import sys import os -from . import __version__ -from ..fuzztools import filetools +from certfuzz import __version__ +from certfuzz.fuzztools import filetools def import_module_by_name(name, logger=None): diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index c73c126..05f552f 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -9,9 +9,9 @@ from threading import Timer import platform -from .campaign import Campaign +from certfuzz.campaign.campaign import Campaign -from ..runners.killableprocess import Popen +from certfuzz.runners.killableprocess import Popen logger = logging.getLogger(__name__) diff --git a/src/certfuzz/campaign/errors.py b/src/certfuzz/campaign/errors.py index a621ff1..6bf3ec8 100644 --- a/src/certfuzz/campaign/errors.py +++ b/src/certfuzz/campaign/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from ..errors import CERTFuzzError +from certfuzz.errors import CERTFuzzError class CampaignError(CERTFuzzError): From 5afa912a3ca8e7e860ff5b9efacd6d2e83b3de7c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 10:58:26 -0400 Subject: [PATCH 0160/1169] replace relative imports with absolute imports --- src/certfuzz/crash/__init__.py | 14 +++++++------- src/certfuzz/crash/android_testcase.py | 10 ++++++---- src/certfuzz/crash/bff_crash.py | 15 ++++++++------- src/certfuzz/crash/crash_base.py | 9 +++++---- src/certfuzz/crash/errors.py | 2 +- src/certfuzz/crash/foe_crash.py | 11 ++++++----- src/certfuzz/crash/testcase_base.py | 6 ++++-- 7 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/certfuzz/crash/__init__.py b/src/certfuzz/crash/__init__.py index f2a7107..05d83c9 100644 --- a/src/certfuzz/crash/__init__.py +++ b/src/certfuzz/crash/__init__.py @@ -1,8 +1,8 @@ -from .testcase_base import TestCaseBase -from .crash_base import Crash -from .bff_crash import BffCrash -from .foe_crash import FoeCrash +from certfuzz.crash.testcase_base import TestCaseBase +from certfuzz.crash.crash_base import Crash +from certfuzz.crash.bff_crash import BffCrash +from certfuzz.crash.foe_crash import FoeCrash # from .android_testcase import AndroidTestCase -from .errors import CrashError -from .errors import TestCaseError -from .errors import AndroidTestCaseError +from certfuzz.crash.errors import CrashError +from certfuzz.crash.errors import TestCaseError +from certfuzz.crash.errors import AndroidTestCaseError diff --git a/src/certfuzz/crash/android_testcase.py b/src/certfuzz/crash/android_testcase.py index 5935240..aef785e 100644 --- a/src/certfuzz/crash/android_testcase.py +++ b/src/certfuzz/crash/android_testcase.py @@ -3,13 +3,15 @@ @organization: cert.org ''' -from . import TestCaseBase -from ..android.api import AdbCmd -from ..android.api.log_helper import pfunc -from ..file_handlers.basicfile import BasicFile import logging import os +from certfuzz.android.api import AdbCmd +from certfuzz.android.api.log_helper import pfunc +from certfuzz.crash import TestCaseBase +from certfuzz.file_handlers.basicfile import BasicFile + + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index d304b6f..0eb51db 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -6,14 +6,15 @@ import logging import os -from .crash_base import Crash -from .crash_base import CrashError -from ..debuggers import registration -from ..fuzztools import hostinfo +from certfuzz.crash.crash_base import Crash, CrashError +from certfuzz.debuggers import registration +from certfuzz.fuzztools import hostinfo + + try: - from ..analyzers import pin_calltrace - from ..analyzers import AnalyzerEmptyOutputError - from ..debuggers.output_parsers.calltracefile import Calltracefile + from certfuzz.analyzers import pin_calltrace + from certfuzz.analyzers import AnalyzerEmptyOutputError + from certfuzz.debuggers.output_parsers.calltracefile import Calltracefile except ImportError: pass diff --git a/src/certfuzz/crash/crash_base.py b/src/certfuzz/crash/crash_base.py index d972d52..d601045 100644 --- a/src/certfuzz/crash/crash_base.py +++ b/src/certfuzz/crash/crash_base.py @@ -7,10 +7,11 @@ import os import tempfile -from .testcase_base import TestCaseBase -from .errors import CrashError -from ..fuzztools import filetools -from ..file_handlers.basicfile import BasicFile +from certfuzz.crash.errors import CrashError +from certfuzz.crash.testcase_base import TestCaseBase +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.fuzztools import filetools + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/crash/errors.py b/src/certfuzz/crash/errors.py index df3f8bc..f6aa42b 100644 --- a/src/certfuzz/crash/errors.py +++ b/src/certfuzz/crash/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from .. import CERTFuzzError +from certfuzz.errors import CERTFuzzError class TestCaseError(CERTFuzzError): diff --git a/src/certfuzz/crash/foe_crash.py b/src/certfuzz/crash/foe_crash.py index ed0b6d3..76a7413 100644 --- a/src/certfuzz/crash/foe_crash.py +++ b/src/certfuzz/crash/foe_crash.py @@ -7,11 +7,12 @@ import logging import os -from .crash_base import Crash -from ..helpers import random_str -from ..file_handlers.basicfile import BasicFile -from ..fuzztools.filetools import best_effort_move -from ..campaign.config.foe_config import get_command_args_list +from certfuzz.campaign.config.foe_config import get_command_args_list +from certfuzz.crash.crash_base import Crash +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.fuzztools.filetools import best_effort_move +from certfuzz.helpers import random_str + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/crash/testcase_base.py b/src/certfuzz/crash/testcase_base.py index 1874b8d..c484724 100644 --- a/src/certfuzz/crash/testcase_base.py +++ b/src/certfuzz/crash/testcase_base.py @@ -3,10 +3,12 @@ @organization: cert.org ''' -from certfuzz.fuzztools import hamming import logging -import tempfile import shutil +import tempfile + +from certfuzz.fuzztools import hamming + logger = logging.getLogger(__name__) From 5e5c65026ea17f5b3449ebbd0a6586fbf2799109 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 11:04:23 -0400 Subject: [PATCH 0161/1169] replace relative imports with absolute imports --- src/certfuzz/debuggers/output_parsers/__init__.py | 13 ++++--------- src/certfuzz/debuggers/output_parsers/abrtfile.py | 10 ++++++---- .../debuggers/output_parsers/calltracefile.py | 11 ++++++----- src/certfuzz/debuggers/output_parsers/cwfile.py | 3 ++- .../debuggers/output_parsers/debugger_file_base.py | 8 ++++---- src/certfuzz/debuggers/output_parsers/errors.py | 7 +++++-- src/certfuzz/debuggers/output_parsers/gdbfile.py | 3 ++- src/certfuzz/debuggers/output_parsers/konqifile.py | 4 ++-- src/certfuzz/debuggers/output_parsers/msec_file.py | 3 ++- 9 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/__init__.py b/src/certfuzz/debuggers/output_parsers/__init__.py index 3d70f66..c8a3214 100644 --- a/src/certfuzz/debuggers/output_parsers/__init__.py +++ b/src/certfuzz/debuggers/output_parsers/__init__.py @@ -1,9 +1,4 @@ -from .errors import DebuggerError -from .errors import DebuggerFileError -from .errors import UnknownDebuggerError -from .debugger_file_base import regex -from .debugger_file_base import DebuggerFile -from .debugger_file_base import detect_format -from .debugger_file_base import check_thread_type -from .debugger_file_base import registers -from .debugger_file_base import blacklist +from certfuzz.debuggers.output_parsers.debugger_file_base import DebuggerFile, \ + blacklist, check_thread_type, detect_format, regex, registers +from certfuzz.debuggers.output_parsers.errors import DebuggerError, \ + DebuggerFileError, UnknownDebuggerError diff --git a/src/certfuzz/debuggers/output_parsers/abrtfile.py b/src/certfuzz/debuggers/output_parsers/abrtfile.py index 5877537..5c4fcb1 100644 --- a/src/certfuzz/debuggers/output_parsers/abrtfile.py +++ b/src/certfuzz/debuggers/output_parsers/abrtfile.py @@ -5,12 +5,13 @@ @organization: cert.org ''' -import re import logging -from . import regex as regex_base -from . import DebuggerFile - from optparse import OptionParser +import re + +from certfuzz.debuggers.output_parsers.debugger_file_base import DebuggerFile, \ + regex as regex_base + logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) @@ -26,6 +27,7 @@ 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+Yes\s.+(/.+)'), }) + class ABRTfile(DebuggerFile): def __init__(self, path, exclude_unmapped_frames=True): self.has_threads = False diff --git a/src/certfuzz/debuggers/output_parsers/calltracefile.py b/src/certfuzz/debuggers/output_parsers/calltracefile.py index 797f3e2..52a9521 100644 --- a/src/certfuzz/debuggers/output_parsers/calltracefile.py +++ b/src/certfuzz/debuggers/output_parsers/calltracefile.py @@ -5,11 +5,11 @@ @organization: cert.org ''' -import re import hashlib import logging from optparse import OptionParser -import os +import re + logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) @@ -20,6 +20,7 @@ 'ct_system_lib': re.compile(r'^/(usr/)?lib.+'), } + class Calltracefile: def __init__(self, f): ''' @@ -36,12 +37,12 @@ def __init__(self, f): self.backtrace = [] self.hashable_backtrace = [] self.hashable_backtrace_string = '' - + # Process lines one-by-one. File can be huge with open(self.file) as pinfile: for line in pinfile: self.calltrace_line(line) - + self._hashable_backtrace() def _hashable_backtrace(self): @@ -80,7 +81,7 @@ def _process_lines(self): for idx, line in enumerate(self.lines): self.calltrace_line(idx, line) - + def get_crash_signature(self, backtrace_level): ''' Determines if a crash is unique. Depending on , diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index f3d6743..7a301e7 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -5,11 +5,12 @@ @organization: cert.org ''' -import re import hashlib import logging from optparse import OptionParser import os +import re + logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) diff --git a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py index dd60dc9..d47868a 100644 --- a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py +++ b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py @@ -3,14 +3,14 @@ @organization: cert.org ''' -import re import hashlib import logging import os +import re + +from certfuzz.debuggers.output_parsers.errors import DebuggerFileError, \ + UnknownDebuggerError -from .errors import DebuggerError -from .errors import DebuggerFileError -from .errors import UnknownDebuggerError logger = logging.getLogger(__name__) diff --git a/src/certfuzz/debuggers/output_parsers/errors.py b/src/certfuzz/debuggers/output_parsers/errors.py index c934dfe..9429ebc 100644 --- a/src/certfuzz/debuggers/output_parsers/errors.py +++ b/src/certfuzz/debuggers/output_parsers/errors.py @@ -3,9 +3,12 @@ @organization: cert.org ''' -class DebuggerError(Exception): - pass +from certfuzz.debuggers.errors import DebuggerError + + class DebuggerFileError(DebuggerError, IOError): pass + + class UnknownDebuggerError(DebuggerError): pass diff --git a/src/certfuzz/debuggers/output_parsers/gdbfile.py b/src/certfuzz/debuggers/output_parsers/gdbfile.py index f6bcc5d..5df1fd7 100644 --- a/src/certfuzz/debuggers/output_parsers/gdbfile.py +++ b/src/certfuzz/debuggers/output_parsers/gdbfile.py @@ -5,7 +5,8 @@ @organization: cert.org ''' -from . import DebuggerFile +from certfuzz.debuggers.output_parsers import DebuggerFile + class GDBfile(DebuggerFile): pass diff --git a/src/certfuzz/debuggers/output_parsers/konqifile.py b/src/certfuzz/debuggers/output_parsers/konqifile.py index 5053b31..8c8f461 100644 --- a/src/certfuzz/debuggers/output_parsers/konqifile.py +++ b/src/certfuzz/debuggers/output_parsers/konqifile.py @@ -5,8 +5,8 @@ ''' import re import logging -from . import DebuggerFile -from . import regex as regex_base +from certfuzz.debuggers.output_parsers import DebuggerFile +from certfuzz.debuggers.output_parsers import regex as regex_base from optparse import OptionParser diff --git a/src/certfuzz/debuggers/output_parsers/msec_file.py b/src/certfuzz/debuggers/output_parsers/msec_file.py index 90cc9ba..a8735d0 100644 --- a/src/certfuzz/debuggers/output_parsers/msec_file.py +++ b/src/certfuzz/debuggers/output_parsers/msec_file.py @@ -5,12 +5,13 @@ ''' import logging -from . import DebuggerFile +from certfuzz.debuggers.output_parsers import DebuggerFile logger = logging.getLogger(__name__) required_checks = ['crash_hash', 'exploitability'] + class MsecFile(DebuggerFile): ''' classdocs From 4c0fc168ff17661c6aadb1ed8939fb8141756b51 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 11:07:56 -0400 Subject: [PATCH 0162/1169] replace relative imports with absolute imports --- src/certfuzz/debuggers/__init__.py | 26 ++++++++++++------------- src/certfuzz/debuggers/crashwrangler.py | 8 ++++---- src/certfuzz/debuggers/debugger_base.py | 8 +++++--- src/certfuzz/debuggers/errors.py | 3 ++- src/certfuzz/debuggers/gdb.py | 14 ++++++------- src/certfuzz/debuggers/jit.py | 2 +- src/certfuzz/debuggers/msec.py | 18 +++++++++-------- src/certfuzz/debuggers/nulldebugger.py | 13 +++++++------ src/certfuzz/debuggers/registration.py | 7 +++++-- 9 files changed, 54 insertions(+), 45 deletions(-) diff --git a/src/certfuzz/debuggers/__init__.py b/src/certfuzz/debuggers/__init__.py index 2220fbd..95637b7 100644 --- a/src/certfuzz/debuggers/__init__.py +++ b/src/certfuzz/debuggers/__init__.py @@ -1,13 +1,13 @@ -from .errors import UndefinedDebuggerError -from .errors import DebuggerNotFoundError -from .errors import DebuggerError -from .registration import get_debug_file -from .registration import register -from .registration import verify_supported_platform -from .registration import get -from .registration import result_fields -from .registration import allowed_exploitability_values -from .registration import debugger -from .registration import debug_class -from .registration import debug_ext -from .debugger_base import Debugger +from certfuzz.debuggers.errors import UndefinedDebuggerError +from certfuzz.debuggers.errors import DebuggerNotFoundError +from certfuzz.debuggers.errors import DebuggerError +from certfuzz.debuggers.registration import get_debug_file +from certfuzz.debuggers.registration import register +from certfuzz.debuggers.registration import verify_supported_platform +from certfuzz.debuggers.registration import get +from certfuzz.debuggers.registration import result_fields +from certfuzz.debuggers.registration import allowed_exploitability_values +from certfuzz.debuggers.registration import debugger +from certfuzz.debuggers.registration import debug_class +from certfuzz.debuggers.registration import debug_ext +from certfuzz.debuggers.debugger_base import Debugger diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index a2f3f09..35b02f7 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -10,10 +10,10 @@ import platform import re -from . import register -from .output_parsers.cwfile import CWfile -from . import Debugger -from ..fuzztools import subprocess_helper as subp +from certfuzz.debuggers import Debugger, register +from certfuzz.debuggers.output_parsers.cwfile import CWfile +from certfuzz.fuzztools import subprocess_helper as subp + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/debuggers/debugger_base.py b/src/certfuzz/debuggers/debugger_base.py index 141e652..ab5750a 100644 --- a/src/certfuzz/debuggers/debugger_base.py +++ b/src/certfuzz/debuggers/debugger_base.py @@ -4,9 +4,11 @@ @organization: cert.org ''' import logging -from .registration import get_debug_file -from .registration import result_fields, allowed_exploitability_values -from .errors import DebuggerError + +from certfuzz.debuggers.errors import DebuggerError +from certfuzz.debuggers.registration import get_debug_file, result_fields, \ + allowed_exploitability_values + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/debuggers/errors.py b/src/certfuzz/debuggers/errors.py index 6c0bfb4..ca97865 100644 --- a/src/certfuzz/debuggers/errors.py +++ b/src/certfuzz/debuggers/errors.py @@ -3,9 +3,10 @@ @organization: cert.org ''' +from certfuzz.errors import CERTFuzzError -class DebuggerError(Exception): +class DebuggerError(CERTFuzzError): pass diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index a382dac..9a348ff 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -5,17 +5,17 @@ @organization: cert.org ''' +import logging +import os from string import Template import tempfile -import os -import logging -from .registration import register -from . import DebuggerError +from certfuzz.debuggers.debugger_base import Debugger +from certfuzz.debuggers.errors import DebuggerError +from certfuzz.debuggers.output_parsers.gdbfile import GDBfile +from certfuzz.debuggers.registration import register +from certfuzz.fuzztools import subprocess_helper as subp -from .debugger_base import Debugger -from .output_parsers.gdbfile import GDBfile -from ..fuzztools import subprocess_helper as subp logger = logging.getLogger(__name__) diff --git a/src/certfuzz/debuggers/jit.py b/src/certfuzz/debuggers/jit.py index e1c562f..598af1e 100644 --- a/src/certfuzz/debuggers/jit.py +++ b/src/certfuzz/debuggers/jit.py @@ -3,9 +3,9 @@ fuzzer that an exception occurred. """ -import sys from ctypes import windll, byref from ctypes.wintypes import HANDLE, BOOL, LPCWSTR +import sys def main(pid=None): diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 8bc79c3..6f0b12c 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -1,18 +1,20 @@ """This module runs cdb on a process and !exploitable on any exceptions. """ import ctypes +import logging +import os from pprint import pformat -from threading import Timer from subprocess import Popen -import os -import logging -import wmi +from threading import Timer import time -from . import Debugger as DebuggerBase -from .registration import register -from .output_parsers.msec_file import MsecFile -from ..helpers import check_os_compatibility +from certfuzz.debuggers import Debugger as DebuggerBase +from certfuzz.debuggers.output_parsers.msec_file import MsecFile +from certfuzz.debuggers.registration import register +from certfuzz.helpers import check_os_compatibility + +import wmi + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/debuggers/nulldebugger.py b/src/certfuzz/debuggers/nulldebugger.py index c1d03df..1b70df4 100644 --- a/src/certfuzz/debuggers/nulldebugger.py +++ b/src/certfuzz/debuggers/nulldebugger.py @@ -3,15 +3,16 @@ @author: adh ''' -from . import Debugger import logging - -# import things needed to inject randomness import random -from . import allowed_exploitability_values -from ..helpers import random_str -from . import register +from certfuzz.debuggers import allowed_exploitability_values, register +from certfuzz.helpers import random_str + +from . import Debugger + + +# import things needed to inject randomness logger = logging.getLogger(__name__) diff --git a/src/certfuzz/debuggers/registration.py b/src/certfuzz/debuggers/registration.py index 9dc3c56..b5cc5e6 100644 --- a/src/certfuzz/debuggers/registration.py +++ b/src/certfuzz/debuggers/registration.py @@ -5,9 +5,12 @@ ''' import logging import os -import subprocess import platform -from .errors import UndefinedDebuggerError, DebuggerNotFoundError +import subprocess + +from certfuzz.debuggers.errors import UndefinedDebuggerError, \ + DebuggerNotFoundError + logger = logging.getLogger(__name__) From 5d732c171736a76ee6157d8f563ad6946da3cbda Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 11:10:12 -0400 Subject: [PATCH 0163/1169] replace relative imports with absolute imports --- src/certfuzz/file_handlers/__init__.py | 10 +++++----- src/certfuzz/file_handlers/basicfile.py | 3 ++- src/certfuzz/file_handlers/directory.py | 11 +++++++---- src/certfuzz/file_handlers/errors.py | 2 +- src/certfuzz/file_handlers/fuzzedfile.py | 2 +- src/certfuzz/file_handlers/seedfile.py | 12 ++++++------ src/certfuzz/file_handlers/seedfile_set.py | 14 ++++++++------ src/certfuzz/file_handlers/tmp_reaper.py | 8 +++++--- 8 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/certfuzz/file_handlers/__init__.py b/src/certfuzz/file_handlers/__init__.py index 5136b94..ec8b3cb 100644 --- a/src/certfuzz/file_handlers/__init__.py +++ b/src/certfuzz/file_handlers/__init__.py @@ -1,5 +1,5 @@ -from .basicfile import BasicFile -from .fuzzedfile import FuzzedFile -from .seedfile import SeedFile -from .errors import FileHandlerError, BasicFileError, FuzzedFileError -from .errors import SeedFileError +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.file_handlers.fuzzedfile import FuzzedFile +from certfuzz.file_handlers.seedfile import SeedFile +from certfuzz.file_handlers.errors import FileHandlerError, BasicFileError, FuzzedFileError +from certfuzz.file_handlers.errors import SeedFileError diff --git a/src/certfuzz/file_handlers/basicfile.py b/src/certfuzz/file_handlers/basicfile.py index fc72041..3b122dc 100644 --- a/src/certfuzz/file_handlers/basicfile.py +++ b/src/certfuzz/file_handlers/basicfile.py @@ -5,7 +5,8 @@ ''' import hashlib import os -from ..fuzztools.filetools import check_zip_content + +from certfuzz.fuzztools.filetools import check_zip_content class BasicFile(object): diff --git a/src/certfuzz/file_handlers/directory.py b/src/certfuzz/file_handlers/directory.py index c8a22c7..bbd4753 100644 --- a/src/certfuzz/file_handlers/directory.py +++ b/src/certfuzz/file_handlers/directory.py @@ -3,11 +3,14 @@ @organization: cert.org ''' -import os -from ..fuzztools import filetools -from .basicfile import BasicFile import logging -from .errors import DirectoryError +import os + +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.file_handlers.errors import DirectoryError +from certfuzz.fuzztools import filetools + + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/file_handlers/errors.py b/src/certfuzz/file_handlers/errors.py index 0afca7f..149457d 100644 --- a/src/certfuzz/file_handlers/errors.py +++ b/src/certfuzz/file_handlers/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from .. import CERTFuzzError +from certfuzz.errors import CERTFuzzError class FileHandlerError(CERTFuzzError): diff --git a/src/certfuzz/file_handlers/fuzzedfile.py b/src/certfuzz/file_handlers/fuzzedfile.py index 5334819..e4a2ee8 100644 --- a/src/certfuzz/file_handlers/fuzzedfile.py +++ b/src/certfuzz/file_handlers/fuzzedfile.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from . import BasicFile +from certfuzz.file_handlers import BasicFile class FuzzedFile(BasicFile): diff --git a/src/certfuzz/file_handlers/seedfile.py b/src/certfuzz/file_handlers/seedfile.py index f4f32a0..870538c 100644 --- a/src/certfuzz/file_handlers/seedfile.py +++ b/src/certfuzz/file_handlers/seedfile.py @@ -3,14 +3,14 @@ @organization: cert.org ''' -import os import json +import os -from .basicfile import BasicFile -from .errors import SeedFileError -from ..fuzztools.rangefinder import RangeFinder -from ..fuzztools import filetools -from ..scoring.scorable_thing import ScorableThing +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.file_handlers.errors import SeedFileError +from certfuzz.fuzztools import filetools +from certfuzz.fuzztools.rangefinder import RangeFinder +from certfuzz.scoring.scorable_thing import ScorableThing # TODO: replace with a common function in some helper module diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 15e8843..edce9ed 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -6,12 +6,14 @@ import logging import os -from .directory import Directory -from ..fuzztools import filetools -from .seedfile import SeedFile -from .errors import SeedFileError -from ..scoring.scorable_set import ScorableSet2 -from ..scoring.errors import EmptySetError +from certfuzz.file_handlers.directory import Directory +from certfuzz.file_handlers.errors import SeedFileError +from certfuzz.file_handlers.seedfile import SeedFile +from certfuzz.fuzztools import filetools +from certfuzz.scoring.errors import EmptySetError +from certfuzz.scoring.scorable_set import ScorableSet2 + + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/file_handlers/tmp_reaper.py b/src/certfuzz/file_handlers/tmp_reaper.py index d51cf85..7b33a40 100644 --- a/src/certfuzz/file_handlers/tmp_reaper.py +++ b/src/certfuzz/file_handlers/tmp_reaper.py @@ -3,12 +3,14 @@ @organization: cert.org ''' +import logging import os +import platform import shutil import tempfile -import logging -import platform -from ..fuzztools.filetools import delete_contents_of + +from certfuzz.fuzztools.filetools import delete_contents_of + logger = logging.getLogger(__name__) From d29c6a1b4b8d99dcabfcd88931bd91d61c8942df Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 11:14:00 -0400 Subject: [PATCH 0164/1169] replace relative imports with absolute imports --- src/certfuzz/fuzzers/__init__.py | 3 +-- src/certfuzz/fuzzers/bitmut.py | 2 +- src/certfuzz/fuzzers/bytemut.py | 7 ++++--- src/certfuzz/fuzzers/copy.py | 2 +- src/certfuzz/fuzzers/crlfmut.py | 5 +++-- src/certfuzz/fuzzers/crmut.py | 5 +++-- src/certfuzz/fuzzers/drop.py | 6 ++++-- src/certfuzz/fuzzers/errors.py | 3 ++- src/certfuzz/fuzzers/fuzzer_base.py | 12 ++++++------ src/certfuzz/fuzzers/insert.py | 6 ++++-- src/certfuzz/fuzzers/nullmut.py | 5 +++-- src/certfuzz/fuzzers/swap.py | 6 ++++-- src/certfuzz/fuzzers/truncate.py | 6 ++++-- src/certfuzz/fuzzers/verify.py | 5 +++-- src/certfuzz/fuzzers/wave.py | 6 ++++-- 15 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/certfuzz/fuzzers/__init__.py b/src/certfuzz/fuzzers/__init__.py index fb387ed..b305044 100644 --- a/src/certfuzz/fuzzers/__init__.py +++ b/src/certfuzz/fuzzers/__init__.py @@ -1,2 +1 @@ -from .fuzzer_base import Fuzzer -from .fuzzer_base import MinimizableFuzzer +from certfuzz.fuzzers.fuzzer_base import Fuzzer, MinimizableFuzzer diff --git a/src/certfuzz/fuzzers/bitmut.py b/src/certfuzz/fuzzers/bitmut.py index 0f5e5d5..745d5a5 100644 --- a/src/certfuzz/fuzzers/bitmut.py +++ b/src/certfuzz/fuzzers/bitmut.py @@ -1,4 +1,4 @@ -from . import MinimizableFuzzer +from certfuzz.fuzzers import MinimizableFuzzer from random import jumpahead, sample, uniform, seed import logging diff --git a/src/certfuzz/fuzzers/bytemut.py b/src/certfuzz/fuzzers/bytemut.py index fd0617c..25abc60 100644 --- a/src/certfuzz/fuzzers/bytemut.py +++ b/src/certfuzz/fuzzers/bytemut.py @@ -1,8 +1,9 @@ -import random import logging +import random + +from certfuzz.fuzzers import MinimizableFuzzer +from certfuzz.fuzzers.fuzzer_base import is_fuzzable as _fuzzable -from . import MinimizableFuzzer -from .fuzzer_base import is_fuzzable as _fuzzable logger = logging.getLogger(__name__) diff --git a/src/certfuzz/fuzzers/copy.py b/src/certfuzz/fuzzers/copy.py index 253d5c0..337b571 100644 --- a/src/certfuzz/fuzzers/copy.py +++ b/src/certfuzz/fuzzers/copy.py @@ -1,4 +1,4 @@ -from . import Fuzzer +from certfuzz.fuzzers import Fuzzer class CopyFuzzer(Fuzzer): diff --git a/src/certfuzz/fuzzers/crlfmut.py b/src/certfuzz/fuzzers/crlfmut.py index c5d88ba..7fc4815 100644 --- a/src/certfuzz/fuzzers/crlfmut.py +++ b/src/certfuzz/fuzzers/crlfmut.py @@ -1,8 +1,9 @@ import logging -logger = logging.getLogger(__name__) +from certfuzz.fuzzers.bytemut import ByteMutFuzzer + -from .bytemut import ByteMutFuzzer +logger = logging.getLogger(__name__) class CRLFMutFuzzer(ByteMutFuzzer): diff --git a/src/certfuzz/fuzzers/crmut.py b/src/certfuzz/fuzzers/crmut.py index ba2d0ef..984bd1d 100644 --- a/src/certfuzz/fuzzers/crmut.py +++ b/src/certfuzz/fuzzers/crmut.py @@ -1,8 +1,9 @@ import logging -logger = logging.getLogger(__name__) +from certfuzz.fuzzers.bytemut import ByteMutFuzzer + -from .bytemut import ByteMutFuzzer +logger = logging.getLogger(__name__) class CRMutFuzzer(ByteMutFuzzer): diff --git a/src/certfuzz/fuzzers/drop.py b/src/certfuzz/fuzzers/drop.py index 9925a48..b345254 100644 --- a/src/certfuzz/fuzzers/drop.py +++ b/src/certfuzz/fuzzers/drop.py @@ -1,9 +1,11 @@ """ """ -from . import Fuzzer -from .errors import FuzzerExhaustedError import logging +from certfuzz.fuzzers import Fuzzer +from certfuzz.fuzzers.errors import FuzzerExhaustedError + + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/fuzzers/errors.py b/src/certfuzz/fuzzers/errors.py index 9c8409e..0dd42b3 100644 --- a/src/certfuzz/fuzzers/errors.py +++ b/src/certfuzz/fuzzers/errors.py @@ -3,9 +3,10 @@ @organization: cert.org ''' +from certfuzz.errors import CERTFuzzError -class FuzzerError(Exception): +class FuzzerError(CERTFuzzError): pass diff --git a/src/certfuzz/fuzzers/fuzzer_base.py b/src/certfuzz/fuzzers/fuzzer_base.py index d424578..71815e4 100644 --- a/src/certfuzz/fuzzers/fuzzer_base.py +++ b/src/certfuzz/fuzzers/fuzzer_base.py @@ -3,15 +3,15 @@ @organization: cert.org ''' -import os -import logging import StringIO -import zipfile import collections +import logging +import os +import zipfile + +from certfuzz.fuzztools.filetools import find_or_create_dir, write_file +from certfuzz.helpers import log_object -from ..fuzztools.filetools import write_file -from ..fuzztools.filetools import find_or_create_dir -from ..helpers import log_object MAXDEPTH = 3 SLEEPTIMER = 0.5 diff --git a/src/certfuzz/fuzzers/insert.py b/src/certfuzz/fuzzers/insert.py index ece821e..ecfb392 100644 --- a/src/certfuzz/fuzzers/insert.py +++ b/src/certfuzz/fuzzers/insert.py @@ -1,10 +1,12 @@ """ """ -from . import Fuzzer -from .errors import FuzzerExhaustedError import logging from random import getrandbits +from certfuzz.fuzzers import Fuzzer +from certfuzz.fuzzers.errors import FuzzerExhaustedError + + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/fuzzers/nullmut.py b/src/certfuzz/fuzzers/nullmut.py index 863639e..fe12bcd 100644 --- a/src/certfuzz/fuzzers/nullmut.py +++ b/src/certfuzz/fuzzers/nullmut.py @@ -1,8 +1,9 @@ import logging -logger = logging.getLogger(__name__) +from certfuzz.fuzzers.bytemut import ByteMutFuzzer + -from .bytemut import ByteMutFuzzer +logger = logging.getLogger(__name__) class NullMutFuzzer(ByteMutFuzzer): diff --git a/src/certfuzz/fuzzers/swap.py b/src/certfuzz/fuzzers/swap.py index 879318d..eacca7d 100644 --- a/src/certfuzz/fuzzers/swap.py +++ b/src/certfuzz/fuzzers/swap.py @@ -2,8 +2,10 @@ bytes. """ import logging -from . import MinimizableFuzzer -from .errors import FuzzerExhaustedError + +from certfuzz.fuzzers import MinimizableFuzzer +from certfuzz.fuzzers.errors import FuzzerExhaustedError + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/fuzzers/truncate.py b/src/certfuzz/fuzzers/truncate.py index c67ed44..330b938 100644 --- a/src/certfuzz/fuzzers/truncate.py +++ b/src/certfuzz/fuzzers/truncate.py @@ -3,10 +3,12 @@ @organization: cert.org ''' -from . import Fuzzer -from .errors import FuzzerExhaustedError import logging +from certfuzz.fuzzers import Fuzzer +from certfuzz.fuzzers.errors import FuzzerExhaustedError + + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/fuzzers/verify.py b/src/certfuzz/fuzzers/verify.py index c575a13..7267289 100644 --- a/src/certfuzz/fuzzers/verify.py +++ b/src/certfuzz/fuzzers/verify.py @@ -3,8 +3,9 @@ @organization: cert.org ''' -from .copy import CopyFuzzer -from . import FuzzerExhaustedError +from certfuzz.fuzzers.copy import CopyFuzzer +from certfuzz.fuzzers.errors import FuzzerExhaustedError + _files_seen = set() diff --git a/src/certfuzz/fuzzers/wave.py b/src/certfuzz/fuzzers/wave.py index ad2e79e..2600ce3 100644 --- a/src/certfuzz/fuzzers/wave.py +++ b/src/certfuzz/fuzzers/wave.py @@ -1,10 +1,12 @@ """This fuzzer module iterates through an input file, trying every byte value as it goes. E.g. try 0-255 for the first byte, 0-255 for the second byte, etc. """ -from . import MinimizableFuzzer -from .errors import FuzzerExhaustedError import logging +from certfuzz.fuzzers import MinimizableFuzzer +from certfuzz.fuzzers.errors import FuzzerExhaustedError + + logger = logging.getLogger(__name__) From 64f5c0d03c13f3f401e140823b90a0990b42e828 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 11:28:32 -0400 Subject: [PATCH 0165/1169] pep8 cleanup --- .../fuzztools/command_line_callable.py | 1 + src/certfuzz/fuzztools/filetools.py | 40 +++++++++++++++++-- src/certfuzz/fuzztools/hamming.py | 8 +++- src/certfuzz/fuzztools/performance.py | 1 + src/certfuzz/fuzztools/probability.py | 9 ++++- src/certfuzz/fuzztools/range.py | 1 + src/certfuzz/fuzztools/subprocess_helper.py | 14 +++++-- src/certfuzz/fuzztools/text.py | 4 ++ src/certfuzz/fuzztools/vectors.py | 5 +++ src/certfuzz/fuzztools/watchdog.py | 1 + src/certfuzz/fuzztools/zzuf.py | 1 + src/certfuzz/fuzztools/zzuflog.py | 1 + 12 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/fuzztools/command_line_callable.py b/src/certfuzz/fuzztools/command_line_callable.py index c43f0b0..322f4cb 100644 --- a/src/certfuzz/fuzztools/command_line_callable.py +++ b/src/certfuzz/fuzztools/command_line_callable.py @@ -8,6 +8,7 @@ logger = logging.getLogger(__name__) + class CommandLineCallable(object): ''' Class intended mainly for binding a python API to an underlying command diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 0637946..b844113 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -24,6 +24,7 @@ logger = logging.getLogger(__name__) + def exponential_backoff(F): def wrapper(*args, **kwargs): naptime = 0.0 @@ -61,6 +62,7 @@ def mkdir_p(path): find_or_create_dir = mkdir_p + # file system helpers def make_directories(*paths): ''' @@ -71,9 +73,11 @@ def make_directories(*paths): if not os.path.exists(d): mkdir_p(d) + def delete_files(*files): delete_files2(files) + @exponential_backoff def delete_files2(files=[]): ''' @@ -90,6 +94,7 @@ def delete_files2(files=[]): for x in os.listdir(d): logger.debug('... %s', x) + def best_effort_copy(src, dst): copied = False try: @@ -99,6 +104,7 @@ def best_effort_copy(src, dst): logger.warning('Unable to copy file: %s', e) return copied + def best_effort_delete(target): deleted = False try: @@ -108,6 +114,7 @@ def best_effort_delete(target): logger.warning('Unable to remove file: %s', e) return deleted + def best_effort_move(src, dst): ''' Tries to move src to dst. If an OSError is thrown, it will try to copy @@ -130,17 +137,21 @@ def best_effort_move(src, dst): deleted = best_effort_delete(src) return copied, deleted + def move_files(dst, *files): ''' Move each file in files to dst. @param dst: file path or dir @param files: one or more source paths ''' - if not os.path.isdir(dst): return + if not os.path.isdir(dst): + return + for src in files: if os.path.exists(src): move_file(src, dst) + def move_file(src, *targets): ''' Move src to each dst in targets. @@ -150,28 +161,34 @@ def move_file(src, *targets): ''' move_file2(src=src, targets=targets) + @exponential_backoff def move_file2(src=None, targets=[]): - if not os.path.exists(src): return + if not os.path.exists(src): + return for dst in targets: shutil.move(src, dst) + def copy_files(dst, *files): ''' Copies a list of to a target ''' # short-circuit unless target dir exists - if not os.path.isdir(dst): return + if not os.path.isdir(dst): + return for src in files: if os.path.exists(src): copy_file(src, dst) + def copy_file(src, *targets): copy_file2(src=src, targets=targets) + @exponential_backoff def copy_file2(src=None, targets=[]): ''' @@ -179,15 +196,18 @@ def copy_file2(src=None, targets=[]): @return: none ''' # short-circuit unless file exists - if not os.path.exists(src): return + if not os.path.exists(src): + return for dst in targets: shutil.copy(src, dst) + def mkdtemp(base_dir=None): path = tempfile.mkdtemp(prefix='BFF-', dir=base_dir) return path + def write_oneline_to_file(line, dst, mode): ''' Opens with mode (hint: 'w' or 'a'), writes to then closes @@ -196,6 +216,7 @@ def write_oneline_to_file(line, dst, mode): with open(dst, mode) as f: f.write("%s\n" % line) + def get_file_md5(infile): h = hashlib.md5() @@ -204,15 +225,18 @@ def get_file_md5(infile): return h.hexdigest() + @exponential_backoff def write_file2(data=None, dst=None): logger.debug('Write to %s', dst) with open(dst, 'wb') as output_file: output_file.write(data) + def write_file(data, dst): write_file2(data=data, dst=dst) + def get_newpath(oldpath, str_to_insert): ''' Inserts a string before the extention of a path. @@ -224,6 +248,7 @@ def get_newpath(oldpath, str_to_insert): newpath = ''.join([root, str_to_insert, ext]) return newpath + def all_files_nonzero_length(root, patterns='*', single_level=False, yield_folders=False): ''' Wrapper around all_files to only return files of nonzero length @@ -236,6 +261,7 @@ def all_files_nonzero_length(root, patterns='*', single_level=False, yield_folde if os.path.getsize(filepath): yield filepath + def delete_files_or_dirs(dirlist, print_via_log=True): skipped_items = [] for item_path in dirlist: @@ -263,6 +289,7 @@ def delete_files_or_dirs(dirlist, print_via_log=True): skipped_items.append((item_path, 'Not a file or dir')) return skipped_items + def delete_contents_of(dirs, print_via_log=True): dirlist = [] skipped_items = [] @@ -278,6 +305,7 @@ def delete_contents_of(dirs, print_via_log=True): return skipped_items + def check_zip_fh(file_like_content): # Make sure that it's not an embedded zip (e.g. a DOC file from Office 2007) file_like_content.seek(0) @@ -289,18 +317,22 @@ def check_zip_fh(file_like_content): else: return zipfile.is_zipfile(file_like_content) + def check_zip_content(content): file_like_content = StringIO.StringIO(content) return check_zip_fh(file_like_content) + def check_zip_file(filepath): with open(filepath, 'rb') as filehandle: return check_zip_fh(filehandle) + def make_writable(filename): mode = os.stat(filename).st_mode os.chmod(filename, mode | stat.S_IWRITE) + # Adapted from Python Cookbook 2nd Ed. p.88 def all_files(root, patterns='*', single_level=False, yield_folders=False): # Expand patterns from semicolon-separated string to list diff --git a/src/certfuzz/fuzztools/hamming.py b/src/certfuzz/fuzztools/hamming.py index e2a06e9..f178091 100644 --- a/src/certfuzz/fuzztools/hamming.py +++ b/src/certfuzz/fuzztools/hamming.py @@ -9,6 +9,7 @@ import itertools import os + def vector_compare(v1, v2): ''' Given two sparse vectors (lists of indices whose value is 1), return the distance between them @@ -29,6 +30,7 @@ def vector_compare(v1, v2): return distance + def bytemap(s1, s2): ''' Given two strings of equal length, return the indices of bytes that differ. @@ -40,6 +42,7 @@ def bytemap(s1, s2): delta.append(idx) return delta + def bytewise_hd(s1, s2): ''' Compute the byte-wise Hamming Distance between two strings. Returns @@ -48,6 +51,7 @@ def bytewise_hd(s1, s2): assert len(s1) == len(s2) return sum(ch1 != ch2 for ch1, ch2 in itertools.izip(s1, s2)) + def bytewise_hamming_distance(file1, file2): ''' Given the names of two files, compute the byte-wise Hamming Distance @@ -56,16 +60,17 @@ def bytewise_hamming_distance(file1, file2): ''' return _file_compare(bytewise_hd, file1, file2) + def _file_compare(distance_function, file1, file2): assert os.path.getsize(file1) == os.path.getsize(file2) - distance = 0 with open(file1, 'rb') as f1: with open(file2, 'rb') as f2: # find the hamming distance for each byte distance = distance_function(f1.read(), f2.read()) return distance + def bitwise_hd(x, y): ''' Given two strings x and y, find the bitwise hamming distance @@ -85,6 +90,7 @@ def bitwise_hd(x, y): hd += 1 return hd + def bitwise_hamming_distance(file1, file2): ''' Given the names of two files, compute the bit-wise Hamming Distance diff --git a/src/certfuzz/fuzztools/performance.py b/src/certfuzz/fuzztools/performance.py index e9ae54d..16359cb 100644 --- a/src/certfuzz/fuzztools/performance.py +++ b/src/certfuzz/fuzztools/performance.py @@ -7,6 +7,7 @@ import itertools import numpy + class TimeStamper(object): ''' classdocs diff --git a/src/certfuzz/fuzztools/probability.py b/src/certfuzz/fuzztools/probability.py index 031baf2..9707ac4 100644 --- a/src/certfuzz/fuzztools/probability.py +++ b/src/certfuzz/fuzztools/probability.py @@ -49,12 +49,14 @@ def weighted_choice(probabilities): if x < cumulative_probability: return k + def lnfactorial(x): ''' Returns ln(x!) as a float. ''' return math.lgamma(x + 1) + def shot_size(N, p): ''' Given the size of (number of elements in) a target space and the probability @@ -67,6 +69,7 @@ def shot_size(N, p): ''' return int(math.floor(p * N)) + def misses_until_quit(c, p): ''' Returns the number of times you can miss before concluding with confidence c that @@ -79,6 +82,7 @@ def misses_until_quit(c, p): return int(math.ceil(x)) + def p_max_hit(x, c=0.95): ''' Return the maximum probability of getting a hit that is consistent @@ -87,8 +91,9 @@ def p_max_hit(x, c=0.95): @param x: The number of tries @param c: The desired confidence level ''' - p_max_hit = 1.0 - pow((1.0 - c), (1.0 / x)) - return p_max_hit + p = 1.0 - pow((1.0 - c), (1.0 / x)) + return p + class FuzzRun: ''' diff --git a/src/certfuzz/fuzztools/range.py b/src/certfuzz/fuzztools/range.py index 325a234..0a2a77a 100644 --- a/src/certfuzz/fuzztools/range.py +++ b/src/certfuzz/fuzztools/range.py @@ -5,6 +5,7 @@ ''' from ..scoring.scorable_thing import ScorableThing + class Range(ScorableThing): def __init__(self, low, high): diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index c852890..66f52c3 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -16,23 +16,26 @@ def on_windows(): return (platform.system() == "Windows") + def on_osx(): return (platform.system() == "Darwin") + def on_linux(): return (platform.system() == "Linux") if on_windows(): import ctypes - + if on_linux(): #gdb can cause SIGTTOU to get sent to python. We don't want python to stop. signal.signal(signal.SIGTTOU, signal.SIG_IGN) + def run_with_timer(args, timeout, killprocname, use_shell=False, **options): ''' - Runs . If it takes longer than we'll - kill as well as hunt down any processes named + Runs . If it takes longer than we'll + kill as well as hunt down any processes named . If you want to redirect stdout and/or stderr, use stdout= or stderr= (or both). @return: none @@ -78,13 +81,15 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): [fh.close() for fh in (output, errors)] return p + def run_without_timer(command): ''' Runs command, returns return code ''' return subprocess.call(command, shell=True) -def _kill(p, returncode, killprocname): #@UnusedVariable + +def _kill(p, returncode, killprocname): #@UnusedVariable if (on_windows()): """_kill function for Win32""" kernel32 = ctypes.windll.kernel32 @@ -97,6 +102,7 @@ def _kill(p, returncode, killprocname): #@UnusedVariable killall(killprocname, signal.SIGKILL) return (0 != ret) + def killall(processname, killsignal): assert (processname != ''), "Cannot kill a blank process name" if (on_osx()): diff --git a/src/certfuzz/fuzztools/text.py b/src/certfuzz/fuzztools/text.py index aa30083..bf2f7a1 100644 --- a/src/certfuzz/fuzztools/text.py +++ b/src/certfuzz/fuzztools/text.py @@ -25,6 +25,7 @@ def _pattern(iterables, length): pattern = ''.join(pattern_parts) return pattern[:length] + def metasploit_pattern_orig(length): ''' Returns the standard metasploit non-repeating pattern "Aa0Aa1...Zz9" as @@ -42,6 +43,7 @@ def metasploit_pattern_orig(length): return _pattern(iterables, length) + def metasploit_pattern_extended(length): ''' Returns the 'extended' metasploit non-repeating pattern @@ -63,6 +65,7 @@ def metasploit_pattern_extended(length): def metasploit_pattern(length): return metasploit_pattern_extended(length) + def _enumerate_string(content, occurences): ''' Replace each position in content given in occurences with @@ -85,6 +88,7 @@ def _enumerate_string(content, occurences): byte_buffer[p] = substr[offset] return byte_buffer + def enumerate_string(path=None, str_to_enum=None): ''' Read file from path.ext and enumerate each occurrence of str_to_enum with diff --git a/src/certfuzz/fuzztools/vectors.py b/src/certfuzz/fuzztools/vectors.py index 694acd9..13fa066 100644 --- a/src/certfuzz/fuzztools/vectors.py +++ b/src/certfuzz/fuzztools/vectors.py @@ -29,9 +29,11 @@ def compare(d1, d2): return similarity(v1, v2) + def similarity(v1, v2): return cos(v1, v2) + def cos(v1, v2): assert len(v1) == len(v2), 'Cannot compare vectors of unequal length' dotproduct = float(dot(v1, v2)) @@ -43,6 +45,7 @@ def cos(v1, v2): return sim + def dot(v1, v2): ''' Calculate the sum of the products of each term in v1 and v2 @@ -56,12 +59,14 @@ def dot(v1, v2): total = sum(products) return total + def norm(v): squares = [float(x) * float(x) for x in v] total = sum(squares) sqrt = math.sqrt(total) return sqrt + class Vector(object): def __init__(self, v): self.vector = v diff --git a/src/certfuzz/fuzztools/watchdog.py b/src/certfuzz/fuzztools/watchdog.py index 8ba520b..a58eb46 100644 --- a/src/certfuzz/fuzztools/watchdog.py +++ b/src/certfuzz/fuzztools/watchdog.py @@ -15,6 +15,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) + class WatchDog: def __init__(self, file, timeout): self.file = file diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index 457a96f..fd864f9 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -32,6 +32,7 @@ def _set_cmdline(self): def generate(self): subp.run_without_timer(self.cmdline) + class Zzuf: def __init__(self, dir, s1, s2, cmd, seedfile, file, copymode, ratiomin, ratiomax, timeout, quiet=True): ''' diff --git a/src/certfuzz/fuzztools/zzuflog.py b/src/certfuzz/fuzztools/zzuflog.py index c878cea..16fa710 100644 --- a/src/certfuzz/fuzztools/zzuflog.py +++ b/src/certfuzz/fuzztools/zzuflog.py @@ -14,6 +14,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) + class ZzufLog: def __init__(self, infile, outfile): ''' From 15ff55ffb0e0a282b8e73c3372b6135d1d2c5360 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 11:28:57 -0400 Subject: [PATCH 0166/1169] remove dead code --- src/certfuzz/fuzztools/hostinfo.py | 1 - src/certfuzz/fuzztools/probability.py | 19 +------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/certfuzz/fuzztools/hostinfo.py b/src/certfuzz/fuzztools/hostinfo.py index 2839e9b..2a5f707 100644 --- a/src/certfuzz/fuzztools/hostinfo.py +++ b/src/certfuzz/fuzztools/hostinfo.py @@ -4,7 +4,6 @@ @organization: cert.org ''' import platform -#from .. import debuggers system = platform.system() diff --git a/src/certfuzz/fuzztools/probability.py b/src/certfuzz/fuzztools/probability.py index 9707ac4..504458d 100644 --- a/src/certfuzz/fuzztools/probability.py +++ b/src/certfuzz/fuzztools/probability.py @@ -8,8 +8,7 @@ ''' import random import math -# from scipy import stats -# from scipy.special import gammaln + def beta_estimate(m, N, a_prior=1.0, b_prior=1.0): numerator = alpha = m + a_prior @@ -19,22 +18,6 @@ def beta_estimate(m, N, a_prior=1.0, b_prior=1.0): p_success = float(numerator) / float(denominator) return (alpha, beta, p_success) -# def ucb(tries, hits=0.0, confidence=0.95): -# ''' -# Given a number of attempts, successes, and desired confidence, -# return the exact confidence interval (min,mean,max) -# @param tries: the number of trials -# @param hits: the number of successes (default=0) -# @param confidence: the desired confidence level (default=0.95) -# ''' -# # see http://www.math.mcmaster.ca/peter/s743/poissonalpha.html -# mean = float(hits) / float(tries) -# chisq = 1.0 - confidence -# df = 2 * (hits + 1) -# delta = (stats.chisqprob(chisq, df) / 2) / tries -# low = mean - delta -# high = mean + delta -# return(low, mean, high) def weighted_choice(probabilities): ''' From 4286101988c3c965580aaa18b61ba0d72ab40c78 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 11:29:07 -0400 Subject: [PATCH 0167/1169] replace relative imports with absolute imports --- src/certfuzz/fuzztools/distance_matrix.py | 2 +- src/certfuzz/fuzztools/errors.py | 2 +- src/certfuzz/fuzztools/performance.py | 2 +- src/certfuzz/fuzztools/ppid_observer.py | 3 ++- src/certfuzz/fuzztools/process_killer.py | 3 ++- src/certfuzz/fuzztools/rangefinder.py | 9 +++++---- src/certfuzz/fuzztools/seedrange.py | 1 + src/certfuzz/fuzztools/similarity_matrix.py | 13 +++++++------ src/certfuzz/fuzztools/subprocess_helper.py | 9 +++++---- src/certfuzz/fuzztools/text.py | 4 +++- src/certfuzz/fuzztools/vectors.py | 1 + src/certfuzz/fuzztools/watchdog.py | 10 ++++++---- src/certfuzz/fuzztools/zzuf.py | 5 ++++- src/certfuzz/fuzztools/zzuflog.py | 5 +++-- 14 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/certfuzz/fuzztools/distance_matrix.py b/src/certfuzz/fuzztools/distance_matrix.py index c0de2bf..19e6b6a 100644 --- a/src/certfuzz/fuzztools/distance_matrix.py +++ b/src/certfuzz/fuzztools/distance_matrix.py @@ -8,7 +8,7 @@ import numpy import hcluster import logging -from .errors import DistanceMatrixError +from certfuzz.fuzztools.errors import DistanceMatrixError logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) diff --git a/src/certfuzz/fuzztools/errors.py b/src/certfuzz/fuzztools/errors.py index 2e9d132..47f0c05 100644 --- a/src/certfuzz/fuzztools/errors.py +++ b/src/certfuzz/fuzztools/errors.py @@ -3,7 +3,7 @@ @author: adh ''' -from ..errors import CERTFuzzError +from certfuzz.errors import CERTFuzzError class FuzztoolError(CERTFuzzError): diff --git a/src/certfuzz/fuzztools/performance.py b/src/certfuzz/fuzztools/performance.py index 16359cb..6d93325 100644 --- a/src/certfuzz/fuzztools/performance.py +++ b/src/certfuzz/fuzztools/performance.py @@ -50,4 +50,4 @@ def delta_stats(self): @param key: the key to collect stats on ''' deltas = self.deltas() - return (numpy.mean(deltas), numpy.std(deltas)) #@UndefinedVariable + return (numpy.mean(deltas), numpy.std(deltas)) #@UndefinedVariable diff --git a/src/certfuzz/fuzztools/ppid_observer.py b/src/certfuzz/fuzztools/ppid_observer.py index ee23736..a518528 100644 --- a/src/certfuzz/fuzztools/ppid_observer.py +++ b/src/certfuzz/fuzztools/ppid_observer.py @@ -3,8 +3,9 @@ @author: adh ''' -import os import logging +import os + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/fuzztools/process_killer.py b/src/certfuzz/fuzztools/process_killer.py index 6f4eddd..319a790 100644 --- a/src/certfuzz/fuzztools/process_killer.py +++ b/src/certfuzz/fuzztools/process_killer.py @@ -3,12 +3,13 @@ @organization: cert.org ''' -from . import subprocess_helper as subp +from certfuzz.fuzztools import subprocess_helper as subp import logging logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) + class ProcessKiller: def __init__(self, script, killprocname, killproctimeout): self.script = script diff --git a/src/certfuzz/fuzztools/rangefinder.py b/src/certfuzz/fuzztools/rangefinder.py index 28a631c..0c8624c 100644 --- a/src/certfuzz/fuzztools/rangefinder.py +++ b/src/certfuzz/fuzztools/rangefinder.py @@ -3,12 +3,13 @@ @organization: cert.org ''' -import math import logging +import math + +from certfuzz.fuzztools.errors import RangeFinderError +from certfuzz.fuzztools.range import Range +from certfuzz.scoring.scorable_set import ScorableSet2 -from ..scoring.scorable_set import ScorableSet2 -from .range import Range -from .errors import RangeFinderError range_scale_factor = (math.sqrt(5) + 1.0) / 2.0 diff --git a/src/certfuzz/fuzztools/seedrange.py b/src/certfuzz/fuzztools/seedrange.py index a174393..10bbd33 100644 --- a/src/certfuzz/fuzztools/seedrange.py +++ b/src/certfuzz/fuzztools/seedrange.py @@ -7,6 +7,7 @@ SEED_INTERVAL = 500 MAX_SEED = 1e10 + class SeedRange(): def __init__(self, start_seed=START_SEED, interval=SEED_INTERVAL, max_seed=MAX_SEED): self.initial_seed = start_seed diff --git a/src/certfuzz/fuzztools/similarity_matrix.py b/src/certfuzz/fuzztools/similarity_matrix.py index 7e094b9..419d9e2 100644 --- a/src/certfuzz/fuzztools/similarity_matrix.py +++ b/src/certfuzz/fuzztools/similarity_matrix.py @@ -3,16 +3,17 @@ @organization: cert.org ''' +import collections import logging import math -import collections -import sys import operator +import sys + +from certfuzz.analyzers.callgrind.annotation_file import AnnotationFile +from certfuzz.fuzztools.errors import SimilarityMatrixError +from certfuzz.fuzztools.filetools import all_files_nonzero_length +from certfuzz.fuzztools.vectors import compare -from .filetools import all_files_nonzero_length -from .vectors import compare -from ..analyzers.callgrind.annotation_file import AnnotationFile -from .errors import SimilarityMatrixError logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 66f52c3..82bae3c 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -5,13 +5,14 @@ @organization: cert.org ''' -import platform -import subprocess -from threading import Timer -import sys import os +import platform import signal import string +import subprocess +import sys +from threading import Timer + def on_windows(): return (platform.system() == "Windows") diff --git a/src/certfuzz/fuzztools/text.py b/src/certfuzz/fuzztools/text.py index bf2f7a1..ebe1c13 100644 --- a/src/certfuzz/fuzztools/text.py +++ b/src/certfuzz/fuzztools/text.py @@ -5,11 +5,13 @@ ''' # pylint complains about importing string and # pylint: disable=w0142,w0402 -import string import itertools import re +import string + from certfuzz.fuzztools.filetools import get_newpath + def _pattern(iterables, length): pattern_parts = [] l = 0 diff --git a/src/certfuzz/fuzztools/vectors.py b/src/certfuzz/fuzztools/vectors.py index 13fa066..ef33dd4 100644 --- a/src/certfuzz/fuzztools/vectors.py +++ b/src/certfuzz/fuzztools/vectors.py @@ -7,6 +7,7 @@ # from numpy.linalg import norm import math + def compare(d1, d2): ''' Turn two dicts into vectors, then calculate their similarity diff --git a/src/certfuzz/fuzztools/watchdog.py b/src/certfuzz/fuzztools/watchdog.py index a58eb46..eefe1ad 100644 --- a/src/certfuzz/fuzztools/watchdog.py +++ b/src/certfuzz/fuzztools/watchdog.py @@ -1,13 +1,15 @@ -import platform -import logging - ''' Created on Oct 25, 2010 @organization: cert.org ''' -from . import subprocess_helper as subp +import logging +import platform + +from certfuzz.fuzztools import subprocess_helper as subp + + system = platform.system() supported_systems = ['Linux'] diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index fd864f9..cf9ce28 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -6,11 +6,14 @@ @organization: cert.org ''' import logging -from . import subprocess_helper as subp + +from certfuzz.fuzztools import subprocess_helper as subp + logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) + class ZzufTestCase: def __init__(self, seedfile, seed, range, outfile): ''' diff --git a/src/certfuzz/fuzztools/zzuflog.py b/src/certfuzz/fuzztools/zzuflog.py index 16fa710..55cffaf 100644 --- a/src/certfuzz/fuzztools/zzuflog.py +++ b/src/certfuzz/fuzztools/zzuflog.py @@ -5,11 +5,12 @@ @organization: cert.org ''' -import re +import logging import os +import re + import filetools -import logging logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) From 53c2a834e73db33b01f510dbb694d485301a2641 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 16:31:03 -0400 Subject: [PATCH 0168/1169] pep8 cleanup --- src/certfuzz/helpers/misc.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certfuzz/helpers/misc.py b/src/certfuzz/helpers/misc.py index 37781af..6bdbb05 100644 --- a/src/certfuzz/helpers/misc.py +++ b/src/certfuzz/helpers/misc.py @@ -11,21 +11,26 @@ my_os = platform.system() + def quoted(string_to_wrap): return '"%s"' % string_to_wrap + def print_dict(d): pprint(d) + def check_os_compatibility(expected_os, module_name=__name__): if not my_os == expected_os: template = 'Module %s is incompatible with %s (%s expected)' raise ImportError(template % (module_name, my_os, expected_os)) + def random_str(length=1): chars = string.ascii_letters + string.digits return ''.join([random.choice(chars) for dummy in xrange(length)]) + def bitswap(input_byte): bits = [1, 2, 4, 8, 16, 32, 64, 128] backwards = list(bits) @@ -46,6 +51,7 @@ def bitswap(input_byte): output_byte |= y return output_byte + def log_object(obj, logger, level=logging.DEBUG): for l in pformat(obj.__dict__).splitlines(): logger.log(level, '%s', l) From 329a2d218b8b3a681288418f3d3ea5f06ef3468f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Mar 2014 16:31:18 -0400 Subject: [PATCH 0169/1169] replace relative imports with absolute imports --- src/certfuzz/helpers/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/helpers/__init__.py b/src/certfuzz/helpers/__init__.py index e985567..68a6612 100644 --- a/src/certfuzz/helpers/__init__.py +++ b/src/certfuzz/helpers/__init__.py @@ -1,6 +1,6 @@ -from .misc import quoted -from .misc import print_dict -from .misc import check_os_compatibility -from .misc import random_str -from .misc import bitswap -from .misc import log_object +from certfuzz.helpers.misc import quoted +from certfuzz.helpers.misc import print_dict +from certfuzz.helpers.misc import check_os_compatibility +from certfuzz.helpers.misc import random_str +from certfuzz.helpers.misc import bitswap +from certfuzz.helpers.misc import log_object From b6b0683709a41bfcd57a5bdc9ede9fe029f3c280 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Mar 2014 16:27:02 -0400 Subject: [PATCH 0170/1169] remove obsolete test module --- .../test/android/worker/test_defaults.py | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/certfuzz/test/android/worker/test_defaults.py diff --git a/src/certfuzz/test/android/worker/test_defaults.py b/src/certfuzz/test/android/worker/test_defaults.py deleted file mode 100644 index b6bc051..0000000 --- a/src/certfuzz/test/android/worker/test_defaults.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.android.worker import defaults - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From 320d28b14d557f12039fedb87a8c933c9f992cc9 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 14 Mar 2014 15:07:09 -0400 Subject: [PATCH 0171/1169] Fix for BFF-518 --- src/certfuzz/tools/windows/drillresults.py | 29 ++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 48ff75b..5a3b777 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -274,6 +274,14 @@ def ratchetscore(crasher, score): scoredcrashes[crasher] = score +def is_number(s): + try: + float(s) + return True + except ValueError: + return False + + def fixefaoffset(instructionline, faultaddr): ''' Adjust faulting address for instructions that use offsets @@ -306,14 +314,15 @@ def fixefaoffset(instructionline, faultaddr): offset = splitaddress[1] offset = offset.replace('h', '') offset = offset.replace(']', '') - if '0x' not in offset: - offset = '0x' + offset - if int(offset, 16) > int(faultaddr, 16): - # TODO: fix up negative numbers - return faultaddr - # Subtract offset to get actual interesting pattern - faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = formataddr(faultaddr.replace('L', '')) + if is_number(offset): + if '0x' not in offset: + offset = '0x' + offset + if int(offset, 16) > int(faultaddr, 16): + # TODO: fix up negative numbers + return faultaddr + # Subtract offset to get actual interesting pattern + faultaddr = hex(eval(faultaddr) - eval(offset)) + faultaddr = formataddr(faultaddr.replace('L', '')) return faultaddr @@ -530,10 +539,10 @@ def score_reports(): if module == 'unloaded' and not ignorejit: ratchetscore(crasher, 20) elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): - #likely heap corruption. Exploitable, but difficult + # likely heap corruption. Exploitable, but difficult ratchetscore(crasher, 45) elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: - #non-continued potential stack buffer overflow + # non-continued potential stack buffer overflow ratchetscore(crasher, 40) elif results[crasher]['exceptions'][exception]['EIF']: # The faulting address pattern is in the fuzzed file From 332e1504b1c24572b79887c57aa9c78befea9e8d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 17 Mar 2014 13:02:54 -0400 Subject: [PATCH 0172/1169] if path is a link, then exc.errno == errno.EEXIST but not os.path.isdir(path) --- src/certfuzz/fuzztools/filetools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index b844113..1c80997 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -54,7 +54,7 @@ def mkdir_p(path): os.makedirs(path) except OSError as exc: # if the dir already exists, just move along - if exc.errno == errno.EEXIST and os.path.isdir(path): + if exc.errno == errno.EEXIST: return True else: raise From d4ad066eaa8a0087676e00f379024fd56c2ab9ab Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 7 Jan 2014 12:24:57 -0500 Subject: [PATCH 0173/1169] revamp imports (+1 squashed commit) Squashed commits: [2ad8578] revamp imports --- src/certfuzz/scoring/multiarmed_bandit/__init__.py | 5 ----- src/certfuzz/scoring/multiarmed_bandit/arms/__init__.py | 2 -- src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py | 3 ++- src/certfuzz/scoring/multiarmed_bandit/arms/errors.py | 2 +- src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py | 2 +- 5 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/__init__.py b/src/certfuzz/scoring/multiarmed_bandit/__init__.py index 841bd35..e69de29 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/__init__.py +++ b/src/certfuzz/scoring/multiarmed_bandit/__init__.py @@ -1,5 +0,0 @@ -from .errors import MultiArmedBanditError -from .multiarmed_bandit_base import MultiArmedBanditBase -from .multiarmed_bandit_base import RoundRobinMultiArmedBandit -from .bayesian_bandit import BayesianMultiArmedBandit -from .random_bandit import RandomMultiArmedBandit diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/__init__.py b/src/certfuzz/scoring/multiarmed_bandit/arms/__init__.py index 555a7c6..e69de29 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/__init__.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/__init__.py @@ -1,2 +0,0 @@ -from .base import BanditArmBase -from .bayes_laplace import BanditArmBayesLaplace diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py b/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py index 09b0c44..666c670 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py @@ -3,7 +3,8 @@ @organization: cert.org ''' -from . import BanditArmBase +from .base import BanditArmBase + class BanditArmBayesLaplace(BanditArmBase): ''' diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py b/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py index a2ea7a1..e21482e 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from .. import MultiArmedBanditError +from ..errors import MultiArmedBanditError class BanditArmError(MultiArmedBanditError): pass diff --git a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py index e8e0744..4b48d46 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py @@ -4,7 +4,7 @@ @organization: cert.org ''' from certfuzz.fuzztools.probability import weighted_choice -from . import MultiArmedBanditBase +from .multiarmed_bandit_base import MultiArmedBanditBase from .arms.bayes_laplace import BanditArmBayesLaplace class BayesianMultiArmedBandit(MultiArmedBanditBase): From cc861d9f991aba586d181b9f8600ba11d8512056 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 7 Jan 2014 12:27:26 -0500 Subject: [PATCH 0174/1169] remove empty tests --- .../multiarmed_bandit/arms/test_arms_pkg.py | 21 ------------------- .../test_multiarmed_bandit_pkg.py | 21 ------------------- 2 files changed, 42 deletions(-) delete mode 100644 src/certfuzz/test/scoring/multiarmed_bandit/arms/test_arms_pkg.py delete mode 100644 src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_pkg.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_arms_pkg.py b/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_arms_pkg.py deleted file mode 100644 index 952e484..0000000 --- a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_arms_pkg.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_pkg.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_pkg.py deleted file mode 100644 index 952e484..0000000 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_pkg.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From 9426eb783c93a3fc696e8312b88ae6495dead46a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 7 Jan 2014 14:17:58 -0500 Subject: [PATCH 0175/1169] pep8 cleanup (+2 squashed commits) Squashed commits: [43fd445] pep8 cleanup [0624111] pep8 cleanup --- src/certfuzz/scoring/multiarmed_bandit/arms/base.py | 1 + src/certfuzz/scoring/multiarmed_bandit/arms/errors.py | 1 + .../scoring/multiarmed_bandit/multiarmed_bandit_base.py | 2 ++ src/certfuzz/scoring/multiarmed_bandit/random_bandit.py | 1 + 4 files changed, 5 insertions(+) diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/base.py b/src/certfuzz/scoring/multiarmed_bandit/arms/base.py index f7d3179..b01f828 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/base.py @@ -5,6 +5,7 @@ ''' from .errors import BanditArmError + class BanditArmBase(object): ''' Base class for multi-armed bandit arms. The base class simply counts diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py b/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py index e21482e..c337fe5 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py @@ -5,5 +5,6 @@ ''' from ..errors import MultiArmedBanditError + class BanditArmError(MultiArmedBanditError): pass diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index a77b697..09c52e4 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -7,6 +7,7 @@ from .errors import MultiArmedBanditError from .arms.base import BanditArmBase + class MultiArmedBanditBase(object): ''' Implements a simple round robin iterator @@ -64,5 +65,6 @@ def __iter__(self): ''' return itertools.cycle(self.things.values()) + class RoundRobinMultiArmedBandit(MultiArmedBanditBase): pass diff --git a/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py index b0391a5..e61e7fc 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py @@ -6,6 +6,7 @@ from .multiarmed_bandit_base import MultiArmedBanditBase import random + class RandomMultiArmedBandit(MultiArmedBanditBase): ''' Returns a random thing from its collection. From 713ebe7bc07530313bb955f96c2dd9aeb015020d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 9 Jan 2014 16:10:11 -0500 Subject: [PATCH 0176/1169] add e-greedy bandit (+7 squashed commits) Squashed commits: [af7255d] when there are multiple maxes, choose randomly among them [2d7008c] raise error if epsilon out of range [3b450f8] fix typo in docstring [ee1c69b] allow epsilon to be specified at init time [f26806f] refactor next, adding tests [4ed0cbe] refactor tests [54377ca] add e-greedy bandit --- .../multiarmed_bandit/e_greedy_bandit.py | 62 +++++++++++ .../multiarmed_bandit/test_e_greedy_bandit.py | 102 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py create mode 100644 src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py diff --git a/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py new file mode 100644 index 0000000..0d68bb6 --- /dev/null +++ b/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py @@ -0,0 +1,62 @@ +''' +Created on Jan 7, 2014 + +@organization: cert.org +''' +from .multiarmed_bandit_base import MultiArmedBanditBase +from .arms.bayes_laplace import BanditArmBayesLaplace +from .errors import MultiArmedBanditError + +import random + + +class EpsilonGreedyMultiArmedBandit(MultiArmedBanditBase): + ''' + Returns a random thing from its collection based on the Epsilon-Greedy MultiArmed Bandit strategy + http://en.wikipedia.org/wiki/Multi-armed_bandit + ''' + arm_type = BanditArmBayesLaplace + + def __init__(self, epsilon=0.1): + ''' + :param epsilon: fraction of time spent exploring (vs. exploiting the best performer) + ''' + MultiArmedBanditBase.__init__(self) + if not 0.0 < epsilon < 1.0: + raise MultiArmedBanditError('epsilon must be between 0.0 and 1.0') + + self.e = epsilon + + def __iter__(self): + return self + + def _max_keys(self): + max_p = 0.0 + _maybe_max_k = [] + for key, arm in self.arms.iteritems(): + if arm.probability >= max_p: + max_p = arm.probability + _maybe_max_k.append((key, max_p)) + + # now we have a list of tuples, but the early ones might be less than max. + # since we went through them all on the way here though we know that max_p is + # the actual max, so all we need to do is test for that on each tuple + max_keys = [k for (k, p) in _maybe_max_k if p == max_p] + + return max_keys + + def _all_except(self, klist): + return [k for k in self.things.iterkeys() if not k in klist] + + def _next_key(self): + _max = self._max_keys() + if random.random() <= 1.0 - self.e: + return random.choice(_max) + else: + return random.choice(self._all_except(_max)) + + def next(self): + ''' + With probability 1-self.e, choose the best performer. Otherwise choose one of the others with equal probability + ''' + return self.things[self._next_key()] diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py new file mode 100644 index 0000000..aeee35f --- /dev/null +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py @@ -0,0 +1,102 @@ +''' +Created on Jan 8, 2014 + +@author: adh +''' +from certfuzz.scoring.multiarmed_bandit.e_greedy_bandit import EpsilonGreedyMultiArmedBandit +from certfuzz.scoring.multiarmed_bandit.errors import MultiArmedBanditError +import math +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + self.mab = EpsilonGreedyMultiArmedBandit() + self.keys = 'abcd' + self._add_things() + + def tearDown(self): + pass + + def _add_things(self): + for v, k in enumerate(self.keys): + self.mab.add(k, v) + + def _bump_p(self, k): + # bump p(k) up half the distance to the goal + p_orig = self.mab.arms[k].probability + + inv_p = 1.0 - p_orig + half_inv_p = 0.5 * inv_p + p_new = p_orig + half_inv_p + + self.mab.arms[k].probability = p_new + + def test_init_raises_err(self): + self.assertRaises(MultiArmedBanditError, EpsilonGreedyMultiArmedBandit, epsilon=1.00001) + self.assertRaises(MultiArmedBanditError, EpsilonGreedyMultiArmedBandit, epsilon=-0.0001) + + def test_max_key(self): + mab = self.mab + self._bump_p('a') + self.assertTrue('a' in mab._max_keys()) + + self._bump_p('b') + # now both a and be should be max + seen = set() + for _i in xrange(20): + ks = mab._max_keys() + for k in ks: + seen.add(k) + # There is a very small probability that this could fail + # but it's analogous to flipping heads 20x in a row. + # You can always bump up the iteration count in the above for loop + # if you want to reduce the probability of failure here + self.assertTrue('a' in seen) + self.assertTrue('b' in seen) + + def test_all_except(self): + mab = self.mab + self._bump_p('a') + + self.assertFalse('a' in mab._all_except('a')) + for k in self.keys[1:]: + self.assertTrue(k in mab._all_except('a')) + + def test__iter__(self): + # iterator should return itself + self.assertEqual(self.mab, self.mab.__iter__()) + + def test_next_key(self): + mab = self.mab + mab.e = 0.1 + self._bump_p('a') + + counters = {} + for _k in self.keys: + counters[_k] = 0 + + N = 10000 + for _i in xrange(N): + k = mab._next_key() + counters[k] += 1 + + # expect 9k 'a's if N=10k + self.assertAlmostEqual(1 - mab.e, counters['a'] / float(N), 1) + other_counts = N - counters['a'] + other_keys = mab._all_except('a') + n_others = len(other_keys) + for x in other_keys: + actual = counters[x] + expected = float(other_counts) / n_others + ratio = actual / expected + err = math.fabs(ratio - 1.0) + tolerance = 0.2 + # I haven't done the math to figure out the probability of this failing, + # but it should be fairly small. In my initial checks the err was typically <0.1 + self.assertLessEqual(err, tolerance) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 17b5b8c1d2778da476c24dcc8fcb99b31f68527f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 09:50:21 -0500 Subject: [PATCH 0177/1169] fix import, pep8 cleanup (+1 squashed commit) Squashed commits: [e1a0603] pep8 cleanup --- src/certfuzz/scoring/errors.py | 2 +- src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py | 1 + .../test/scoring/multiarmed_bandit/test_bayesian_bandit.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/scoring/errors.py b/src/certfuzz/scoring/errors.py index 152d776..25569d3 100644 --- a/src/certfuzz/scoring/errors.py +++ b/src/certfuzz/scoring/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from certfuzz.errors import CERTFuzzError +from ..errors import CERTFuzzError class ScoringError(CERTFuzzError): diff --git a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py index 4b48d46..c341d3f 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py @@ -7,6 +7,7 @@ from .multiarmed_bandit_base import MultiArmedBanditBase from .arms.bayes_laplace import BanditArmBayesLaplace + class BayesianMultiArmedBandit(MultiArmedBanditBase): ''' Bayesian arms, weighted choice proportionate to each arm's share diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py index 3dfa067..fdd9cdb 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py @@ -6,6 +6,7 @@ import unittest from certfuzz.scoring.multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit + class Test(unittest.TestCase): def setUp(self): From 85871364094d7a15dfe7adbce54833286d2c5e47 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 10:52:15 -0500 Subject: [PATCH 0178/1169] refactor __iter__ into base class, make base class raise exception since it does not fully implement an iterator (round robin will moved to a separate module in the next commit) --- .../multiarmed_bandit/bayesian_bandit.py | 3 --- .../multiarmed_bandit/e_greedy_bandit.py | 3 --- .../multiarmed_bandit_base.py | 11 +++-------- .../multiarmed_bandit/random_bandit.py | 3 --- .../scoring/multiarmed_bandit/test_errors.py | 1 + .../test_multiarmed_bandit_base.py | 19 ++----------------- .../multiarmed_bandit/test_random_bandit.py | 1 + 7 files changed, 7 insertions(+), 34 deletions(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py index c341d3f..816e776 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py @@ -28,9 +28,6 @@ def _scaled_scores(self): def _next_key(self): return weighted_choice(self._scaled_scores) - def __iter__(self): - return self - def next(self): key = self._next_key() return self.things[key] diff --git a/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py index 0d68bb6..cd0108f 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py @@ -27,9 +27,6 @@ def __init__(self, epsilon=0.1): self.e = epsilon - def __iter__(self): - return self - def _max_keys(self): max_p = 0.0 _maybe_max_k = [] diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index 09c52e4..b77454f 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -3,7 +3,6 @@ @organization: cert.org ''' -import itertools from .errors import MultiArmedBanditError from .arms.base import BanditArmBase @@ -60,11 +59,7 @@ def mean_p_with_trials(self): return float(total) / count def __iter__(self): - ''' - Implements a simple round robin iterator - ''' - return itertools.cycle(self.things.values()) + return self - -class RoundRobinMultiArmedBandit(MultiArmedBanditBase): - pass + def next(self): + raise NotImplementedError() diff --git a/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py index e61e7fc..6a883f2 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py @@ -11,8 +11,5 @@ class RandomMultiArmedBandit(MultiArmedBanditBase): ''' Returns a random thing from its collection. ''' - def __iter__(self): - return self - def next(self): return random.choice(self.things.values()) diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_errors.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_errors.py index e53094d..34f0acd 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_errors.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_errors.py @@ -6,6 +6,7 @@ import unittest from certfuzz.scoring.multiarmed_bandit import errors + class Test(unittest.TestCase): def setUp(self): diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py index 4dc1d89..de7edc2 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py @@ -7,6 +7,7 @@ from certfuzz.scoring.multiarmed_bandit.multiarmed_bandit_base import MultiArmedBanditBase from certfuzz.scoring.multiarmed_bandit.errors import MultiArmedBanditError + class Test(unittest.TestCase): def setUp(self): @@ -74,24 +75,8 @@ def test_total_p(self): self.assertEqual(total * 0.5, self.mab._total_p) def test_next(self): - arms = 'abcdefghijklmnopqrstuvwxyz' - for arm in arms: - self.mab.add(arm, arm) + self.assertRaises(NotImplementedError, self.mab.next) - i = 1 - n = 1000 - limit = n * len(arms) - from collections import defaultdict - seen = defaultdict(int) - for arm in self.mab: - if i > limit: - break - seen[arm] += 1 - i += 1 - - for arm in arms: - # ensure we saw each arm n times - self.assertEqual(n, seen[arm]) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py index fd0f8de..a3cec14 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py @@ -6,6 +6,7 @@ import unittest from certfuzz.scoring.multiarmed_bandit.random_bandit import RandomMultiArmedBandit + class Test(unittest.TestCase): def setUp(self): From 244bd77b8380fed4372307aad404ab1fd14b1207 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 10:52:44 -0500 Subject: [PATCH 0179/1169] add round robin bandit (refactored out of base class in previous commit) --- .../multiarmed_bandit/round_robin_bandit.py | 15 +++++++ .../test_round_robin_bandit.py | 43 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/certfuzz/scoring/multiarmed_bandit/round_robin_bandit.py create mode 100644 src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py diff --git a/src/certfuzz/scoring/multiarmed_bandit/round_robin_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/round_robin_bandit.py new file mode 100644 index 0000000..68da68c --- /dev/null +++ b/src/certfuzz/scoring/multiarmed_bandit/round_robin_bandit.py @@ -0,0 +1,15 @@ +''' +Created on Jan 10, 2014 + +@author: adh +''' +import itertools +from certfuzz.scoring.multiarmed_bandit.multiarmed_bandit_base import MultiArmedBanditBase + + +class RoundRobinMultiArmedBandit(MultiArmedBanditBase): + def __iter__(self): + ''' + Implements a simple round robin iterator + ''' + return itertools.cycle(self.things.values()) diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py new file mode 100644 index 0000000..c90cd1c --- /dev/null +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py @@ -0,0 +1,43 @@ +''' +Created on Feb 22, 2013 + +@organization: cert.org +''' +import unittest +from certfuzz.scoring.multiarmed_bandit.multiarmed_bandit_base import MultiArmedBanditBase + + +class Test(unittest.TestCase): + + def setUp(self): + self.mab = MultiArmedBanditBase() + self.keys = 'abcdefghijklmnopqrstuvwxyz' + for arm in self.keys: + self.mab.add(arm, arm) + + def tearDown(self): + pass + + def test_next(self): + arms = 'abcdefghijklmnopqrstuvwxyz' + for arm in arms: + self.mab.add(arm, arm) + + i = 1 + n = 1000 + limit = n * len(arms) + from collections import defaultdict + seen = defaultdict(int) + for arm in self.mab: + if i > limit: + break + seen[arm] += 1 + i += 1 + + for arm in arms: + # ensure we saw each arm n times + self.assertEqual(n, seen[arm]) + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 5c574b61941daf709c3045260018a6d454499579 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 12:48:20 -0500 Subject: [PATCH 0180/1169] turn property back into method --- src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py | 3 +-- .../test/scoring/multiarmed_bandit/test_bayesian_bandit.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py index 816e776..25725ae 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py @@ -15,7 +15,6 @@ class BayesianMultiArmedBandit(MultiArmedBanditBase): ''' arm_type = BanditArmBayesLaplace - @property def _scaled_scores(self): scaled_scores = {} total = self._total_p @@ -26,7 +25,7 @@ def _scaled_scores(self): return scaled_scores def _next_key(self): - return weighted_choice(self._scaled_scores) + return weighted_choice(self._scaled_scores()) def next(self): key = self._next_key() diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py index fdd9cdb..d4ed76f 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py @@ -49,7 +49,7 @@ def test_score(self): # high probability zarm = self.mab.arms['z'] self.assertEqual(0.5, zarm.probability) - scaled = self.mab._scaled_scores + scaled = self.mab._scaled_scores() self.assertEqual(0.8, scaled['z']) for key in 'abcdefghijklmnopqrstuvwxy': arm = self.mab.arms[key] @@ -57,7 +57,7 @@ def test_score(self): self.assertEqual(0.008, scaled[key]) # go ahead and pull z zarm.update(successes=0, trials=198) - scaled = self.mab._scaled_scores + scaled = self.mab._scaled_scores() for key in self.arms: # now they should all be equal again arm = self.mab.arms[key] From fbe36aa4c9aa7a3dc0c4848c7214616255d6e81d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 12:50:48 -0500 Subject: [PATCH 0181/1169] use relative imports --- src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py index 25725ae..a85ef80 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from certfuzz.fuzztools.probability import weighted_choice +from ...fuzztools.probability import weighted_choice from .multiarmed_bandit_base import MultiArmedBanditBase from .arms.bayes_laplace import BanditArmBayesLaplace From 0c1fa7a2262411da8551ecd4d91027ebdd4c6323 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Jan 2014 14:10:30 -0500 Subject: [PATCH 0182/1169] oops, testing wrong class --- .../test/scoring/multiarmed_bandit/test_round_robin_bandit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py index c90cd1c..fc99711 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py @@ -4,13 +4,13 @@ @organization: cert.org ''' import unittest -from certfuzz.scoring.multiarmed_bandit.multiarmed_bandit_base import MultiArmedBanditBase +from certfuzz.scoring.multiarmed_bandit.round_robin_bandit import RoundRobinMultiArmedBandit class Test(unittest.TestCase): def setUp(self): - self.mab = MultiArmedBanditBase() + self.mab = RoundRobinMultiArmedBandit() self.keys = 'abcdefghijklmnopqrstuvwxyz' for arm in self.keys: self.mab.add(arm, arm) From 48462650200a3e5a60b8d8a06dd6e9246762662c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 09:08:08 -0500 Subject: [PATCH 0183/1169] clean up unit tests --- src/certfuzz/test/file_handlers/test_seedfile_set.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/test/file_handlers/test_seedfile_set.py b/src/certfuzz/test/file_handlers/test_seedfile_set.py index 0a71893..c96a5fc 100644 --- a/src/certfuzz/test/file_handlers/test_seedfile_set.py +++ b/src/certfuzz/test/file_handlers/test_seedfile_set.py @@ -17,6 +17,7 @@ from certfuzz.scoring.scorable_set import EmptySetError #from pprint import pprint + class Test(unittest.TestCase): def setUp(self): @@ -57,7 +58,9 @@ def test_pickle(self): # confirm that the files are there self.assertEqual(self.file_count, len(self.sfs.things)) unpickled = pickle.loads(pickle.dumps(self.sfs)) - pprint(unpickled.__dict__) + + self.assertTrue(hasattr(unpickled, 'things')) + self.assertEqual(self.file_count, len(unpickled.things)) def test_set_directories(self): self.assertEqual(self.sfs.originpath, self.origindir) @@ -116,7 +119,7 @@ def test_getstate(self): state = self.sfs.__getstate__() self.assertEqual(dict, type(state)) - for k, v in self.sfs.__dict__.iteritems(): + for k in self.sfs.__dict__.iterkeys(): # make sure we're deleting what we need to if k in ['localdir', 'origindir', 'outputdir']: self.assertFalse(k in state) @@ -139,7 +142,7 @@ def test_setstate(self): # is there a corresponding thing in sfs? self.assertTrue(k in self.sfs.things) - for x, y in thing.iteritems(): + for x in thing.iterkeys(): # was it set correctly? self.assertEqual(thing[x], self.sfs.things[k].__dict__[x]) From ce755ed1f7e6c73446c558aa60e3550c6fdb67a6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 10:09:30 -0500 Subject: [PATCH 0184/1169] clean up unused imports --- src/certfuzz/test/fuzztools/test_rangefinder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certfuzz/test/fuzztools/test_rangefinder.py b/src/certfuzz/test/fuzztools/test_rangefinder.py index 5d9857d..ac6bfac 100644 --- a/src/certfuzz/test/fuzztools/test_rangefinder.py +++ b/src/certfuzz/test/fuzztools/test_rangefinder.py @@ -6,7 +6,6 @@ import os import tempfile -import json import unittest from certfuzz.fuzztools.rangefinder import RangeFinder From d2e947997244e0b8b63b88cf8e291a46f74603d2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 10:39:08 -0500 Subject: [PATCH 0185/1169] first try at eradicating ScorableThing from SeedfileSet --- src/certfuzz/file_handlers/seedfile.py | 13 +- src/certfuzz/file_handlers/seedfile_set.py | 28 +-- .../multiarmed_bandit_base.py | 2 +- src/certfuzz/scoring/scorable_set.py | 8 + .../test/file_handlers/test_seedfile.py | 16 +- .../test/file_handlers/test_seedfile_set.py | 225 +++++++++--------- .../multiarmed_bandit/test_bayesian_bandit.py | 2 +- .../multiarmed_bandit/test_e_greedy_bandit.py | 2 +- .../test_multiarmed_bandit_base.py | 10 +- .../multiarmed_bandit/test_random_bandit.py | 2 +- .../test_round_robin_bandit.py | 4 +- .../test/scoring/test_scorable_set.py | 1 + 12 files changed, 148 insertions(+), 165 deletions(-) diff --git a/src/certfuzz/file_handlers/seedfile.py b/src/certfuzz/file_handlers/seedfile.py index 870538c..71d298e 100644 --- a/src/certfuzz/file_handlers/seedfile.py +++ b/src/certfuzz/file_handlers/seedfile.py @@ -10,7 +10,6 @@ from certfuzz.file_handlers.errors import SeedFileError from certfuzz.fuzztools import filetools from certfuzz.fuzztools.rangefinder import RangeFinder -from certfuzz.scoring.scorable_thing import ScorableThing # TODO: replace with a common function in some helper module @@ -24,9 +23,7 @@ def print_dict(d, indent=0): print indent_str + "%s (%s): %s" % (k, type(v).__name__, v) -# ScorableThing mixin gives us the probability stuff needed for use as part of -# a scorable set like SeedfileSet -class SeedFile(BasicFile, ScorableThing): +class SeedFile(BasicFile): ''' ''' @@ -37,7 +34,6 @@ def __init__(self, output_base_dir, path): @raise SeedFileError: zero-length files will raise a SeedFileError ''' BasicFile.__init__(self, path) - ScorableThing.__init__(self, key=self.md5) if not self.len > 0: raise SeedFileError('You cannot do bitwise fuzzing on a zero-length file: %s' % self.path) @@ -73,13 +69,6 @@ def __getstate__(self): def __setstate__(self, state): old_rf = state.pop('rangefinder') - self.a = state['a'] - self.b = state['b'] - self.seen = state['seen'] - self.successes = state['successes'] - self.tries = state['tries'] - self.uniques_only = state['uniques_only'] - # rebuild the rangefinder new_rf = self._get_rangefinder() old_ranges = old_rf['things'] diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index edce9ed..447a531 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -11,13 +11,13 @@ from certfuzz.file_handlers.seedfile import SeedFile from certfuzz.fuzztools import filetools from certfuzz.scoring.errors import EmptySetError -from certfuzz.scoring.scorable_set import ScorableSet2 +from certfuzz.scoring.scorable_set import ScorableSet3 logger = logging.getLogger(__name__) -class SeedfileSet(ScorableSet2): +class SeedfileSet(ScorableSet3): ''' classdocs ''' @@ -147,15 +147,15 @@ def __setstate__(self, state): self.things[k].__setstate__(old_sf) self.sfcount += 1 - def __getstate__(self): - state = ScorableSet2.__getstate__(self) - - # remove things we can recreate - try: - for k in ('origindir', 'localdir', 'outputdir'): - del state[k] - except KeyError: - # it's ok if they don't exist - pass - - return state +# def __getstate__(self): +# state = ScorableSet3.__getstate__(self) +# +# # remove things we can recreate +# try: +# for k in ('origindir', 'localdir', 'outputdir'): +# del state[k] +# except KeyError: +# # it's ok if they don't exist +# pass +# +# return state diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index b77454f..6d33cc0 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -17,7 +17,7 @@ def __init__(self): self.things = {} self.arms = {} - def add(self, key=None, obj=None): + def add_item(self, key=None, obj=None): if key is None: raise MultiArmedBanditError('unspecified key for arm') if obj is None: diff --git a/src/certfuzz/scoring/scorable_set.py b/src/certfuzz/scoring/scorable_set.py index b0ac0d2..5075906 100644 --- a/src/certfuzz/scoring/scorable_set.py +++ b/src/certfuzz/scoring/scorable_set.py @@ -13,6 +13,14 @@ logger = logging.getLogger(__name__) +from .multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit + + +class ScorableSet3(BayesianMultiArmedBandit): + def __init__(self, datafile=None): + self.datafile = datafile + BayesianMultiArmedBandit.__init__(self) + # Simplified reimplementation of ScorableSet with a Bayesian approach class ScorableSet2(object): diff --git a/src/certfuzz/test/file_handlers/test_seedfile.py b/src/certfuzz/test/file_handlers/test_seedfile.py index bac922e..651035f 100644 --- a/src/certfuzz/test/file_handlers/test_seedfile.py +++ b/src/certfuzz/test/file_handlers/test_seedfile.py @@ -8,9 +8,9 @@ import tempfile import os from certfuzz.file_handlers.seedfile import SeedFile -from pprint import pprint from certfuzz.fuzztools.rangefinder import RangeFinder + class Test(unittest.TestCase): def setUp(self): @@ -28,13 +28,6 @@ def tearDown(self): def test_init(self): self.assertEqual(self.sf.output_dir, os.path.join(self.dir, self.sf.md5)) - def test_record_hit(self): - self.assertEqual(0, self.sf.successes) - self.assertFalse('x' in self.sf.seen) - self.sf.record_success('x') - self.assertEqual(1, self.sf.successes) - self.assertTrue('x' in self.sf.seen) - def test_getstate(self): self.assertEqual(RangeFinder, type(self.sf.rangefinder)) state = self.sf.__getstate__() @@ -43,14 +36,7 @@ def test_getstate(self): def test_setstate(self): state = self.sf.__getstate__() - - self.assertEqual(0, self.sf.tries) - self.assertEqual(0, state['tries']) - - # can we change something? - state['tries'] = 1000 self.sf.__setstate__(state) - self.assertEqual(1000, self.sf.tries) # make sure we restore rangefinder self.assertEqual(RangeFinder, type(self.sf.rangefinder)) diff --git a/src/certfuzz/test/file_handlers/test_seedfile_set.py b/src/certfuzz/test/file_handlers/test_seedfile_set.py index c96a5fc..16c2b2b 100644 --- a/src/certfuzz/test/file_handlers/test_seedfile_set.py +++ b/src/certfuzz/test/file_handlers/test_seedfile_set.py @@ -4,7 +4,6 @@ @organization: cert.org ''' import unittest -import logging import tempfile import os import shutil @@ -48,19 +47,19 @@ def tearDown(self): shutil.rmtree(d) self.assertFalse(os.path.exists(d)) - def test_pickle(self): - import pickle - self.assertTrue(hasattr(self.sfs, 'things')) - # no files added yet - self.assertEqual(0, len(self.sfs.things)) - # add the files - self.sfs._setup() - # confirm that the files are there - self.assertEqual(self.file_count, len(self.sfs.things)) - unpickled = pickle.loads(pickle.dumps(self.sfs)) - - self.assertTrue(hasattr(unpickled, 'things')) - self.assertEqual(self.file_count, len(unpickled.things)) +# def test_pickle(self): +# import pickle +# self.assertTrue(hasattr(self.sfs, 'things')) +# # no files added yet +# self.assertEqual(0, len(self.sfs.things)) +# # add the files +# self.sfs._setup() +# # confirm that the files are there +# self.assertEqual(self.file_count, len(self.sfs.things)) +# unpickled = pickle.loads(pickle.dumps(self.sfs)) +# +# self.assertTrue(hasattr(unpickled, 'things')) +# self.assertEqual(self.file_count, len(unpickled.things)) def test_set_directories(self): self.assertEqual(self.sfs.originpath, self.origindir) @@ -106,101 +105,101 @@ def test_init(self): self.assertEqual(self.outputdir, self.sfs.seedfile_output_base_dir) self.assertEqual(0, len(self.sfs.things)) - def test_getstate_is_pickle_friendly(self): - # getstate should return a pickleable object - import pickle - state = self.sfs.__getstate__() - try: - pickle.dumps(state) - except Exception, e: - self.fail('Failed to pickle state: %s' % e) - - def test_getstate(self): - state = self.sfs.__getstate__() - self.assertEqual(dict, type(state)) - - for k in self.sfs.__dict__.iterkeys(): - # make sure we're deleting what we need to - if k in ['localdir', 'origindir', 'outputdir']: - self.assertFalse(k in state) - else: - self.assertTrue(k in state, '%s not found' % k) - - def test_setstate(self): - self.sfs.__enter__() - state_before = self.sfs.__getstate__() - self.sfs.__setstate__(state_before) - self.assertEqual(self.file_count, self.sfs.sfcount) - state_after = self.sfs.__getstate__() - - for k, v in state_before.iteritems(): - self.assertTrue(k in state_after) - if not k == 'things': - self.assertEqual(v, state_after[k]) - - for k, thing in state_before['things'].iteritems(): - # is there a corresponding thing in sfs? - self.assertTrue(k in self.sfs.things) - - for x in thing.iterkeys(): - # was it set correctly? - self.assertEqual(thing[x], self.sfs.things[k].__dict__[x]) - - self.assertEqual(self.file_count, self.sfs.sfcount) - - def test_setstate_with_changed_files(self): - # refresh the sfs - self.sfs.__enter__() - - # get the original state - state_before = self.sfs.__getstate__() - self.assertEqual(len(state_before['things']), self.file_count) - - # delete one of the files - file_to_remove = self.files.pop() - localfile_md5 = hashlib.md5(open(file_to_remove, 'rb').read()).hexdigest() - localfilename = "sf_%s" % localfile_md5 - - # remove it from origin - os.remove(file_to_remove) - self.assertFalse(file_to_remove in self.files) - self.assertFalse(os.path.exists(file_to_remove)) -# print "removed %s" % file_to_remove - -# # remove it from localdir - localfile_to_remove = os.path.join(self.localdir, localfilename) - os.remove(localfile_to_remove) - self.assertFalse(os.path.exists(localfile_to_remove)) - - # create a new sfs - new_sfs = SeedfileSet() - new_sfs.__setstate__(state_before) - - self.assertEqual(len(new_sfs.things), (self.file_count - 1)) - -# print "Newthings: %s" % new_sfs.things.keys() - for k, thing in state_before['things'].iteritems(): -# print "k: %s" % k - if k == localfile_md5: - self.assertFalse(k in new_sfs.things) - continue - else: - # is there a corresponding thing in sfs? - self.assertTrue(k in new_sfs.things) - - for x, y in thing.iteritems(): - # was it set correctly? - sfsthing = new_sfs.things[k].__dict__[x] - if hasattr(sfsthing, '__dict__'): - # some things are complex objects themselves - # so we have to compare their __dict__ versions - self._same_dict(y, sfsthing.__dict__) - else: - # others are just simple objects and we can - # compare them directly - self.assertEqual(y, sfsthing) - - self.assertEqual(self.file_count - 1, new_sfs.sfcount) +# def test_getstate_is_pickle_friendly(self): +# # getstate should return a pickleable object +# import pickle +# state = self.sfs.__getstate__() +# try: +# pickle.dumps(state) +# except Exception, e: +# self.fail('Failed to pickle state: %s' % e) +# +# def test_getstate(self): +# state = self.sfs.__getstate__() +# self.assertEqual(dict, type(state)) +# +# for k in self.sfs.__dict__.iterkeys(): +# # make sure we're deleting what we need to +# if k in ['localdir', 'origindir', 'outputdir']: +# self.assertFalse(k in state) +# else: +# self.assertTrue(k in state, '%s not found' % k) + +# def test_setstate(self): +# self.sfs.__enter__() +# state_before = self.sfs.__getstate__() +# self.sfs.__setstate__(state_before) +# self.assertEqual(self.file_count, self.sfs.sfcount) +# state_after = self.sfs.__getstate__() +# +# for k, v in state_before.iteritems(): +# self.assertTrue(k in state_after) +# if not k == 'things': +# self.assertEqual(v, state_after[k]) +# +# for k, thing in state_before['things'].iteritems(): +# # is there a corresponding thing in sfs? +# self.assertTrue(k in self.sfs.things) +# +# for x in thing.iterkeys(): +# # was it set correctly? +# self.assertEqual(thing[x], self.sfs.things[k].__dict__[x]) +# +# self.assertEqual(self.file_count, self.sfs.sfcount) + +# def test_setstate_with_changed_files(self): +# # refresh the sfs +# self.sfs.__enter__() +# +# # get the original state +# state_before = self.sfs.__getstate__() +# self.assertEqual(len(state_before['things']), self.file_count) +# +# # delete one of the files +# file_to_remove = self.files.pop() +# localfile_md5 = hashlib.md5(open(file_to_remove, 'rb').read()).hexdigest() +# localfilename = "sf_%s" % localfile_md5 +# +# # remove it from origin +# os.remove(file_to_remove) +# self.assertFalse(file_to_remove in self.files) +# self.assertFalse(os.path.exists(file_to_remove)) +## print "removed %s" % file_to_remove +# +## # remove it from localdir +# localfile_to_remove = os.path.join(self.localdir, localfilename) +# os.remove(localfile_to_remove) +# self.assertFalse(os.path.exists(localfile_to_remove)) +# +# # create a new sfs +# new_sfs = SeedfileSet() +# new_sfs.__setstate__(state_before) +# +# self.assertEqual(len(new_sfs.things), (self.file_count - 1)) +# +## print "Newthings: %s" % new_sfs.things.keys() +# for k, thing in state_before['things'].iteritems(): +## print "k: %s" % k +# if k == localfile_md5: +# self.assertFalse(k in new_sfs.things) +# continue +# else: +# # is there a corresponding thing in sfs? +# self.assertTrue(k in new_sfs.things) +# +# for x, y in thing.iteritems(): +# # was it set correctly? +# sfsthing = new_sfs.things[k].__dict__[x] +# if hasattr(sfsthing, '__dict__'): +# # some things are complex objects themselves +# # so we have to compare their __dict__ versions +# self._same_dict(y, sfsthing.__dict__) +# else: +# # others are just simple objects and we can +# # compare them directly +# self.assertEqual(y, sfsthing) +# +# self.assertEqual(self.file_count - 1, new_sfs.sfcount) def _same_dict(self, d1, d2): for k, v in d1.iteritems(): @@ -212,10 +211,10 @@ def _same_dict(self, d1, d2): self.assertEqual(v, d2[k]) - def test_next_item(self): - self.assertEqual(0, len(self.sfs.things)) - self.assertRaises(EmptySetError, self.sfs.next_key) - self.assertRaises(EmptySetError, self.sfs.next_item) +# def test_next_item(self): +# self.assertEqual(0, len(self.sfs.things)) +# self.assertRaises(EmptySetError, self.sfs.next_key) +# self.assertRaises(EmptySetError, self.sfs.next_item) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py index d4ed76f..0e3bce8 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py @@ -13,7 +13,7 @@ def setUp(self): self.mab = BayesianMultiArmedBandit() self.arms = 'abcdefghijklmnopqrstuvwxyz' for arm in self.arms: - self.mab.add(arm, arm) + self.mab.add_item(arm, arm) def tearDown(self): pass diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py index aeee35f..07ebb4b 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py @@ -21,7 +21,7 @@ def tearDown(self): def _add_things(self): for v, k in enumerate(self.keys): - self.mab.add(k, v) + self.mab.add_item(k, v) def _bump_p(self, k): # bump p(k) up half the distance to the goal diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py index de7edc2..c8ca37f 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py @@ -14,19 +14,19 @@ def setUp(self): self.mab = MultiArmedBanditBase() self.keys = 'abcdefghijklmnopqrstuvwxyz' for arm in self.keys: - self.mab.add(arm, arm) + self.mab.add_item(arm, arm) def tearDown(self): pass def test_add(self): - self.assertRaises(MultiArmedBanditError, self.mab.add) - self.assertRaises(MultiArmedBanditError, self.mab.add, key=None, obj='obj') - self.assertRaises(MultiArmedBanditError, self.mab.add, key='key', obj=None) + self.assertRaises(MultiArmedBanditError, self.mab.add_item) + self.assertRaises(MultiArmedBanditError, self.mab.add_item, key=None, obj='obj') + self.assertRaises(MultiArmedBanditError, self.mab.add_item, key='key', obj=None) self.assertEqual(len(self.keys), len(self.mab.things)) self.assertEqual(len(self.keys), len(self.mab.arms)) - self.mab.add('foo', 'bar') + self.mab.add_item('foo', 'bar') self.assertEqual(len(self.keys) + 1, len(self.mab.things)) self.assertEqual(len(self.keys) + 1, len(self.mab.arms)) self.assertEqual(1.0, self.mab.arms['foo'].probability) diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py index a3cec14..73a6fb5 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py @@ -18,7 +18,7 @@ def tearDown(self): def test_next(self): arms = 'abcdefghijklmnopqrstuvwxyz' for arm in arms: - self.mab.add(arm, arm) + self.mab.add_item(arm, arm) i = 1 n = 10000 diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py index fc99711..e47cdc7 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py @@ -13,7 +13,7 @@ def setUp(self): self.mab = RoundRobinMultiArmedBandit() self.keys = 'abcdefghijklmnopqrstuvwxyz' for arm in self.keys: - self.mab.add(arm, arm) + self.mab.add_item(arm, arm) def tearDown(self): pass @@ -21,7 +21,7 @@ def tearDown(self): def test_next(self): arms = 'abcdefghijklmnopqrstuvwxyz' for arm in arms: - self.mab.add(arm, arm) + self.mab.add_item(arm, arm) i = 1 n = 1000 diff --git a/src/certfuzz/test/scoring/test_scorable_set.py b/src/certfuzz/test/scoring/test_scorable_set.py index 19a7657..8021ce2 100644 --- a/src/certfuzz/test/scoring/test_scorable_set.py +++ b/src/certfuzz/test/scoring/test_scorable_set.py @@ -15,6 +15,7 @@ import shutil import csv + class MockScorableThing(object): def __init__(self): self.key = random_str(8) From a4b9f1545ecbb2bce1b6958856253fa99fa7e886 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 12:13:28 -0500 Subject: [PATCH 0186/1169] no need to print the config --- src/certfuzz/test/campaign/config/test_bff_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/test/campaign/config/test_bff_config.py b/src/certfuzz/test/campaign/config/test_bff_config.py index c11a62e..9ed8999 100644 --- a/src/certfuzz/test/campaign/config/test_bff_config.py +++ b/src/certfuzz/test/campaign/config/test_bff_config.py @@ -11,6 +11,7 @@ import tempfile from certfuzz.campaign.config.bff_config import read_config_options + class Test(unittest.TestCase): def delete_file(self, f): os.remove(f) @@ -95,7 +96,6 @@ def test_check_program_file_type(self): # trim the last char ('c') f = f[:-1] self.cfg.program = f - print f self.assertTrue(self.cfg.program_is_script()) def test_get_minimized_file(self): From 1dabd9206162750e62db29595eea3d74b0ca9500 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 12:14:06 -0500 Subject: [PATCH 0187/1169] make the iterator behave like an iterator --- .../multiarmed_bandit/bayesian_bandit.py | 4 + .../multiarmed_bandit_base.py | 13 +- .../test_multiarmed_bandit_base.py | 4 +- .../test/scoring/test_scorable_set.py | 148 +++++++++--------- 4 files changed, 88 insertions(+), 81 deletions(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py index a85ef80..571525e 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py @@ -28,5 +28,9 @@ def _next_key(self): return weighted_choice(self._scaled_scores()) def next(self): + # if there aren't any arms, we're done. + if not len(self.arms): + raise StopIteration + key = self._next_key() return self.things[key] diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index 6d33cc0..50cf66d 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -26,6 +26,17 @@ def add_item(self, key=None, obj=None): # create a new arm of the desired type self.arms[key] = self.arm_type() + def del_item(self, key=None): + if key is None: + return + + for d in (self.things, self.arms): + try: + del(d[key]) + except KeyError: + # if there was a keyerror, our job is already done + pass + def record_result(self, key, successes=0, trials=0): arm = self.arms[key] arm.update(successes, trials) @@ -62,4 +73,4 @@ def __iter__(self): return self def next(self): - raise NotImplementedError() + raise StopIteration() diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py index c8ca37f..fcbc3c3 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py @@ -75,8 +75,8 @@ def test_total_p(self): self.assertEqual(total * 0.5, self.mab._total_p) def test_next(self): - self.assertRaises(NotImplementedError, self.mab.next) - + # empty set raises StopIteration + self.assertRaises(StopIteration, self.mab.next) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/certfuzz/test/scoring/test_scorable_set.py b/src/certfuzz/test/scoring/test_scorable_set.py index 8021ce2..9c0072a 100644 --- a/src/certfuzz/test/scoring/test_scorable_set.py +++ b/src/certfuzz/test/scoring/test_scorable_set.py @@ -8,7 +8,7 @@ import random import tempfile -from certfuzz.scoring.scorable_set import ScorableSet2 +from certfuzz.scoring.scorable_set import ScorableSet3 from certfuzz.scoring.errors import ScorableSetError, EmptySetError from certfuzz.helpers import random_str import os @@ -16,16 +16,10 @@ import csv -class MockScorableThing(object): - def __init__(self): - self.key = random_str(8) - self.probability = random.uniform(0.0, 1.0) - - class Test(unittest.TestCase): def setUp(self): - self.ss = ScorableSet2() + self.ss = ScorableSet3() self.things = [] self.tmpdir = tempfile.mkdtemp() fd, f = tempfile.mkstemp(dir=self.tmpdir) @@ -34,16 +28,16 @@ def setUp(self): self.tmpfile = f for _x in xrange(5): - thing = MockScorableThing() - self.ss.add_item(thing.key, thing) + thing = 'thing_%d' % _x + self.ss.add_item(thing, _x) self.things.append(thing) def tearDown(self): shutil.rmtree(self.tmpdir) def test_scaled_score(self): - self.ss._update_probabilities() - score_sum = sum([x for x in self.ss.scaled_score.itervalues()]) +# self.ss._update_probabilities() + score_sum = sum([x for x in self.ss._scaled_scores().itervalues()]) self.assertAlmostEqual(score_sum, 1.0) def test_del_item(self): @@ -53,79 +47,77 @@ def test_del_item(self): n_before = len(self.things) self.assertEqual(len(self.ss.things), n_before) thing_to_del = self.things.pop() - self.ss.del_item(thing_to_del.key) + self.ss.del_item(thing_to_del) n_after = n_before - 1 self.assertEqual(len(self.things), n_after) self.assertEqual(len(self.ss.things), n_after) def test_add_item(self): - ss = ScorableSet2() - for _x in xrange(100): - thing = MockScorableThing() - self.assertEqual(len(ss.things), _x) - ss.add_item(thing.key, thing) - self.assertEqual(len(ss.things), _x + 1) - self.assertTrue(thing.key in ss.things) - self.assertTrue(thing in ss.things.values()) + ss = ScorableSet3() + for _value in xrange(100): + key = 'thing_%d' % _value + self.assertEqual(len(ss.things), _value) + ss.add_item(key, _value) + self.assertEqual(len(ss.things), _value + 1) + self.assertTrue(key in ss.things) + self.assertTrue(_value in ss.things.values()) def test_empty_set(self): - ss = ScorableSet2() - self.assertEqual(0, len(ss.things)) - self.assertRaises(EmptySetError, ss.next_key) - self.assertRaises(EmptySetError, ss.next_item) - - def test_read_csv(self): - self.assertRaises(ScorableSetError, self.ss._read_csv) - - self.ss.datafile = self.tmpfile - d = {'x': 1, 'y': 2, 'z': 3} - keys = list(d.keys()) - - with open(self.ss.datafile, 'wb') as datafile: - writer = csv.writer(datafile) - writer.writerow(keys) - row = [d[k] for k in keys] - writer.writerow(row) - - read_csv = self.ss._read_csv() - d_out = read_csv.pop(0) - for k in keys: - self.assertTrue(k in d_out) - self.assertEqual(d[k], int(d_out[k])) - - def test_update_csv(self): - self.ss._update_probabilities() - # raise error if datafile is undefined - self.assertRaises(ScorableSetError, self.ss.update_csv) - - self.ss.datafile = self.tmpfile - self.assertFalse(os.path.exists(self.tmpfile)) - - # make sure it can create a file from scratch - self.ss.update_csv() - self.assertTrue(os.path.exists(self.tmpfile)) - with open(self.ss.datafile, 'rb') as f: - data = list(csv.DictReader(f)) - - self.assertEqual(len(data), 1) - for row in data: - for k in self.ss.things.keys(): - self.assertTrue(k in row) - - # make sure it adds a second row - self.ss.update_csv() - with open(self.ss.datafile, 'rb') as f: - data = list(csv.DictReader(f)) - - self.assertEqual(len(data), 2) - for row in data: - for k in self.ss.things.keys(): - self.assertTrue(k in row) - - self.ss.update_csv() - # we should be at 4 lines total now (1 header, 3 data) - with open(self.ss.datafile, 'rb') as f: - self.assertEqual(len(f.readlines()), 4) + ss = ScorableSet3() + self.assertRaises(StopIteration, ss.next) + +# def test_read_csv(self): +# self.assertRaises(ScorableSetError, self.ss._read_csv) +# +# self.ss.datafile = self.tmpfile +# d = {'x': 1, 'y': 2, 'z': 3} +# keys = list(d.keys()) +# +# with open(self.ss.datafile, 'wb') as datafile: +# writer = csv.writer(datafile) +# writer.writerow(keys) +# row = [d[k] for k in keys] +# writer.writerow(row) +# +# read_csv = self.ss._read_csv() +# d_out = read_csv.pop(0) +# for k in keys: +# self.assertTrue(k in d_out) +# self.assertEqual(d[k], int(d_out[k])) + +# def test_update_csv(self): +# self.ss._update_probabilities() +# # raise error if datafile is undefined +# self.assertRaises(ScorableSetError, self.ss.update_csv) +# +# self.ss.datafile = self.tmpfile +# self.assertFalse(os.path.exists(self.tmpfile)) +# +# # make sure it can create a file from scratch +# self.ss.update_csv() +# self.assertTrue(os.path.exists(self.tmpfile)) +# with open(self.ss.datafile, 'rb') as f: +# data = list(csv.DictReader(f)) +# +# self.assertEqual(len(data), 1) +# for row in data: +# for k in self.ss.things.keys(): +# self.assertTrue(k in row) +# +# # make sure it adds a second row +# self.ss.update_csv() +# with open(self.ss.datafile, 'rb') as f: +# data = list(csv.DictReader(f)) +# +# self.assertEqual(len(data), 2) +# for row in data: +# for k in self.ss.things.keys(): +# self.assertTrue(k in row) +# +# self.ss.update_csv() +# # we should be at 4 lines total now (1 header, 3 data) +# with open(self.ss.datafile, 'rb') as f: +# self.assertEqual(len(f.readlines()), 4) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] From 815bb88be491b778130076ff002de7d8101bdae2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 14:20:43 -0500 Subject: [PATCH 0188/1169] =?UTF-8?q?replace=20seedfile.record=5Ftries()?= =?UTF-8?q?=20with=20seedfile=5Fset.record=5Ftries(key=E2=80=A6)=E2=80=A6p?= =?UTF-8?q?art=20of=20eliminating=20ScorableThing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/certfuzz/file_handlers/seedfile_set.py | 42 +++++++++---------- .../multiarmed_bandit_base.py | 3 ++ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 447a531..3024fcb 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -117,7 +117,7 @@ def next_item(self): while len(self.things): logger.debug('Thing count: %d', len(self.things)) # continue until we find one that exists, or else the set is empty - sf = ScorableSet2.next_item(self) + sf = ScorableSet3.next(self) if sf.exists(): # it's still there, proceed return sf @@ -126,26 +126,26 @@ def next_item(self): logger.warning('Seedfile no longer exists, removing from set: %s', sf.path) self.del_item(sf.md5) - def __setstate__(self, state): - newstate = state.copy() - - # copy out old things and replace with an empty dict - oldthings = newstate.pop('things') - newstate['things'] = {} - - # refresh the directories - self.__dict__.update(newstate) - self._setup() - - # clean up things that no longer exist - self.sfcount = 0 - self.sfdel = 0 - for k, old_sf in oldthings.iteritems(): - # update the seedfiles for ones that are still present - if k in self.things: -# print "%s in things..." % k - self.things[k].__setstate__(old_sf) - self.sfcount += 1 +# def __setstate__(self, state): +# newstate = state.copy() +# +# # copy out old things and replace with an empty dict +# oldthings = newstate.pop('things') +# newstate['things'] = {} +# +# # refresh the directories +# self.__dict__.update(newstate) +# self._setup() +# +# # clean up things that no longer exist +# self.sfcount = 0 +# self.sfdel = 0 +# for k, old_sf in oldthings.iteritems(): +# # update the seedfiles for ones that are still present +# if k in self.things: +## print "%s in things..." % k +# self.things[k].__setstate__(old_sf) +# self.sfcount += 1 # def __getstate__(self): # state = ScorableSet3.__getstate__(self) diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index 50cf66d..481ce18 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -41,6 +41,9 @@ def record_result(self, key, successes=0, trials=0): arm = self.arms[key] arm.update(successes, trials) + def record_tries(self, key=None, tries=0): + self.record_result(key, successes=0, trials=tries) + @property def successes(self): return sum([a.successes for a in self.arms.values()]) From af7307aa8c797a6a1d396657fbe397c6e4ffb2ea Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 15:14:07 -0500 Subject: [PATCH 0189/1169] still crashes but now on a temp file problem Traceback (most recent call last): File "/home/fuzz/bff/bff.py", line 9, in main() File "/mnt/hgfs/fuzz/certfuzz/bff/linux.py", line 543, in main cfg.crashers_dir, sr.s1, r) as c: File "/mnt/hgfs/fuzz/certfuzz/crash/crash_base.py", line 54, in __enter__ self.update_crash_details() File "/mnt/hgfs/fuzz/certfuzz/crash/bff_crash.py", line 78, in update_crash_details self.is_crash = self.confirm_crash() File "/mnt/hgfs/fuzz/certfuzz/crash/bff_crash.py", line 119, in confirm_crash self.get_debug_output(self.fuzzedfile.path) File "/mnt/hgfs/fuzz/certfuzz/crash/bff_crash.py", line 115, in get_debug_output self.dbg = debugger_obj.go() File "/mnt/hgfs/fuzz/certfuzz/debuggers/gdb.py", line 106, in go self._remove_temp_file() File "/mnt/hgfs/fuzz/certfuzz/debuggers/gdb.py", line 75, in _remove_temp_file os.remove(self.input_file) OSError: [Errno 2] No such file or directory: '/tmp/tmptkYgN0' --- src/certfuzz/bff/linux.py | 505 +++++++++++++++++- src/certfuzz/file_handlers/seedfile_set.py | 15 +- .../multiarmed_bandit_base.py | 5 +- src/certfuzz/scoring/scorable_set.py | 8 - 4 files changed, 492 insertions(+), 41 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index c2a94eb..6700941 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -11,38 +11,311 @@ import os import platform import sys +<<<<<<< HEAD from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.fuzztools import filetools from certfuzz.campaign.linux import Campaign +======= +import time + +from .. import debuggers, file_handlers +from ..analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind +from ..analyzers.callgrind import callgrind +from ..analyzers.callgrind.annotate import annotate_callgrind +from ..analyzers.callgrind.annotate import annotate_callgrind_tree +from ..analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError +from ..analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError +from ..analyzers.errors import AnalyzerEmptyOutputError +from ..campaign.config import bff_config as cfg_helper +from ..crash.bff_crash import BffCrash +from ..debuggers import crashwrangler # @UnusedImport +from ..debuggers import gdb # @UnusedImport +from ..file_handlers.seedfile_set import SeedfileSet +from ..file_handlers.tmp_reaper import TmpReaper +from ..fuzztools import bff_helper as z, filetools, performance +from ..fuzztools.object_caching import cache_state, get_cached_state +from ..fuzztools.process_killer import ProcessKiller +from ..fuzztools.seedrange import SeedRange +from ..fuzztools.state_timer import StateTimer +from ..fuzztools.watchdog import WatchDog +from ..fuzztools.zzuf import Zzuf +from ..fuzztools.zzuflog import ZzufLog +from ..minimizer import MinimizerError, UnixMinimizer as Minimizer +>>>>>>> still crashes but now on a temp file problem DEBUG = True SEED_INTERVAL = 500 -#SEED_TS = performance.TimeStamper() -#START_SEED = 0 +SEED_TS = performance.TimeStamper() +START_SEED = 0 + +STATE_TIMER = StateTimer() logger = logging.getLogger() logger.name = 'bff' logger.setLevel(0) -#def get_rate(current_seed): -# seeds = current_seed - START_SEED -# rate = seeds / SEED_TS.since_start() -# return rate +def get_rate(current_seed): + seeds = current_seed - START_SEED + rate = seeds / SEED_TS.since_start() + return rate + + +def get_uniq_logger(logfile): + l = logging.getLogger('uniq_crash') + if len(l.handlers) == 0: + hdlr = logging.FileHandler(logfile) + l.addHandler(hdlr) + return l + + +def determine_uniqueness(crash, hashes): + ''' + Gets the crash signature, then compares it against known crashes. + Sets crash.is_unique = True if it is new + ''' + + # short-circuit on crashes with no signature + if not crash.signature: + logger.warning('Crash has no signature, cleaning up') + crash.delete_files() + return + + if crash.signature in hashes: + crash.is_unique = False + return + + # fall back to checking if the crash directory exists + crash_dir_found = filetools.find_or_create_dir(crash.result_dir) + + crash.is_unique = not crash_dir_found + + +def analyze_crasher(cfg, crash): + ''' + Runs multiple analyses and collects data about a crash. Returns a list of other crashes + encountered during the process of analyzing the current crash. + @param cfg: A BFF config object + @param crash: A crash object + @return: a list of Crasher objects + ''' + other_crashers_found = [] + + dbg_out_file_orig = crash.dbg.file + logger.debug('Original debugger file: %s', dbg_out_file_orig) + + if cfg.minimizecrashers: + STATE_TIMER.enter_state('minimize_testcase') + # try to reduce the Hamming Distance between the crasher file and the known good seedfile + # crash.fuzzedfile will be replaced with the minimized result + try: + with Minimizer(cfg=cfg, crash=crash, bitwise=False, + seedfile_as_target=True, confidence=0.999, + tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout + ) as minimizer: + minimizer.go() + other_crashers_found.extend(minimizer.other_crashes.values()) + except MinimizerError, e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) + minimizer = None + + touch_watchdog_file(cfg) + # calculate the hamming distances for this crash + # between the original seedfile and the minimized fuzzed file + crash.calculate_hamming_distances() + + if cfg.minimize_to_string: + STATE_TIMER.enter_state('minimize_testcase_to_string') + # Minimize to a string of 'x's + # crash.fuzzedfile will be replaced with the minimized result + try: + with Minimizer(cfg=cfg, crash=crash, bitwise=False, + seedfile_as_target=False, confidence=0.9, + tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout + ) as min2string: + min2string.go() + other_crashers_found.extend(min2string.other_crashes.values()) + except MinimizerError, e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) + min2string = None + touch_watchdog_file(cfg) + + STATE_TIMER.enter_state('analyze_testcase') + + # get one last debugger output for the newly minimized file + if crash.pc_in_function: + # change the debugger template + crash.set_debugger_template('complete') + else: + # use a debugger template that specifies fixed offsets from $pc for disassembly + crash.set_debugger_template('complete_nofunction') + logger.info('Getting complete debugger output for crash: %s', crash.fuzzedfile.path) + crash.get_debug_output(crash.fuzzedfile.path) + + if dbg_out_file_orig != crash.dbg.file: + # we have a new debugger output + # remove the old one + filetools.delete_files(dbg_out_file_orig) + if os.path.exists(dbg_out_file_orig): + logger.warning('Failed to remove old debugger file %s', dbg_out_file_orig) + else: + logger.debug('Removed old debug file %s', dbg_out_file_orig) + + # use the minimized file for the rest of the analyses + analyzers = [ + stderr.StdErr, + cw_gmalloc.CrashWranglerGmalloc, + ] + if cfg.use_valgrind: + analyzers.extend([ + valgrind.Valgrind, + callgrind.Callgrind, + ]) + if cfg.use_pin_calltrace: + analyzers.extend([ + pin_calltrace.Pin_calltrace, + ]) + + for analyzer in analyzers: + touch_watchdog_file(cfg) + + analyzer_instance = analyzer(cfg, crash) + if analyzer_instance: + try: + analyzer_instance.go() + except AnalyzerEmptyOutputError: + logger.warning('Unexpected empty output from analyzer. Continuing') + + logger.info('Annotating callgrind output') + try: + annotate_callgrind(crash) + annotate_callgrind_tree(crash) + except CallgrindAnnotateEmptyOutputFileError: + logger.warning('Unexpected empty output from annotate_callgrind. Continuing') + except CallgrindAnnotateMissingInputFileError: + logger.warning('Missing callgrind output. Continuing') + + return other_crashers_found + + +def verify_crasher(c, hashes, cfg, seedfile_set): + logger.debug('verifying crash') + new_crash_count = 0 + + crashes = [] + crashes.append(c) + + for crash in crashes: + # loop until we're out of crashes to verify + logger.debug('crashes to verify: %d', len(crashes)) + STATE_TIMER.enter_state('verify_testcase') + + # crashes may be added as a result of minimization + crash.is_unique = False + determine_uniqueness(crash, hashes) + crash.get_logger() + if crash.is_unique: + hashes.append(crash) + new_crash_count += 1 + + logger.debug("%s did not exist in cache, crash is unique", crash.signature) + more_crashes = analyze_crasher(cfg, crash) + + if cfg.recycle_crashers: + logger.debug('Recycling crash as seedfile') + iterstring = crash.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + crash.seedfile.md5 + '-' + iterstring + crash.seedfile.ext + crasherseed_path = os.path.join(cfg.seedfile_origin_dir, crasherseedname) + filetools.copy_file(crash.fuzzedfile.path, crasherseed_path) + seedfile_set.add_file(crasherseed_path) + # add new crashes to the queue + crashes.extend(more_crashes) + crash.copy_files() + + uniqlogger = get_uniq_logger(cfg.uniq_log) + uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', crash.seedfile.basename, crash.signature, crash.seednum, crash.range, crash.hd_bits, crash.hd_bytes) + logger.info('%s first seen at %d', crash.signature, crash.seednum) + else: + logger.debug('%s was found, not unique', crash.signature) + # always clean up after yourself + crash.clean_tmpdir() + + # clean up + crash.delete_files() + # whether it was unique or not, record some details for posterity + # record the details of this crash so we can regenerate it later if needed + crash.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', crash.seedfile.basename, crash.seednum, crash.range, crash.fuzzedfile.path) + crash.logger.info('PC=%s', crash.pc) + + # score this crash for the seedfile + seedfile_set.record_success(crash.seedfile.md5) + if crash.range: + # ...and for the range + crash.range.record_success(crash.signature, tries=0) + + return new_crash_count + + +def check_for_script(cfg): + if cfg.program_is_script(): + logger.warning("Target application is a shell script.") + raise + #cfg.disable_verification() + #time.sleep(10) + + +def touch_watchdog_file(cfg): + if cfg.watchdogtimeout: + # this one just checks the permission + if os.access(cfg.remote_dir, os.W_OK): + # equivalent to 'touch cfg.watchdogfile' + open(cfg.watchdogfile, 'w').close() + + +def start_process_killer(scriptpath, cfg): + # set up and spawn the process killer + killscript = cfg.get_killscript_path(scriptpath) + ProcessKiller(killscript, cfg.killprocname, cfg.killproctimeout).go() + logger.debug("Process killer started: %s %s %d", killscript, cfg.killprocname, cfg.killproctimeout) + + +<<<<<<< HEAD +======= +def add_log_handler(log_obj, level, hdlr, formatter): + hdlr.setLevel(level) + hdlr.setFormatter(formatter) + log_obj.addHandler(hdlr) +>>>>>>> still crashes but now on a temp file problem def setup_logging_to_console(log_obj, level): hdlr = logging.StreamHandler() formatter = logging.Formatter('%(name)s %(message)s') add_log_handler(log_obj, level, hdlr, formatter) +def setup_logfile(logdir, log_basename='bff.log', level=logging.DEBUG, + max_bytes=1e8, backup_count=5): + ''' + Creates a log file in / at level + @param logdir: the directory where the log file should reside + @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') + @param level: the logging level (defaults to logging.DEBUG) + ''' + filetools.make_directories(logdir) + logfile = os.path.join(logdir, log_basename) + handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) + formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") + add_log_handler(logger, level, handler, formatter) + logger.info('Logging %s at %s', logging.getLevelName(level), logfile) + + def get_config_file(basedir): config_dir = os.path.join(basedir, 'conf.d') @@ -60,21 +333,14 @@ def get_config_file(basedir): return config_file -def add_log_handler(log_obj, level, hdlr, formatter): - hdlr.setLevel(level) - hdlr.setFormatter(formatter) - log_obj.addHandler(hdlr) - - def main(): -# global START_SEED -# hashes = [] + global START_SEED + hashes = [] -# # give up if we don't have a debugger -# debuggers.verify_supported_platform() + # give up if we don't have a debugger + debuggers.verify_supported_platform() setup_logging_to_console(logger, logging.INFO) -# setup_logfile() logger.info("Welcome to BFF!") scriptpath = os.path.dirname(sys.argv[0]) @@ -82,8 +348,6 @@ def main(): # parse command line options logger.info('Parsing command line options') - - #TODO: replace OptionParser with argparse parser = OptionParser() parser.add_option('', '--debug', dest='debug', help='Turn on debugging output', action='store_true') parser.add_option('-c', '--config', dest='cfg', help='Config file location') @@ -102,9 +366,204 @@ def main(): local_cfg_file = os.path.expanduser('~/bff.cfg') filetools.copy_file(remote_cfg_file, local_cfg_file) - with Campaign(cfg_path=local_cfg_file) as campaign: - campaign.go() + # Read the cfg file + logger.info('Reading config from %s', local_cfg_file) + cfg = cfg_helper.read_config_options(local_cfg_file) + + # set up local logging + setup_logfile(cfg.local_dir, log_basename='bff.log', level=logging.DEBUG, + max_bytes=1e8, backup_count=3) + # set up remote logging + setup_logfile(cfg.output_dir, log_basename='bff.log', level=logging.INFO, + max_bytes=1e7, backup_count=5) -if __name__ == '__main__': - main() + try: + check_for_script(cfg) + except: + logger.warning("Please configure BFF to fuzz a binary. Exiting...") + sys.exit() + + z.setup_dirs_and_files(local_cfg_file, cfg) + + # make sure we cache it for the next run +# cache_state(cfg.campaign_id, 'cfg', cfg, cfg.cached_config_file) + + sr = get_cached_state('seedrange', cfg.campaign_id, cfg.cached_seedrange_file) + if not sr: + sr = SeedRange(cfg.start_seed, cfg.seed_interval, cfg.max_seed) + + # set START_SEED global for timestamping + START_SEED = sr.s1 + + start_process_killer(scriptpath, cfg) + + z.set_unbuffered_stdout() + + # set up the seedfile set so we can pick seedfiles for everything else... + seedfile_set = get_cached_state('seedfile_set', cfg.campaign_id, cfg.cached_seedfile_set) + if not seedfile_set: + logger.info('Building seedfile set') + sfs_logfile = os.path.join(cfg.seedfile_output_dir, 'seedfile_set.log') + with SeedfileSet(campaign_id=cfg.campaign_id, + originpath=cfg.seedfile_origin_dir, + localpath=cfg.seedfile_local_dir, + outputpath=cfg.seedfile_output_dir, + logfile=sfs_logfile, + ) as sfset: + seedfile_set = sfset + + # set up the watchdog timeout within the VM and restart the daemon + if cfg.watchdogtimeout: + watchdog = WatchDog(cfg.watchdogfile, cfg.watchdogtimeout) + touch_watchdog_file(cfg) + watchdog.go() + + cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) + + sf = seedfile_set.next_item() + + # Run the program once to cache it into memory + z.cache_program_once(cfg, sf.path) + + # Give target time to die + time.sleep(1) + + # flag to indicate whether this is a fresh script start up or not + first_chunk = True + + # remember our parent process id so we can tell if it changes later + _last_ppid = os.getppid() + + # campaign.go + while sr.in_max_range(): + + # wipe the tmp dir clean to try to avoid filling the VM disk + TmpReaper().clean_tmp() + + sf = seedfile_set.next_item() + + r = sf.rangefinder.next_item() + sr.set_s2() + logger.info(STATE_TIMER) + + while sr.in_range(): + # interval.go + logger.debug('Starting interval %d-%d', sr.s1, sr.s2) + + # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot + touch_watchdog_file(cfg) + + # check parent process id + _ppid_now = os.getppid() + if not _ppid_now == _last_ppid: + logger.warning('Parent process ID changed from %d to %d', _last_ppid, _ppid_now) + _last_ppid = _ppid_now + + # do the fuzz + cmdline = cfg.get_command(sf.path) + + if first_chunk: + # disable the --quiet option in zzuf + # on the first chunk only + quiet_flag = False + first_chunk = False + else: + quiet_flag = True + + STATE_TIMER.enter_state('fuzzing') + zzuf = Zzuf(cfg.local_dir, + sr.s1, + sr.s2, + cmdline, + sf.path, + cfg.zzuf_log_file, + cfg.copymode, + r.min, + r.max, + cfg.progtimeout, + quiet_flag, + ) + saw_crash = zzuf.go() + STATE_TIMER.enter_state('checking_results') + + if not saw_crash: + # we must have made it through this chunk without a crash + # so go to next chunk + try_count = sr.s1_s2_delta() + seedfile_set.record_tries(key=sf.md5, tries=try_count) + r.record_tries(tries=try_count) + + # emit a log entry + crashcount = z.get_crashcount(cfg.crashers_dir) + rate = get_rate(sr.s1) + seed_str = "seeds=%d-%d" % (sr.s1, sr.s2) + range_str = "range=%.6f-%.6f" % (r.min, r.max) + rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) + logger.info('Fuzzing %s %s %s %s crash_count=%d', + sf.path, seed_str, range_str, rate_str, crashcount) + + # set s1 to s2 so that as soon as we continue we'll break out of the sr.in_range() loop + sr.set_s1_to_s2() + continue + + # we must have seen a crash + + # get the results + zzuf_log = ZzufLog(cfg.zzuf_log_file, cfg.zzuf_log_out(sf.output_dir)) + + # Don't generate cases for killed process or out-of-memory + # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will + # report the exit code in its output log. The exit code is 128 + the signal number. + crash_status = zzuf_log.crash_logged(cfg.copymode) + sr.bookmark_s1() + sr.s1 = zzuf_log.seed + + # record the fact that we've made it this far + try_count = sr.s1_delta() + seedfile_set.record_tries(key=sf.md5, tries=try_count) + r.record_tries(tries=try_count) + + new_uniq_crash = None + if crash_status: + logger.info('Generating testcase for %s', zzuf_log.line) + # a true crash + zzuf_range = zzuf_log.range + # create the temp dir for the results + cfg.create_tmpdir() + outfile = cfg.get_testcase_outfile(sf.path, sr.s1) + logger.debug('Output file is %s', outfile) + testcase = zzuf.generate_test_case(sf.path, sr.s1, zzuf_range, outfile) + + # Do internal verification using GDB / Valgrind / Stderr + fuzzedfile = file_handlers.basicfile.BasicFile(outfile) + + with BffCrash(cfg, sf, fuzzedfile, cfg.program, cfg.debugger_timeout, + cfg.killprocname, cfg.backtracelevels, + cfg.crashers_dir, sr.s1, r) as c: + if c.is_crash: + _crash_count = verify_crasher(c, hashes, cfg, seedfile_set) + new_uniq_crash = _crash_count > 0 + + # record the zzuf log line for this crash + if not c.logger: + c.get_logger() + c.logger.debug("zzuflog: %s", zzuf_log.line) + c.logger.info('Command: %s', testcase.cmdline) + + cfg.clean_tmpdir() + + sr.increment_seed() + + # cache objects in case of reboot + cache_state(cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) + pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) + cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) + cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) + + if new_uniq_crash: + # we had a hit, so break the inner while() loop + # so we can pick a new range. This is to avoid + # having a crash-rich range run away with the + # probability before other ranges have been tried + break diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 3024fcb..9a976cb 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -11,27 +11,24 @@ from certfuzz.file_handlers.seedfile import SeedFile from certfuzz.fuzztools import filetools from certfuzz.scoring.errors import EmptySetError -from certfuzz.scoring.scorable_set import ScorableSet3 +# Using a generic name here so we can easily swap out other MAB implementations if we want to +from ..scoring.multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit as MultiArmedBandit logger = logging.getLogger(__name__) class SeedfileSet(ScorableSet3): +class SeedfileSet(MultiArmedBandit): ''' classdocs ''' def __init__(self, campaign_id=None, originpath=None, localpath=None, - outputpath='.', logfile=None, datafile=None): + outputpath='.', logfile=None): ''' Constructor ''' - - if not datafile: - datafile = os.path.join(outputpath, 'seedfile_set_data.csv') - - super(self.__class__, self).__init__(datafile=datafile) - + MultiArmedBandit.__init__(self) self.campaign_id = campaign_id self.seedfile_output_base_dir = outputpath @@ -117,7 +114,7 @@ def next_item(self): while len(self.things): logger.debug('Thing count: %d', len(self.things)) # continue until we find one that exists, or else the set is empty - sf = ScorableSet3.next(self) + sf = MultiArmedBandit.next(self) if sf.exists(): # it's still there, proceed return sf diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index 481ce18..53a09cb 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -41,9 +41,12 @@ def record_result(self, key, successes=0, trials=0): arm = self.arms[key] arm.update(successes, trials) - def record_tries(self, key=None, tries=0): + def record_tries(self, key=None, tries=1): self.record_result(key, successes=0, trials=tries) + def record_success(self, key=None, successes=1): + self.record_result(key, successes, trials=0) + @property def successes(self): return sum([a.successes for a in self.arms.values()]) diff --git a/src/certfuzz/scoring/scorable_set.py b/src/certfuzz/scoring/scorable_set.py index 5075906..b0ac0d2 100644 --- a/src/certfuzz/scoring/scorable_set.py +++ b/src/certfuzz/scoring/scorable_set.py @@ -13,14 +13,6 @@ logger = logging.getLogger(__name__) -from .multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit - - -class ScorableSet3(BayesianMultiArmedBandit): - def __init__(self, datafile=None): - self.datafile = datafile - BayesianMultiArmedBandit.__init__(self) - # Simplified reimplementation of ScorableSet with a Bayesian approach class ScorableSet2(object): From 31e17dbf8993efc3ea40a2debc3fea5c4c8819ba Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 13 Jan 2014 16:16:28 -0500 Subject: [PATCH 0190/1169] =?UTF-8?q?don=E2=80=99t=20blow=20up=20when=20yo?= =?UTF-8?q?u=20try=20to=20delete=20a=20temp=20file=20that=20is=20already?= =?UTF-8?q?=20gone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/certfuzz/debuggers/gdb.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index 9a348ff..c5abe0d 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -26,7 +26,7 @@ class GDB(Debugger): _ext = 'gdb' def __init__(self, program, cmd_args, outfile_base, timeout, killprocname, template=None, exclude_unmapped_frames=True, keep_uniq_faddr=False, **options): - super(self.__class__, self).__init__(program, cmd_args, outfile_base, timeout, killprocname, **options) + Debugger.__init__(self, program, cmd_args, outfile_base, timeout, killprocname, **options) self.template = template self.exclude_unmapped_frames = exclude_unmapped_frames self.keep_uniq_faddr = keep_uniq_faddr @@ -72,7 +72,11 @@ def _create_input_file(self): logger.warning("Failed to create GDB input file %s", self.input_file) def _remove_temp_file(self): - os.remove(self.input_file) + try: + os.remove(self.input_file) + except OSError as e: + logger.warning("Caught OSError attempting to remove %s: %s", self.input_file, e) + if os.path.exists(self.input_file): logger.warning("Failed to delete %s", self.input_file) From af705f3019e88dfddbd6d4631a4f96317ac1c11c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 14 Jan 2014 08:25:12 -0500 Subject: [PATCH 0191/1169] fix imports --- src/certfuzz/test/scoring/test_scorable_set.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/certfuzz/test/scoring/test_scorable_set.py b/src/certfuzz/test/scoring/test_scorable_set.py index 9c0072a..6a0fe24 100644 --- a/src/certfuzz/test/scoring/test_scorable_set.py +++ b/src/certfuzz/test/scoring/test_scorable_set.py @@ -5,16 +5,12 @@ ''' import unittest -import random import tempfile -from certfuzz.scoring.scorable_set import ScorableSet3 -from certfuzz.scoring.errors import ScorableSetError, EmptySetError -from certfuzz.helpers import random_str import os import shutil -import csv +from certfuzz.scoring.multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit as ScorableSet3 class Test(unittest.TestCase): From f33f6a1d5a1c9fa9b64e8bcbac0e930baccc1dbc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jan 2014 14:43:55 -0500 Subject: [PATCH 0192/1169] replace ScorableSet2 and ScorableThing in rangefinder with MultiArmedBandit --- src/certfuzz/bff/linux.py | 15 ++-- src/certfuzz/fuzztools/range.py | 9 +-- src/certfuzz/fuzztools/rangefinder.py | 36 +++++---- src/certfuzz/test/fuzztools/test_range.py | 76 +++++++++---------- .../test/fuzztools/test_rangefinder.py | 44 +++++------ 5 files changed, 89 insertions(+), 91 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 6700941..77f8418 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -204,7 +204,7 @@ def analyze_crasher(cfg, crash): return other_crashers_found -def verify_crasher(c, hashes, cfg, seedfile_set): +def verify_crasher(c, hashes, cfg, seedfile_set, rangefinder): logger.debug('verifying crash') new_crash_count = 0 @@ -257,7 +257,7 @@ def verify_crasher(c, hashes, cfg, seedfile_set): seedfile_set.record_success(crash.seedfile.md5) if crash.range: # ...and for the range - crash.range.record_success(crash.signature, tries=0) + rangefinder.record_success(crash.signature, tries=0) return new_crash_count @@ -387,7 +387,7 @@ def main(): z.setup_dirs_and_files(local_cfg_file, cfg) # make sure we cache it for the next run -# cache_state(cfg.campaign_id, 'cfg', cfg, cfg.cached_config_file) + # cache_state(cfg.campaign_id, 'cfg', cfg, cfg.cached_config_file) sr = get_cached_state('seedrange', cfg.campaign_id, cfg.cached_seedrange_file) if not sr: @@ -442,8 +442,9 @@ def main(): TmpReaper().clean_tmp() sf = seedfile_set.next_item() + rangefinder = sf.rangefinder + r = rangefinder.next_item() - r = sf.rangefinder.next_item() sr.set_s2() logger.info(STATE_TIMER) @@ -492,7 +493,7 @@ def main(): # so go to next chunk try_count = sr.s1_s2_delta() seedfile_set.record_tries(key=sf.md5, tries=try_count) - r.record_tries(tries=try_count) + rangefinder.record_tries(tries=try_count) # emit a log entry crashcount = z.get_crashcount(cfg.crashers_dir) @@ -522,7 +523,7 @@ def main(): # record the fact that we've made it this far try_count = sr.s1_delta() seedfile_set.record_tries(key=sf.md5, tries=try_count) - r.record_tries(tries=try_count) + rangefinder.record_tries(tries=try_count) new_uniq_crash = None if crash_status: @@ -542,7 +543,7 @@ def main(): cfg.killprocname, cfg.backtracelevels, cfg.crashers_dir, sr.s1, r) as c: if c.is_crash: - _crash_count = verify_crasher(c, hashes, cfg, seedfile_set) + _crash_count = verify_crasher(c, hashes, cfg, seedfile_set, rangefinder) new_uniq_crash = _crash_count > 0 # record the zzuf log line for this crash diff --git a/src/certfuzz/fuzztools/range.py b/src/certfuzz/fuzztools/range.py index 0a2a77a..b56d479 100644 --- a/src/certfuzz/fuzztools/range.py +++ b/src/certfuzz/fuzztools/range.py @@ -3,20 +3,19 @@ @organization: cert.org ''' -from ..scoring.scorable_thing import ScorableThing -class Range(ScorableThing): + +class Range(object): def __init__(self, low, high): self.min = float(low) self.max = float(high) self.mean = (self.min + self.max) / 2.0 self.span = self.max - self.min - ScorableThing.__init__(self) def __repr__(self): return '%0.6f-%0.6f' % (self.min, self.max) - def __setstate__(self, state): - self.__dict__.update(state) +# def __setstate__(self, state): +# self.__dict__.update(state) diff --git a/src/certfuzz/fuzztools/rangefinder.py b/src/certfuzz/fuzztools/rangefinder.py index 0c8624c..2eba782 100644 --- a/src/certfuzz/fuzztools/rangefinder.py +++ b/src/certfuzz/fuzztools/rangefinder.py @@ -9,14 +9,14 @@ from certfuzz.fuzztools.errors import RangeFinderError from certfuzz.fuzztools.range import Range from certfuzz.scoring.scorable_set import ScorableSet2 - +from certfuzz.scoring.multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit as MultiArmedBandit range_scale_factor = (math.sqrt(5) + 1.0) / 2.0 logger = logging.getLogger(__name__) -class RangeFinder(ScorableSet2): +class RangeFinder(MultiArmedBandit): ''' Provides facilities to maintain: 1. a set of ranges (typically from min=1.0/filesize to max=1.0-1.0/filesize) @@ -24,9 +24,8 @@ class RangeFinder(ScorableSet2): 3. a probability distribution across all ranges as well as a picker method to randomly choose a range based on the probability distribution. ''' - def __init__(self, low, high, logfile, datafile=None): - - super(self.__class__, self).__init__(datafile=datafile) + def __init__(self, low, high, logfile): + MultiArmedBandit.__init__(self) self.min = low self.max = high @@ -43,19 +42,19 @@ def __init__(self, low, high, logfile, datafile=None): self._set_ranges() - def __getstate__(self): - # we can't pickle the logger. - # But that's okay. We can get it back in __setstate__ - state = ScorableSet2.__getstate__(self) - del state['logger'] - return state - - def __setstate__(self, d): - self.__dict__.update(d) - for k, thing in self.things: - assert type(thing) == Range, 'Type is %s' % type(thing) - # recover the logger we had to drop in __getstate__ - self._set_logger() +# def __getstate__(self): +# # we can't pickle the logger. +# # But that's okay. We can get it back in __setstate__ +# state = ScorableSet2.__getstate__(self) +# del state['logger'] +# return state +# +# def __setstate__(self, d): +# self.__dict__.update(d) +# for thing in self.things.iteritems(): +# assert type(thing) == Range, 'Type is %s' % type(thing) +# # recover the logger we had to drop in __getstate__ +# self._set_logger() def _set_logger(self): self.logger = logging.getLogger(self.logfile) @@ -94,4 +93,3 @@ def _set_ranges(self): self.add_item(r.__repr__(), r) self.logger.debug('Ranges: [%s]', ', '.join([str(r) for r in self.things.keys()])) - diff --git a/src/certfuzz/test/fuzztools/test_range.py b/src/certfuzz/test/fuzztools/test_range.py index ac86e7a..4b0462a 100644 --- a/src/certfuzz/test/fuzztools/test_range.py +++ b/src/certfuzz/test/fuzztools/test_range.py @@ -24,44 +24,44 @@ def test_init(self): def test_repr(self): self.assertEqual(self.r.__repr__(), '0.000000-1.000000') - def test_getstate_is_pickle_friendly(self): - # getstate should return a pickleable object - import pickle - state = self.r.__getstate__() - try: - pickle.dumps(state) - except Exception, e: - self.fail('Failed to pickle state: %s' % e) - - def test_getstate_has_all_expected_items(self): - state = self.r.__getstate__() - for k, v in self.r.__dict__.iteritems(): - # make sure we're deleting what we need to - if k in ['logger']: - self.assertFalse(k in state) - else: - self.assertTrue(k in state, '%s not found' % k) - self.assertEqual(state[k], v) - - def test_getstate(self): - state = self.r.__getstate__() - self.assertEqual(dict, type(state)) - print 'as dict...' - pprint.pprint(state) - - def test_to_json(self): - as_json = self.r.to_json(indent=4) - - print 'as JSON...' - for l in as_json.splitlines(): - print l - - from_json = json.loads(as_json) - - # make sure we can round-trip it - for k, v in self.r.__getstate__().iteritems(): - self.assertTrue(k in from_json) - self.assertEqual(from_json[k], v) +# def test_getstate_is_pickle_friendly(self): +# # getstate should return a pickleable object +# import pickle +# state = self.r.__getstate__() +# try: +# pickle.dumps(state) +# except Exception, e: +# self.fail('Failed to pickle state: %s' % e) +# +# def test_getstate_has_all_expected_items(self): +# state = self.r.__getstate__() +# for k, v in self.r.__dict__.iteritems(): +# # make sure we're deleting what we need to +# if k in ['logger']: +# self.assertFalse(k in state) +# else: +# self.assertTrue(k in state, '%s not found' % k) +# self.assertEqual(state[k], v) +# +# def test_getstate(self): +# state = self.r.__getstate__() +# self.assertEqual(dict, type(state)) +# print 'as dict...' +# pprint.pprint(state) +# +# def test_to_json(self): +# as_json = self.r.to_json(indent=4) +# +# print 'as JSON...' +# for l in as_json.splitlines(): +# print l +# +# from_json = json.loads(as_json) +# +# # make sure we can round-trip it +# for k, v in self.r.__getstate__().iteritems(): +# self.assertTrue(k in from_json) +# self.assertEqual(from_json[k], v) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/certfuzz/test/fuzztools/test_rangefinder.py b/src/certfuzz/test/fuzztools/test_rangefinder.py index ac6bfac..cb1c92b 100644 --- a/src/certfuzz/test/fuzztools/test_rangefinder.py +++ b/src/certfuzz/test/fuzztools/test_rangefinder.py @@ -67,28 +67,28 @@ def test_range_mean(self): # mean should be halfway between min and max [self.assertAlmostEqual(x.mean, ((x.max + x.min) / 2)) for x in self.r.things.values()] - def test_getstate_is_pickle_friendly(self): - # getstate should return a pickleable object - import pickle - state = self.r.__getstate__() - try: - pickle.dumps(state) - except Exception, e: - self.fail('Failed to pickle state: %s' % e) - - def test_getstate_has_all_expected_items(self): - state = self.r.__getstate__() - for k, v in self.r.__dict__.iteritems(): - # make sure we're deleting what we need to - if k in ['logger']: - self.assertFalse(k in state) - else: - self.assertTrue(k in state, '%s not found' % k) - self.assertEqual(type(state[k]), type(v)) - - def test_getstate(self): - state = self.r.__getstate__() - self.assertEqual(dict, type(state)) +# def test_getstate_is_pickle_friendly(self): +# # getstate should return a pickleable object +# import pickle +# state = self.r.__getstate__() +# try: +# pickle.dumps(state) +# except Exception, e: +# self.fail('Failed to pickle state: %s' % e) +# +# def test_getstate_has_all_expected_items(self): +# state = self.r.__getstate__() +# for k, v in self.r.__dict__.iteritems(): +# # make sure we're deleting what we need to +# if k in ['logger']: +# self.assertFalse(k in state) +# else: +# self.assertTrue(k in state, '%s not found' % k) +# self.assertEqual(type(state[k]), type(v)) +# +# def test_getstate(self): +# state = self.r.__getstate__() +# self.assertEqual(dict, type(state)) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From b37ceb66993ce5932929fc9cc00a6938136955b2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jan 2014 14:54:40 -0500 Subject: [PATCH 0193/1169] reboot recovery is pretty well broken at this point due to ripping out ScorableSet2. Looks like it runs otherwise. --- src/certfuzz/bff/linux.py | 31 +++++----- src/certfuzz/fuzztools/range.py | 2 +- src/certfuzz/fuzztools/rangefinder.py | 5 +- src/certfuzz/test/campaign/test_campaign.py | 56 +++++++++---------- .../test/file_handlers/test_seedfile.py | 22 ++++---- .../test/fuzztools/test_rangefinder.py | 16 ++++-- 6 files changed, 71 insertions(+), 61 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 77f8418..fff07c9 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -204,7 +204,7 @@ def analyze_crasher(cfg, crash): return other_crashers_found -def verify_crasher(c, hashes, cfg, seedfile_set, rangefinder): +def verify_crasher(c, hashes, cfg, seedfile_set, rangefinder, r): logger.debug('verifying crash') new_crash_count = 0 @@ -254,10 +254,10 @@ def verify_crasher(c, hashes, cfg, seedfile_set, rangefinder): crash.logger.info('PC=%s', crash.pc) # score this crash for the seedfile - seedfile_set.record_success(crash.seedfile.md5) + seedfile_set.record_success(key=crash.seedfile.md5) if crash.range: # ...and for the range - rangefinder.record_success(crash.signature, tries=0) + rangefinder.record_success(key=r.id) return new_crash_count @@ -389,10 +389,12 @@ def main(): # make sure we cache it for the next run # cache_state(cfg.campaign_id, 'cfg', cfg, cfg.cached_config_file) - sr = get_cached_state('seedrange', cfg.campaign_id, cfg.cached_seedrange_file) + sr = None +# sr = get_cached_state('seedrange', cfg.campaign_id, cfg.cached_seedrange_file) if not sr: sr = SeedRange(cfg.start_seed, cfg.seed_interval, cfg.max_seed) + # set START_SEED global for timestamping START_SEED = sr.s1 @@ -400,8 +402,9 @@ def main(): z.set_unbuffered_stdout() - # set up the seedfile set so we can pick seedfiles for everything else... - seedfile_set = get_cached_state('seedfile_set', cfg.campaign_id, cfg.cached_seedfile_set) + seedfile_set = None +# # set up the seedfile set so we can pick seedfiles for everything else... +# seedfile_set = get_cached_state('seedfile_set', cfg.campaign_id, cfg.cached_seedfile_set) if not seedfile_set: logger.info('Building seedfile set') sfs_logfile = os.path.join(cfg.seedfile_output_dir, 'seedfile_set.log') @@ -419,7 +422,7 @@ def main(): touch_watchdog_file(cfg) watchdog.go() - cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) +# cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) sf = seedfile_set.next_item() @@ -493,7 +496,7 @@ def main(): # so go to next chunk try_count = sr.s1_s2_delta() seedfile_set.record_tries(key=sf.md5, tries=try_count) - rangefinder.record_tries(tries=try_count) + rangefinder.record_tries(key=r.id, tries=try_count) # emit a log entry crashcount = z.get_crashcount(cfg.crashers_dir) @@ -523,7 +526,7 @@ def main(): # record the fact that we've made it this far try_count = sr.s1_delta() seedfile_set.record_tries(key=sf.md5, tries=try_count) - rangefinder.record_tries(tries=try_count) + rangefinder.record_tries(key=r.id, tries=try_count) new_uniq_crash = None if crash_status: @@ -543,7 +546,7 @@ def main(): cfg.killprocname, cfg.backtracelevels, cfg.crashers_dir, sr.s1, r) as c: if c.is_crash: - _crash_count = verify_crasher(c, hashes, cfg, seedfile_set, rangefinder) + _crash_count = verify_crasher(c, hashes, cfg, seedfile_set, rangefinder, r) new_uniq_crash = _crash_count > 0 # record the zzuf log line for this crash @@ -557,10 +560,10 @@ def main(): sr.increment_seed() # cache objects in case of reboot - cache_state(cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) - pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) - cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) - cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) +# cache_state(cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) +# pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) +# cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) +# cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) if new_uniq_crash: # we had a hit, so break the inner while() loop diff --git a/src/certfuzz/fuzztools/range.py b/src/certfuzz/fuzztools/range.py index b56d479..89057dd 100644 --- a/src/certfuzz/fuzztools/range.py +++ b/src/certfuzz/fuzztools/range.py @@ -8,7 +8,7 @@ class Range(object): def __init__(self, low, high): - + self.id = id(self) self.min = float(low) self.max = float(high) self.mean = (self.min + self.max) / 2.0 diff --git a/src/certfuzz/fuzztools/rangefinder.py b/src/certfuzz/fuzztools/rangefinder.py index 2eba782..600bce4 100644 --- a/src/certfuzz/fuzztools/rangefinder.py +++ b/src/certfuzz/fuzztools/rangefinder.py @@ -90,6 +90,9 @@ def _set_ranges(self): ranges.append(merged_range) for r in ranges: - self.add_item(r.__repr__(), r) + self.add_item(r.id, r) self.logger.debug('Ranges: [%s]', ', '.join([str(r) for r in self.things.keys()])) + + def next_item(self): + return self.next() diff --git a/src/certfuzz/test/campaign/test_campaign.py b/src/certfuzz/test/campaign/test_campaign.py index f5c444f..1795cc3 100644 --- a/src/certfuzz/test/campaign/test_campaign.py +++ b/src/certfuzz/test/campaign/test_campaign.py @@ -72,38 +72,38 @@ def test_init(self): self.assertEqual('%s.%s' % (campaign.packages['debuggers'], 'debuggermodule'), self.campaign.debugger_module_name) - def test_getstate(self): - self.campaign.seedfile_set = Mock() - - # get_state should return a pickleable result - state = self.campaign.__getstate__() - - import pickle - try: - pickle.dumps(state) - except Exception, e: - self.fail(e) +# def test_getstate(self): +# self.campaign.seedfile_set = Mock() +# +# # get_state should return a pickleable result +# state = self.campaign.__getstate__() +# +# import pickle +# try: +# pickle.dumps(state) +# except Exception, e: +# self.fail(e) def counter(self, *args): self.count += 1 - def test_save_state(self): - - # make sure we can write - self.assertFalse(os.path.exists(self.campaign.cached_state_file)) - self.campaign._save_state() - self.assertTrue(os.path.exists(self.campaign.cached_state_file)) - - def test_set_state(self): - state = {'crashes_seen': [1, 2, 3, 3], - 'seedfile_set': {'things': {}}, - 'id': 2134, - 'seed_dir_in': mkdtemp(dir=self.tmpdir), - 'seed_dir_local': mkdtemp(dir=self.tmpdir), - 'sf_set_out': tempfile.mktemp(dir=self.tmpdir)[1], - } - self.campaign.__setstate__(state) - self.assertEqual(self.campaign.crashes_seen, set([1, 2, 3])) +# def test_save_state(self): +# +# # make sure we can write +# self.assertFalse(os.path.exists(self.campaign.cached_state_file)) +# self.campaign._save_state() +# self.assertTrue(os.path.exists(self.campaign.cached_state_file)) +# +# def test_set_state(self): +# state = {'crashes_seen': [1, 2, 3, 3], +# 'seedfile_set': {'things': {}}, +# 'id': 2134, +# 'seed_dir_in': mkdtemp(dir=self.tmpdir), +# 'seed_dir_local': mkdtemp(dir=self.tmpdir), +# 'sf_set_out': tempfile.mktemp(dir=self.tmpdir)[1], +# } +# self.campaign.__setstate__(state) +# self.assertEqual(self.campaign.crashes_seen, set([1, 2, 3])) def test_write_version(self): vf = os.path.join(self.campaign.outdir, 'version.txt') diff --git a/src/certfuzz/test/file_handlers/test_seedfile.py b/src/certfuzz/test/file_handlers/test_seedfile.py index 651035f..c5cef6c 100644 --- a/src/certfuzz/test/file_handlers/test_seedfile.py +++ b/src/certfuzz/test/file_handlers/test_seedfile.py @@ -28,17 +28,17 @@ def tearDown(self): def test_init(self): self.assertEqual(self.sf.output_dir, os.path.join(self.dir, self.sf.md5)) - def test_getstate(self): - self.assertEqual(RangeFinder, type(self.sf.rangefinder)) - state = self.sf.__getstate__() - self.assertEqual(dict, type(state)) - self.assertEqual(dict, type(state['rangefinder'])) - - def test_setstate(self): - state = self.sf.__getstate__() - self.sf.__setstate__(state) - # make sure we restore rangefinder - self.assertEqual(RangeFinder, type(self.sf.rangefinder)) +# def test_getstate(self): +# self.assertEqual(RangeFinder, type(self.sf.rangefinder)) +# state = self.sf.__getstate__() +# self.assertEqual(dict, type(state)) +# self.assertEqual(dict, type(state['rangefinder'])) +# +# def test_setstate(self): +# state = self.sf.__getstate__() +# self.sf.__setstate__(state) +# # make sure we restore rangefinder +# self.assertEqual(RangeFinder, type(self.sf.rangefinder)) if __name__ == "__main__": diff --git a/src/certfuzz/test/fuzztools/test_rangefinder.py b/src/certfuzz/test/fuzztools/test_rangefinder.py index cb1c92b..ceee034 100644 --- a/src/certfuzz/test/fuzztools/test_rangefinder.py +++ b/src/certfuzz/test/fuzztools/test_rangefinder.py @@ -31,10 +31,10 @@ def test_get_ranges(self): ranges = self._ranges() # the high end of the last range should be the max - self.assertAlmostEqual(ranges[-1].max, self.max) + self.assertAlmostEqual(ranges[-1].max, self.max, 3) # the low end of the first range should be the min - self.assertAlmostEqual(ranges[0].min, self.min) + self.assertAlmostEqual(ranges[0].min, self.min, 3) # make sure the internal ranges match up for (this, next_element) in zip(ranges[:-1], ranges[1:]): @@ -50,22 +50,26 @@ def test_get_ranges(self): self.assertAlmostEqual(ranges[1].max, 0.999) def _ranges(self): - keys = sorted(self.r.things.keys()) + minkeys = sorted([(v.min, k) for (k, v) in self.r.things.iteritems()]) + keys = [k[1] for k in minkeys] return [self.r.things[k] for k in keys] def test_range_orderings(self): # first term should be smaller than second term ranges = self.r.things.values() - [self.assertTrue(x.min <= x.max) for x in ranges] + for x in ranges: + self.assertTrue(x.min <= x.max) def test_range_overlaps(self): # this one's min should be the next_element one's max ranges = self._ranges() - [self.assertEqual(x.min, y.max) for (x, y) in zip(ranges[1:], ranges[:-1])] + for (x, y) in zip(ranges[1:], ranges[:-1]): + self.assertEqual(x.min, y.max) def test_range_mean(self): # mean should be halfway between min and max - [self.assertAlmostEqual(x.mean, ((x.max + x.min) / 2)) for x in self.r.things.values()] + for x in self.r.things.values(): + self.assertAlmostEqual(x.mean, ((x.max + x.min) / 2)) # def test_getstate_is_pickle_friendly(self): # # getstate should return a pickleable object From cbdcce0ddebb5a99bb932857e45464240a03f2f3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jan 2014 16:10:34 -0500 Subject: [PATCH 0194/1169] =?UTF-8?q?remove=20scorable=5Fset=20and=20scora?= =?UTF-8?q?ble=5Fthing=20objects.=20They=E2=80=99ve=20been=20superseded=20?= =?UTF-8?q?by=20multiarmed=5Fbandits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/certfuzz/scoring/errors.py | 8 - .../scoring/multiarmed_bandit/errors.py | 1 + src/certfuzz/scoring/scorable_set.py | 143 ------------------ src/certfuzz/scoring/scorable_thing.py | 123 --------------- .../test/scoring/test_scorable_set.py | 120 --------------- .../test/scoring/test_scorable_thing.py | 115 -------------- 6 files changed, 1 insertion(+), 509 deletions(-) delete mode 100644 src/certfuzz/scoring/scorable_set.py delete mode 100644 src/certfuzz/scoring/scorable_thing.py delete mode 100644 src/certfuzz/test/scoring/test_scorable_set.py delete mode 100644 src/certfuzz/test/scoring/test_scorable_thing.py diff --git a/src/certfuzz/scoring/errors.py b/src/certfuzz/scoring/errors.py index 25569d3..7e7de41 100644 --- a/src/certfuzz/scoring/errors.py +++ b/src/certfuzz/scoring/errors.py @@ -8,11 +8,3 @@ class ScoringError(CERTFuzzError): pass - - -class ScorableSetError(ScoringError): - pass - - -class EmptySetError(ScorableSetError): - pass diff --git a/src/certfuzz/scoring/multiarmed_bandit/errors.py b/src/certfuzz/scoring/multiarmed_bandit/errors.py index 698faf6..74a0696 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/errors.py +++ b/src/certfuzz/scoring/multiarmed_bandit/errors.py @@ -5,5 +5,6 @@ ''' from ..errors import ScoringError + class MultiArmedBanditError(ScoringError): pass diff --git a/src/certfuzz/scoring/scorable_set.py b/src/certfuzz/scoring/scorable_set.py deleted file mode 100644 index b0ac0d2..0000000 --- a/src/certfuzz/scoring/scorable_set.py +++ /dev/null @@ -1,143 +0,0 @@ -''' -Created on Sep 9, 2011 - -@organization: cert.org -''' -import logging -import csv -import os -import time - -from ..fuzztools.probability import weighted_choice -from .errors import ScorableSetError, EmptySetError - -logger = logging.getLogger(__name__) - - -# Simplified reimplementation of ScorableSet with a Bayesian approach -class ScorableSet2(object): - ''' - ''' - def __init__(self, datafile=None): - self.things = {} - self.scaled_score = {} - self.datafile = datafile - self.expected_crash_density = None - - def add_item(self, key, thing): - if not hasattr(thing, 'probability'): - raise ScorableSetError('Items must have a "probability" attribute.') - - self.things[key] = thing - - def del_item(self, key): - for d in (self.things, self.scaled_score): - try: - del d[key] - except KeyError: - # if there was a keyerror, our job is already done - pass - - def _sum_scores(self): - return sum([thing.probability for thing in self.things.itervalues()]) - - def _update_probabilities(self): - total = self._sum_scores() - - crash_densities = [] - for key, thing in self.things.iteritems(): - p = thing.probability - score = p / total - self.scaled_score[key] = score - logger.debug('probability(%s)=%f', key, score) - crash_densities.append(p * score) - self.expected_crash_density = sum(crash_densities) - - def next_key(self): - if not len(self.things): - raise EmptySetError - self._update_probabilities() - choice = weighted_choice(self.scaled_score) - logger.debug('next_key=%s', choice) - return choice - - def next_item(self): - next_key = self.next_key() - - try: - next_thing = self.things[next_key] - except KeyError: - self.del_item(next_key) - return self.next_item() - - if not next_thing: - # next_thing must be an actual thing - self.del_item(next_key) - return self.next_item() - - # if you got here, next_thing is not None - return next_thing - - def status(self): - status = [] - for k in sorted(self.things.keys()): - thing = self.things[k] - status.append((k, self.scaled_score[k], thing.successes, thing.tries, thing.probability)) - return status - - def __getstate__(self): - state = self.__dict__.copy() - state['scaled_score'] = self.scaled_score.copy() - state['things'] = self.things.copy() - for k, thing in self.things.iteritems(): - if hasattr(thing, '__getstate__'): - state['things'][k] = thing.__getstate__() - elif hasattr(thing, '__dict__'): - state['things'][k] = thing.__dict__.copy() - else: - state['things'][k] = thing - - return state - - def _read_csv(self): - ''' - Reads in self.datafile and returns a list of dicts from its contents. - Assumes data is compatible with that written by csv.DictWriter() - Returns an empty list if the file is not found or any IOErrors occur - in opening or reading. - ''' - if not self.datafile: - raise ScorableSetError('Scorable Set datafile not set.') - - data = [] - if os.path.exists(self.datafile): - try: - with open(self.datafile, 'rb') as f: - reader = csv.DictReader(f) - data = list(reader) - except IOError, e: - logger.warning('Unable to read from %s, proceeding without it: %s' % (self.datafile, e)) - return data - - def update_csv(self): - if not self.datafile: - raise ScorableSetError('Scorable Set datafile not set.') - - row = self.scaled_score.copy() - - sorted_keys = sorted(self.scaled_score.keys()) - - row['timestamp'] = time.asctime() - sorted_keys.insert(0, 'timestamp') - - # the file doesn't exist or is empty, so create it and add headers - if not os.path.exists(self.datafile) or not os.path.getsize(self.datafile): - # note use of 'b' in mode. see - # http://stackoverflow.com/questions/3191528/csv-in-python-adding-extra-carriage-return - with open(self.datafile, 'wb') as datafile: - writer = csv.writer(datafile) - writer.writerow(sorted_keys) - - with open(self.datafile, 'ab') as datafile: - writer = csv.DictWriter(datafile, fieldnames=sorted_keys) - writer.writerow(row) diff --git a/src/certfuzz/scoring/scorable_thing.py b/src/certfuzz/scoring/scorable_thing.py deleted file mode 100644 index cdcb692..0000000 --- a/src/certfuzz/scoring/scorable_thing.py +++ /dev/null @@ -1,123 +0,0 @@ -''' -Created on Mar 26, 2012 - -@organization: cert.org -''' -import json -from ..helpers import random_str - - -def beta_estimate(m, N, a_prior=1.0, b_prior=1.0): - numerator = alpha = m + a_prior - l = N - m - denominator = m + a_prior + l + b_prior - beta = denominator - alpha - p_success = float(numerator) / float(denominator) - return (alpha, beta, p_success) - - -class ScorableThing(object): - def __init__(self, key=None, a=None, b=None, uniques_only=True): - ''' - - All parameters are optional - @param key: A string to associate with this thing - @param a: parameter for Beta distribution - @param b: parameter for Beta distribution - @param uniques_only: If False, count all successes. Otherwise count - only unique ones. - ''' - if not key: - self.key = 'scorable_thing_' + random_str(8) - else: - self.key = key - - self.a = a or 1 - self.b = b or 1 - self.uniques_only = uniques_only - self.seen = {} - self.successes = 0 - self.tries = 0 - self.probability = beta_estimate(0, 0, self.a, self.b)[2] - - def __repr__(self): - return self.key - - def record_failure(self, tries=1): - ''' - Convenience method for recording failed trials - @param tries: number of trials - ''' - self.record_tries(tries) - - def record_tries(self, tries=0): - self.record_result(0, tries) - - def record_success(self, key, tries=1): - ''' - Convenience method for recording successful trials - @param key: a key (string) of the thing you're recording - @param successes: number of successes - @param tries: number of trials - ''' - is_new = False - try: - self.seen[key] += 1 - except KeyError: - self.seen[key] = 1 - is_new = True - - if is_new or not self.uniques_only: - # this one is new, so update the stats - self.record_result(1, tries) - else: - self.record_failure(tries) - - def record_result(self, successes=0, tries=0): - ''' - Records successes or failures. - - @param successes: number of successes - @param tries: number of trials - ''' - - self.successes += successes - self.tries += tries - self.update(m=successes, N=tries) - - def update(self, m=0, N=0): - a, b, p = beta_estimate(m, N, self.a, self.b) - self.a = a - self.b = b - self.probability = p - - def doubt(self, factor=None): - ''' - Refactor a and b parameters, reducing them by the specified - factor (defaults to using the value of a as the factor, such that - a becomes 1 and b becomes b/a). Will always prefer the lesser of the - factor specified or the current value of a. (can't wind up with a < 1). - - This has the effect of maintaining the probability, but allowing new - observations to influence our estimated parameters more than they would - have otherwise. Essentially, it allows us to more easily reconfirm or - adjust our beliefs by injecting doubt in what we think we know. - - @param factor: the factor to divide by - ''' - import math - - if not factor: - _factor = float(self.a) - else: - _factor = float(min((factor, self.a))) - - self.a = int(math.ceil(self.a / _factor)) - self.b = int(math.ceil(self.b / _factor)) - - def __getstate__(self): - return self.__dict__.copy() - - def to_json(self, sort_keys=True, indent=None): - state = self.__getstate__() - return json.dumps(state, sort_keys=sort_keys, indent=indent) diff --git a/src/certfuzz/test/scoring/test_scorable_set.py b/src/certfuzz/test/scoring/test_scorable_set.py deleted file mode 100644 index 6a0fe24..0000000 --- a/src/certfuzz/test/scoring/test_scorable_set.py +++ /dev/null @@ -1,120 +0,0 @@ -''' -Created on Mar 26, 2012 - -@organization: cert.org -''' - -import unittest -import tempfile - -import os -import shutil - -from certfuzz.scoring.multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit as ScorableSet3 - -class Test(unittest.TestCase): - - def setUp(self): - self.ss = ScorableSet3() - self.things = [] - self.tmpdir = tempfile.mkdtemp() - fd, f = tempfile.mkstemp(dir=self.tmpdir) - os.close(fd) - os.remove(f) - self.tmpfile = f - - for _x in xrange(5): - thing = 'thing_%d' % _x - self.ss.add_item(thing, _x) - self.things.append(thing) - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - def test_scaled_score(self): -# self.ss._update_probabilities() - score_sum = sum([x for x in self.ss._scaled_scores().itervalues()]) - self.assertAlmostEqual(score_sum, 1.0) - - def test_del_item(self): - # progressively delete all the things and make sure they go away - # as expected - while self.things: - n_before = len(self.things) - self.assertEqual(len(self.ss.things), n_before) - thing_to_del = self.things.pop() - self.ss.del_item(thing_to_del) - n_after = n_before - 1 - self.assertEqual(len(self.things), n_after) - self.assertEqual(len(self.ss.things), n_after) - - def test_add_item(self): - ss = ScorableSet3() - for _value in xrange(100): - key = 'thing_%d' % _value - self.assertEqual(len(ss.things), _value) - ss.add_item(key, _value) - self.assertEqual(len(ss.things), _value + 1) - self.assertTrue(key in ss.things) - self.assertTrue(_value in ss.things.values()) - - def test_empty_set(self): - ss = ScorableSet3() - self.assertRaises(StopIteration, ss.next) - -# def test_read_csv(self): -# self.assertRaises(ScorableSetError, self.ss._read_csv) -# -# self.ss.datafile = self.tmpfile -# d = {'x': 1, 'y': 2, 'z': 3} -# keys = list(d.keys()) -# -# with open(self.ss.datafile, 'wb') as datafile: -# writer = csv.writer(datafile) -# writer.writerow(keys) -# row = [d[k] for k in keys] -# writer.writerow(row) -# -# read_csv = self.ss._read_csv() -# d_out = read_csv.pop(0) -# for k in keys: -# self.assertTrue(k in d_out) -# self.assertEqual(d[k], int(d_out[k])) - -# def test_update_csv(self): -# self.ss._update_probabilities() -# # raise error if datafile is undefined -# self.assertRaises(ScorableSetError, self.ss.update_csv) -# -# self.ss.datafile = self.tmpfile -# self.assertFalse(os.path.exists(self.tmpfile)) -# -# # make sure it can create a file from scratch -# self.ss.update_csv() -# self.assertTrue(os.path.exists(self.tmpfile)) -# with open(self.ss.datafile, 'rb') as f: -# data = list(csv.DictReader(f)) -# -# self.assertEqual(len(data), 1) -# for row in data: -# for k in self.ss.things.keys(): -# self.assertTrue(k in row) -# -# # make sure it adds a second row -# self.ss.update_csv() -# with open(self.ss.datafile, 'rb') as f: -# data = list(csv.DictReader(f)) -# -# self.assertEqual(len(data), 2) -# for row in data: -# for k in self.ss.things.keys(): -# self.assertTrue(k in row) -# -# self.ss.update_csv() -# # we should be at 4 lines total now (1 header, 3 data) -# with open(self.ss.datafile, 'rb') as f: -# self.assertEqual(len(f.readlines()), 4) - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/scoring/test_scorable_thing.py b/src/certfuzz/test/scoring/test_scorable_thing.py deleted file mode 100644 index 546dc32..0000000 --- a/src/certfuzz/test/scoring/test_scorable_thing.py +++ /dev/null @@ -1,115 +0,0 @@ -''' -Created on Apr 10, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.scoring.scorable_thing import ScorableThing - - -class Test(unittest.TestCase): - - def setUp(self): - self.thing1 = ScorableThing() - self.thing2 = ScorableThing(key='Thing2') - - def tearDown(self): - pass - - def test_init(self): - self.assertTrue(self.thing1.key.startswith('scorable_thing_')) - self.assertEqual(self.thing2.key, 'Thing2') - - self.assertEqual(0, self.thing1.successes) - self.assertEqual(0, self.thing1.tries) - self.assertEqual(0.5, self.thing1.probability) - - def test_repr(self): - self.assertEqual(self.thing2.__repr__(), 'Thing2') - - def test_record_failure(self): - self.assertEqual(0, self.thing1.tries) - self.assertEqual(0, self.thing1.successes) - self.thing1.record_failure() - self.assertEqual(1, self.thing1.tries) - self.assertEqual(0, self.thing1.successes) - # with tries as keyword - self.thing1.record_failure(tries=3) - self.assertEqual(4, self.thing1.tries) - self.assertEqual(0, self.thing1.successes) - # with tries, no keyword - self.thing1.record_failure(32) - self.assertEqual(36, self.thing1.tries) - self.assertEqual(0, self.thing1.successes) - - def test_record_success(self): - self.assertEqual(0, self.thing1.tries) - self.assertEqual(0, self.thing1.successes) - self.thing1.record_success('a') - self.assertEqual(1, self.thing1.tries) - self.assertEqual(1, self.thing1.successes) - # with tries as keyword - self.thing1.record_success('b', tries=3) - self.assertEqual(4, self.thing1.tries) - self.assertEqual(2, self.thing1.successes) - # with tries, no keyword - self.thing1.record_success('c', 32) - self.assertEqual(36, self.thing1.tries) - self.assertEqual(3, self.thing1.successes) - # repeat - self.thing1.record_success('c') - self.assertEqual(37, self.thing1.tries) - self.assertEqual(3, self.thing1.successes) - self.thing1.record_success('a', 3) - self.assertEqual(40, self.thing1.tries) - self.assertEqual(3, self.thing1.successes) - - def test_record_result(self): - self.assertEqual(0, self.thing1.tries) - self.assertEqual(0, self.thing1.successes) - self.thing1.record_result(successes=0, tries=0) - self.assertEqual(0, self.thing1.tries) - self.assertEqual(0, self.thing1.successes) - - self.thing1.record_result(successes=0, tries=1) - self.assertEqual(1, self.thing1.tries) - self.assertEqual(0, self.thing1.successes) - - self.thing1.record_result(successes=1, tries=1) - self.assertEqual(2, self.thing1.tries) - self.assertEqual(1, self.thing1.successes) - - def test_update(self): - self.assertEqual(1, self.thing1.a) - self.assertEqual(1, self.thing1.b) - self.assertEqual(0.5, self.thing1.probability) - self.thing1.update(1, 1) - self.assertEqual(2, self.thing1.a) - self.assertEqual(1, self.thing1.b) - self.assertAlmostEqual(0.667, self.thing1.probability, places=3) - self.thing1.update(1, 10) - self.assertEqual(3, self.thing1.a) - self.assertEqual(10, self.thing1.b) - self.assertAlmostEqual(0.231, self.thing1.probability, places=3) - - def test_getstate_is_picklable(self): - # getstate should return picklable thing - import cPickle - try: - cPickle.dumps(self.thing1.__getstate__()) - except: - self.fail('Unable to pickle __getstate__ result') - - def test_getstate_returns_dict(self): - self.assertEqual(dict, type(self.thing1.__getstate__())) - self.assertEqual(self.thing1.__getstate__(), self.thing1.__dict__) - - def test_to_json(self): - try: - self.assertEqual(str, type(self.thing1.to_json())) - except Exception, e: - self.fail('json.dumps failed: %s' % e) - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From abc189629e5462e56b29e37d92675d99bd5324cc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jan 2014 16:26:49 -0500 Subject: [PATCH 0195/1169] fix imports --- src/certfuzz/iteration/iteration_android.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/iteration/iteration_android.py b/src/certfuzz/iteration/iteration_android.py index 2dde6b1..0f6c88e 100644 --- a/src/certfuzz/iteration/iteration_android.py +++ b/src/certfuzz/iteration/iteration_android.py @@ -15,9 +15,9 @@ from ..fuzzers.bytemut import ByteMutFuzzer from ..fuzztools.filetools import find_or_create_dir from ..runners.android_runner import AndroidRunner -from ..runners import RunnerError -from .iteration_base import IterationBase2 -from ..fuzzers import FuzzerExhaustedError +from ..runners.errors import RunnerError +from .iteration_base import IterationBase +from ..fuzzers.errors import FuzzerExhaustedError import logging import os @@ -75,7 +75,7 @@ def do_iteration(iter_args): return True -class AndroidIteration(IterationBase2): +class AndroidIteration(IterationBase): def __init__(self, campaign_id=None, db_config=None, num=0, fuzzopts=None, runopts=None, sf=None, emu_handle=None, sf_dir=None, intent=None): self.campaign_id = campaign_id From 069d854aaa8c08fb23437fa6eedc0dd05dc4b868 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 17 Feb 2014 16:15:04 -0500 Subject: [PATCH 0196/1169] remove unused imports --- src/certfuzz/bff/linux.py | 1 - src/certfuzz/fuzztools/rangefinder.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index fff07c9..fc8551a 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -36,7 +36,6 @@ from ..file_handlers.seedfile_set import SeedfileSet from ..file_handlers.tmp_reaper import TmpReaper from ..fuzztools import bff_helper as z, filetools, performance -from ..fuzztools.object_caching import cache_state, get_cached_state from ..fuzztools.process_killer import ProcessKiller from ..fuzztools.seedrange import SeedRange from ..fuzztools.state_timer import StateTimer diff --git a/src/certfuzz/fuzztools/rangefinder.py b/src/certfuzz/fuzztools/rangefinder.py index 600bce4..b0c3ef3 100644 --- a/src/certfuzz/fuzztools/rangefinder.py +++ b/src/certfuzz/fuzztools/rangefinder.py @@ -8,7 +8,6 @@ from certfuzz.fuzztools.errors import RangeFinderError from certfuzz.fuzztools.range import Range -from certfuzz.scoring.scorable_set import ScorableSet2 from certfuzz.scoring.multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit as MultiArmedBandit range_scale_factor = (math.sqrt(5) + 1.0) / 2.0 From 2d2054739b4f7b7c9961a66ab39a65cc550c2ebb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 17 Feb 2014 16:15:49 -0500 Subject: [PATCH 0197/1169] replace error --- src/certfuzz/file_handlers/errors.py | 4 ++++ src/certfuzz/file_handlers/seedfile_set.py | 8 +++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/file_handlers/errors.py b/src/certfuzz/file_handlers/errors.py index 149457d..28018fe 100644 --- a/src/certfuzz/file_handlers/errors.py +++ b/src/certfuzz/file_handlers/errors.py @@ -24,3 +24,7 @@ class SeedFileError(BasicFileError): class DirectoryError(FileHandlerError): pass + + +class SeedfileSetError(FileHandlerError): + pass diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 9a976cb..4f0d9a3 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -7,18 +7,16 @@ import os from certfuzz.file_handlers.directory import Directory -from certfuzz.file_handlers.errors import SeedFileError +from certfuzz.file_handlers.errors import SeedFileError, SeedfileSetError from certfuzz.file_handlers.seedfile import SeedFile from certfuzz.fuzztools import filetools -from certfuzz.scoring.errors import EmptySetError # Using a generic name here so we can easily swap out other MAB implementations if we want to -from ..scoring.multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit as MultiArmedBandit +from certfuzz.scoring.multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit as MultiArmedBandit logger = logging.getLogger(__name__) -class SeedfileSet(ScorableSet3): class SeedfileSet(MultiArmedBandit): ''' classdocs @@ -109,7 +107,7 @@ def next_item(self): seedfiles from the set ''' if not len(self.things): - raise EmptySetError + raise SeedfileSetError while len(self.things): logger.debug('Thing count: %d', len(self.things)) From 96592f91ee3bc061bd7027cb80eda053373a294c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 10 Mar 2014 11:22:48 -0400 Subject: [PATCH 0198/1169] remove unused imports --- src/certfuzz/test/file_handlers/test_seedfile_set.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/certfuzz/test/file_handlers/test_seedfile_set.py b/src/certfuzz/test/file_handlers/test_seedfile_set.py index 16c2b2b..b36b455 100644 --- a/src/certfuzz/test/file_handlers/test_seedfile_set.py +++ b/src/certfuzz/test/file_handlers/test_seedfile_set.py @@ -12,9 +12,6 @@ from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.file_handlers.directory import Directory from certfuzz.file_handlers.seedfile import SeedFile -import hashlib -from certfuzz.scoring.scorable_set import EmptySetError -#from pprint import pprint class Test(unittest.TestCase): From e719dff09d911ecd109a33fee812f04b6e9ef9ed Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Mar 2014 15:47:19 -0400 Subject: [PATCH 0199/1169] add docstring --- src/certfuzz/campaign/campaign.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index 9eb7496..b4ca5ce 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -369,6 +369,9 @@ def _do_iteration(self, sf, rng_seed, seednum): tries, p) def go(self): + ''' + Starts campaign + ''' while self._keep_going(): try: self._do_interval() From eb02cc4c3ab684cbea5d23f18dac475f247026ce Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2014 10:51:46 -0400 Subject: [PATCH 0200/1169] removed obsolete exception --- src/certfuzz/campaign/campaign.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index b4ca5ce..34c1576 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -22,7 +22,6 @@ from certfuzz.fuzztools.object_caching import dump_obj_to_file from certfuzz.runners.errors import RunnerArchitectureError, \ RunnerPlatformVersionError -from certfuzz.scoring.errors import EmptySetError import cPickle as pickle from certfuzz.iteration.iteration_windows import Iteration @@ -373,8 +372,4 @@ def go(self): Starts campaign ''' while self._keep_going(): - try: - self._do_interval() - except EmptySetError: - logger.info('Seedfile set is empty. Nothing more to do.') - return + self._do_interval() From 4fe1a4f50cb79c487f476416650a594a2bfa51b3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2014 10:53:26 -0400 Subject: [PATCH 0201/1169] previous merge clobbered certfuzz.bff.linux.py, fix that --- src/certfuzz/bff/linux.py | 508 ++------------------------------------ 1 file changed, 23 insertions(+), 485 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index fc8551a..c2a94eb 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -11,310 +11,38 @@ import os import platform import sys -<<<<<<< HEAD from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.fuzztools import filetools from certfuzz.campaign.linux import Campaign -======= -import time - -from .. import debuggers, file_handlers -from ..analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind -from ..analyzers.callgrind import callgrind -from ..analyzers.callgrind.annotate import annotate_callgrind -from ..analyzers.callgrind.annotate import annotate_callgrind_tree -from ..analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError -from ..analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError -from ..analyzers.errors import AnalyzerEmptyOutputError -from ..campaign.config import bff_config as cfg_helper -from ..crash.bff_crash import BffCrash -from ..debuggers import crashwrangler # @UnusedImport -from ..debuggers import gdb # @UnusedImport -from ..file_handlers.seedfile_set import SeedfileSet -from ..file_handlers.tmp_reaper import TmpReaper -from ..fuzztools import bff_helper as z, filetools, performance -from ..fuzztools.process_killer import ProcessKiller -from ..fuzztools.seedrange import SeedRange -from ..fuzztools.state_timer import StateTimer -from ..fuzztools.watchdog import WatchDog -from ..fuzztools.zzuf import Zzuf -from ..fuzztools.zzuflog import ZzufLog -from ..minimizer import MinimizerError, UnixMinimizer as Minimizer ->>>>>>> still crashes but now on a temp file problem DEBUG = True SEED_INTERVAL = 500 -SEED_TS = performance.TimeStamper() -START_SEED = 0 - -STATE_TIMER = StateTimer() +#SEED_TS = performance.TimeStamper() +#START_SEED = 0 logger = logging.getLogger() logger.name = 'bff' logger.setLevel(0) -def get_rate(current_seed): - seeds = current_seed - START_SEED - rate = seeds / SEED_TS.since_start() - return rate - - -def get_uniq_logger(logfile): - l = logging.getLogger('uniq_crash') - if len(l.handlers) == 0: - hdlr = logging.FileHandler(logfile) - l.addHandler(hdlr) - return l - - -def determine_uniqueness(crash, hashes): - ''' - Gets the crash signature, then compares it against known crashes. - Sets crash.is_unique = True if it is new - ''' - - # short-circuit on crashes with no signature - if not crash.signature: - logger.warning('Crash has no signature, cleaning up') - crash.delete_files() - return - - if crash.signature in hashes: - crash.is_unique = False - return - - # fall back to checking if the crash directory exists - crash_dir_found = filetools.find_or_create_dir(crash.result_dir) - - crash.is_unique = not crash_dir_found - - -def analyze_crasher(cfg, crash): - ''' - Runs multiple analyses and collects data about a crash. Returns a list of other crashes - encountered during the process of analyzing the current crash. - @param cfg: A BFF config object - @param crash: A crash object - @return: a list of Crasher objects - ''' - other_crashers_found = [] - - dbg_out_file_orig = crash.dbg.file - logger.debug('Original debugger file: %s', dbg_out_file_orig) - - if cfg.minimizecrashers: - STATE_TIMER.enter_state('minimize_testcase') - # try to reduce the Hamming Distance between the crasher file and the known good seedfile - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=cfg, crash=crash, bitwise=False, - seedfile_as_target=True, confidence=0.999, - tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout - ) as minimizer: - minimizer.go() - other_crashers_found.extend(minimizer.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) - minimizer = None - - touch_watchdog_file(cfg) - # calculate the hamming distances for this crash - # between the original seedfile and the minimized fuzzed file - crash.calculate_hamming_distances() - - if cfg.minimize_to_string: - STATE_TIMER.enter_state('minimize_testcase_to_string') - # Minimize to a string of 'x's - # crash.fuzzedfile will be replaced with the minimized result - try: - with Minimizer(cfg=cfg, crash=crash, bitwise=False, - seedfile_as_target=False, confidence=0.9, - tempdir=cfg.local_dir, maxtime=cfg.minimizertimeout - ) as min2string: - min2string.go() - other_crashers_found.extend(min2string.other_crashes.values()) - except MinimizerError, e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', crash.signature, e) - min2string = None - touch_watchdog_file(cfg) - - STATE_TIMER.enter_state('analyze_testcase') - - # get one last debugger output for the newly minimized file - if crash.pc_in_function: - # change the debugger template - crash.set_debugger_template('complete') - else: - # use a debugger template that specifies fixed offsets from $pc for disassembly - crash.set_debugger_template('complete_nofunction') - logger.info('Getting complete debugger output for crash: %s', crash.fuzzedfile.path) - crash.get_debug_output(crash.fuzzedfile.path) - - if dbg_out_file_orig != crash.dbg.file: - # we have a new debugger output - # remove the old one - filetools.delete_files(dbg_out_file_orig) - if os.path.exists(dbg_out_file_orig): - logger.warning('Failed to remove old debugger file %s', dbg_out_file_orig) - else: - logger.debug('Removed old debug file %s', dbg_out_file_orig) - - # use the minimized file for the rest of the analyses - analyzers = [ - stderr.StdErr, - cw_gmalloc.CrashWranglerGmalloc, - ] - if cfg.use_valgrind: - analyzers.extend([ - valgrind.Valgrind, - callgrind.Callgrind, - ]) - if cfg.use_pin_calltrace: - analyzers.extend([ - pin_calltrace.Pin_calltrace, - ]) - - for analyzer in analyzers: - touch_watchdog_file(cfg) - - analyzer_instance = analyzer(cfg, crash) - if analyzer_instance: - try: - analyzer_instance.go() - except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from analyzer. Continuing') - - logger.info('Annotating callgrind output') - try: - annotate_callgrind(crash) - annotate_callgrind_tree(crash) - except CallgrindAnnotateEmptyOutputFileError: - logger.warning('Unexpected empty output from annotate_callgrind. Continuing') - except CallgrindAnnotateMissingInputFileError: - logger.warning('Missing callgrind output. Continuing') - - return other_crashers_found - - -def verify_crasher(c, hashes, cfg, seedfile_set, rangefinder, r): - logger.debug('verifying crash') - new_crash_count = 0 - - crashes = [] - crashes.append(c) - - for crash in crashes: - # loop until we're out of crashes to verify - logger.debug('crashes to verify: %d', len(crashes)) - STATE_TIMER.enter_state('verify_testcase') - - # crashes may be added as a result of minimization - crash.is_unique = False - determine_uniqueness(crash, hashes) - crash.get_logger() - if crash.is_unique: - hashes.append(crash) - new_crash_count += 1 - - logger.debug("%s did not exist in cache, crash is unique", crash.signature) - more_crashes = analyze_crasher(cfg, crash) - - if cfg.recycle_crashers: - logger.debug('Recycling crash as seedfile') - iterstring = crash.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + crash.seedfile.md5 + '-' + iterstring + crash.seedfile.ext - crasherseed_path = os.path.join(cfg.seedfile_origin_dir, crasherseedname) - filetools.copy_file(crash.fuzzedfile.path, crasherseed_path) - seedfile_set.add_file(crasherseed_path) - # add new crashes to the queue - crashes.extend(more_crashes) - crash.copy_files() - - uniqlogger = get_uniq_logger(cfg.uniq_log) - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', crash.seedfile.basename, crash.signature, crash.seednum, crash.range, crash.hd_bits, crash.hd_bytes) - logger.info('%s first seen at %d', crash.signature, crash.seednum) - else: - logger.debug('%s was found, not unique', crash.signature) - # always clean up after yourself - crash.clean_tmpdir() - - # clean up - crash.delete_files() - # whether it was unique or not, record some details for posterity - # record the details of this crash so we can regenerate it later if needed - crash.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', crash.seedfile.basename, crash.seednum, crash.range, crash.fuzzedfile.path) - crash.logger.info('PC=%s', crash.pc) - - # score this crash for the seedfile - seedfile_set.record_success(key=crash.seedfile.md5) - if crash.range: - # ...and for the range - rangefinder.record_success(key=r.id) - - return new_crash_count - - -def check_for_script(cfg): - if cfg.program_is_script(): - logger.warning("Target application is a shell script.") - raise - #cfg.disable_verification() - #time.sleep(10) - - -def touch_watchdog_file(cfg): - if cfg.watchdogtimeout: - # this one just checks the permission - if os.access(cfg.remote_dir, os.W_OK): - # equivalent to 'touch cfg.watchdogfile' - open(cfg.watchdogfile, 'w').close() - - -def start_process_killer(scriptpath, cfg): - # set up and spawn the process killer - killscript = cfg.get_killscript_path(scriptpath) - ProcessKiller(killscript, cfg.killprocname, cfg.killproctimeout).go() - logger.debug("Process killer started: %s %s %d", killscript, cfg.killprocname, cfg.killproctimeout) - - -<<<<<<< HEAD -======= -def add_log_handler(log_obj, level, hdlr, formatter): - hdlr.setLevel(level) - hdlr.setFormatter(formatter) - log_obj.addHandler(hdlr) +#def get_rate(current_seed): +# seeds = current_seed - START_SEED +# rate = seeds / SEED_TS.since_start() +# return rate ->>>>>>> still crashes but now on a temp file problem def setup_logging_to_console(log_obj, level): hdlr = logging.StreamHandler() formatter = logging.Formatter('%(name)s %(message)s') add_log_handler(log_obj, level, hdlr, formatter) -def setup_logfile(logdir, log_basename='bff.log', level=logging.DEBUG, - max_bytes=1e8, backup_count=5): - ''' - Creates a log file in / at level - @param logdir: the directory where the log file should reside - @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') - @param level: the logging level (defaults to logging.DEBUG) - ''' - filetools.make_directories(logdir) - logfile = os.path.join(logdir, log_basename) - handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) - formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") - add_log_handler(logger, level, handler, formatter) - logger.info('Logging %s at %s', logging.getLevelName(level), logfile) - - def get_config_file(basedir): config_dir = os.path.join(basedir, 'conf.d') @@ -332,14 +60,21 @@ def get_config_file(basedir): return config_file +def add_log_handler(log_obj, level, hdlr, formatter): + hdlr.setLevel(level) + hdlr.setFormatter(formatter) + log_obj.addHandler(hdlr) + + def main(): - global START_SEED - hashes = [] +# global START_SEED +# hashes = [] - # give up if we don't have a debugger - debuggers.verify_supported_platform() +# # give up if we don't have a debugger +# debuggers.verify_supported_platform() setup_logging_to_console(logger, logging.INFO) +# setup_logfile() logger.info("Welcome to BFF!") scriptpath = os.path.dirname(sys.argv[0]) @@ -347,6 +82,8 @@ def main(): # parse command line options logger.info('Parsing command line options') + + #TODO: replace OptionParser with argparse parser = OptionParser() parser.add_option('', '--debug', dest='debug', help='Turn on debugging output', action='store_true') parser.add_option('-c', '--config', dest='cfg', help='Config file location') @@ -365,208 +102,9 @@ def main(): local_cfg_file = os.path.expanduser('~/bff.cfg') filetools.copy_file(remote_cfg_file, local_cfg_file) - # Read the cfg file - logger.info('Reading config from %s', local_cfg_file) - cfg = cfg_helper.read_config_options(local_cfg_file) - - # set up local logging - setup_logfile(cfg.local_dir, log_basename='bff.log', level=logging.DEBUG, - max_bytes=1e8, backup_count=3) - - # set up remote logging - setup_logfile(cfg.output_dir, log_basename='bff.log', level=logging.INFO, - max_bytes=1e7, backup_count=5) + with Campaign(cfg_path=local_cfg_file) as campaign: + campaign.go() - try: - check_for_script(cfg) - except: - logger.warning("Please configure BFF to fuzz a binary. Exiting...") - sys.exit() - z.setup_dirs_and_files(local_cfg_file, cfg) - - # make sure we cache it for the next run - # cache_state(cfg.campaign_id, 'cfg', cfg, cfg.cached_config_file) - - sr = None -# sr = get_cached_state('seedrange', cfg.campaign_id, cfg.cached_seedrange_file) - if not sr: - sr = SeedRange(cfg.start_seed, cfg.seed_interval, cfg.max_seed) - - - # set START_SEED global for timestamping - START_SEED = sr.s1 - - start_process_killer(scriptpath, cfg) - - z.set_unbuffered_stdout() - - seedfile_set = None -# # set up the seedfile set so we can pick seedfiles for everything else... -# seedfile_set = get_cached_state('seedfile_set', cfg.campaign_id, cfg.cached_seedfile_set) - if not seedfile_set: - logger.info('Building seedfile set') - sfs_logfile = os.path.join(cfg.seedfile_output_dir, 'seedfile_set.log') - with SeedfileSet(campaign_id=cfg.campaign_id, - originpath=cfg.seedfile_origin_dir, - localpath=cfg.seedfile_local_dir, - outputpath=cfg.seedfile_output_dir, - logfile=sfs_logfile, - ) as sfset: - seedfile_set = sfset - - # set up the watchdog timeout within the VM and restart the daemon - if cfg.watchdogtimeout: - watchdog = WatchDog(cfg.watchdogfile, cfg.watchdogtimeout) - touch_watchdog_file(cfg) - watchdog.go() - -# cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) - - sf = seedfile_set.next_item() - - # Run the program once to cache it into memory - z.cache_program_once(cfg, sf.path) - - # Give target time to die - time.sleep(1) - - # flag to indicate whether this is a fresh script start up or not - first_chunk = True - - # remember our parent process id so we can tell if it changes later - _last_ppid = os.getppid() - - # campaign.go - while sr.in_max_range(): - - # wipe the tmp dir clean to try to avoid filling the VM disk - TmpReaper().clean_tmp() - - sf = seedfile_set.next_item() - rangefinder = sf.rangefinder - r = rangefinder.next_item() - - sr.set_s2() - logger.info(STATE_TIMER) - - while sr.in_range(): - # interval.go - logger.debug('Starting interval %d-%d', sr.s1, sr.s2) - - # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot - touch_watchdog_file(cfg) - - # check parent process id - _ppid_now = os.getppid() - if not _ppid_now == _last_ppid: - logger.warning('Parent process ID changed from %d to %d', _last_ppid, _ppid_now) - _last_ppid = _ppid_now - - # do the fuzz - cmdline = cfg.get_command(sf.path) - - if first_chunk: - # disable the --quiet option in zzuf - # on the first chunk only - quiet_flag = False - first_chunk = False - else: - quiet_flag = True - - STATE_TIMER.enter_state('fuzzing') - zzuf = Zzuf(cfg.local_dir, - sr.s1, - sr.s2, - cmdline, - sf.path, - cfg.zzuf_log_file, - cfg.copymode, - r.min, - r.max, - cfg.progtimeout, - quiet_flag, - ) - saw_crash = zzuf.go() - STATE_TIMER.enter_state('checking_results') - - if not saw_crash: - # we must have made it through this chunk without a crash - # so go to next chunk - try_count = sr.s1_s2_delta() - seedfile_set.record_tries(key=sf.md5, tries=try_count) - rangefinder.record_tries(key=r.id, tries=try_count) - - # emit a log entry - crashcount = z.get_crashcount(cfg.crashers_dir) - rate = get_rate(sr.s1) - seed_str = "seeds=%d-%d" % (sr.s1, sr.s2) - range_str = "range=%.6f-%.6f" % (r.min, r.max) - rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) - logger.info('Fuzzing %s %s %s %s crash_count=%d', - sf.path, seed_str, range_str, rate_str, crashcount) - - # set s1 to s2 so that as soon as we continue we'll break out of the sr.in_range() loop - sr.set_s1_to_s2() - continue - - # we must have seen a crash - - # get the results - zzuf_log = ZzufLog(cfg.zzuf_log_file, cfg.zzuf_log_out(sf.output_dir)) - - # Don't generate cases for killed process or out-of-memory - # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will - # report the exit code in its output log. The exit code is 128 + the signal number. - crash_status = zzuf_log.crash_logged(cfg.copymode) - sr.bookmark_s1() - sr.s1 = zzuf_log.seed - - # record the fact that we've made it this far - try_count = sr.s1_delta() - seedfile_set.record_tries(key=sf.md5, tries=try_count) - rangefinder.record_tries(key=r.id, tries=try_count) - - new_uniq_crash = None - if crash_status: - logger.info('Generating testcase for %s', zzuf_log.line) - # a true crash - zzuf_range = zzuf_log.range - # create the temp dir for the results - cfg.create_tmpdir() - outfile = cfg.get_testcase_outfile(sf.path, sr.s1) - logger.debug('Output file is %s', outfile) - testcase = zzuf.generate_test_case(sf.path, sr.s1, zzuf_range, outfile) - - # Do internal verification using GDB / Valgrind / Stderr - fuzzedfile = file_handlers.basicfile.BasicFile(outfile) - - with BffCrash(cfg, sf, fuzzedfile, cfg.program, cfg.debugger_timeout, - cfg.killprocname, cfg.backtracelevels, - cfg.crashers_dir, sr.s1, r) as c: - if c.is_crash: - _crash_count = verify_crasher(c, hashes, cfg, seedfile_set, rangefinder, r) - new_uniq_crash = _crash_count > 0 - - # record the zzuf log line for this crash - if not c.logger: - c.get_logger() - c.logger.debug("zzuflog: %s", zzuf_log.line) - c.logger.info('Command: %s', testcase.cmdline) - - cfg.clean_tmpdir() - - sr.increment_seed() - - # cache objects in case of reboot -# cache_state(cfg.campaign_id, 'seedrange', sr, cfg.cached_seedrange_file) -# pickled_seedfile_file = os.path.join(cfg.cached_objects_dir, sf.pkl_file()) -# cache_state(cfg.campaign_id, sf.cache_key(), sf, pickled_seedfile_file) -# cache_state(cfg.campaign_id, 'seedfile_set', seedfile_set, cfg.cached_seedfile_set) - - if new_uniq_crash: - # we had a hit, so break the inner while() loop - # so we can pick a new range. This is to avoid - # having a crash-rich range run away with the - # probability before other ranges have been tried - break +if __name__ == '__main__': + main() From 77d0d623b5cdf0ff0dd0d9a4465ff3e8e834506c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2014 11:13:34 -0400 Subject: [PATCH 0202/1169] todo: BFF-520 --- src/certfuzz/iteration/linux.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 04a2309..d641504 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -276,13 +276,14 @@ def _post_analyze(self, testcase): except CallgrindAnnotateMissingInputFileError: logger.warning('Missing callgrind output. Continuing') - if self.cfg.recycle_crashers: - logger.debug('Recycling crash as seedfile') - iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext - crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) - filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) - seedfile_set.add_file(crasherseed_path) +# TODO +# if self.cfg.recycle_crashers: +# logger.debug('Recycling crash as seedfile') +# iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] +# crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext +# crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) +# filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) +# seedfile_set.add_file(crasherseed_path) # score this crash for the seedfile testcase.seedfile.record_success(testcase.signature, tries=0) From 14dc94fadfbf9f06d347fc74668f5254c1c6df64 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 11:39:23 -0400 Subject: [PATCH 0203/1169] convert to fully qualified package names, add logging --- .../scoring/multiarmed_bandit/multiarmed_bandit_base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index 53a09cb..b96fc96 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -3,8 +3,12 @@ @organization: cert.org ''' -from .errors import MultiArmedBanditError -from .arms.base import BanditArmBase +import logging + +from certfuzz.scoring.multiarmed_bandit.errors import MultiArmedBanditError +from certfuzz.scoring.multiarmed_bandit.arms.base import BanditArmBase + +logger = logging.getLogger(__name__) class MultiArmedBanditBase(object): From 654033ce5f741c55bca792815c2da5058c42599f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 11:40:15 -0400 Subject: [PATCH 0204/1169] add debug log messages --- src/certfuzz/iteration/iteration_base3.py | 7 +++++++ .../scoring/multiarmed_bandit/multiarmed_bandit_base.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 4f51dc6..54648e1 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -14,6 +14,7 @@ class IterationBase3(object): def __init__(self, workdirbase): + logger.debug('init') self.workdirbase = workdirbase self.working_dir = None self.analyzer_classes = [] @@ -101,31 +102,37 @@ def _post_report(self, testcase): pass def fuzz(self): + logger.debug('fuzz') self._pre_fuzz() self._fuzz() self._post_fuzz() def run(self): + logger.debug('run') self._pre_run() self._run() self._post_run() def verify(self, testcase): + logger.debug('verify') self._pre_verify(testcase) self._verify(testcase) self._post_verify(testcase) def analyze(self, testcase): + logger.debug('analyze') self._pre_analyze(testcase) self._analyze(testcase) self._post_analyze(testcase) def report(self, testcase): + logger.debug('report') self._pre_report(testcase) self._report(testcase) self._post_report(testcase) def go(self): + logger.debug('go') self.fuzz() self.run() diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index b96fc96..e6b6756 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -26,6 +26,7 @@ def add_item(self, key=None, obj=None): raise MultiArmedBanditError('unspecified key for arm') if obj is None: raise MultiArmedBanditError('unspecified value for arm') + logger.debug('Creating arm %s', key) self.things[key] = obj # create a new arm of the desired type self.arms[key] = self.arm_type() @@ -42,6 +43,7 @@ def del_item(self, key=None): pass def record_result(self, key, successes=0, trials=0): + logger.debug('Recording result: key=%s successes=%d trials=%d', key, successes, trials) arm = self.arms[key] arm.update(successes, trials) From 5b6bc85b802087acc170325bf923c1dfe2df57de Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 11:45:00 -0400 Subject: [PATCH 0205/1169] refactor scoring calls --- src/certfuzz/campaign/linux.py | 5 ++++- src/certfuzz/iteration/linux.py | 40 +++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 1a26e98..272e9fb 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -256,9 +256,12 @@ def _do_interval(self, s1, s2, first_chunk=False): touch_watchdog_file() with Iteration(cfg=self.cfg, seednum=s, seedfile=sf, r=r, workdirbase=self.working_dir, quiet=qf, - uniq_func=self._crash_is_unique,) as iteration: + uniq_func=self._crash_is_unique, + sf_set=self.seedfile_set, + rf=sf.rangefinder) as iteration: iteration.go() + def go(self): # campaign.go cfg = self.cfg diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index d641504..729e662 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -37,13 +37,16 @@ def get_uniq_logger(logfile): class Iteration(IterationBase3): - def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None): + def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None, + sf_set=None, rf=None): IterationBase3.__init__(self, workdirbase) self.cfg = cfg self.seednum = seednum self.seedfile = seedfile self.r = r self.quiet_flag = quiet + self.sf_set = sf_set + self.rf = rf if uniq_func is None: self.uniq_func = lambda _tc_id: True @@ -63,8 +66,21 @@ def __enter__(self): return self def __exit__(self, etype, value, traceback): - IterationBase3.__exit__(self, etype, value, traceback) + handled = IterationBase3.__exit__(self, etype, value, traceback) + self.cfg.clean_tmpdir() + return handled + + def record_success(self): + self.sf_set.record_success(key=self.seedfile.md5) + self.rf.record_success(key=self.r.id) + + def record_failure(self): + self.record_tries() + + def record_tries(self): + self.sf_set.record_tries(key=self.seedfile.md5, tries=1) + self.rf.record_tries(key=self.r.id, tries=1) def _log(self): # # emit a log entry @@ -118,8 +134,9 @@ def _post_run(self): STATE_TIMER.enter_state('checking_results') # we must have made it through this chunk without a crash # so go to next chunk - self.sf.record_tries(tries=1) - self.r.record_tries(tries=1) + + self.record_tries() + if not self.zzuf.saw_crash: self._log() return @@ -285,11 +302,16 @@ def _post_analyze(self, testcase): # filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) # seedfile_set.add_file(crasherseed_path) - # score this crash for the seedfile - testcase.seedfile.record_success(testcase.signature, tries=0) - if testcase.range: - # ...and for the range - testcase.range.record_success(testcase.signature, tries=0) +# # score this crash for the seedfile +# testcase.seedfile.record_success(testcase.signature, tries=0) +# if testcase.range: +# # ...and for the range +# testcase.range.record_success(testcase.signature, tries=0) + # TODO: make sure this is scoring the right thing. + # in older code (see above) we kept track of specific crashes seen per seedfile + # & range. Should we still do that? + self.record_success() + def _pre_report(self, testcase): uniqlogger = get_uniq_logger(self.cfg.uniq_log) From e361fddf2636fdcbdfa6519c111cdfd7b618e9bb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 13:58:03 -0400 Subject: [PATCH 0206/1169] set the handler on the root logger, not the certfuzz.bff.linux logger --- src/certfuzz/campaign/linux.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 272e9fb..690ef1e 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -121,7 +121,6 @@ def _cleanup_workdir(self): else: logger.debug('Removed campaign working dir: %s', self.working_dir) - def _setup_dirs(self): logger.debug('setup dirs') paths = [self.cfg.local_dir, @@ -153,7 +152,10 @@ def _setup_logfile(self, logdir, log_basename='bff.log', level=logging.DEBUG, hdlr.setLevel(level) hdlr.setFormatter(formatter) - logger.addHandler(hdlr) + + # add the handler to the root logger (not the logger for this module) + root_logger = logging.getLogger() + root_logger.addHandler(hdlr) logger.info('Logging %s at %s', logging.getLevelName(level), logfile) From 8772b5d4d9b9a310af6738db427467c3a4dbdbf3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 14:15:20 -0400 Subject: [PATCH 0207/1169] log probabilities on updates --- .../scoring/multiarmed_bandit/multiarmed_bandit_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index e6b6756..bc63ae7 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -50,8 +50,14 @@ def record_result(self, key, successes=0, trials=0): def record_tries(self, key=None, tries=1): self.record_result(key, successes=0, trials=tries) + def _log_arm_p(self): + logger.debug('Updated probabilities') + for k, v in self.arms.iteritems(): + logger.debug('key=%s probability=%f', k, v.probability) + def record_success(self, key=None, successes=1): self.record_result(key, successes, trials=0) + self._log_arm_p() @property def successes(self): From 58f9c76f5fb55bf53cfa7f25b3902de6a9841f33 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 15:15:00 -0400 Subject: [PATCH 0208/1169] when adding a new item to a MAB, give it the average probability of the other items in the set --- .../multiarmed_bandit/multiarmed_bandit_base.py | 13 ++++++++++++- .../multiarmed_bandit/arms/test_bayes_laplace.py | 4 ++++ .../multiarmed_bandit/test_bayesian_bandit.py | 13 +++++++++++++ .../test_multiarmed_bandit_base.py | 14 ++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index bc63ae7..08e68bf 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -29,7 +29,18 @@ def add_item(self, key=None, obj=None): logger.debug('Creating arm %s', key) self.things[key] = obj # create a new arm of the desired type - self.arms[key] = self.arm_type() + new_arm = self.arm_type() + + # set the new arm's params based on the results we've already found + new_arm.successes = self.successes + new_arm.trials = self.trials + + # but don't trust those averages too strongly + new_arm.doubt() + + + # add the new arm to the set + self.arms[key] = new_arm def del_item(self, key=None): if key is None: diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py b/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py index 8d571d5..3185e32 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py @@ -6,6 +6,7 @@ import unittest from certfuzz.scoring.multiarmed_bandit.arms.bayes_laplace import BanditArmBayesLaplace + class Test(unittest.TestCase): def setUp(self): @@ -15,6 +16,9 @@ def tearDown(self): pass def test_update_p(self): + self.assertEqual(0, self.arm.trials) + self.assertEqual(0, self.arm.successes) + self.assertEqual(1.0 / 2.0, self.arm.probability) self.arm.update(1, 1) self.assertEqual(2.0 / 3.0, self.arm.probability) self.arm.update(1, 1) diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py index 0e3bce8..ebfe6c6 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py @@ -64,6 +64,19 @@ def test_score(self): self.assertEqual(0.005, arm.probability) self.assertAlmostEqual(1.0 / len(self.arms), scaled[key], 6) + def test_add_items(self): + # check if we have some successes already + for arm in self.mab.arms.itervalues(): + arm.update(successes=1, trials=5) + + self.mab.add_item(key='newarm', obj='this_string') + newarm = self.mab.arms['newarm'] + self.assertEqual(1, newarm.successes) + self.assertEqual(5, newarm.trials) + # probability is always = 1 in default mabbase + self.assertAlmostEqual(2.0 / 7.0, newarm.probability) + + if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py b/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py index fcbc3c3..ed326d6 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py @@ -29,8 +29,22 @@ def test_add(self): self.mab.add_item('foo', 'bar') self.assertEqual(len(self.keys) + 1, len(self.mab.things)) self.assertEqual(len(self.keys) + 1, len(self.mab.arms)) + foo_arm = self.mab.arms['foo'] + self.assertEqual(0, foo_arm.trials) + self.assertEqual(0, foo_arm.successes) self.assertEqual(1.0, self.mab.arms['foo'].probability) + # check if we have some successes already + for arm in self.mab.arms.itervalues(): + arm.update(successes=1, trials=5) + + self.mab.add_item(key='newarm', obj='this_string') + newarm = self.mab.arms['newarm'] + self.assertEqual(1, newarm.successes) + self.assertEqual(5, newarm.trials) + # probability is always = 1 in default mabbase + self.assertEqual(1.0, newarm.probability) + def test_record_result(self): self.assertEqual(0, self.mab.successes) self.assertEqual(0, self.mab.trials) From afef7db1a45c3bad1f588e789197d0cd37c5542b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Mar 2014 16:20:02 -0400 Subject: [PATCH 0209/1169] replace relative imports with absolute imports --- src/certfuzz/iteration/errors.py | 2 +- src/certfuzz/iteration/iteration_android.py | 35 +++++++++---------- src/certfuzz/iteration/iteration_base3.py | 7 ++-- src/certfuzz/iteration/iteration_windows.py | 30 +++++++++-------- src/certfuzz/iteration/linux.py | 37 +++++++++++---------- 5 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/certfuzz/iteration/errors.py b/src/certfuzz/iteration/errors.py index e9a7d63..cb1c36c 100644 --- a/src/certfuzz/iteration/errors.py +++ b/src/certfuzz/iteration/errors.py @@ -3,7 +3,7 @@ @author: adh ''' -from ..errors import CERTFuzzError +from certfuzz.errors import CERTFuzzError class IterationError(CERTFuzzError): diff --git a/src/certfuzz/iteration/iteration_android.py b/src/certfuzz/iteration/iteration_android.py index 0f6c88e..44e3055 100644 --- a/src/certfuzz/iteration/iteration_android.py +++ b/src/certfuzz/iteration/iteration_android.py @@ -3,28 +3,29 @@ @organization: cert.org ''' -from ..android.api.activity_manager import ActivityManagerError -from ..android.api.errors import AdbCmdError -# from ..android.worker.defaults import TOMBSTONE_TIMEOUT, DBCFG, SF_CACHE_DIR -from ..android.worker.errors import WorkerError -from ..crash.android_testcase import AndroidTestCase -from ..db.couchdb.datatypes import FileDoc -from ..db.couchdb.db import TestCaseDb, put_file -from ..file_handlers.fuzzedfile import FuzzedFile -from ..file_handlers.seedfile import SeedFile -from ..fuzzers.bytemut import ByteMutFuzzer -from ..fuzztools.filetools import find_or_create_dir -from ..runners.android_runner import AndroidRunner -from ..runners.errors import RunnerError -from .iteration_base import IterationBase -from ..fuzzers.errors import FuzzerExhaustedError - import logging import os import shutil import tempfile +from certfuzz.android.api.activity_manager import ActivityManagerError +from certfuzz.android.api.errors import AdbCmdError +from certfuzz.android.worker.errors import WorkerError +from certfuzz.crash.android_testcase import AndroidTestCase +from certfuzz.db.couchdb.datatypes import FileDoc +from certfuzz.db.couchdb.db import TestCaseDb, put_file +from certfuzz.file_handlers.fuzzedfile import FuzzedFile +from certfuzz.file_handlers.seedfile import SeedFile +from certfuzz.fuzzers.bytemut import ByteMutFuzzer +from certfuzz.fuzzers.errors import FuzzerExhaustedError +from certfuzz.fuzztools.filetools import find_or_create_dir +from certfuzz.runners.android_runner import AndroidRunner +from certfuzz.runners.errors import RunnerError + +from certfuzz.iteration.iteration_base import IterationBase2 + +# from ..android.worker.defaults import TOMBSTONE_TIMEOUT, DBCFG, SF_CACHE_DIR logger = logging.getLogger(__name__) @@ -75,7 +76,7 @@ def do_iteration(iter_args): return True -class AndroidIteration(IterationBase): +class AndroidIteration(IterationBase2): def __init__(self, campaign_id=None, db_config=None, num=0, fuzzopts=None, runopts=None, sf=None, emu_handle=None, sf_dir=None, intent=None): self.campaign_id = campaign_id diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 54648e1..0c898b4 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -3,12 +3,15 @@ @author: adh ''' -import tempfile import logging import shutil -from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +import tempfile + from certfuzz.analyzers.errors import AnalyzerEmptyOutputError +from certfuzz.file_handlers.watchdog_file import touch_watchdog_file + + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 5da1dad..2af1b6a 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -3,20 +3,6 @@ @author: adh ''' -from .. import debuggers -from ..crash.foe_crash import FoeCrash -from ..debuggers.output_parsers import DebuggerFileError -from ..file_handlers.basicfile import BasicFile -from ..file_handlers.tmp_reaper import TmpReaper -from ..fuzzers.errors import FuzzerError, FuzzerExhaustedError, \ - FuzzerInputMatchesOutputError -from ..fuzztools import filetools -from ..fuzztools.filetools import delete_files_or_dirs -from ..minimizer import MinimizerError, WindowsMinimizer as Minimizer -from ..runners.errors import RunnerRegistryError -from ..campaign.config.foe_config import get_command_args_list -from .errors import IterationError -from .iteration_base import IterationBase2 import glob import logging import os @@ -24,6 +10,22 @@ import string import tempfile +from certfuzz import debuggers +from certfuzz.campaign.config.foe_config import get_command_args_list +from certfuzz.crash.foe_crash import FoeCrash +from certfuzz.debuggers.output_parsers import DebuggerFileError +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.file_handlers.tmp_reaper import TmpReaper +from certfuzz.fuzzers.errors import FuzzerError, FuzzerExhaustedError, \ + FuzzerInputMatchesOutputError +from certfuzz.fuzztools import filetools +from certfuzz.fuzztools.filetools import delete_files_or_dirs +from certfuzz.minimizer import MinimizerError, WindowsMinimizer as Minimizer +from certfuzz.runners.errors import RunnerRegistryError + +from certfuzz.iteration.errors import IterationError +from certfuzz.iteration.iteration_base import IterationBase2 + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 729e662..219579f 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -4,27 +4,30 @@ @author: adh ''' import logging - -from .. import file_handlers -from ..analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind -from ..analyzers.callgrind import callgrind -from ..analyzers.callgrind.annotate import annotate_callgrind -from ..analyzers.callgrind.annotate import annotate_callgrind_tree -from ..analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError -from ..analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError -from ..crash.bff_crash import BffCrash -from ..debuggers import crashwrangler # @UnusedImport -from ..debuggers import gdb # @UnusedImport -from ..fuzztools import bff_helper as z, filetools -from ..fuzztools.state_timer import STATE_TIMER -from ..fuzztools.zzuf import Zzuf -from ..fuzztools.zzuflog import ZzufLog -from ..minimizer import MinimizerError, UnixMinimizer as Minimizer import os + +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind +from certfuzz.analyzers.callgrind import callgrind +from certfuzz.analyzers.callgrind.annotate import annotate_callgrind, \ + annotate_callgrind_tree +from certfuzz.analyzers.callgrind.errors import \ + CallgrindAnnotateEmptyOutputFileError, CallgrindAnnotateMissingInputFileError +from certfuzz.crash.bff_crash import BffCrash +from certfuzz.debuggers import crashwrangler # @UnusedImport +from certfuzz.debuggers import gdb # @UnusedImport +from certfuzz.file_handlers import seedfile_set +from certfuzz.fuzztools import bff_helper as z, filetools +from certfuzz.fuzztools.state_timer import STATE_TIMER +from certfuzz.fuzztools.zzuf import Zzuf +from certfuzz.fuzztools.zzuflog import ZzufLog +from certfuzz.minimizer import MinimizerError, UnixMinimizer as Minimizer + from certfuzz.file_handlers.watchdog_file import touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.iteration_base3 import IterationBase3 + logger = logging.getLogger(__name__) @@ -163,7 +166,7 @@ def _post_run(self): self.zzuf.generate_test_case(self.seedfile.path, self.s1, zzuf_range, outfile) # Do internal verification using GDB / Valgrind / Stderr - fuzzedfile = file_handlers.basicfile.BasicFile(outfile) + fuzzedfile = BasicFile(outfile) testcase = BffCrash(cfg=self.cfg, seedfile=self.seedfile, From 5425f6844fc7cb47abd7734b782d58122d4c165b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Mar 2014 16:24:34 -0400 Subject: [PATCH 0210/1169] replace relative imports with absolute imports --- src/certfuzz/minimizer/__init__.py | 8 ++++---- src/certfuzz/minimizer/errors.py | 6 +++++- src/certfuzz/minimizer/minimizer_base.py | 23 ++++++++++++----------- src/certfuzz/minimizer/unix_minimizer.py | 3 ++- src/certfuzz/minimizer/win_minimizer.py | 11 ++++++----- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/minimizer/__init__.py b/src/certfuzz/minimizer/__init__.py index c94834c..81029d0 100644 --- a/src/certfuzz/minimizer/__init__.py +++ b/src/certfuzz/minimizer/__init__.py @@ -1,5 +1,5 @@ # convenience imports to expose objects for use in other packages -from .minimizer_base import Minimizer -from .unix_minimizer import UnixMinimizer -from .win_minimizer import WindowsMinimizer -from .errors import MinimizerError, WindowsMinimizerError +from certfuzz.minimizer.minimizer_base import Minimizer +from certfuzz.minimizer.unix_minimizer import UnixMinimizer +from certfuzz.minimizer.win_minimizer import WindowsMinimizer +from certfuzz.minimizer.errors import MinimizerError, WindowsMinimizerError diff --git a/src/certfuzz/minimizer/errors.py b/src/certfuzz/minimizer/errors.py index cc27c3e..22e9627 100644 --- a/src/certfuzz/minimizer/errors.py +++ b/src/certfuzz/minimizer/errors.py @@ -3,8 +3,12 @@ @organization: cert.org ''' -class MinimizerError(Exception): +from certfuzz.errors import CERTFuzzError + + +class MinimizerError(CERTFuzzError): pass + class WindowsMinimizerError(MinimizerError): pass diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 3551d31..70056bd 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -3,22 +3,23 @@ @organization: cert.org ''' +import hashlib +import itertools import logging +import numpy import os +import random +import shutil import tempfile import time -import numpy -import shutil -import random -import hashlib -import itertools -from .. import debuggers -from ..fuzztools import hamming, filetools, probability, text -from ..fuzztools.filetools import delete_files, write_file -from ..file_handlers.basicfile import BasicFile -from ..file_handlers.tmp_reaper import TmpReaper -from .errors import MinimizerError +from certfuzz import debuggers +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.file_handlers.tmp_reaper import TmpReaper +from certfuzz.fuzztools import hamming, filetools, probability, text +from certfuzz.fuzztools.filetools import delete_files, write_file +from certfuzz.minimizer.errors import MinimizerError + try: from ..analyzers import pin_calltrace diff --git a/src/certfuzz/minimizer/unix_minimizer.py b/src/certfuzz/minimizer/unix_minimizer.py index b3db05d..451d507 100644 --- a/src/certfuzz/minimizer/unix_minimizer.py +++ b/src/certfuzz/minimizer/unix_minimizer.py @@ -3,9 +3,10 @@ @organization: cert.org ''' -from . import Minimizer as MinimizerBase +from certfuzz.minimizer import Minimizer as MinimizerBase import os + class UnixMinimizer(MinimizerBase): use_watchdog = True diff --git a/src/certfuzz/minimizer/win_minimizer.py b/src/certfuzz/minimizer/win_minimizer.py index 3c78d2a..47444e5 100644 --- a/src/certfuzz/minimizer/win_minimizer.py +++ b/src/certfuzz/minimizer/win_minimizer.py @@ -1,10 +1,11 @@ +import collections import logging -from . import Minimizer as MinimizerBase -from ..fuzztools.filetools import write_file -from ..fuzztools.filetools import check_zip_file import zipfile -import collections -from .errors import WindowsMinimizerError + +from certfuzz.fuzztools.filetools import check_zip_file, write_file +from certfuzz.minimizer import Minimizer as MinimizerBase +from certfuzz.minimizer.errors import WindowsMinimizerError + logger = logging.getLogger(__name__) From 0c66d92b0f5bdde2c0749dc669313c1214d2ed92 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 15:36:04 -0400 Subject: [PATCH 0211/1169] use absolute imports --- src/certfuzz/runners/android_runner.py | 12 ++++++------ src/certfuzz/runners/winrun.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/runners/android_runner.py b/src/certfuzz/runners/android_runner.py index c4ecc8f..0db946a 100644 --- a/src/certfuzz/runners/android_runner.py +++ b/src/certfuzz/runners/android_runner.py @@ -3,11 +3,11 @@ @organization: cert.org ''' -from .runner_base import Runner -from .errors import AndroidRunnerError -from ..android.api.adb_cmd import AdbCmd -from ..android.api.activity_manager import ActivityManager -from ..android.api.errors import AdbCmdError +from certfuzz.runners.runner_base import Runner +from certfuzz.runners.errors import AndroidRunnerError +from certfuzz.android.api.adb_cmd import AdbCmd +from certfuzz.android.api.activity_manager import ActivityManager +from certfuzz.android.api.errors import AdbCmdError # from ..db.couchdb.datatypes import TestCaseDoc # from ..db.couchdb.db import TestCaseDb # from ..helpers.misc import random_str @@ -21,6 +21,7 @@ logger = logging.getLogger(__name__) + class AndroidRunner(Runner): ''' Runner object for Android platform @@ -128,7 +129,6 @@ def _check_result(self): else: logger.debug('No tombstones found') - @property def _fuzzed_file_uri(self): return 'file://{}'.format(self.dst) diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 03502ff..24d8b91 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -1,4 +1,4 @@ -from ..helpers import check_os_compatibility +from certfuzz.helpers import check_os_compatibility check_os_compatibility('Windows') @@ -17,10 +17,10 @@ import sys import wmi import time -from .errors import RunnerArchitectureError, RunnerRegistryError -from .errors import RunnerError -from ..campaign.config.foe_config import get_command_args_list -from ..fuzztools.filetools import find_or_create_dir +from certfuzz.runners.errors import RunnerArchitectureError, RunnerRegistryError +from certfuzz.runners.errors import RunnerError +from certfuzz.campaign.config.foe_config import get_command_args_list +from certfuzz.fuzztools.filetools import find_or_create_dir logger = logging.getLogger(__name__) From 4f90b51a6b9186d3f68f0b3532df595aa30479dd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 15:36:25 -0400 Subject: [PATCH 0212/1169] make RunnerError inherit from CERTFuzzError --- src/certfuzz/runners/errors.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/runners/errors.py b/src/certfuzz/runners/errors.py index f65ee10..1659a86 100644 --- a/src/certfuzz/runners/errors.py +++ b/src/certfuzz/runners/errors.py @@ -3,17 +3,24 @@ @organization: cert.org ''' -class RunnerError(Exception): +from certfuzz.errors import CERTFuzzError + + +class RunnerError(CERTFuzzError): pass + class RunnerArchitectureError(RunnerError): pass + class RunnerPlatformVersionError(RunnerError): pass + class RunnerRegistryError(RunnerError): pass + class AndroidRunnerError(RunnerError): pass From dcea2400ce971af7792c7794b1423f77f0db4c6b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 15:37:59 -0400 Subject: [PATCH 0213/1169] use absolute imports --- src/certfuzz/scoring/multiarmed_bandit/arms/base.py | 2 +- src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py | 2 +- src/certfuzz/scoring/multiarmed_bandit/arms/errors.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/base.py b/src/certfuzz/scoring/multiarmed_bandit/arms/base.py index b01f828..03d78f6 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/base.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from .errors import BanditArmError +from certfuzz.scoring.multiarmed_bandit.arms.errors import BanditArmError class BanditArmBase(object): diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py b/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py index 666c670..7263d8a 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from .base import BanditArmBase +from certfuzz.scoring.multiarmed_bandit.arms.base import BanditArmBase class BanditArmBayesLaplace(BanditArmBase): diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py b/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py index c337fe5..300d20a 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from ..errors import MultiArmedBanditError +from certfuzz.scoring.multiarmed_bandit.errors import MultiArmedBanditError class BanditArmError(MultiArmedBanditError): From 1620eb0833ce9d6d0644a218260d0a2fe5c0552e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 15:39:17 -0400 Subject: [PATCH 0214/1169] use absolute imports --- src/certfuzz/scoring/errors.py | 2 +- src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py | 6 +++--- src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py | 6 +++--- src/certfuzz/scoring/multiarmed_bandit/errors.py | 2 +- src/certfuzz/scoring/multiarmed_bandit/random_bandit.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/scoring/errors.py b/src/certfuzz/scoring/errors.py index 7e7de41..c767464 100644 --- a/src/certfuzz/scoring/errors.py +++ b/src/certfuzz/scoring/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from ..errors import CERTFuzzError +from certfuzz.errors import CERTFuzzError class ScoringError(CERTFuzzError): diff --git a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py index 571525e..755d610 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/bayesian_bandit.py @@ -3,9 +3,9 @@ @organization: cert.org ''' -from ...fuzztools.probability import weighted_choice -from .multiarmed_bandit_base import MultiArmedBanditBase -from .arms.bayes_laplace import BanditArmBayesLaplace +from certfuzz.fuzztools.probability import weighted_choice +from certfuzz.scoring.multiarmed_bandit.multiarmed_bandit_base import MultiArmedBanditBase +from certfuzz.scoring.multiarmed_bandit.arms.bayes_laplace import BanditArmBayesLaplace class BayesianMultiArmedBandit(MultiArmedBanditBase): diff --git a/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py index cd0108f..3094be0 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/e_greedy_bandit.py @@ -3,9 +3,9 @@ @organization: cert.org ''' -from .multiarmed_bandit_base import MultiArmedBanditBase -from .arms.bayes_laplace import BanditArmBayesLaplace -from .errors import MultiArmedBanditError +from certfuzz.scoring.multiarmed_bandit.multiarmed_bandit_base import MultiArmedBanditBase +from certfuzz.scoring.multiarmed_bandit.arms.bayes_laplace import BanditArmBayesLaplace +from certfuzz.scoring.multiarmed_bandit.errors import MultiArmedBanditError import random diff --git a/src/certfuzz/scoring/multiarmed_bandit/errors.py b/src/certfuzz/scoring/multiarmed_bandit/errors.py index 74a0696..1ad6dab 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/errors.py +++ b/src/certfuzz/scoring/multiarmed_bandit/errors.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from ..errors import ScoringError +from certfuzz.scoring.errors import ScoringError class MultiArmedBanditError(ScoringError): diff --git a/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py b/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py index 6a883f2..f70f992 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py +++ b/src/certfuzz/scoring/multiarmed_bandit/random_bandit.py @@ -3,7 +3,7 @@ @organization: cert.org ''' -from .multiarmed_bandit_base import MultiArmedBanditBase +from certfuzz.scoring.multiarmed_bandit.multiarmed_bandit_base import MultiArmedBanditBase import random From 6e1fc8b412a82bfaf4d25a222d7984d90c6a4866 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 15:43:23 -0400 Subject: [PATCH 0215/1169] use absolute imports --- src/certfuzz/tools/linux/callsim.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/tools/linux/callsim.py b/src/certfuzz/tools/linux/callsim.py index 120b8eb..d93d8f2 100755 --- a/src/certfuzz/tools/linux/callsim.py +++ b/src/certfuzz/tools/linux/callsim.py @@ -6,9 +6,9 @@ import logging from optparse import OptionParser -from ...fuzztools.similarity_matrix import SimilarityMatrix -from ...fuzztools.similarity_matrix import SimilarityMatrixError -from ...fuzztools.distance_matrix import DistanceMatrixError +from certfuzz.fuzztools.similarity_matrix import SimilarityMatrix +from certfuzz.fuzztools.similarity_matrix import SimilarityMatrixError +from certfuzz.fuzztools.distance_matrix import DistanceMatrixError logger = logging.getLogger() logger.setLevel(logging.WARNING) From 968ef17fa93138375b501008c3008d46cfccd181 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 16:16:21 -0400 Subject: [PATCH 0216/1169] revamp setup.py for bff 3.0 --- src/LICENSE.txt | 46 +++++++++++++++++++++++++++++++++++++++++ src/setup.py | 54 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 src/LICENSE.txt diff --git a/src/LICENSE.txt b/src/LICENSE.txt new file mode 100644 index 0000000..b0eda2f --- /dev/null +++ b/src/LICENSE.txt @@ -0,0 +1,46 @@ +Use of the CERT Basic Fuzzing Framework (BFF) system and related source code +is subject to the terms of the license below. Please note that winprocess.py +and killableprocess.py are subject to different licenses; see those files for +their respective licenses. + +------------------------------------------------------------------------ +Copyright (C) 2013 Carnegie Mellon University. All Rights Reserved. +------------------------------------------------------------------------ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following acknowledgments + and disclaimers. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. The names "Department of Homeland Security," "Carnegie Mellon + University," "CERT" and/or "Software Engineering Institute" shall + not be used to endorse or promote products derived from this software + without prior written permission. For written permission, please + contact permission@sei.cmu.edu. + +4. Products derived from this software may not be called "CERT" nor + may "CERT" appear in their names without prior written permission of + permission@sei.cmu.edu. + +5. Redistributions of any form whatsoever must retain the following + acknowledgment: + + "This product includes software developed by CERT with funding + and support from the Department of Homeland Security under + Contract No. FA 8721-05-C-0003." + +THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. \ No newline at end of file diff --git a/src/setup.py b/src/setup.py index e223607..e945de9 100644 --- a/src/setup.py +++ b/src/setup.py @@ -4,10 +4,27 @@ @organization: cert.org ''' from setuptools import setup, find_packages +import platform + + +def _bff_for_platform(): + parts = ['certfuzz', 'bff'] + _platform = platform.system() + if _platform == 'Windows': + parts.append('windows') + else: + # actually covers linux and osx + parts.append('linux') + + module = '.'.join(parts) + return module + +_bff_main = 'bff = {}:main'.format(_bff_for_platform()) + setup(name="CERT_Basic_Fuzzing_Framework", - version="1.0-android", - description="A fuzzing framework for Android, based on BFF 2.7", + version="3.0a", + description="CERT Basic Fuzzing Framework 3.0", author="CERT", author_email="cert@cert.org", url="http://www.cert.org", @@ -17,19 +34,30 @@ packages=find_packages(where='.'), install_requires=[ 'pyyaml', - 'couchdb', +# 'couchdb', 'numpy', + 'matplotlib', ], - scripts=['scripts/start_bff_android.sh', - 'scripts/reset_bff_android.sh', - 'scripts/ubufuzz_first_time_setup.sh', + scripts=[ +# 'scripts/start_bff_android.sh', +# 'scripts/reset_bff_android.sh', +# 'scripts/ubufuzz_first_time_setup.sh', ], - entry_points={'console_scripts': [ - 'bff_avd_mgr = certfuzz.android.avd_mgr.main:main', - 'bff_android = certfuzz.bff.android:main', - 'bff_avd_cloner = certfuzz.android.celery.avd_mgr.cloner:main', - 'bff_apk_dumper = certfuzz.tools.android.apk_dumper:main', - 'config_init = certfuzz.tools.android.config_tools:main' - ]}, + entry_points={ + 'console_scripts': [ +# # bff for android +# 'bff_avd_mgr = certfuzz.android.avd_mgr.main:main', +# 'bff_android = certfuzz.android.controller.bff_android:main', +# 'bff_avd_cloner = certfuzz.android.celery.avd_mgr.cloner:main', +# 'bff_apk_dumper = certfuzz.android.tools.apk_dumper:main', +# 'config_init = certfuzz.android.tools.config_tools:main', + # bff for linux, osx, windows, + _bff_main, + ] + }, include_package_data=True, + license='See LICENSE.txt', + data_files=[ + ('', ['LICENSE.txt']) + ] ) From 01288702936b68a8bc52b9882e39e3aca0c66537 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 16:18:31 -0400 Subject: [PATCH 0217/1169] basic virtualenv setup stuff --- setup_env/README.txt | 14 ++++++++++++++ setup_env/deploy_virtualenv.sh | 5 +++++ setup_env/pip_freeze.sh | 3 +++ setup_env/requirements.txt | 12 ++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 setup_env/README.txt create mode 100755 setup_env/deploy_virtualenv.sh create mode 100755 setup_env/pip_freeze.sh create mode 100644 setup_env/requirements.txt diff --git a/setup_env/README.txt b/setup_env/README.txt new file mode 100644 index 0000000..d925766 --- /dev/null +++ b/setup_env/README.txt @@ -0,0 +1,14 @@ +This directory is an experiment in progress. + +Based on the responses to +http://stackoverflow.com/questions/6590688/is-it-bad-to-have-my-virtualenv-directory-inside-my-git-repository + +I (adh) wanted to try automating the creation and initialization of a virtualenv for BFF. + +What's in here: +* deploy_virtualenv.sh - Creates a virtualenv named bff.env, activates it, and attempts to pip install the +requirements listed in requirements.txt. +* pip_freeze.sh - Dumps currently installed pip packages into a list found in requirements.txt. Note that the +raw dump may include packages not relevant to BFF, so it will likely require editing prior to use. (or any commits) +* README.txt - this file +* requirements.txt - The list of pip packages that will be installed by deploy_virtualenv.sh diff --git a/setup_env/deploy_virtualenv.sh b/setup_env/deploy_virtualenv.sh new file mode 100755 index 0000000..4688fa5 --- /dev/null +++ b/setup_env/deploy_virtualenv.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +ENV=bff.env + +virtualenv --system-site-packages --distribute $ENV && source $ENV/bin/activate && pip install -r requirements.txt diff --git a/setup_env/pip_freeze.sh b/setup_env/pip_freeze.sh new file mode 100755 index 0000000..ee0d2c5 --- /dev/null +++ b/setup_env/pip_freeze.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +pip freeze > requirements.txt diff --git a/setup_env/requirements.txt b/setup_env/requirements.txt new file mode 100644 index 0000000..c73da73 --- /dev/null +++ b/setup_env/requirements.txt @@ -0,0 +1,12 @@ +numpy==1.6.2 +matplotlib==1.1.1 +# Commenting out these ones as they didn't install cleanly +# during my initial testing - adh +# hcluster==0.2.0 +# scipy==0.11.0 + + +# Used only for running unit tests. +# Not necessary for deployments. +nose==1.3.0 +pylint==0.28.0 From 8f87cce4672f9ddf2d7045c3589c7e0de0036249 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2014 16:26:16 -0400 Subject: [PATCH 0218/1169] refactor entry points into their own method --- src/setup.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/setup.py b/src/setup.py index e945de9..49d5e82 100644 --- a/src/setup.py +++ b/src/setup.py @@ -22,6 +22,21 @@ def _bff_for_platform(): _bff_main = 'bff = {}:main'.format(_bff_for_platform()) +def get_entry_points(): + ''' + Returns a dict containing entry points. + ''' + console_scripts = [] + console_scripts.append(_bff_main) + + # TODO: add linux scripts here + # TODO: add windows scripts here + # TODO: add osx scripts here + + eps = {} + eps['console_scripts'] = console_scripts + return eps + setup(name="CERT_Basic_Fuzzing_Framework", version="3.0a", description="CERT Basic Fuzzing Framework 3.0", @@ -43,18 +58,7 @@ def _bff_for_platform(): # 'scripts/reset_bff_android.sh', # 'scripts/ubufuzz_first_time_setup.sh', ], - entry_points={ - 'console_scripts': [ -# # bff for android -# 'bff_avd_mgr = certfuzz.android.avd_mgr.main:main', -# 'bff_android = certfuzz.android.controller.bff_android:main', -# 'bff_avd_cloner = certfuzz.android.celery.avd_mgr.cloner:main', -# 'bff_apk_dumper = certfuzz.android.tools.apk_dumper:main', -# 'config_init = certfuzz.android.tools.config_tools:main', - # bff for linux, osx, windows, - _bff_main, - ] - }, + entry_points=get_entry_points(), include_package_data=True, license='See LICENSE.txt', data_files=[ From 291c5bb06d090d052e734908fec2003350cc3f60 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 08:56:25 -0400 Subject: [PATCH 0219/1169] add placeholder comments --- src/setup.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/setup.py b/src/setup.py index 49d5e82..2f68fb5 100644 --- a/src/setup.py +++ b/src/setup.py @@ -19,19 +19,34 @@ def _bff_for_platform(): module = '.'.join(parts) return module -_bff_main = 'bff = {}:main'.format(_bff_for_platform()) - def get_entry_points(): ''' Returns a dict containing entry points. ''' console_scripts = [] - console_scripts.append(_bff_main) + console_scripts.append('bff = {}:main'.format(_bff_for_platform())) # TODO: add linux scripts here + # bff_stats + # callsim + # create_crasher_script + # debugger_file + # drillresults + # minimize + # minimizer_plot + # mtsp_enum + # repro + # TODO: add windows scripts here - # TODO: add osx scripts here + # clean_foe + # copycrashers + # drillresults + # minimize + # mtsp_enum + # quickstats + # repro + # zipdiff eps = {} eps['console_scripts'] = console_scripts From e5d5cd0a3099d5df5e3436b2d2f086ad7394191c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 09:15:09 -0400 Subject: [PATCH 0220/1169] add tool names to entry points --- src/setup.py | 57 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/setup.py b/src/setup.py index 2f68fb5..2adfe1f 100644 --- a/src/setup.py +++ b/src/setup.py @@ -19,6 +19,42 @@ def _bff_for_platform(): module = '.'.join(parts) return module +TOOL_NAMES = {'linux': ['bff_stats', + 'callsim', + 'create_crasher_script', + 'debugger_file', + 'drillresults', + 'minimize', + 'minimizer_plot', + 'mtsp_enum', + 'repro', + ], + 'windows': ['clean_foe', + 'copycrashers', + 'drillresults', + 'minimize', + 'mtsp_enum', + 'quickstats', + 'repro', + 'zipdiff', + ], + } + + +def _platform_scripts(): + ''' + Assumes tool foo will have an entry point certfuzz.tools..foo:main + :param _platform: + ''' + _platform = platform.system().lower() + script_spec_template = '{} = certfuzz.tools.{}.{}:main' + scripts = [] + + for s in TOOL_NAMES[_platform]: + # will turn "callsim" into "callsim = certfuzz.tools.linux.callsim" + scripts.append(script_spec_template.format(s, _platform, s)) + + return scripts def get_entry_points(): ''' @@ -27,26 +63,7 @@ def get_entry_points(): console_scripts = [] console_scripts.append('bff = {}:main'.format(_bff_for_platform())) - # TODO: add linux scripts here - # bff_stats - # callsim - # create_crasher_script - # debugger_file - # drillresults - # minimize - # minimizer_plot - # mtsp_enum - # repro - - # TODO: add windows scripts here - # clean_foe - # copycrashers - # drillresults - # minimize - # mtsp_enum - # quickstats - # repro - # zipdiff + console_scripts.extend(_platform_scripts()) eps = {} eps['console_scripts'] = console_scripts From f3817341aa13a07ebd06516da9f1b58d3a98d9e1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 09:19:19 -0400 Subject: [PATCH 0221/1169] handle osx, which just uses the linux scripts --- src/setup.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/setup.py b/src/setup.py index 2adfe1f..21dc0f5 100644 --- a/src/setup.py +++ b/src/setup.py @@ -41,18 +41,17 @@ def _bff_for_platform(): } -def _platform_scripts(): +def _platform_scripts(target_platform): ''' Assumes tool foo will have an entry point certfuzz.tools..foo:main :param _platform: ''' - _platform = platform.system().lower() script_spec_template = '{} = certfuzz.tools.{}.{}:main' scripts = [] - for s in TOOL_NAMES[_platform]: + for s in TOOL_NAMES[target_platform]: # will turn "callsim" into "callsim = certfuzz.tools.linux.callsim" - scripts.append(script_spec_template.format(s, _platform, s)) + scripts.append(script_spec_template.format(s, target_platform, s)) return scripts @@ -60,10 +59,15 @@ def get_entry_points(): ''' Returns a dict containing entry points. ''' + + _platform = platform.system().lower() + if _platform == 'darwin': + _platform = 'linux' + console_scripts = [] console_scripts.append('bff = {}:main'.format(_bff_for_platform())) - console_scripts.extend(_platform_scripts()) + console_scripts.extend(_platform_scripts(_platform)) eps = {} eps['console_scripts'] = console_scripts From 790a0ae506598992576ae746868197b8c4671ab8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 09:36:25 -0400 Subject: [PATCH 0222/1169] replace bff for platform method with string template --- src/setup.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/setup.py b/src/setup.py index 21dc0f5..7cc8022 100644 --- a/src/setup.py +++ b/src/setup.py @@ -7,18 +7,6 @@ import platform -def _bff_for_platform(): - parts = ['certfuzz', 'bff'] - _platform = platform.system() - if _platform == 'Windows': - parts.append('windows') - else: - # actually covers linux and osx - parts.append('linux') - - module = '.'.join(parts) - return module - TOOL_NAMES = {'linux': ['bff_stats', 'callsim', 'create_crasher_script', @@ -55,18 +43,19 @@ def _platform_scripts(target_platform): return scripts + def get_entry_points(): ''' Returns a dict containing entry points. ''' + bff_template = 'bff = certfuzz.bff.{}:main' _platform = platform.system().lower() if _platform == 'darwin': _platform = 'linux' console_scripts = [] - console_scripts.append('bff = {}:main'.format(_bff_for_platform())) - + console_scripts.append(bff_template.format(_platform)) console_scripts.extend(_platform_scripts(_platform)) eps = {} From 62f03feaac4a64bd44f0e5ac6fb6fe7c3ae57aa1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 09:37:08 -0400 Subject: [PATCH 0223/1169] replace scripts list with method --- src/setup.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/setup.py b/src/setup.py index 7cc8022..4018b61 100644 --- a/src/setup.py +++ b/src/setup.py @@ -62,6 +62,14 @@ def get_entry_points(): eps['console_scripts'] = console_scripts return eps +def get_scripts(): + _s = [ +# 'scripts/start_bff_android.sh', +# 'scripts/reset_bff_android.sh', +# 'scripts/ubufuzz_first_time_setup.sh', + ] + return _s + setup(name="CERT_Basic_Fuzzing_Framework", version="3.0a", description="CERT Basic Fuzzing Framework 3.0", @@ -78,11 +86,7 @@ def get_entry_points(): 'numpy', 'matplotlib', ], - scripts=[ -# 'scripts/start_bff_android.sh', -# 'scripts/reset_bff_android.sh', -# 'scripts/ubufuzz_first_time_setup.sh', - ], + scripts=get_scripts(), entry_points=get_entry_points(), include_package_data=True, license='See LICENSE.txt', From 0bf6b90765cc17e1ed85782881c27893bbd804e6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 10:44:47 -0400 Subject: [PATCH 0224/1169] method name cleanup --- src/setup.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/setup.py b/src/setup.py index 4018b61..cd94777 100644 --- a/src/setup.py +++ b/src/setup.py @@ -29,7 +29,7 @@ } -def _platform_scripts(target_platform): +def _ep_scripts(target_platform): ''' Assumes tool foo will have an entry point certfuzz.tools..foo:main :param _platform: @@ -44,7 +44,7 @@ def _platform_scripts(target_platform): return scripts -def get_entry_points(): +def _entry_points(): ''' Returns a dict containing entry points. ''' @@ -56,13 +56,13 @@ def get_entry_points(): console_scripts = [] console_scripts.append(bff_template.format(_platform)) - console_scripts.extend(_platform_scripts(_platform)) + console_scripts.extend(_ep_scripts(_platform)) eps = {} eps['console_scripts'] = console_scripts return eps -def get_scripts(): +def _scripts(): _s = [ # 'scripts/start_bff_android.sh', # 'scripts/reset_bff_android.sh', @@ -86,8 +86,8 @@ def get_scripts(): 'numpy', 'matplotlib', ], - scripts=get_scripts(), - entry_points=get_entry_points(), + scripts=_scripts(), + entry_points=_entry_points(), include_package_data=True, license='See LICENSE.txt', data_files=[ From a21c5cc88ac4ce3848d6db79209f63265bd9019e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 15:01:55 -0400 Subject: [PATCH 0225/1169] use >= instead of == in requirements --- setup_env/requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup_env/requirements.txt b/setup_env/requirements.txt index c73da73..b2e5ccd 100644 --- a/setup_env/requirements.txt +++ b/setup_env/requirements.txt @@ -1,5 +1,5 @@ -numpy==1.6.2 -matplotlib==1.1.1 +numpy>=1.6.2 +matplotlib>=1.1.1 # Commenting out these ones as they didn't install cleanly # during my initial testing - adh # hcluster==0.2.0 @@ -8,5 +8,5 @@ matplotlib==1.1.1 # Used only for running unit tests. # Not necessary for deployments. -nose==1.3.0 -pylint==0.28.0 +nose>=1.3.0 +pylint>=0.28.0 From d3a26d11d8ebce011bdb88aa4ac2a9229244a740 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 15:08:32 -0400 Subject: [PATCH 0226/1169] get rid of requirements.txt. Just build a plain vanilla virtualenv that inherits from the system --- setup_env/deploy_virtualenv.sh | 2 +- setup_env/requirements.txt | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 setup_env/requirements.txt diff --git a/setup_env/deploy_virtualenv.sh b/setup_env/deploy_virtualenv.sh index 4688fa5..e97f6e1 100755 --- a/setup_env/deploy_virtualenv.sh +++ b/setup_env/deploy_virtualenv.sh @@ -2,4 +2,4 @@ ENV=bff.env -virtualenv --system-site-packages --distribute $ENV && source $ENV/bin/activate && pip install -r requirements.txt +virtualenv --system-site-packages --distribute $ENV \ No newline at end of file diff --git a/setup_env/requirements.txt b/setup_env/requirements.txt deleted file mode 100644 index b2e5ccd..0000000 --- a/setup_env/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -numpy>=1.6.2 -matplotlib>=1.1.1 -# Commenting out these ones as they didn't install cleanly -# during my initial testing - adh -# hcluster==0.2.0 -# scipy==0.11.0 - - -# Used only for running unit tests. -# Not necessary for deployments. -nose>=1.3.0 -pylint>=0.28.0 From 7620b2c2c204e93b62e8aa82d0c88981688731de Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 15:21:54 -0400 Subject: [PATCH 0227/1169] add options to virtualenv invocation --- setup_env/deploy_virtualenv.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup_env/deploy_virtualenv.sh b/setup_env/deploy_virtualenv.sh index e97f6e1..1c9677a 100755 --- a/setup_env/deploy_virtualenv.sh +++ b/setup_env/deploy_virtualenv.sh @@ -2,4 +2,6 @@ ENV=bff.env -virtualenv --system-site-packages --distribute $ENV \ No newline at end of file +virtualenv --verbose --python=python2.7 \ + --system-site-packages --distribute \ + --never-download $ENV \ No newline at end of file From a358be5a65df31801602df9097ea35232d74dd87 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2014 15:58:09 -0400 Subject: [PATCH 0228/1169] disable matplotlib requirement --- src/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup.py b/src/setup.py index cd94777..14eef2c 100644 --- a/src/setup.py +++ b/src/setup.py @@ -84,7 +84,7 @@ def _scripts(): 'pyyaml', # 'couchdb', 'numpy', - 'matplotlib', +# 'matplotlib', ], scripts=_scripts(), entry_points=_entry_points(), From 28a02286f36b9845793aa2c6e3020424ac6e98b1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 11:30:58 -0400 Subject: [PATCH 0229/1169] make error message more informative --- src/certfuzz/scoring/multiarmed_bandit/arms/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/base.py b/src/certfuzz/scoring/multiarmed_bandit/arms/base.py index 03d78f6..6f4b0e8 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/base.py @@ -38,7 +38,7 @@ def update(self, successes=0, trials=0): if self.probability is None: raise BanditArmError('probability not set') elif not (0.0 <= self.probability <= 1.0): - raise BanditArmError('probability must be between 0.0 and 1.0') + raise BanditArmError('probability must be between 0.0 <= {:f} <= 1.0'.format(self.probability)) def _update_p(self, *_unused_args): ''' From 220b9dd6fb6c658264c5a7de470f7a9ef03d22f0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 13:18:49 -0400 Subject: [PATCH 0230/1169] add more informative debug log --- src/certfuzz/scoring/multiarmed_bandit/arms/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/base.py b/src/certfuzz/scoring/multiarmed_bandit/arms/base.py index 6f4b0e8..80030bd 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/base.py @@ -3,8 +3,12 @@ @organization: cert.org ''' +import logging + from certfuzz.scoring.multiarmed_bandit.arms.errors import BanditArmError +logger = logging.getLogger(__name__) + class BanditArmBase(object): ''' @@ -36,8 +40,10 @@ def update(self, successes=0, trials=0): self.trials += trials self._update_p(successes, trials) if self.probability is None: + logger.debug("MAB arm: %s", self) raise BanditArmError('probability not set') elif not (0.0 <= self.probability <= 1.0): + logger.debug("MAB arm: %s", self) raise BanditArmError('probability must be between 0.0 <= {:f} <= 1.0'.format(self.probability)) def _update_p(self, *_unused_args): From 263687b5ac7c0917841b601c3a1deaeaec98cf9c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 20:39:53 -0400 Subject: [PATCH 0231/1169] remove dead code --- src/certfuzz/bff/linux.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index c2a94eb..fc718bb 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -23,18 +23,11 @@ SEED_INTERVAL = 500 -#SEED_TS = performance.TimeStamper() -#START_SEED = 0 - logger = logging.getLogger() logger.name = 'bff' logger.setLevel(0) -#def get_rate(current_seed): -# seeds = current_seed - START_SEED -# rate = seeds / SEED_TS.since_start() -# return rate def setup_logging_to_console(log_obj, level): @@ -67,14 +60,7 @@ def add_log_handler(log_obj, level, hdlr, formatter): def main(): -# global START_SEED -# hashes = [] - -# # give up if we don't have a debugger -# debuggers.verify_supported_platform() - setup_logging_to_console(logger, logging.INFO) -# setup_logfile() logger.info("Welcome to BFF!") scriptpath = os.path.dirname(sys.argv[0]) From 4d726fdfc79054f5eee23b5b2f33bf91dea88949 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 20:40:22 -0400 Subject: [PATCH 0232/1169] add methods to match certfuzz.bff.windows --- src/certfuzz/bff/linux.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index fc718bb..1c161e6 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -28,8 +28,23 @@ logger.setLevel(0) +def _setup_logging_to_screen(options, logger, fmt): + pass +def _setup_logging_to_file(options, logger, fmt): + pass + + +def setup_logging(options): + pass + +def parse_options(): + pass + +def setup_debugging(logger): + pass + def setup_logging_to_console(log_obj, level): hdlr = logging.StreamHandler() formatter = logging.Formatter('%(name)s %(message)s') From 1b0e928a15fdca1d657a7762bfc7076ac46a35a0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 20:45:28 -0400 Subject: [PATCH 0233/1169] import version from main certfuzz version --- src/certfuzz/bff/linux.py | 2 +- src/certfuzz/bff/windows.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 1c161e6..cebe197 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -3,7 +3,7 @@ @author: adh ''' -__version__ = '2.8' +from certfuzz.version import __version__ import logging from logging.handlers import RotatingFileHandler diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 51defec..47ca88d 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -4,14 +4,13 @@ @author: adh ''' -__version__ = '2.2' - import logging from logging.handlers import RotatingFileHandler from optparse import OptionParser import os import sys +from certfuzz.version import __version__ from certfuzz.campaign.campaign_windows import WindowsCampaign From 1dd81de8d7674d0a575e0724543e475abfb07b39 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 22:47:44 -0400 Subject: [PATCH 0234/1169] drop unneeded code --- src/certfuzz/bff/linux.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index cebe197..415b767 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -19,13 +19,8 @@ from certfuzz.campaign.linux import Campaign -DEBUG = True -SEED_INTERVAL = 500 -logger = logging.getLogger() -logger.name = 'bff' -logger.setLevel(0) def _setup_logging_to_screen(options, logger, fmt): From 4cd1ec476f36a4be05006a1a4a5aff63101cde6b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 22:48:38 -0400 Subject: [PATCH 0235/1169] pull in add_log_handler method from certfuzz.bff.linux --- src/certfuzz/bff/windows.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 47ca88d..0c24931 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -17,14 +17,23 @@ def _setup_logging_to_screen(options, logger, fmt): # logging to screen hdlr = logging.StreamHandler() - hdlr.setFormatter(fmt) - hdlr.setLevel(logging.INFO) - # override if debug or quiet + if options.debug: - hdlr.setLevel(logging.DEBUG) - elif options.quiet and not options.verbose: - hdlr.setLevel(logging.WARNING) - logger.addHandler(hdlr) + level = logging.DEBUG + elif options.verbose: + level = logging.INFO + elif options.quiet: + level = logging.WARNING + else: + level = logging.INFO + + add_log_handler(logger, level, hdlr, fmt) + + +def add_log_handler(log_obj, level, hdlr, formatter): + hdlr.setLevel(level) + hdlr.setFormatter(formatter) + log_obj.addHandler(hdlr) def _setup_logging_to_file(options, logger, fmt): From 3d1b66b675801a3f78277eea777dca6c457e7d0b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 22:49:00 -0400 Subject: [PATCH 0236/1169] remove unused import --- src/certfuzz/bff/linux.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 415b767..63f4394 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -6,7 +6,6 @@ from certfuzz.version import __version__ import logging -from logging.handlers import RotatingFileHandler from optparse import OptionParser import os import platform From 014207e6cf8973c8b195d226c25dac6a7cfc8959 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 22:50:41 -0400 Subject: [PATCH 0237/1169] refactor option parsing into separate method --- src/certfuzz/bff/linux.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 63f4394..9fb2664 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -34,7 +34,11 @@ def setup_logging(options): pass def parse_options(): - pass + parser = OptionParser() + parser.add_option('', '--debug', dest='debug', help='Turn on debugging output', action='store_true') + parser.add_option('-c', '--config', dest='cfg', help='Config file location') + return parser.parse_args() #@UnusedVariable + def setup_debugging(logger): pass @@ -75,15 +79,6 @@ def main(): scriptpath = os.path.dirname(sys.argv[0]) logger.info('Scriptpath is %s', scriptpath) - # parse command line options - logger.info('Parsing command line options') - - #TODO: replace OptionParser with argparse - parser = OptionParser() - parser.add_option('', '--debug', dest='debug', help='Turn on debugging output', action='store_true') - parser.add_option('-c', '--config', dest='cfg', help='Config file location') - (options, args) = parser.parse_args() #@UnusedVariable - # Get the cfg file name if options.cfg: remote_cfg_file = options.cfg From c84d767d9e94febd5f5d7aaa6198a212f4b8e693 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 22:51:38 -0400 Subject: [PATCH 0238/1169] bring over & integrate methods from certfuzz.bff.windows --- src/certfuzz/bff/linux.py | 55 ++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 9fb2664..e1fa7ce 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -18,12 +18,26 @@ from certfuzz.campaign.linux import Campaign +def _setup_logging_to_screen(options, logger, fmt): + # logging to screen + hdlr = logging.StreamHandler() + if options.debug: + level = logging.DEBUG + elif options.verbose: + level = logging.INFO + elif options.quiet: + level = logging.WARNING + else: + level = logging.INFO + add_log_handler(logger, level, hdlr, fmt) -def _setup_logging_to_screen(options, logger, fmt): - pass +def add_log_handler(log_obj, level, hdlr, formatter): + hdlr.setLevel(level) + hdlr.setFormatter(formatter) + log_obj.addHandler(hdlr) def _setup_logging_to_file(options, logger, fmt): @@ -31,7 +45,17 @@ def _setup_logging_to_file(options, logger, fmt): def setup_logging(options): - pass + logger = logging.getLogger() + + if options.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + fmt = logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(message)s') + _setup_logging_to_screen(options, logger, fmt) + _setup_logging_to_file(options, logger, fmt) + return logger def parse_options(): parser = OptionParser() @@ -43,11 +67,6 @@ def parse_options(): def setup_debugging(logger): pass -def setup_logging_to_console(log_obj, level): - hdlr = logging.StreamHandler() - formatter = logging.Formatter('%(name)s %(message)s') - add_log_handler(log_obj, level, hdlr, formatter) - def get_config_file(basedir): config_dir = os.path.join(basedir, 'conf.d') @@ -66,15 +85,18 @@ def get_config_file(basedir): return config_file -def add_log_handler(log_obj, level, hdlr, formatter): - hdlr.setLevel(level) - hdlr.setFormatter(formatter) - log_obj.addHandler(hdlr) +def main(): + # parse command line + options, args = parse_options() + # start logging + logger = setup_logging(options) + logger.info('Welcome to %s version %s', sys.argv[0], __version__) + for a in args: + logger.warning('Ignoring unrecognized argument: %s', a) -def main(): - setup_logging_to_console(logger, logging.INFO) - logger.info("Welcome to BFF!") + if options.debug: + setup_debugging(logger) scriptpath = os.path.dirname(sys.argv[0]) logger.info('Scriptpath is %s', scriptpath) @@ -93,8 +115,11 @@ def main(): filetools.copy_file(remote_cfg_file, local_cfg_file) with Campaign(cfg_path=local_cfg_file) as campaign: + logger.info('Initiating campaign') campaign.go() + logger.info('Campaign complete') + if __name__ == '__main__': main() From e9fc04e2dd554c4da343f94b960fe772b9dde454 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 22:54:57 -0400 Subject: [PATCH 0239/1169] organize imports --- src/certfuzz/bff/linux.py | 3 +-- src/certfuzz/bff/windows.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index e1fa7ce..2ed257f 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -3,8 +3,6 @@ @author: adh ''' -from certfuzz.version import __version__ - import logging from optparse import OptionParser import os @@ -16,6 +14,7 @@ from certfuzz.fuzztools import filetools from certfuzz.campaign.linux import Campaign +from certfuzz.version import __version__ def _setup_logging_to_screen(options, logger, fmt): diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 0c24931..fdd506f 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -10,9 +10,10 @@ import os import sys -from certfuzz.version import __version__ from certfuzz.campaign.campaign_windows import WindowsCampaign +from certfuzz.version import __version__ + def _setup_logging_to_screen(options, logger, fmt): # logging to screen From fa1691387e1a9d5a077aff4e852dd93e0b5fcd88 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 23:01:42 -0400 Subject: [PATCH 0240/1169] pull in command line opts from certfuzz.bff.linux --- src/certfuzz/bff/linux.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 2ed257f..dbd766a 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -56,11 +56,22 @@ def setup_logging(options): _setup_logging_to_file(options, logger, fmt) return logger + def parse_options(): - parser = OptionParser() - parser.add_option('', '--debug', dest='debug', help='Turn on debugging output', action='store_true') - parser.add_option('-c', '--config', dest='cfg', help='Config file location') - return parser.parse_args() #@UnusedVariable + u = '%prog [options]' + v = ' '.join(['%prog', 'v%s' % __version__]) + parser = OptionParser(usage=u, version=v) + parser.add_option('-d', '--debug', dest='debug', action='store_true', + help='Enable debug messages to screen and log file (overrides --quiet)') + parser.add_option('-q', '--quiet', dest='quiet', action='store_true', + help='Silence messages to screen (log file will remain at INFO level') + parser.add_option('-v', '--verbose', dest='verbose', action='store_true', + help='Enable verbose logging messages to screen and log file (overrides --quiet)') + parser.add_option('-c', '--config', dest='configfile', help='Path to config file', metavar='FILE') +# TODO enable these options +# parser.add_option('-l', '--logfile', dest='logfile', help='Path to log file', metavar='FILE') +# parser.add_option('-r', '--result-dir', dest='resultdir', help='Path to result directory (overrides config)', metavar='DIR') + return parser.parse_args() def setup_debugging(logger): @@ -101,8 +112,8 @@ def main(): logger.info('Scriptpath is %s', scriptpath) # Get the cfg file name - if options.cfg: - remote_cfg_file = options.cfg + if options.configfile: + remote_cfg_file = options.configfile else: remote_cfg_file = get_config_file(scriptpath) From befffd98d8ee2f8fdf3e3669fbe228a61f7d645f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2014 23:06:48 -0400 Subject: [PATCH 0241/1169] TODO: investigate why we're checking for config files in script path --- src/certfuzz/bff/linux.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index dbd766a..d801c09 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -115,6 +115,7 @@ def main(): if options.configfile: remote_cfg_file = options.configfile else: + # TODO why are we doing this again? remote_cfg_file = get_config_file(scriptpath) # die unless the remote config is present From ed2b6a541bbb3422db92aafe529955266af15b9d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Apr 2014 15:37:44 -0400 Subject: [PATCH 0242/1169] Change debugger pw to bff instead of foe (only used when --debug option given) --- src/certfuzz/bff/windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index fdd506f..823b5fd 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -90,7 +90,7 @@ def parse_options(): def setup_debugging(logger): - logger.debug('Instantiating embedded rpdb2 debugger with password "foe"...') + logger.debug('Instantiating embedded rpdb2 debugger with password "bff"...') try: import rpdb2 rpdb2.start_embedded_debugger("foe", timeout=0.0) From c2aec5f93dbafea4e8b3b0d9ee2bee6a239e69cb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Apr 2014 15:40:57 -0400 Subject: [PATCH 0243/1169] fix for BFF-178 --- src/certfuzz/bff/linux.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index d801c09..5638715 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -121,11 +121,7 @@ def main(): # die unless the remote config is present assert os.path.exists(remote_cfg_file), 'Cannot find remote config file: %s, Please create it or use --config option to specify a different location.' % remote_cfg_file - # copy remote config to local: - local_cfg_file = os.path.expanduser('~/bff.cfg') - filetools.copy_file(remote_cfg_file, local_cfg_file) - - with Campaign(cfg_path=local_cfg_file) as campaign: + with Campaign(cfg_path=options.configfile) as campaign: logger.info('Initiating campaign') campaign.go() From c4b5e91ccae6c721f978d438b8cf488dfdc56798 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Apr 2014 15:42:04 -0400 Subject: [PATCH 0244/1169] simplify bff.cfg default if --config not specified --- src/certfuzz/bff/linux.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 5638715..fdcecdb 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -78,21 +78,6 @@ def setup_debugging(logger): pass -def get_config_file(basedir): - config_dir = os.path.join(basedir, 'conf.d') - - # check for a platform-specific file - platform_cfg = 'bff-%s.cfg' % platform.system() - - fullpath_platform_cfg = os.path.join(config_dir, platform_cfg) - - if os.path.exists(fullpath_platform_cfg): - config_file = fullpath_platform_cfg - else: - # default if nothing else is around - config_file = os.path.join(config_dir, "bff.cfg") - - return config_file def main(): @@ -108,19 +93,6 @@ def main(): if options.debug: setup_debugging(logger) - scriptpath = os.path.dirname(sys.argv[0]) - logger.info('Scriptpath is %s', scriptpath) - - # Get the cfg file name - if options.configfile: - remote_cfg_file = options.configfile - else: - # TODO why are we doing this again? - remote_cfg_file = get_config_file(scriptpath) - - # die unless the remote config is present - assert os.path.exists(remote_cfg_file), 'Cannot find remote config file: %s, Please create it or use --config option to specify a different location.' % remote_cfg_file - with Campaign(cfg_path=options.configfile) as campaign: logger.info('Initiating campaign') campaign.go() From 8b45a08b8e123247de5a504787b751d4867a7baf Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Apr 2014 15:42:58 -0400 Subject: [PATCH 0245/1169] bring over debugger setup when --debug option is used (fix for BFF-528) --- src/certfuzz/bff/linux.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index fdcecdb..7b8bdba 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -75,9 +75,19 @@ def parse_options(): def setup_debugging(logger): - pass - - + logger.debug('Instantiating embedded rpdb2 debugger with password "bff"...') + try: + import rpdb2 + rpdb2.start_embedded_debugger("foe", timeout=0.0) + except ImportError: + logger.debug('Error importing rpdb2. Is Winpdb installed?') + + logger.debug('Enabling heapy remote monitoring...') + try: + from guppy import hpy # @UnusedImport + import guppy.heapy.RM # @UnusedImport + except ImportError: + logger.debug('Error importing heapy. Is Guppy-PE installed?') def main(): From 93bbece74989433b7d3611e5aec9c33bd8c1dc75 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Apr 2014 15:51:36 -0400 Subject: [PATCH 0246/1169] refactor common code into separate module --- src/certfuzz/bff/common.py | 46 ++++++++++++++++++++++++++++++++++ src/certfuzz/bff/linux.py | 49 ++++--------------------------------- src/certfuzz/bff/windows.py | 43 +++----------------------------- 3 files changed, 54 insertions(+), 84 deletions(-) create mode 100644 src/certfuzz/bff/common.py diff --git a/src/certfuzz/bff/common.py b/src/certfuzz/bff/common.py new file mode 100644 index 0000000..e4048a6 --- /dev/null +++ b/src/certfuzz/bff/common.py @@ -0,0 +1,46 @@ +''' +Created on Apr 3, 2014 + +@author: adh +''' +import logging + +logger = logging.getLogger(__name__) + + +def _setup_logging_to_screen(options, fmt): + # logging to screen + hdlr = logging.StreamHandler() + + if options.debug: + level = logging.DEBUG + elif options.verbose: + level = logging.INFO + elif options.quiet: + level = logging.WARNING + else: + level = logging.INFO + + add_log_handler(logger, level, hdlr, fmt) + + +def add_log_handler(log_obj, level, hdlr, formatter): + hdlr.setLevel(level) + hdlr.setFormatter(formatter) + log_obj.addHandler(hdlr) + + +def setup_debugging(): + logger.debug('Instantiating embedded rpdb2 debugger with password "bff"...') + try: + import rpdb2 + rpdb2.start_embedded_debugger("foe", timeout=0.0) + except ImportError: + logger.debug('Error importing rpdb2. Is Winpdb installed?') + + logger.debug('Enabling heapy remote monitoring...') + try: + from guppy import hpy # @UnusedImport + import guppy.heapy.RM # @UnusedImport + except ImportError: + logger.debug('Error importing heapy. Is Guppy-PE installed?') diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 7b8bdba..4ccf173 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -5,40 +5,16 @@ ''' import logging from optparse import OptionParser -import os -import platform import sys from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport -from certfuzz.fuzztools import filetools +from certfuzz.bff.common import setup_debugging, _setup_logging_to_screen from certfuzz.campaign.linux import Campaign from certfuzz.version import __version__ -def _setup_logging_to_screen(options, logger, fmt): - # logging to screen - hdlr = logging.StreamHandler() - - if options.debug: - level = logging.DEBUG - elif options.verbose: - level = logging.INFO - elif options.quiet: - level = logging.WARNING - else: - level = logging.INFO - - add_log_handler(logger, level, hdlr, fmt) - - -def add_log_handler(log_obj, level, hdlr, formatter): - hdlr.setLevel(level) - hdlr.setFormatter(formatter) - log_obj.addHandler(hdlr) - - def _setup_logging_to_file(options, logger, fmt): pass @@ -52,7 +28,7 @@ def setup_logging(options): logger.setLevel(logging.INFO) fmt = logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(message)s') - _setup_logging_to_screen(options, logger, fmt) + _setup_logging_to_screen(options, fmt) _setup_logging_to_file(options, logger, fmt) return logger @@ -67,29 +43,14 @@ def parse_options(): help='Silence messages to screen (log file will remain at INFO level') parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Enable verbose logging messages to screen and log file (overrides --quiet)') - parser.add_option('-c', '--config', dest='configfile', help='Path to config file', metavar='FILE') + parser.add_option('-c', '--config', dest='configfile', help='Path to config file', + default='conf.d/bff.cfg', metavar='FILE') # TODO enable these options # parser.add_option('-l', '--logfile', dest='logfile', help='Path to log file', metavar='FILE') # parser.add_option('-r', '--result-dir', dest='resultdir', help='Path to result directory (overrides config)', metavar='DIR') return parser.parse_args() -def setup_debugging(logger): - logger.debug('Instantiating embedded rpdb2 debugger with password "bff"...') - try: - import rpdb2 - rpdb2.start_embedded_debugger("foe", timeout=0.0) - except ImportError: - logger.debug('Error importing rpdb2. Is Winpdb installed?') - - logger.debug('Enabling heapy remote monitoring...') - try: - from guppy import hpy # @UnusedImport - import guppy.heapy.RM # @UnusedImport - except ImportError: - logger.debug('Error importing heapy. Is Guppy-PE installed?') - - def main(): # parse command line options, args = parse_options() @@ -101,7 +62,7 @@ def main(): logger.warning('Ignoring unrecognized argument: %s', a) if options.debug: - setup_debugging(logger) + setup_debugging() with Campaign(cfg_path=options.configfile) as campaign: logger.info('Initiating campaign') diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 823b5fd..202af13 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -12,31 +12,10 @@ from certfuzz.campaign.campaign_windows import WindowsCampaign +from certfuzz.bff.common import _setup_logging_to_screen, setup_debugging from certfuzz.version import __version__ -def _setup_logging_to_screen(options, logger, fmt): - # logging to screen - hdlr = logging.StreamHandler() - - if options.debug: - level = logging.DEBUG - elif options.verbose: - level = logging.INFO - elif options.quiet: - level = logging.WARNING - else: - level = logging.INFO - - add_log_handler(logger, level, hdlr, fmt) - - -def add_log_handler(log_obj, level, hdlr, formatter): - hdlr.setLevel(level) - hdlr.setFormatter(formatter) - log_obj.addHandler(hdlr) - - def _setup_logging_to_file(options, logger, fmt): # logging to file # override if option specified @@ -66,7 +45,7 @@ def setup_logging(options): logger.setLevel(logging.INFO) fmt = logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(message)s') - _setup_logging_to_screen(options, logger, fmt) + _setup_logging_to_screen(options, fmt) _setup_logging_to_file(options, logger, fmt) return logger @@ -89,22 +68,6 @@ def parse_options(): return parser.parse_args() -def setup_debugging(logger): - logger.debug('Instantiating embedded rpdb2 debugger with password "bff"...') - try: - import rpdb2 - rpdb2.start_embedded_debugger("foe", timeout=0.0) - except ImportError: - logger.debug('Error importing rpdb2. Is Winpdb installed?') - - logger.debug('Enabling heapy remote monitoring...') - try: - from guppy import hpy # @UnusedImport - import guppy.heapy.RM # @UnusedImport - except ImportError: - logger.debug('Error importing heapy. Is Guppy-PE installed?') - - def main(): # parse command line options, args = parse_options() @@ -116,7 +79,7 @@ def main(): logger.warning('Ignoring unrecognized argument: %s', a) if options.debug: - setup_debugging(logger) + setup_debugging() with WindowsCampaign(config_file=options.configfile, result_dir=options.resultdir, debug=options.debug) as campaign: logger.info('Initiating campaign') From 99d09667c040578c33cc25ab13a60704d2072d00 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Apr 2014 15:54:03 -0400 Subject: [PATCH 0247/1169] use Campaign instead of WindowsCampaign in main method --- src/certfuzz/bff/windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 202af13..3302059 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -10,7 +10,7 @@ import os import sys -from certfuzz.campaign.campaign_windows import WindowsCampaign +from certfuzz.campaign.campaign_windows import WindowsCampaign as Campaign from certfuzz.bff.common import _setup_logging_to_screen, setup_debugging from certfuzz.version import __version__ @@ -81,7 +81,7 @@ def main(): if options.debug: setup_debugging() - with WindowsCampaign(config_file=options.configfile, result_dir=options.resultdir, debug=options.debug) as campaign: + with Campaign(config_file=options.configfile, result_dir=options.resultdir, debug=options.debug) as campaign: logger.info('Initiating campaign') campaign.go() From 3d15ee24d28a3633c9e21565a2d849e11e374c66 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Apr 2014 16:00:17 -0400 Subject: [PATCH 0248/1169] make Campaign initialization in linux match that on windows --- src/certfuzz/bff/linux.py | 2 +- src/certfuzz/campaign/linux.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 4ccf173..5e740fb 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -64,7 +64,7 @@ def main(): if options.debug: setup_debugging() - with Campaign(cfg_path=options.configfile) as campaign: + with Campaign(config_file=options.configfile, result_dir=options.resultdir, debug=options.debug) as campaign: logger.info('Initiating campaign') campaign.go() diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 690ef1e..566602b 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -36,17 +36,17 @@ class Campaign(object): - def __init__(self, cfg_path=None, scriptpath=None): + def __init__(self, config_file=None, result_dir=None, debug=False): # Read the cfg file - self.cfg_path = cfg_path - logger.info('Reading config from %s', cfg_path) - self.cfg = cfg_helper.read_config_options(cfg_path) - self.scriptpath = scriptpath + self.cfg_path = config_file + self.result_dir = result_dir + self.debug = debug + logger.info('Reading config from %s', config_file) + self.cfg = cfg_helper.read_config_options(config_file) self.seedfile_set = None self.hashes = [] self.workdirbase = self.cfg.testscase_tmp_dir self.working_dir = None - self.debug = True self.crashes_seen = set() # give up if we don't have a debugger From 295055bf6bba2a14a60d30a20b9442a823102804 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Apr 2014 16:16:35 -0400 Subject: [PATCH 0249/1169] add empty test module for certfuzz.bff.common --- src/certfuzz/test/bff/test_common.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/certfuzz/test/bff/test_common.py diff --git a/src/certfuzz/test/bff/test_common.py b/src/certfuzz/test/bff/test_common.py new file mode 100644 index 0000000..99932e0 --- /dev/null +++ b/src/certfuzz/test/bff/test_common.py @@ -0,0 +1,23 @@ +''' +Created on Apr 3, 2014 + +@organization: cert.org +''' +import unittest +import certfuzz.bff.common + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From ef0517bf66288d48ffc17ce9d4dd4acd15b8f418 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Apr 2014 16:19:33 -0400 Subject: [PATCH 0250/1169] it's bff_log now not foe2log --- src/certfuzz/bff/windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 3302059..144b38a 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -22,7 +22,7 @@ def _setup_logging_to_file(options, logger, fmt): if options.logfile: logfile = options.logfile else: - logfile = os.path.join('log', 'foe2log.txt') + logfile = os.path.join('log', 'bff_log.txt') hdlr = RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) From 10eaa92088f1fac34d8caebb9f3efd2594b9bfec Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 08:12:48 -0400 Subject: [PATCH 0251/1169] complete command line option parity --- src/certfuzz/bff/linux.py | 9 ++++++--- src/certfuzz/bff/windows.py | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 5e740fb..0ec927a 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -45,9 +45,12 @@ def parse_options(): help='Enable verbose logging messages to screen and log file (overrides --quiet)') parser.add_option('-c', '--config', dest='configfile', help='Path to config file', default='conf.d/bff.cfg', metavar='FILE') -# TODO enable these options -# parser.add_option('-l', '--logfile', dest='logfile', help='Path to log file', metavar='FILE') -# parser.add_option('-r', '--result-dir', dest='resultdir', help='Path to result directory (overrides config)', metavar='DIR') + parser.add_option('-l', '--logfile', dest='logfile', default=None, + help='Path to log file', metavar='FILE') + parser.add_option('-r', '--result-dir', dest='resultdir', + default=None, + help='Path to result directory (overrides config)', metavar='DIR') + return parser.parse_args() diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 144b38a..1d22590 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -62,8 +62,11 @@ def parse_options(): help='Enable verbose logging messages to screen and log file (overrides --quiet)') parser.add_option('-c', '--config', dest='configfile', help='Path to config file', default='configs/foe.yaml', metavar='FILE') - parser.add_option('-l', '--logfile', dest='logfile', help='Path to log file', metavar='FILE') - parser.add_option('-r', '--result-dir', dest='resultdir', help='Path to result directory (overrides config)', metavar='DIR') + parser.add_option('-l', '--logfile', dest='logfile', default=None, + help='Path to log file', metavar='FILE') + parser.add_option('-r', '--result-dir', dest='resultdir', + default=None, + help='Path to result directory (overrides config)', metavar='DIR') return parser.parse_args() From 3607bedf58b007932990b40a4ad31b082395695b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 08:15:36 -0400 Subject: [PATCH 0252/1169] staging logging to file method from windows into linux --- src/certfuzz/bff/linux.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 0ec927a..1432a7e 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -17,6 +17,24 @@ def _setup_logging_to_file(options, logger, fmt): pass + # TODO make this work + # logging to file + # override if option specified +# if options.logfile: +# logfile = options.logfile +# else: +# logfile = os.path.join('log', 'bff_log.txt') +# +# hdlr = RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) +# +# hdlr.setFormatter(fmt) +# hdlr.setLevel(logging.WARNING) +# # override if debug +# if options.debug: +# hdlr.setLevel(logging.DEBUG) +# elif options.verbose: +# hdlr.setLevel(logging.INFO) +# logger.addHandler(hdlr) def setup_logging(options): @@ -50,7 +68,7 @@ def parse_options(): parser.add_option('-r', '--result-dir', dest='resultdir', default=None, help='Path to result directory (overrides config)', metavar='DIR') - + return parser.parse_args() From 46e7e4b2b8a2ca6b13e8a20e44da2423fdd98b0f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 10:36:16 -0400 Subject: [PATCH 0253/1169] add config arg to bff startup script --- src/linux/batch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index 6df4ff7..3416aa7 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -96,7 +96,7 @@ fi echo "Using python interpreter: $mypython" if [[ -f "$scriptlocation/bff.py" ]]; then - $mypython $scriptlocation/bff.py + $mypython $scriptlocation/bff.py --config=$scriptlocation/conf.d/bff.cfg else read -p "Cannot find $scriptlocation/bff.py Please verify script locations." fi From 504fe2dad70c8b52239e524b9993057b453c58de Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 10:40:35 -0400 Subject: [PATCH 0254/1169] add certfuzz.bff.errors module --- src/certfuzz/bff/errors.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/certfuzz/bff/errors.py diff --git a/src/certfuzz/bff/errors.py b/src/certfuzz/bff/errors.py new file mode 100644 index 0000000..b9af9db --- /dev/null +++ b/src/certfuzz/bff/errors.py @@ -0,0 +1,10 @@ +''' +Created on Apr 4, 2014 + +@author: adh +''' +from certfuzz.errors import CERTFuzzError + + +class BFFerror(CERTFuzzError): + pass From f7bcb149ca754c08b1d94aaafc32cec9df967c85 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 10:41:36 -0400 Subject: [PATCH 0255/1169] emit less threatening log messages when debugging tools are not present --- src/certfuzz/bff/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/bff/common.py b/src/certfuzz/bff/common.py index e4048a6..db0f5b5 100644 --- a/src/certfuzz/bff/common.py +++ b/src/certfuzz/bff/common.py @@ -36,11 +36,11 @@ def setup_debugging(): import rpdb2 rpdb2.start_embedded_debugger("foe", timeout=0.0) except ImportError: - logger.debug('Error importing rpdb2. Is Winpdb installed?') + logger.debug('Skipping import of rpdb2. Is Winpdb installed?') logger.debug('Enabling heapy remote monitoring...') try: from guppy import hpy # @UnusedImport import guppy.heapy.RM # @UnusedImport except ImportError: - logger.debug('Error importing heapy. Is Guppy-PE installed?') + logger.debug('Skipping import of heapy. Is Guppy-PE installed?') From b4f8f2f1e2676d827e5ab6324ac217109e18b7e7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 11:23:59 -0400 Subject: [PATCH 0256/1169] refactor common code into common BFF object --- src/certfuzz/bff/common.py | 125 +++++++++++++++++++++++++++++++----- src/certfuzz/bff/linux.py | 85 ++---------------------- src/certfuzz/bff/windows.py | 92 +++++++------------------- 3 files changed, 138 insertions(+), 164 deletions(-) diff --git a/src/certfuzz/bff/common.py b/src/certfuzz/bff/common.py index db0f5b5..4ecab83 100644 --- a/src/certfuzz/bff/common.py +++ b/src/certfuzz/bff/common.py @@ -4,24 +4,14 @@ @author: adh ''' import logging +from optparse import OptionParser +import sys -logger = logging.getLogger(__name__) - +from certfuzz.bff.errors import BFFerror +from certfuzz.version import __version__ -def _setup_logging_to_screen(options, fmt): - # logging to screen - hdlr = logging.StreamHandler() - if options.debug: - level = logging.DEBUG - elif options.verbose: - level = logging.INFO - elif options.quiet: - level = logging.WARNING - else: - level = logging.INFO - - add_log_handler(logger, level, hdlr, fmt) +logger = logging.getLogger(__name__) def add_log_handler(log_obj, level, hdlr, formatter): @@ -44,3 +34,108 @@ def setup_debugging(): import guppy.heapy.RM # @UnusedImport except ImportError: logger.debug('Skipping import of heapy. Is Guppy-PE installed?') + + +class BFF(object): + def __init__(self, config_path=None, campaign_class=None): + self.config_path = config_path + self.campaign_class = campaign_class + + def __enter__(self): + self._parse_options() + self._setup_logging() + if self.options.debug: + setup_debugging() + + return self + + def __exit__(self, etype, value, traceback): + pass + + def _parse_options(self): + u = '%prog [options]' + v = ' '.join(['%prog', 'v%s' % __version__]) + parser = OptionParser(usage=u, version=v) + parser.add_option('-d', '--debug', dest='debug', action='store_true', + help='Enable debug messages to screen and log file (overrides --quiet)') + parser.add_option('-q', '--quiet', dest='quiet', action='store_true', + help='Silence messages to screen (log file will remain at INFO level') + parser.add_option('-v', '--verbose', dest='verbose', action='store_true', + help='Enable verbose logging messages to screen and log file (overrides --quiet)') + parser.add_option('-c', '--config', dest='configfile', help='Path to config file', + default=self.config_path, metavar='FILE') + parser.add_option('-l', '--logfile', dest='logfile', default=None, + help='Path to log file', metavar='FILE') + parser.add_option('-r', '--result-dir', dest='resultdir', + default=None, + help='Path to result directory (overrides config)', metavar='DIR') + + (self.options, self.args) = parser.parse_args() + + for a in self.args: + logger.warning('Ignoring unrecognized argument: %s', a) + + def _setup_logging_to_screen(self, options, fmt): + # logging to screen + hdlr = logging.StreamHandler() + + if self.options.debug: + level = logging.DEBUG + elif self.options.verbose: + level = logging.INFO + elif self.options.quiet: + level = logging.WARNING + else: + level = logging.INFO + + root_logger = logging.getLogger() + + add_log_handler(root_logger, level, hdlr, fmt) + + def _setup_logging_to_file(self, options, logger_, fmt): + pass + # TODO make this work + # logging to file + # override if option specified + # if options.logfile: + # logfile = options.logfile + # else: + # logfile = os.path.join('log', 'bff_log.txt') + # + # hdlr = RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) + # + # hdlr.setFormatter(fmt) + # hdlr.setLevel(logging.WARNING) + # # override if debug + # if options.debug: + # hdlr.setLevel(logging.DEBUG) + # elif options.verbose: + # hdlr.setLevel(logging.INFO) + # logger.addHandler(hdlr) + + def _setup_logging(self): + root_logger = logging.getLogger() + + if self.options.debug: + root_logger.setLevel(logging.DEBUG) + else: + root_logger.setLevel(logging.INFO) + + fmt = logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(message)s') + self._setup_logging_to_screen(self.options, fmt) + self._setup_logging_to_file(self.options, logger, fmt) + + def go(self): + logger.info('Welcome to %s version %s', sys.argv[0], __version__) + + if self.campaign_class is None: + raise BFFerror('Campaign class is undefined') + + with self.campaign_class(config_file=self.options.configfile, + result_dir=self.options.resultdir, + debug=self.options.debug) as campaign: + logger.info('Initiating campaign') + campaign.go() + + logger.info('Campaign complete') + diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 1432a7e..a24bdc0 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -4,93 +4,20 @@ @author: adh ''' import logging -from optparse import OptionParser -import sys +import os from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport -from certfuzz.bff.common import setup_debugging, _setup_logging_to_screen +from certfuzz.bff.common import BFF from certfuzz.campaign.linux import Campaign -from certfuzz.version import __version__ -def _setup_logging_to_file(options, logger, fmt): - pass - # TODO make this work - # logging to file - # override if option specified -# if options.logfile: -# logfile = options.logfile -# else: -# logfile = os.path.join('log', 'bff_log.txt') -# -# hdlr = RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) -# -# hdlr.setFormatter(fmt) -# hdlr.setLevel(logging.WARNING) -# # override if debug -# if options.debug: -# hdlr.setLevel(logging.DEBUG) -# elif options.verbose: -# hdlr.setLevel(logging.INFO) -# logger.addHandler(hdlr) - - -def setup_logging(options): - logger = logging.getLogger() - - if options.debug: - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - - fmt = logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(message)s') - _setup_logging_to_screen(options, fmt) - _setup_logging_to_file(options, logger, fmt) - return logger - - -def parse_options(): - u = '%prog [options]' - v = ' '.join(['%prog', 'v%s' % __version__]) - parser = OptionParser(usage=u, version=v) - parser.add_option('-d', '--debug', dest='debug', action='store_true', - help='Enable debug messages to screen and log file (overrides --quiet)') - parser.add_option('-q', '--quiet', dest='quiet', action='store_true', - help='Silence messages to screen (log file will remain at INFO level') - parser.add_option('-v', '--verbose', dest='verbose', action='store_true', - help='Enable verbose logging messages to screen and log file (overrides --quiet)') - parser.add_option('-c', '--config', dest='configfile', help='Path to config file', - default='conf.d/bff.cfg', metavar='FILE') - parser.add_option('-l', '--logfile', dest='logfile', default=None, - help='Path to log file', metavar='FILE') - parser.add_option('-r', '--result-dir', dest='resultdir', - default=None, - help='Path to result directory (overrides config)', metavar='DIR') - - return parser.parse_args() +logger = logging.getLogger(__name__) def main(): - # parse command line - options, args = parse_options() - - # start logging - logger = setup_logging(options) - logger.info('Welcome to %s version %s', sys.argv[0], __version__) - for a in args: - logger.warning('Ignoring unrecognized argument: %s', a) - - if options.debug: - setup_debugging() - - with Campaign(config_file=options.configfile, result_dir=options.resultdir, debug=options.debug) as campaign: - logger.info('Initiating campaign') - campaign.go() - - logger.info('Campaign complete') - + cfg = os.path.abspath(os.path.join(os.getcwd(), 'conf.d', 'bff.cfg')) -if __name__ == '__main__': - main() + with BFF(config_path=cfg, campaign_class=Campaign) as bff: + bff.go() diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 1d22590..ae446a2 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -5,87 +5,39 @@ ''' import logging -from logging.handlers import RotatingFileHandler -from optparse import OptionParser import os -import sys from certfuzz.campaign.campaign_windows import WindowsCampaign as Campaign -from certfuzz.bff.common import _setup_logging_to_screen, setup_debugging -from certfuzz.version import __version__ +from certfuzz.bff.common import BFF -def _setup_logging_to_file(options, logger, fmt): - # logging to file - # override if option specified - if options.logfile: - logfile = options.logfile - else: - logfile = os.path.join('log', 'bff_log.txt') +logger = logging.getLogger(__name__) - hdlr = RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) - hdlr.setFormatter(fmt) - hdlr.setLevel(logging.WARNING) - # override if debug - if options.debug: - hdlr.setLevel(logging.DEBUG) - elif options.verbose: - hdlr.setLevel(logging.INFO) - logger.addHandler(hdlr) +class BFFWindows(BFF): + def _setup_logging_to_file(self, logger_, fmt): + # logging to file + # override if option specified + if self.options.logfile: + logfile = self.options.logfile + else: + logfile = os.path.join('log', 'bff_log.txt') + hdlr = logging.handlers.RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) -def setup_logging(options): - logger = logging.getLogger() - - if options.debug: - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - - fmt = logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(message)s') - _setup_logging_to_screen(options, fmt) - _setup_logging_to_file(options, logger, fmt) - return logger - - -def parse_options(): - u = '%prog [options]' - v = ' '.join(['%prog', 'v%s' % __version__]) - parser = OptionParser(usage=u, version=v) - parser.add_option('-d', '--debug', dest='debug', action='store_true', - help='Enable debug messages to screen and log file (overrides --quiet)') - parser.add_option('-q', '--quiet', dest='quiet', action='store_true', - help='Silence messages to screen (log file will remain at INFO level') - parser.add_option('-v', '--verbose', dest='verbose', action='store_true', - help='Enable verbose logging messages to screen and log file (overrides --quiet)') - parser.add_option('-c', '--config', dest='configfile', help='Path to config file', - default='configs/foe.yaml', metavar='FILE') - parser.add_option('-l', '--logfile', dest='logfile', default=None, - help='Path to log file', metavar='FILE') - parser.add_option('-r', '--result-dir', dest='resultdir', - default=None, - help='Path to result directory (overrides config)', metavar='DIR') - - return parser.parse_args() + hdlr.setFormatter(fmt) + hdlr.setLevel(logging.WARNING) + # override if debug + if self.options.debug: + hdlr.setLevel(logging.DEBUG) + elif self.options.verbose: + hdlr.setLevel(logging.INFO) + logger_.addHandler(hdlr) def main(): - # parse command line - options, args = parse_options() - - # start logging - logger = setup_logging(options) - logger.info('Welcome to %s version %s', sys.argv[0], __version__) - for a in args: - logger.warning('Ignoring unrecognized argument: %s', a) - - if options.debug: - setup_debugging() - - with Campaign(config_file=options.configfile, result_dir=options.resultdir, debug=options.debug) as campaign: - logger.info('Initiating campaign') - campaign.go() + cfg = os.path.abspath(os.path.join(os.getcwd(), 'configs', 'foe.yaml')) - logger.info('Campaign complete') + with BFFWindows(config_path=cfg, campaign_class=Campaign) as bff: + bff.go() From 1665a764d15d8f0558ba550ad0f8ea51a455b29f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 14:41:12 -0400 Subject: [PATCH 0257/1169] refactor logging setup --- src/certfuzz/bff/common.py | 80 ++++++++++++++++++------------------- src/certfuzz/bff/windows.py | 25 +----------- 2 files changed, 40 insertions(+), 65 deletions(-) diff --git a/src/certfuzz/bff/common.py b/src/certfuzz/bff/common.py index 4ecab83..fc53d22 100644 --- a/src/certfuzz/bff/common.py +++ b/src/certfuzz/bff/common.py @@ -5,8 +5,11 @@ ''' import logging from optparse import OptionParser +import os import sys +from certfuzz.fuzztools.filetools import mkdir_p + from certfuzz.bff.errors import BFFerror from certfuzz.version import __version__ @@ -41,9 +44,17 @@ def __init__(self, config_path=None, campaign_class=None): self.config_path = config_path self.campaign_class = campaign_class + self._logdir = 'log' + self._logfile = os.path.abspath(os.path.join(self._logdir, 'bff.log')) + self.logfile = None + self.log_level = logging.INFO + def __enter__(self): self._parse_options() + self._process_options() + self._setup_logging() + if self.options.debug: setup_debugging() @@ -72,58 +83,43 @@ def _parse_options(self): (self.options, self.args) = parser.parse_args() + def _process_options(self): for a in self.args: logger.warning('Ignoring unrecognized argument: %s', a) - def _setup_logging_to_screen(self, options, fmt): - # logging to screen - hdlr = logging.StreamHandler() + # set logfile destination + if self.options.logfile: + self.logfile = self.options.logfile + else: + self.logfile = self._logfile + # set log level if self.options.debug: - level = logging.DEBUG + self.log_level = logging.DEBUG elif self.options.verbose: - level = logging.INFO + self.log_level = logging.INFO elif self.options.quiet: - level = logging.WARNING - else: - level = logging.INFO - - root_logger = logging.getLogger() - - add_log_handler(root_logger, level, hdlr, fmt) - - def _setup_logging_to_file(self, options, logger_, fmt): - pass - # TODO make this work - # logging to file - # override if option specified - # if options.logfile: - # logfile = options.logfile - # else: - # logfile = os.path.join('log', 'bff_log.txt') - # - # hdlr = RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) - # - # hdlr.setFormatter(fmt) - # hdlr.setLevel(logging.WARNING) - # # override if debug - # if options.debug: - # hdlr.setLevel(logging.DEBUG) - # elif options.verbose: - # hdlr.setLevel(logging.INFO) - # logger.addHandler(hdlr) + self.log_level = logging.WARNING def _setup_logging(self): - root_logger = logging.getLogger() + logdir = os.path.abspath(os.path.dirname(self.logfile)) + mkdir_p(logdir) - if self.options.debug: - root_logger.setLevel(logging.DEBUG) - else: - root_logger.setLevel(logging.INFO) + root_logger = logging.getLogger() + root_logger.setLevel(self.log_level) fmt = logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(message)s') - self._setup_logging_to_screen(self.options, fmt) - self._setup_logging_to_file(self.options, logger, fmt) + + handlers = [] + handlers.append(logging.StreamHandler()) + handlers.append(logging.handlers.RotatingFileHandler(self.logfile, + mode='w', + maxBytes=1e7, + backupCount=5) + ) + + for handler in handlers: + add_log_handler(root_logger, self.log_level, handler, fmt) def go(self): logger.info('Welcome to %s version %s', sys.argv[0], __version__) @@ -131,11 +127,11 @@ def go(self): if self.campaign_class is None: raise BFFerror('Campaign class is undefined') + logger.info('Creating campaign') with self.campaign_class(config_file=self.options.configfile, result_dir=self.options.resultdir, debug=self.options.debug) as campaign: - logger.info('Initiating campaign') + logger.info('Starting campaign') campaign.go() logger.info('Campaign complete') - diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index ae446a2..65932c3 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -7,7 +7,7 @@ import logging import os -from certfuzz.campaign.campaign_windows import WindowsCampaign as Campaign +from certfuzz.campaign.campaign_windows import WindowsCampaign from certfuzz.bff.common import BFF @@ -15,29 +15,8 @@ logger = logging.getLogger(__name__) -class BFFWindows(BFF): - def _setup_logging_to_file(self, logger_, fmt): - # logging to file - # override if option specified - if self.options.logfile: - logfile = self.options.logfile - else: - logfile = os.path.join('log', 'bff_log.txt') - - hdlr = logging.handlers.RotatingFileHandler(logfile, mode='w', maxBytes=1e7, backupCount=5) - - hdlr.setFormatter(fmt) - hdlr.setLevel(logging.WARNING) - # override if debug - if self.options.debug: - hdlr.setLevel(logging.DEBUG) - elif self.options.verbose: - hdlr.setLevel(logging.INFO) - logger_.addHandler(hdlr) - - def main(): cfg = os.path.abspath(os.path.join(os.getcwd(), 'configs', 'foe.yaml')) - with BFFWindows(config_path=cfg, campaign_class=Campaign) as bff: + with BFF(config_path=cfg, campaign_class=WindowsCampaign) as bff: bff.go() From 90a3b9e6f4913ca3d7c783b3b424d190869fe124 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 14:51:41 -0400 Subject: [PATCH 0258/1169] eliminate separate logging to ~/fuzzing/bff.log and ~/results/bff.log Logging setup is in certfuzz.bff.common.BFF now. --- src/certfuzz/campaign/linux.py | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/linux.py index 566602b..40b6079 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/linux.py @@ -6,7 +6,6 @@ import itertools import logging -from logging.handlers import RotatingFileHandler import os import shutil import sys @@ -22,7 +21,7 @@ from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.fuzztools import subprocess_helper as subp -from certfuzz.fuzztools.filetools import mkdir_p, make_directories, copy_file +from certfuzz.fuzztools.filetools import mkdir_p, copy_file from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.fuzztools.watchdog import WatchDog @@ -60,12 +59,6 @@ def __enter__(self): self.working_dir = tempfile.mkdtemp(prefix='campaign_', dir=self.workdirbase) logger.debug('workdir=%s', self.working_dir) - # set up local logging - self._setup_logfile(logdir=self.cfg.local_dir, backup_count=3) - - # set up remote logging - self._setup_logfile(logdir=self.cfg.output_dir, level=logging.INFO, max_bytes=1e7) - self._check_for_script() self._copy_config() self._start_process_killer() @@ -137,28 +130,6 @@ def _setup_dirs(self): logger.debug('Creating dir %s', d) mkdir_p(d) - def _setup_logfile(self, logdir, log_basename='bff.log', level=logging.DEBUG, - max_bytes=1e8, backup_count=5): - ''' - Creates a log file in / at level - @param logdir: the directory where the log file should reside - @param log_basename: the basename of the logfile (defaults to 'bff_log.txt') - @param level: the logging level (defaults to logging.DEBUG) - ''' - make_directories(logdir) - logfile = os.path.join(logdir, log_basename) - hdlr = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) - formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s") - - hdlr.setLevel(level) - hdlr.setFormatter(formatter) - - # add the handler to the root logger (not the logger for this module) - root_logger = logging.getLogger() - root_logger.addHandler(hdlr) - - logger.info('Logging %s at %s', logging.getLevelName(level), logfile) - def _copy_config(self): logger.debug('copy config') From b5f55e8acb50dca86d0d9b77c606f7c324994c6c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 15:31:05 -0400 Subject: [PATCH 0259/1169] replace opt parse with argparse (BFF-514) --- src/certfuzz/bff/common.py | 68 ++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/src/certfuzz/bff/common.py b/src/certfuzz/bff/common.py index fc53d22..faf7d6c 100644 --- a/src/certfuzz/bff/common.py +++ b/src/certfuzz/bff/common.py @@ -4,7 +4,7 @@ @author: adh ''' import logging -from optparse import OptionParser +from logging.handlers import RotatingFileHandler import os import sys @@ -12,6 +12,7 @@ from certfuzz.bff.errors import BFFerror from certfuzz.version import __version__ +import argparse logger = logging.getLogger(__name__) @@ -50,12 +51,12 @@ def __init__(self, config_path=None, campaign_class=None): self.log_level = logging.INFO def __enter__(self): - self._parse_options() - self._process_options() + self._parse_args() + self._process_args() self._setup_logging() - if self.options.debug: + if self.args.debug: setup_debugging() return self @@ -63,42 +64,37 @@ def __enter__(self): def __exit__(self, etype, value, traceback): pass - def _parse_options(self): - u = '%prog [options]' - v = ' '.join(['%prog', 'v%s' % __version__]) - parser = OptionParser(usage=u, version=v) - parser.add_option('-d', '--debug', dest='debug', action='store_true', - help='Enable debug messages to screen and log file (overrides --quiet)') - parser.add_option('-q', '--quiet', dest='quiet', action='store_true', - help='Silence messages to screen (log file will remain at INFO level') - parser.add_option('-v', '--verbose', dest='verbose', action='store_true', - help='Enable verbose logging messages to screen and log file (overrides --quiet)') - parser.add_option('-c', '--config', dest='configfile', help='Path to config file', + def _parse_args(self): + parser = argparse.ArgumentParser(description='CERT Basic Fuzzing Framework {}'.format(__version__)) + + group = parser.add_mutually_exclusive_group() + group.add_argument('-d', '--debug', dest='debug', action='store_true', + help='Set logging to DEBUG and enable additional debuggers if available') + group.add_argument('-q', '--quiet', dest='quiet', action='store_true', + help='Set logging to WARNING level') + group.add_argument('-v', '--verbose', dest='verbose', action='store_true', + help='Set logging to INFO level') + + parser.add_argument('-c', '--config', dest='configfile', type=str, help='Path to config file', default=self.config_path, metavar='FILE') - parser.add_option('-l', '--logfile', dest='logfile', default=None, + parser.add_argument('-l', '--logfile', dest='logfile', type=str, default=self._logfile, help='Path to log file', metavar='FILE') - parser.add_option('-r', '--result-dir', dest='resultdir', + parser.add_argument('-r', '--result-dir', dest='resultdir', type=str, default=None, help='Path to result directory (overrides config)', metavar='DIR') - (self.options, self.args) = parser.parse_args() - - def _process_options(self): - for a in self.args: - logger.warning('Ignoring unrecognized argument: %s', a) + self.args = parser.parse_args() + def _process_args(self): # set logfile destination - if self.options.logfile: - self.logfile = self.options.logfile - else: - self.logfile = self._logfile + self.logfile = self.args.logfile # set log level - if self.options.debug: + if self.args.debug: self.log_level = logging.DEBUG - elif self.options.verbose: + elif self.args.verbose: self.log_level = logging.INFO - elif self.options.quiet: + elif self.args.quiet: self.log_level = logging.WARNING def _setup_logging(self): @@ -112,10 +108,10 @@ def _setup_logging(self): handlers = [] handlers.append(logging.StreamHandler()) - handlers.append(logging.handlers.RotatingFileHandler(self.logfile, - mode='w', - maxBytes=1e7, - backupCount=5) + handlers.append(RotatingFileHandler(self.logfile, + mode='w', + maxBytes=1e7, + backupCount=5) ) for handler in handlers: @@ -128,9 +124,9 @@ def go(self): raise BFFerror('Campaign class is undefined') logger.info('Creating campaign') - with self.campaign_class(config_file=self.options.configfile, - result_dir=self.options.resultdir, - debug=self.options.debug) as campaign: + with self.campaign_class(config_file=self.args.configfile, + result_dir=self.args.resultdir, + debug=self.args.debug) as campaign: logger.info('Starting campaign') campaign.go() From 9f87911f089227c5dbdbaa03d040f20f8960e8f8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 4 Apr 2014 15:51:44 -0400 Subject: [PATCH 0260/1169] rename linux campaign module and class for consistency with windows/android --- src/certfuzz/bff/linux.py | 4 ++-- src/certfuzz/campaign/{linux.py => campaign_linux.py} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/certfuzz/campaign/{linux.py => campaign_linux.py} (99%) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index a24bdc0..211adf5 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -10,7 +10,7 @@ from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.bff.common import BFF -from certfuzz.campaign.linux import Campaign +from certfuzz.campaign.campaign_linux import LinuxCampaign logger = logging.getLogger(__name__) @@ -19,5 +19,5 @@ def main(): cfg = os.path.abspath(os.path.join(os.getcwd(), 'conf.d', 'bff.cfg')) - with BFF(config_path=cfg, campaign_class=Campaign) as bff: + with BFF(config_path=cfg, campaign_class=LinuxCampaign) as bff: bff.go() diff --git a/src/certfuzz/campaign/linux.py b/src/certfuzz/campaign/campaign_linux.py similarity index 99% rename from src/certfuzz/campaign/linux.py rename to src/certfuzz/campaign/campaign_linux.py index 40b6079..ab88858 100644 --- a/src/certfuzz/campaign/linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) -class Campaign(object): +class LinuxCampaign(object): def __init__(self, config_file=None, result_dir=None, debug=False): # Read the cfg file self.cfg_path = config_file From 33bbef5260ee9b8401ce6bc59852f7f9403f959a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 10 Apr 2014 11:24:33 -0400 Subject: [PATCH 0261/1169] explicit import of version from certfuzz.version --- src/certfuzz/campaign/campaign_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index ec006be..ec09447 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -10,7 +10,7 @@ import sys import os -from certfuzz import __version__ +from certfuzz.version import __version__ from certfuzz.fuzztools import filetools From 0982107ea108ea2a8d564a22e5b368e271bda739 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 May 2014 13:37:25 -0400 Subject: [PATCH 0262/1169] add epydoc example config from http://epydoc.sourceforge.net/configfile.html --- src/doc/epydoc.conf | 105 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/doc/epydoc.conf diff --git a/src/doc/epydoc.conf b/src/doc/epydoc.conf new file mode 100644 index 0000000..f410b1d --- /dev/null +++ b/src/doc/epydoc.conf @@ -0,0 +1,105 @@ +[epydoc] # Epydoc section marker (required by ConfigParser) + +# modules +# The list of objects to document. Objects can be named using +# dotted names, module filenames, or package directory names. +# Alases for this option include "objects" and "values". +modules: sys, os.path, re + +# output +# The type of output that should be generated. Should be one +# of: html, text, latex, dvi, ps, pdf. +output: html + +# target +# The path to the output directory. May be relative or absolute. +target: html/ + +# docformat +# The default markup language for docstrings, for modules that do +# not define __docformat__. Defaults to epytext. +docformat: epytext + +# css +# The CSS stylesheet for HTML output. Can be the name of a builtin +# stylesheet, or the name of a file. +css: white + +# name +# The documented project's name. +name: Example + +# url +# The documented project's URL. +url: http://some.project/ + +# link +# HTML code for the project link in the navigation bar. If left +# unspecified, the project link will be generated based on the +# project's name and URL. +link: My Cool Project + +# top +# The "top" page for the documentation. Can be a URL, the name +# of a module or class, or one of the special names "trees.html", +# "indices.html", or "help.html" +top: os.path + +# help +# An alternative help file. The named file should contain the +# body of an HTML file; navigation bars will be added to it. +help: my_helpfile.html + +# frames +# Whether or not to include a frames-based table of contents. +frames: yes + +# private +# Whether or not to inclue private variables. (Even if included, +# private variables will be hidden by default.) +private: yes + +# imports +# Whether or not to list each module's imports. +imports: no + +# verbosity +# An integer indicating how verbose epydoc should be. The default +# value is 0; negative values will supress warnings and errors; +# positive values will give more verbose output. +verbosity: 0 + +# parse +# Whether or not parsing should be used to examine objects. +parse: yes + +# introspect +# Whether or not introspection should be used to examine objects. +introspect: yes + +# graph +# The list of graph types that should be automatically included +# in the output. Graphs are generated using the Graphviz "dot" +# executable. Graph types include: "classtree", "callgraph", +# "umlclass". Use "all" to include all graph types +graph: all + +# dotpath +# The path to the Graphviz "dot" executable, used to generate +# graphs. +dotpath: /usr/local/bin/dot + +# sourcecode +# Whether or not to include syntax highlighted source code in +# the output (HTML only). +sourcecode: yes + +# pstat +# The name of one or more pstat files (generated by the profile +# or hotshot module). These are used to generate call graphs. +pstat: profile.out + +# separate-classes +# Whether each class should be listed in its own section when +# generating LaTeX or PDF output. +separate-classes: no \ No newline at end of file From 337d7cf90b453ca372f4c410654b3c6ac6ed68ec Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 May 2014 13:38:03 -0400 Subject: [PATCH 0263/1169] edit epydoc config for bff --- src/doc/epydoc.conf | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/doc/epydoc.conf b/src/doc/epydoc.conf index f410b1d..201f3c4 100644 --- a/src/doc/epydoc.conf +++ b/src/doc/epydoc.conf @@ -4,7 +4,7 @@ # The list of objects to document. Objects can be named using # dotted names, module filenames, or package directory names. # Alases for this option include "objects" and "values". -modules: sys, os.path, re +modules: certfuzz # output # The type of output that should be generated. Should be one @@ -27,28 +27,28 @@ css: white # name # The documented project's name. -name: Example +name: CERT Basic Fuzzing Framework # url # The documented project's URL. -url: http://some.project/ +url: http://www.cert.org/vulnerability-analysis/tools/bff.cfm # link # HTML code for the project link in the navigation bar. If left # unspecified, the project link will be generated based on the # project's name and URL. -link: My Cool Project +; link: My Cool Project # top # The "top" page for the documentation. Can be a URL, the name # of a module or class, or one of the special names "trees.html", # "indices.html", or "help.html" -top: os.path +; top: os.path # help # An alternative help file. The named file should contain the # body of an HTML file; navigation bars will be added to it. -help: my_helpfile.html +; help: my_helpfile.html # frames # Whether or not to include a frames-based table of contents. @@ -61,7 +61,7 @@ private: yes # imports # Whether or not to list each module's imports. -imports: no +imports: yes # verbosity # An integer indicating how verbose epydoc should be. The default @@ -97,9 +97,9 @@ sourcecode: yes # pstat # The name of one or more pstat files (generated by the profile # or hotshot module). These are used to generate call graphs. -pstat: profile.out +; pstat: profile.out # separate-classes # Whether each class should be listed in its own section when # generating LaTeX or PDF output. -separate-classes: no \ No newline at end of file +separate-classes: no From 57e13a7317206171f35d37d1dac42d837665c8de Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 May 2014 13:54:21 -0400 Subject: [PATCH 0264/1169] move epydoc stuff to top level subdir --- doc/docgen.sh | 5 +++++ {src/doc => doc}/epydoc.conf | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100755 doc/docgen.sh rename {src/doc => doc}/epydoc.conf (98%) diff --git a/doc/docgen.sh b/doc/docgen.sh new file mode 100755 index 0000000..cf35be8 --- /dev/null +++ b/doc/docgen.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +EPYDOC=/opt/local/bin/epydoc-2.7 + +$EPYDOC --config=epydoc.conf \ No newline at end of file diff --git a/src/doc/epydoc.conf b/doc/epydoc.conf similarity index 98% rename from src/doc/epydoc.conf rename to doc/epydoc.conf index 201f3c4..6914825 100644 --- a/src/doc/epydoc.conf +++ b/doc/epydoc.conf @@ -4,7 +4,7 @@ # The list of objects to document. Objects can be named using # dotted names, module filenames, or package directory names. # Alases for this option include "objects" and "values". -modules: certfuzz +modules: ../src/certfuzz # output # The type of output that should be generated. Should be one @@ -67,7 +67,7 @@ imports: yes # An integer indicating how verbose epydoc should be. The default # value is 0; negative values will supress warnings and errors; # positive values will give more verbose output. -verbosity: 0 +verbosity: 1 # parse # Whether or not parsing should be used to examine objects. From b505817bdf719d7f94b59dae019aaae47f6f5f86 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 May 2014 13:54:58 -0400 Subject: [PATCH 0265/1169] ignore fontconfig dir --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 340847d..814ae59 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dev_builds src/dist build/dist/osx/installer/Readme.txt build/dist/osx/installer/License.txt +doc/fontconfig From 0cc22880351c43b71be1da8ed0c11cd92daf25b0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 May 2014 14:03:34 -0400 Subject: [PATCH 0266/1169] ignore generated docs See http://stackoverflow.com/questions/4983060/should-generated-documentation-go-into-source-control for arguments against putting generated docs in source control --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 814ae59..6ad6156 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ src/dist build/dist/osx/installer/Readme.txt build/dist/osx/installer/License.txt doc/fontconfig +doc/html From 52c4b13ae8b7e444ca95b43c66b4e487117c1ba4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 May 2014 14:26:55 -0400 Subject: [PATCH 0267/1169] ignore pdf dir (contains generated docs) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ad6156..bb05132 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/dist/osx/installer/Readme.txt build/dist/osx/installer/License.txt doc/fontconfig doc/html +doc/pdf From 6a176378cb914e0165971e75a122c59d573b6066 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 May 2014 14:27:37 -0400 Subject: [PATCH 0268/1169] use a makefile instead --- doc/Makefile | 18 ++++++++++++++++++ doc/docgen.sh | 5 ----- doc/epydoc.conf | 4 ++-- 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 doc/Makefile delete mode 100755 doc/docgen.sh diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..562b037 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,18 @@ +EPYDOC=/opt/local/bin/epydoc-2.7 +EPYDOC_CFG=./epydoc.conf + +doc: html pdf + +html: + ${EPYDOC} --config=${EPYDOC_CFG} --html --output=html + +pdf: + ${EPYDOC} --config=${EPYDOC_CFG} --pdf --output=pdf + +# text: +# ${EPYDOC} --config=${EPYDOC_CFG} --text --output=text + +check: + ${EPYDOC} --config=${EPYDOC_CFG} --check + + diff --git a/doc/docgen.sh b/doc/docgen.sh deleted file mode 100755 index cf35be8..0000000 --- a/doc/docgen.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -EPYDOC=/opt/local/bin/epydoc-2.7 - -$EPYDOC --config=epydoc.conf \ No newline at end of file diff --git a/doc/epydoc.conf b/doc/epydoc.conf index 6914825..464722a 100644 --- a/doc/epydoc.conf +++ b/doc/epydoc.conf @@ -9,11 +9,11 @@ modules: ../src/certfuzz # output # The type of output that should be generated. Should be one # of: html, text, latex, dvi, ps, pdf. -output: html +; output: html # target # The path to the output directory. May be relative or absolute. -target: html/ +; target: html/ # docformat # The default markup language for docstrings, for modules that do From 99cc39af6fc3b233acd3e6e3d9e858efbe90d306 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 May 2014 15:34:36 -0400 Subject: [PATCH 0269/1169] turn off for doc target --- doc/Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index 562b037..63b2fe4 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -1,7 +1,7 @@ EPYDOC=/opt/local/bin/epydoc-2.7 EPYDOC_CFG=./epydoc.conf -doc: html pdf +doc: html html: ${EPYDOC} --config=${EPYDOC_CFG} --html --output=html @@ -9,8 +9,8 @@ html: pdf: ${EPYDOC} --config=${EPYDOC_CFG} --pdf --output=pdf -# text: -# ${EPYDOC} --config=${EPYDOC_CFG} --text --output=text +text: + ${EPYDOC} --config=${EPYDOC_CFG} --text --output=text check: ${EPYDOC} --config=${EPYDOC_CFG} --check From 90e1fb1cfa4d98a5277e9064075bd689328ae02c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 May 2014 16:01:22 -0400 Subject: [PATCH 0270/1169] pull dot path out of config file --- doc/Makefile | 5 +++-- doc/epydoc.conf | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index 63b2fe4..cb86258 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -1,13 +1,14 @@ EPYDOC=/opt/local/bin/epydoc-2.7 EPYDOC_CFG=./epydoc.conf +DOTPATH=/usr/local/bin/dot doc: html html: - ${EPYDOC} --config=${EPYDOC_CFG} --html --output=html + ${EPYDOC} --config=${EPYDOC_CFG} --dotpath=${DOTPATH} --html --output=html pdf: - ${EPYDOC} --config=${EPYDOC_CFG} --pdf --output=pdf + ${EPYDOC} --config=${EPYDOC_CFG} --dotpath=${DOTPATH} --pdf --output=pdf text: ${EPYDOC} --config=${EPYDOC_CFG} --text --output=text diff --git a/doc/epydoc.conf b/doc/epydoc.conf index 464722a..d164758 100644 --- a/doc/epydoc.conf +++ b/doc/epydoc.conf @@ -87,7 +87,7 @@ graph: all # dotpath # The path to the Graphviz "dot" executable, used to generate # graphs. -dotpath: /usr/local/bin/dot +; dotpath: /usr/local/bin/dot # sourcecode # Whether or not to include syntax highlighted source code in From 2f3e47cacd97c123a15b4b8569d7414698121784 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 May 2014 07:35:43 -0400 Subject: [PATCH 0271/1169] rename linux unit test file for consistency --- .../test/campaign/{test_linux.py => test_campaign_linux.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/certfuzz/test/campaign/{test_linux.py => test_campaign_linux.py} (100%) diff --git a/src/certfuzz/test/campaign/test_linux.py b/src/certfuzz/test/campaign/test_campaign_linux.py similarity index 100% rename from src/certfuzz/test/campaign/test_linux.py rename to src/certfuzz/test/campaign/test_campaign_linux.py From cbf0837693e91bffb08f1497ba2841bcd2962909 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 9 May 2014 09:28:56 -0400 Subject: [PATCH 0272/1169] add clean target to makefile --- doc/Makefile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index cb86258..10a9a70 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -1,19 +1,23 @@ EPYDOC=/opt/local/bin/epydoc-2.7 EPYDOC_CFG=./epydoc.conf DOTPATH=/usr/local/bin/dot +RM=rm -rfv +HTML_DIR=./html +PDF_DIR=./pdf doc: html html: - ${EPYDOC} --config=${EPYDOC_CFG} --dotpath=${DOTPATH} --html --output=html + ${EPYDOC} --config=${EPYDOC_CFG} --dotpath=${DOTPATH} --html --output=${HTML_DIR} pdf: - ${EPYDOC} --config=${EPYDOC_CFG} --dotpath=${DOTPATH} --pdf --output=pdf + ${EPYDOC} --config=${EPYDOC_CFG} --dotpath=${DOTPATH} --pdf --output=${PDF_DIR} text: - ${EPYDOC} --config=${EPYDOC_CFG} --text --output=text + ${EPYDOC} --config=${EPYDOC_CFG} --text check: ${EPYDOC} --config=${EPYDOC_CFG} --check - +clean: + ${RM} ${HTML_DIR} ${PDF_DIR} From c4056ddfa0870a12fc3816e89b8c2ce0a3a05aff Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:06:42 -0400 Subject: [PATCH 0273/1169] rename foe_config.py -> config_windows.py --- src/certfuzz/campaign/campaign.py | 2 +- .../campaign/config/{foe_config.py => config_windows.py} | 0 src/certfuzz/crash/foe_crash.py | 2 +- src/certfuzz/iteration/iteration_windows.py | 2 +- src/certfuzz/runners/winrun.py | 2 +- .../config/{test_foe_config.py => test_config_windows.py} | 2 +- src/certfuzz/tools/windows/minimize.py | 4 ++-- src/certfuzz/tools/windows/repro.py | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) rename src/certfuzz/campaign/config/{foe_config.py => config_windows.py} (100%) rename src/certfuzz/test/campaign/config/{test_foe_config.py => test_config_windows.py} (97%) diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index 34c1576..fb7a91a 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -13,7 +13,7 @@ import traceback from certfuzz.campaign import CampaignBase, __version__, import_module_by_name -from certfuzz.campaign.config.foe_config import Config +from certfuzz.campaign.config.config_windows import Config from certfuzz.campaign.errors import CampaignError from certfuzz.debuggers import registration from certfuzz.file_handlers.seedfile_set import SeedfileSet diff --git a/src/certfuzz/campaign/config/foe_config.py b/src/certfuzz/campaign/config/config_windows.py similarity index 100% rename from src/certfuzz/campaign/config/foe_config.py rename to src/certfuzz/campaign/config/config_windows.py diff --git a/src/certfuzz/crash/foe_crash.py b/src/certfuzz/crash/foe_crash.py index 76a7413..bb8b732 100644 --- a/src/certfuzz/crash/foe_crash.py +++ b/src/certfuzz/crash/foe_crash.py @@ -7,7 +7,7 @@ import logging import os -from certfuzz.campaign.config.foe_config import get_command_args_list +from certfuzz.campaign.config.config_windows import get_command_args_list from certfuzz.crash.crash_base import Crash from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import best_effort_move diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 2af1b6a..cc3df2b 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -11,7 +11,7 @@ import tempfile from certfuzz import debuggers -from certfuzz.campaign.config.foe_config import get_command_args_list +from certfuzz.campaign.config.config_windows import get_command_args_list from certfuzz.crash.foe_crash import FoeCrash from certfuzz.debuggers.output_parsers import DebuggerFileError from certfuzz.file_handlers.basicfile import BasicFile diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 24d8b91..abda00a 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -19,7 +19,7 @@ import time from certfuzz.runners.errors import RunnerArchitectureError, RunnerRegistryError from certfuzz.runners.errors import RunnerError -from certfuzz.campaign.config.foe_config import get_command_args_list +from certfuzz.campaign.config.config_windows import get_command_args_list from certfuzz.fuzztools.filetools import find_or_create_dir logger = logging.getLogger(__name__) diff --git a/src/certfuzz/test/campaign/config/test_foe_config.py b/src/certfuzz/test/campaign/config/test_config_windows.py similarity index 97% rename from src/certfuzz/test/campaign/config/test_foe_config.py rename to src/certfuzz/test/campaign/config/test_config_windows.py index 439fe89..faa001e 100644 --- a/src/certfuzz/test/campaign/config/test_foe_config.py +++ b/src/certfuzz/test/campaign/config/test_config_windows.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz.campaign.config.foe_config import Config +from certfuzz.campaign.config.config_windows import Config import os import yaml import tempfile diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index ab03b24..eb80d94 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -14,7 +14,7 @@ from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer import WindowsMinimizer as Minimizer - from certfuzz.campaign.config.foe_config import Config, get_command_args_list + from certfuzz.campaign.config.config_windows import Config, get_command_args_list from certfuzz.crash import FoeCrash from certfuzz.debuggers import msec # @UnusedImport except ImportError: @@ -27,7 +27,7 @@ from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer import WindowsMinimizer as Minimizer - from certfuzz.campaign.config.foe_config import Config, get_command_args_list + from certfuzz.campaign.config.config_windows import Config, get_command_args_list from certfuzz.crash import FoeCrash from certfuzz.debuggers import msec # @UnusedImport diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index 4ecd3b8..2a66a27 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -14,7 +14,7 @@ from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.runners.runner_base import get_command_args_list - from certfuzz.campaign.config.foe_config import Config + from certfuzz.campaign.config.config_windows import Config from certfuzz.debuggers import msec # @UnusedImport except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH @@ -26,7 +26,7 @@ from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.runners.runner_base import get_command_args_list - from certfuzz.campaign.config.foe_config import Config + from certfuzz.campaign.config.config_windows import Config from certfuzz.debuggers import msec # @UnusedImport logger = logging.getLogger() From 4483944f3d768efd7b345ae5a6cc8b818f24c0c9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:10:47 -0400 Subject: [PATCH 0274/1169] rename foe_crash -> crash_windows --- src/certfuzz/crash/__init__.py | 2 +- src/certfuzz/crash/{foe_crash.py => crash_windows.py} | 0 src/certfuzz/iteration/iteration_windows.py | 2 +- .../test/crash/{test_foe_crash.py => test_crash_windows.py} | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/certfuzz/crash/{foe_crash.py => crash_windows.py} (100%) rename src/certfuzz/test/crash/{test_foe_crash.py => test_crash_windows.py} (89%) diff --git a/src/certfuzz/crash/__init__.py b/src/certfuzz/crash/__init__.py index 05d83c9..b7f47b1 100644 --- a/src/certfuzz/crash/__init__.py +++ b/src/certfuzz/crash/__init__.py @@ -1,7 +1,7 @@ from certfuzz.crash.testcase_base import TestCaseBase from certfuzz.crash.crash_base import Crash from certfuzz.crash.bff_crash import BffCrash -from certfuzz.crash.foe_crash import FoeCrash +from certfuzz.crash.crash_windows import FoeCrash # from .android_testcase import AndroidTestCase from certfuzz.crash.errors import CrashError from certfuzz.crash.errors import TestCaseError diff --git a/src/certfuzz/crash/foe_crash.py b/src/certfuzz/crash/crash_windows.py similarity index 100% rename from src/certfuzz/crash/foe_crash.py rename to src/certfuzz/crash/crash_windows.py diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index cc3df2b..ad22888 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -12,7 +12,7 @@ from certfuzz import debuggers from certfuzz.campaign.config.config_windows import get_command_args_list -from certfuzz.crash.foe_crash import FoeCrash +from certfuzz.crash.crash_windows import FoeCrash from certfuzz.debuggers.output_parsers import DebuggerFileError from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.file_handlers.tmp_reaper import TmpReaper diff --git a/src/certfuzz/test/crash/test_foe_crash.py b/src/certfuzz/test/crash/test_crash_windows.py similarity index 89% rename from src/certfuzz/test/crash/test_foe_crash.py rename to src/certfuzz/test/crash/test_crash_windows.py index 19e9e4a..cf0bf77 100644 --- a/src/certfuzz/test/crash/test_foe_crash.py +++ b/src/certfuzz/test/crash/test_crash_windows.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -#import certfuzz.crash.foe_crash +#import certfuzz.crash.crash_windows class Test(unittest.TestCase): From f310c85abdbba798024475a96cdf877da15bc173 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:12:22 -0400 Subject: [PATCH 0275/1169] clean_foe -> clean_windows --- src/certfuzz/tools/windows/{clean_foe.py => clean_windows.py} | 0 src/windows/tools/clean_foe.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/certfuzz/tools/windows/{clean_foe.py => clean_windows.py} (100%) diff --git a/src/certfuzz/tools/windows/clean_foe.py b/src/certfuzz/tools/windows/clean_windows.py similarity index 100% rename from src/certfuzz/tools/windows/clean_foe.py rename to src/certfuzz/tools/windows/clean_windows.py diff --git a/src/windows/tools/clean_foe.py b/src/windows/tools/clean_foe.py index 6481d42..1959190 100644 --- a/src/windows/tools/clean_foe.py +++ b/src/windows/tools/clean_foe.py @@ -3,7 +3,7 @@ @author: adh ''' -from certfuzz.tools.windows.clean_foe import main +from certfuzz.tools.windows.clean_windows import main if __name__ == '__main__': main() From 620d881ec3cdf33d7f99bc5913c66d5121e1bcb9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:13:34 -0400 Subject: [PATCH 0276/1169] pep8 cleanup --- src/certfuzz/tools/windows/clean_windows.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tools/windows/clean_windows.py b/src/certfuzz/tools/windows/clean_windows.py index fe40c2c..b35ccd1 100644 --- a/src/certfuzz/tools/windows/clean_windows.py +++ b/src/certfuzz/tools/windows/clean_windows.py @@ -20,6 +20,7 @@ SLEEPTIMER = 0.5 BACKOFF_FACTOR = 2 + def main(): import optparse try: @@ -43,7 +44,7 @@ def main(): parser.add_option('', '--remove-results', dest='remove_results', action='store_true', default=defaults['remove_results'], help='Removes results dir contents') parser.add_option('', '--all', dest='nuke', action='store_true', default=defaults['nuke'], help='Equivalent to --remove-results') parser.add_option('', '--debug', dest='debug', action='store_true', default=defaults['debug']) - options, args = parser.parse_args() + options, _args = parser.parse_args() cfgobj = Config(options.configfile) c = cfgobj.config From 0daa51c3d4254727aef72f819da8a5c9762267ad Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:15:37 -0400 Subject: [PATCH 0277/1169] clean_foe -> clean_windows --- .../tools/windows/{test_clean_foe.py => test_clean_windows.py} | 0 src/windows/tools/{clean_foe.py => clean_windows.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/certfuzz/test/tools/windows/{test_clean_foe.py => test_clean_windows.py} (100%) rename src/windows/tools/{clean_foe.py => clean_windows.py} (100%) diff --git a/src/certfuzz/test/tools/windows/test_clean_foe.py b/src/certfuzz/test/tools/windows/test_clean_windows.py similarity index 100% rename from src/certfuzz/test/tools/windows/test_clean_foe.py rename to src/certfuzz/test/tools/windows/test_clean_windows.py diff --git a/src/windows/tools/clean_foe.py b/src/windows/tools/clean_windows.py similarity index 100% rename from src/windows/tools/clean_foe.py rename to src/windows/tools/clean_windows.py From a36c9b0649af259d46fce563d9c2edaf4ba116cb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:17:26 -0400 Subject: [PATCH 0278/1169] foe2->bff --- src/windows/{foe2.py => bff.py} | 0 src/windows/test/{test_foe2.py => test_bff.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/windows/{foe2.py => bff.py} (100%) rename src/windows/test/{test_foe2.py => test_bff.py} (100%) diff --git a/src/windows/foe2.py b/src/windows/bff.py similarity index 100% rename from src/windows/foe2.py rename to src/windows/bff.py diff --git a/src/windows/test/test_foe2.py b/src/windows/test/test_bff.py similarity index 100% rename from src/windows/test/test_foe2.py rename to src/windows/test/test_bff.py From e2e76805565abc84240d8be69e605691ac951d08 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:18:14 -0400 Subject: [PATCH 0279/1169] rename foe.yaml -> bff.yaml --- src/certfuzz/bff/windows.py | 2 +- src/windows/configs/examples/{foe.yaml => bff.yaml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/windows/configs/examples/{foe.yaml => bff.yaml} (100%) diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 65932c3..03a2821 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -16,7 +16,7 @@ def main(): - cfg = os.path.abspath(os.path.join(os.getcwd(), 'configs', 'foe.yaml')) + cfg = os.path.abspath(os.path.join(os.getcwd(), 'configs', 'bff.yaml')) with BFF(config_path=cfg, campaign_class=WindowsCampaign) as bff: bff.go() diff --git a/src/windows/configs/examples/foe.yaml b/src/windows/configs/examples/bff.yaml similarity index 100% rename from src/windows/configs/examples/foe.yaml rename to src/windows/configs/examples/bff.yaml From c4ab36ef2ed08d95d1dc80574485b396c9b44d13 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:21:57 -0400 Subject: [PATCH 0280/1169] rename FoeCrash -> WindowsCrash --- src/certfuzz/crash/__init__.py | 2 -- src/certfuzz/crash/crash_windows.py | 2 +- src/certfuzz/iteration/iteration_windows.py | 4 ++-- src/certfuzz/tools/windows/minimize.py | 6 +++--- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/crash/__init__.py b/src/certfuzz/crash/__init__.py index b7f47b1..f20efe4 100644 --- a/src/certfuzz/crash/__init__.py +++ b/src/certfuzz/crash/__init__.py @@ -1,8 +1,6 @@ from certfuzz.crash.testcase_base import TestCaseBase from certfuzz.crash.crash_base import Crash from certfuzz.crash.bff_crash import BffCrash -from certfuzz.crash.crash_windows import FoeCrash -# from .android_testcase import AndroidTestCase from certfuzz.crash.errors import CrashError from certfuzz.crash.errors import TestCaseError from certfuzz.crash.errors import AndroidTestCaseError diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/crash/crash_windows.py index bb8b732..8e4b1b3 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/crash/crash_windows.py @@ -37,7 +37,7 @@ def logerror(func, path, excinfo): } -class FoeCrash(Crash): +class WindowsCrash(Crash): tmpdir_pfx = 'foe-crash-' # TODO: do we still need fuzzer as an arg? diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index ad22888..fe2838a 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -12,7 +12,7 @@ from certfuzz import debuggers from certfuzz.campaign.config.config_windows import get_command_args_list -from certfuzz.crash.crash_windows import FoeCrash +from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers.output_parsers import DebuggerFileError from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.file_handlers.tmp_reaper import TmpReaper @@ -271,7 +271,7 @@ def _log_crash(self, crash): def _build_crash(self, fuzzer, cmdlist, dbg_opts, fuzzed_file): logger.debug('Building crash object') - with FoeCrash(self.cmd_template, self.sf, fuzzed_file, cmdlist, fuzzer, self.debugger_class, + with WindowsCrash(self.cmd_template, self.sf, fuzzed_file, cmdlist, fuzzer, self.debugger_class, dbg_opts, self.working_dir, self.config['runoptions']['keep_unique_faddr'], self.config['target']['program'], heisenbug_retries=self.retries, copy_fuzzedfile=fuzzer.fuzzed_changes_input) as crash: diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index eb80d94..be0bc92 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -15,7 +15,7 @@ from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer import WindowsMinimizer as Minimizer from certfuzz.campaign.config.config_windows import Config, get_command_args_list - from certfuzz.crash import FoeCrash + from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH @@ -28,7 +28,7 @@ from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer import WindowsMinimizer as Minimizer from certfuzz.campaign.config.config_windows import Config, get_command_args_list - from certfuzz.crash import FoeCrash + from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport logger = logging.getLogger() @@ -159,7 +159,7 @@ def main(): debugger_class = msec.MsecDebugger template = string.Template(config['target']['cmdline_template']) cmd_as_args = get_command_args_list(template, fuzzed_file.path)[1] - with FoeCrash(template, seedfile, fuzzed_file, cmd_as_args, None, debugger_class, + with WindowsCrash(template, seedfile, fuzzed_file, cmd_as_args, None, debugger_class, config['debugger'], outdir, options.keep_uniq_faddr, config['target']['program'], retries) as crash: filetools.make_directories(crash.tempdir) From 318d153f6faeed0d16d7bd13fb101179fa3d6daa Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:30:22 -0400 Subject: [PATCH 0281/1169] s/foe/bff/g --- src/certfuzz/__init__.py | 3 +-- src/certfuzz/android/config.yaml | 2 +- src/certfuzz/bff/common.py | 2 +- src/certfuzz/crash/crash_windows.py | 2 +- src/certfuzz/debuggers/msec.py | 2 +- src/certfuzz/test/crash/test_crash_pkg.py | 2 +- src/certfuzz/tools/windows/clean_windows.py | 4 ++-- src/certfuzz/tools/windows/minimize.py | 2 +- src/certfuzz/tools/windows/repro.py | 4 ++-- src/setup.py | 2 +- 10 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/certfuzz/__init__.py b/src/certfuzz/__init__.py index 7fc59e8..9a6234c 100644 --- a/src/certfuzz/__init__.py +++ b/src/certfuzz/__init__.py @@ -1,6 +1,5 @@ ''' -The certfuzz package forms the basis of the CERT Failure Observation Engine (FOE) -and Basic Fuzzing Framework (BFF). +The certfuzz package forms the basis of the CERT Basic Fuzzing Framework (BFF). ''' from certfuzz.errors import CERTFuzzError from certfuzz.version import __version__ diff --git a/src/certfuzz/android/config.yaml b/src/certfuzz/android/config.yaml index 064a94b..5979050 100644 --- a/src/certfuzz/android/config.yaml +++ b/src/certfuzz/android/config.yaml @@ -45,7 +45,7 @@ directories: # until the fuzzer runs out of things to do. # minimize: Create a file that is minimally-different than the seed # file, yet crashes with the same hash -# minimizer_timeout: The maximum amount of time that FOE will spend on +# minimizer_timeout: The maximum amount of time that BFF will spend on # a minimization run before giving up # keep_unique_faddr: Consider the Exception Faulting Address value as # part of the crash hash diff --git a/src/certfuzz/bff/common.py b/src/certfuzz/bff/common.py index faf7d6c..725ea3a 100644 --- a/src/certfuzz/bff/common.py +++ b/src/certfuzz/bff/common.py @@ -28,7 +28,7 @@ def setup_debugging(): logger.debug('Instantiating embedded rpdb2 debugger with password "bff"...') try: import rpdb2 - rpdb2.start_embedded_debugger("foe", timeout=0.0) + rpdb2.start_embedded_debugger("bff", timeout=0.0) except ImportError: logger.debug('Skipping import of rpdb2. Is Winpdb installed?') diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/crash/crash_windows.py index 8e4b1b3..613e26a 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/crash/crash_windows.py @@ -38,7 +38,7 @@ def logerror(func, path, excinfo): class WindowsCrash(Crash): - tmpdir_pfx = 'foe-crash-' + tmpdir_pfx = 'bff-crash-' # TODO: do we still need fuzzer as an arg? def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 6f0b12c..25f44dd 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -60,7 +60,7 @@ def debugger_test(self): return [self.debugger_app(), '-version'] def _get_cmdline(self, outfile): - cdb_command = '$$Found_with_CERT_FOE_2.1;r;!exploitable -v;q' + cdb_command = '$$Found_with_CERT_BFF_2.8;r;!exploitable -v;q' args = [] args.append(self.debugger_app()) args.append('-amsec.dll') diff --git a/src/certfuzz/test/crash/test_crash_pkg.py b/src/certfuzz/test/crash/test_crash_pkg.py index 999c9c2..b462ed4 100644 --- a/src/certfuzz/test/crash/test_crash_pkg.py +++ b/src/certfuzz/test/crash/test_crash_pkg.py @@ -20,7 +20,7 @@ def test_api(self): api_list = ['Crash', 'CrashError', 'BffCrash', - 'FoeCrash', + 'WindowsCrash', ] (is_fail, msg) = misc.check_for_apis(module, api_list) self.assertFalse(is_fail, msg) diff --git a/src/certfuzz/tools/windows/clean_windows.py b/src/certfuzz/tools/windows/clean_windows.py index b35ccd1..c9e5bdf 100644 --- a/src/certfuzz/tools/windows/clean_windows.py +++ b/src/certfuzz/tools/windows/clean_windows.py @@ -9,7 +9,7 @@ import tempfile import pprint -defaults = {'config': 'configs/foe.yaml', +defaults = {'config': 'configs/bff.yaml', 'remove_results': False, 'pretend': False, 'retry': 3, @@ -35,7 +35,7 @@ def main(): from certfuzz.fuzztools.filetools import delete_contents_of from certfuzz.campaign.config import Config if not os.path.exists(defaults['config']): - defaults['config'] = '../configs/foe.yaml' + defaults['config'] = '../configs/bff.yaml' parser = optparse.OptionParser() parser.add_option('-c', '--config', dest='configfile', default=defaults['config'], metavar='FILE') diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index be0bc92..ba7cf01 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -83,7 +83,7 @@ def main(): help='The desired confidence level (default: 0.999)') parser.add_option('-g', '--target-size-guess', dest='initial_target_size', help='A guess at the minimal value (int)') - parser.add_option('', '--config', default='configs/foe.yaml', + parser.add_option('', '--config', default='configs/bff.yaml', dest='config', help='path to the configuration file to use') parser.add_option('', '--timeout', dest='timeout', metavar='N', type='int', default=0, diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index 2a66a27..60e4f6d 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -36,7 +36,7 @@ def parseiterpath(commandline): # Return the path to the iteration's fuzzed file for part in commandline.split(): - if 'foe-crash-' in part: + if 'bff-crash-' in part: return part @@ -63,7 +63,7 @@ def main(): help='Enable debug messages (overrides --verbose)') parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') - parser.add_option('-c', '--config', default='configs/foe.yaml', + parser.add_option('-c', '--config', default='configs/bff.yaml', dest='config', help='path to the configuration file to use') parser.add_option('-w', '--windbg', dest='use_windbg', diff --git a/src/setup.py b/src/setup.py index 14eef2c..21ef1d0 100644 --- a/src/setup.py +++ b/src/setup.py @@ -17,7 +17,7 @@ 'mtsp_enum', 'repro', ], - 'windows': ['clean_foe', + 'windows': ['clean_windows', 'copycrashers', 'drillresults', 'minimize', From 89ea64079598e8f1b617a24acb02a314150c0154 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 14:33:50 -0400 Subject: [PATCH 0282/1169] remove obsolete test --- src/certfuzz/test/crash/test_crash_pkg.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/certfuzz/test/crash/test_crash_pkg.py b/src/certfuzz/test/crash/test_crash_pkg.py index b462ed4..9704909 100644 --- a/src/certfuzz/test/crash/test_crash_pkg.py +++ b/src/certfuzz/test/crash/test_crash_pkg.py @@ -15,16 +15,6 @@ def setUp(self): def tearDown(self): pass - def test_api(self): - module = certfuzz.crash - api_list = ['Crash', - 'CrashError', - 'BffCrash', - 'WindowsCrash', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From e629d61fbeb659e9fe63e27e0817da95463dba7d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 14 Mar 2014 16:33:34 -0400 Subject: [PATCH 0283/1169] Fix for BFF-515 --- src/certfuzz/minimizer/win_minimizer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) mode change 100644 => 100755 src/certfuzz/minimizer/win_minimizer.py diff --git a/src/certfuzz/minimizer/win_minimizer.py b/src/certfuzz/minimizer/win_minimizer.py old mode 100644 new mode 100755 index 47444e5..4f2e204 --- a/src/certfuzz/minimizer/win_minimizer.py +++ b/src/certfuzz/minimizer/win_minimizer.py @@ -3,6 +3,7 @@ import zipfile from certfuzz.fuzztools.filetools import check_zip_file, write_file +from certfuzz.fuzztools.filetools import exponential_backoff from certfuzz.minimizer import Minimizer as MinimizerBase from certfuzz.minimizer.errors import WindowsMinimizerError @@ -15,14 +16,16 @@ class WindowsMinimizer(MinimizerBase): def __init__(self, cfg=None, crash=None, crash_dst_dir=None, seedfile_as_target=False, bitwise=False, confidence=0.999, - logfile=None, tempdir=None, maxtime=3600, preferx=False, keep_uniq_faddr=False, watchcpu=False): + logfile=None, tempdir=None, maxtime=3600, preferx=False, + keep_uniq_faddr=False, watchcpu=False): self.saved_arcinfo = None self.is_zipfile = check_zip_file(crash.fuzzedfile.path) - MinimizerBase.__init__(self, cfg, crash, crash_dst_dir, seedfile_as_target, - bitwise, confidence, logfile, tempdir, maxtime, - preferx, keep_uniq_faddr, watchcpu) + MinimizerBase.__init__(self, cfg, crash, crash_dst_dir, + seedfile_as_target, bitwise, confidence, + logfile, tempdir, maxtime, preferx, + keep_uniq_faddr, watchcpu) def get_signature(self, dbg, backtracelevels): # get the basic signature @@ -83,6 +86,11 @@ def _readzip(self, filepath): tempzip.close() return unzippedbytes + @exponential_backoff + def _safe_createzip(self, filepath): + tempzip = zipfile.ZipFile(filepath, 'w') + return tempzip + def _writezip(self): '''rebuild the zip file and put it in self.fuzzed Note: We assume that the fuzzer has not changes the lengths From 2cb617576150bbe31b3f90c37ef86becfdc251f4 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 11 Jun 2014 16:27:20 -0400 Subject: [PATCH 0284/1169] ignore .bak files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bb05132..a3670c4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ build/dist/osx/installer/License.txt doc/fontconfig doc/html doc/pdf +*.bak From 834c58e25ff668f541be435242a6dc8d20ac92a7 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 11 Jun 2014 16:42:06 -0400 Subject: [PATCH 0285/1169] Ignore *.pyc files too --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a3670c4..e58ab0e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ doc/fontconfig doc/html doc/pdf *.bak +*.pyc From fcb6269ab73627b07f8d6eb86953e9e1287686e0 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 11 Jun 2014 16:43:07 -0400 Subject: [PATCH 0286/1169] The rest of the patch for BFF-515 --- src/certfuzz/minimizer/win_minimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/minimizer/win_minimizer.py b/src/certfuzz/minimizer/win_minimizer.py index 4f2e204..93f5e6e 100755 --- a/src/certfuzz/minimizer/win_minimizer.py +++ b/src/certfuzz/minimizer/win_minimizer.py @@ -104,7 +104,7 @@ def _writezip(self): filepath = self.tempfile logger.debug('Creating zip with mutated contents.') - tempzip = zipfile.ZipFile(filepath, 'w') + tempzip = self._safe_createzip(filepath, 'w') ''' reconstruct archived files, using the same compression scheme as From fc790d3061882c212cb2776be10e8821ecb4c4bc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 16 Jun 2014 11:38:07 -0400 Subject: [PATCH 0287/1169] Create results directory *after* running cleanup. --- build/dist/build_base2.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build/dist/build_base2.py b/build/dist/build_base2.py index 13658ed..ecc173e 100644 --- a/build/dist/build_base2.py +++ b/build/dist/build_base2.py @@ -93,11 +93,10 @@ def prune(self): def refine(self): logger.info('Refining') - logger.info('Set up results dir') - self._create_results_dir() - logger.info('Clean up build tmp_dir') self._clean_up(self.build_dir, remove_blacklist=False) + logger.info('Set up results dir') + self._create_results_dir() def prepend_license(self): ''' From cfcf0825ebd00141e661d2d4d4089d7ec12d38a8 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 16 Jun 2014 13:59:42 -0400 Subject: [PATCH 0288/1169] Hacky zero-length directory in zip --- build/dist/build_base2.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build/dist/build_base2.py b/build/dist/build_base2.py index ecc173e..03c22b5 100644 --- a/build/dist/build_base2.py +++ b/build/dist/build_base2.py @@ -8,6 +8,7 @@ import tempfile import logging import zipfile +import datetime from dev.misc import copydir, copyfile, onerror @@ -22,6 +23,11 @@ def _zipdir(path, zip_): for root, _dirs, files in os.walk('.'): for f in files: zip_.write(os.path.join(root, f)) + if not files and not _dirs: + # Include empty top-level directories as well + zipinfo = zipfile.ZipInfo(os.path.basename(root)) + zipinfo.external_attr = 16 + zip_.writestr(zipinfo, '') os.chdir(cwd) From bbfe7b513b09a483821e8414c77e961848db3762 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 16 Jun 2014 14:06:43 -0400 Subject: [PATCH 0289/1169] Make sure that empty directories are included in zip. BFF-743 --- build/dist/build_base2.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/build/dist/build_base2.py b/build/dist/build_base2.py index 03c22b5..f126e3d 100644 --- a/build/dist/build_base2.py +++ b/build/dist/build_base2.py @@ -8,7 +8,6 @@ import tempfile import logging import zipfile -import datetime from dev.misc import copydir, copyfile, onerror @@ -24,10 +23,8 @@ def _zipdir(path, zip_): for f in files: zip_.write(os.path.join(root, f)) if not files and not _dirs: - # Include empty top-level directories as well - zipinfo = zipfile.ZipInfo(os.path.basename(root)) - zipinfo.external_attr = 16 - zip_.writestr(zipinfo, '') + # Include empty directories as well + zip_.write(root, compress_type=zipfile.ZIP_STORED) os.chdir(cwd) @@ -99,10 +96,10 @@ def prune(self): def refine(self): logger.info('Refining') - logger.info('Clean up build tmp_dir') - self._clean_up(self.build_dir, remove_blacklist=False) logger.info('Set up results dir') self._create_results_dir() + logger.info('Clean up build tmp_dir') + self._clean_up(self.build_dir, remove_blacklist=False) def prepend_license(self): ''' From 746f1b500313284726cf4c0365e0af53c824b13c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 16 Jun 2014 15:38:34 -0400 Subject: [PATCH 0290/1169] fix bff-534 test --- src/certfuzz/test/fuzztools/test_rangefinder.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/test/fuzztools/test_rangefinder.py b/src/certfuzz/test/fuzztools/test_rangefinder.py index ceee034..208aab1 100644 --- a/src/certfuzz/test/fuzztools/test_rangefinder.py +++ b/src/certfuzz/test/fuzztools/test_rangefinder.py @@ -45,9 +45,12 @@ def test_get_ranges(self): # so we should only see two ranges r = RangeFinder(0.375, 0.999, self.tmpfile) self.assertEqual(len(r.things), 2) - ranges = [v for (dummy, v) in sorted(r.things.items())] - self.assertAlmostEqual(ranges[0].min, 0.375) - self.assertAlmostEqual(ranges[1].max, 0.999) + mins = sorted([thing.min for thing in r.things.itervalues()]) + maxs = sorted([thing.max for thing in r.things.itervalues()]) + self.assertEqual(0.375, mins[0]) + self.assertAlmostEqual(0.61, mins[1], places=2) + self.assertAlmostEqual(0.61, maxs[0], places=2) + self.assertEqual(0.999, maxs[1]) def _ranges(self): minkeys = sorted([(v.min, k) for (k, v) in self.r.things.iteritems()]) From 5afb2f9134e9e16a0466fb734a503138f723e481 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 16 Jun 2014 16:38:36 -0400 Subject: [PATCH 0291/1169] Change CWD to ~/bff before running BFF --- src/linux/batch.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index 3416aa7..8a7391f 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -94,6 +94,8 @@ if [[ "$platform" =~ "Linux" ]]; then fi fi +cd $scriptlocation + echo "Using python interpreter: $mypython" if [[ -f "$scriptlocation/bff.py" ]]; then $mypython $scriptlocation/bff.py --config=$scriptlocation/conf.d/bff.cfg From 4699972fdec200c7e1f51a0bc51bc4c6979c3e2b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 17 Jun 2014 10:39:22 -0400 Subject: [PATCH 0292/1169] Rudimentary Windows support. Rangefinder, state caching etc. still broken. (Flagged as FIXME) --- src/certfuzz/campaign/campaign.py | 18 +++++++++++------- src/certfuzz/iteration/iteration_windows.py | 14 ++++++++++---- src/certfuzz/runners/winrun.py | 2 +- src/windows/configs/examples/bff.yaml | 2 +- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index fb7a91a..ea446be 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -293,7 +293,8 @@ def __getstate__(self): def _save_state(self, cachefile=None): if not cachefile: cachefile = self.cached_state_file - dump_obj_to_file(cachefile, self) + # FIXME + # dump_obj_to_file(cachefile, self) def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): ''' @@ -352,20 +353,23 @@ def _do_iteration(self, sf, rng_seed, seednum): # newly created Iteration() with Iteration(*iter_args) as iteration: try: - iteration.go(self.seedfile_set) + iteration.go() except FuzzerExhaustedError: # Some fuzzers run out of things to do. They should # raise a FuzzerExhaustedError when that happens. logger.info('Done with %s, removing from set', sf.basename) - self.seedfile_set.del_item(sf.md5) + # FIXME + # self.seedfile_set.del_item(sf.md5) if not seednum % self.status_interval: logger.info('Iteration: %d Crashes found: %d', self.current_seed, len(self.crashes_seen)) - self.seedfile_set.update_csv() + # FIXME + # self.seedfile_set.update_csv() logger.info('Seedfile Set Status:') - for k, score, successes, tries, p in self.seedfile_set.status(): - logger.info('%s %0.6f %d %d %0.6f', k, score, successes, - tries, p) + logger.info('FIXME') + # for k, score, successes, tries, p in self.seedfile_set.status(): + # logger.info('%s %0.6f %d %d %0.6f', k, score, successes, + # tries, p) def go(self): ''' diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index fe2838a..6699acf 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -232,13 +232,19 @@ def record_success(self): # ranges should only get scored on the first crash # found in this iteration. Others found via minimization # don't count for this r - self.r.record_success(crash.signature) - self.sf.record_success(crash.signature) + # FIXME + pass + # self.r.record_success(crash.signature) + # FIXME + # self.sf.record_success(crash.signature) def record_failure(self): if self.r: - self.r.record_failure() - self.sf.record_failure() + # FIXME + pass + # self.r.record_failure() + # FIXME + # self.sf.record_failure() def _process_crash(self, crash): ''' diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index abda00a..8c8bb35 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -60,7 +60,7 @@ def kill(p): class WinRunner(RunnerBase): def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): - RunnerBase.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) + RunnerBase.__init__(self, options, workingdir_base) logger.debug('Initialize Runner') self.runtimeout = None diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 480c6d7..6c6f337 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -51,7 +51,7 @@ target: # results_dir: Location of fuzzing results (relative to foe2.py) ##################################################################### directories: - seedfile_dir: seedfiles + seedfile_dir: seedfiles\examples working_dir: C:\FOE2\fuzzdir results_dir: results From c0ba389ee49d21ad4f676d6b38325da779c50840 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 17 Jun 2014 13:58:04 -0400 Subject: [PATCH 0293/1169] Windows installer changes FOE -> BFF --- build/dev/windows/windows_build.py | 2 +- build/dist/windows/nsis/buildnsi.py | 8 ++++---- build/dist/windows/nsis/nsis_footer.txt | 10 +++++----- build/dist/windows/nsis/nsis_header.txt | 12 ++++++------ build/dist/windows/nsis/nsis_mid.txt | 14 +++++++------- build/dist/windows/nsis/nsis_top.txt | 2 +- build/dist/windows/nsis/vmwarning.txt | 2 +- build/dist/windows/windows_build2.py | 8 ++++---- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/build/dev/windows/windows_build.py b/build/dev/windows/windows_build.py index 0b0bafa..e4c67bd 100644 --- a/build/dev/windows/windows_build.py +++ b/build/dev/windows/windows_build.py @@ -7,5 +7,5 @@ class WindowsBuild(Build): - _name = 'FOE' + _name = 'BFF' _platform = 'windows' diff --git a/build/dist/windows/nsis/buildnsi.py b/build/dist/windows/nsis/buildnsi.py index 2f90b4b..ddd3224 100644 --- a/build/dist/windows/nsis/buildnsi.py +++ b/build/dist/windows/nsis/buildnsi.py @@ -30,9 +30,9 @@ def main(version_string='', outfile=None, build_dir=None): fp.write('!define VERSION "%s"\n' % version_string) fp.write('!define COPYRIGHT "CERT 2013"\n') - fp.write('!define DESCRIPTION "FOE %s"\n' % version_string) + fp.write('!define DESCRIPTION "BFF %s"\n' % version_string) fp.write('!define LICENSE_TXT "%s\..\COPYING.txt"\n' % distpath) - fp.write('!define INSTALLER_NAME "%s\..\..\FOE-%s-setup.exe"\n' % (distpath, version_string)) + fp.write('!define INSTALLER_NAME "%s\..\..\BFF-%s-setup.exe"\n' % (distpath, version_string)) headerfile = open("nsis_header.txt", "r") headertext = headerfile.read() @@ -52,8 +52,8 @@ def main(version_string='', outfile=None, build_dir=None): midtext = midfile.read() midfile.close() - # End of installer section - + # End of installer section + # Write uninstaller section fp.write(midtext) diff --git a/build/dist/windows/nsis/nsis_footer.txt b/build/dist/windows/nsis/nsis_footer.txt index 14242e0..e9ce9eb 100644 --- a/build/dist/windows/nsis/nsis_footer.txt +++ b/build/dist/windows/nsis/nsis_footer.txt @@ -1,4 +1,4 @@ -Delete "$INSTDIR\configs\foe.yaml" +Delete "$INSTDIR\configs\bff.yaml" Delete "$INSTDIR\uninstall.exe" !ifdef WEB_SITE Delete "$INSTDIR\CERT website.url" @@ -26,12 +26,12 @@ RmDir "$SMPROGRAMS\$SM_Folder" !endif !ifndef REG_START_MENU -Delete "$SMPROGRAMS\FOE2\${APP_NAME}.lnk" -Delete "$SMPROGRAMS\FOE2\README.lnk" +Delete "$SMPROGRAMS\BFF\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\BFF\README.lnk" !ifdef WEB_SITE -Delete "$SMPROGRAMS\FOE2\CERT Website.lnk" +Delete "$SMPROGRAMS\BFF\CERT Website.lnk" !endif -RmDir "$SMPROGRAMS\FOE2" +RmDir "$SMPROGRAMS\BFF" !endif #DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" diff --git a/build/dist/windows/nsis/nsis_header.txt b/build/dist/windows/nsis/nsis_header.txt index 3a1ef3b..c21107d 100644 --- a/build/dist/windows/nsis/nsis_header.txt +++ b/build/dist/windows/nsis/nsis_header.txt @@ -48,7 +48,7 @@ OutFile "${INSTALLER_NAME}" BrandingText "${APP_NAME}" XPStyle on InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" "" -InstallDir "C:\FOE2" +InstallDir "C:\BFF" !define MUI_ICON "cert.ico" ###################################################################### @@ -72,7 +72,7 @@ InstallDir "C:\FOE2" !ifdef REG_START_MENU !define MUI_STARTMENUPAGE_NODISABLE -!define MUI_STARTMENUPAGE_DEFAULTFOLDER "FOE2" +!define MUI_STARTMENUPAGE_DEFAULTFOLDER "BFF" !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}" !define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}" @@ -82,7 +82,7 @@ InstallDir "C:\FOE2" !insertmacro MUI_PAGE_INSTFILES !define MUI_FINISHPAGE_RUN -!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchFOE" +!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchBFF" !define MUI_FINISHPAGE_SHOWREADME $INSTDIR\readme.txt !insertmacro MUI_PAGE_FINISH @@ -118,15 +118,15 @@ Function .onInit ${EndIf} FunctionEnd -Function LaunchFOE +Function LaunchBFF ${If} $FUZZ_IMAGEMAGICK == "Y" CopyFiles "$INSTDIR\seedfiles\examples\*.*" "$INSTDIR\seedfiles" ExecShell "open" "results\convert v5.5.7\" sleep 2000 BringToFront - Exec '"cmd.exe" /k @echo off & set PATH=%PATH%;$DEBUGDIR& @echo on & cd $INSTDIR & foe2.py' + Exec '"cmd.exe" /k @echo off & set PATH=%PATH%;$DEBUGDIR& @echo on & cd $INSTDIR & bff.py' ${Else} - Exec '"cmd.exe" /k @echo off & set PATH=%PATH%;$DEBUGDIR& @echo on & cd $INSTDIR & echo Edit configs\foe.yaml and run foe2.py to start fuzzing & echo See readme.txt for documentation & foe2.py -h' + Exec '"cmd.exe" /k @echo off & set PATH=%PATH%;$DEBUGDIR& @echo on & cd $INSTDIR & echo Edit configs\bff.yaml and run bff.py to start fuzzing & echo See readme.txt for documentation & bff.py -h' ${EndIf} FunctionEnd diff --git a/build/dist/windows/nsis/nsis_mid.txt b/build/dist/windows/nsis/nsis_mid.txt index 2c34136..3dd61ed 100644 --- a/build/dist/windows/nsis/nsis_mid.txt +++ b/build/dist/windows/nsis/nsis_mid.txt @@ -303,9 +303,9 @@ VCRInstalled: #msecInstalled: -IfFileExists "$INSTDIR\configs\foe.yaml" configCopied +IfFileExists "$INSTDIR\configs\bff.yaml" configCopied -CopyFiles "$INSTDIR\configs\examples\foe.yaml" "$INSTDIR\configs\foe.yaml" +CopyFiles "$INSTDIR\configs\examples\bff.yaml" "$INSTDIR\configs\bff.yaml" StrCpy $FUZZ_IMAGEMAGICK "Y" @@ -340,7 +340,7 @@ WriteUninstaller "$INSTDIR\uninstall.exe" !ifdef REG_START_MENU !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateDirectory "$SMPROGRAMS\$SM_Folder" -CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "${MAIN_APP_EXE}" " /k echo Edit configs\foe.yaml and run foe2.py to start fuzzing & echo See readme.txt for documentation & foe2.py -h" +CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "${MAIN_APP_EXE}" " /k echo Edit configs\bff.yaml and run bff.py to start fuzzing & echo See readme.txt for documentation & bff.py -h" CreateShortCut "$SMPROGRAMS\$SM_Folder\README.lnk" "$INSTDIR\readme.txt" !ifdef WEB_SITE WriteIniStr "$INSTDIR\CERT website.url" "InternetShortcut" "URL" "${WEB_SITE}" @@ -350,12 +350,12 @@ CreateShortCut "$SMPROGRAMS\$SM_Folder\CERT Website.lnk" "$INSTDIR\CERT website. !endif !ifndef REG_START_MENU -CreateDirectory "$SMPROGRAMS\FOE" -CreateShortCut "$SMPROGRAMS\FOE\${APP_NAME}.lnk" "${MAIN_APP_EXE} /k echo Edit configs\foe.yaml and run foe2.py to start fuzzing & echo See readme.txt for documentation & foe2.py -h" -CreateShortCut "$SMPROGRAMS\FOE\README.lnk" "$INSTDIR\readme.txt" +CreateDirectory "$SMPROGRAMS\BFF" +CreateShortCut "$SMPROGRAMS\BFF\${APP_NAME}.lnk" "${MAIN_APP_EXE} /k echo Edit configs\bff.yaml and run bff.py to start fuzzing & echo See readme.txt for documentation & bff.py -h" +CreateShortCut "$SMPROGRAMS\BFF\README.lnk" "$INSTDIR\readme.txt" !ifdef WEB_SITE WriteIniStr "$INSTDIR\CERT website.url" "InternetShortcut" "URL" "${WEB_SITE}" -CreateShortCut "$SMPROGRAMS\FOE\CERT Website.lnk" "$INSTDIR\CERT website.url" +CreateShortCut "$SMPROGRAMS\BFF\CERT Website.lnk" "$INSTDIR\CERT website.url" !endif !endif diff --git a/build/dist/windows/nsis/nsis_top.txt b/build/dist/windows/nsis/nsis_top.txt index 0987ee1..a6af576 100644 --- a/build/dist/windows/nsis/nsis_top.txt +++ b/build/dist/windows/nsis/nsis_top.txt @@ -1,3 +1,3 @@ -!define APP_NAME "FOE2" +!define APP_NAME "BFF" !define COMP_NAME "CERT" !define WEB_SITE "http://www.cert.org" diff --git a/build/dist/windows/nsis/vmwarning.txt b/build/dist/windows/nsis/vmwarning.txt index 6a6423d..c187ffa 100644 --- a/build/dist/windows/nsis/vmwarning.txt +++ b/build/dist/windows/nsis/vmwarning.txt @@ -1,3 +1,3 @@ WARNING: Fuzzing can cause unexpected behavior with any operating system. -FOE should only be installed within a virtual machine. \ No newline at end of file +BFF should only be installed within a virtual machine. \ No newline at end of file diff --git a/build/dist/windows/windows_build2.py b/build/dist/windows/windows_build2.py index f35814d..9c171e3 100644 --- a/build/dist/windows/windows_build2.py +++ b/build/dist/windows/windows_build2.py @@ -32,7 +32,7 @@ def package(self): logger.debug('Copy %s -> %s', src, self.build_dir) shutil.copy(src, self.build_dir) - nsifile = os.path.join(self.build_dir, 'foe2.nsi') + nsifile = os.path.join(self.build_dir, 'bff.nsi') logger.debug('nsi file is %s', nsifile) version_string = "02.01.00.99" @@ -42,12 +42,12 @@ def package(self): # invoke makensis on the file we just made logger.debug('invoking makensis on %s', nsifile) subprocess.call(['makensis', nsifile]) - + distpath = 'BFF-windows-export' if self.build_dir: distpath = os.path.join(self.build_dir, distpath) - - exename = 'FOE-%s-setup.exe' % version_string + + exename = 'BFF-%s-setup.exe' % version_string exefile = '%s\..\..\%s' % (distpath, exename) self.target = os.path.join(os.path.dirname(self.target), exename) self._move_to_target(exefile) From 920bab4da7012bfcbc13af0b5cb360301d12d7ec Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 18 Jun 2014 10:21:13 -0400 Subject: [PATCH 0294/1169] Fix relative importing of certfuzz (it's not a package) --- src/certfuzz/tools/windows/minimize.py | 8 +++----- src/certfuzz/tools/windows/repro.py | 25 ++++++------------------ src/linux/tools/bff_stats.py | 11 ++++++++++- src/linux/tools/callsim.py | 11 ++++++++++- src/linux/tools/create_crasher_script.py | 11 ++++++++++- src/linux/tools/debuggerfile.py | 11 ++++++++++- src/linux/tools/drillresults.py | 11 ++++++++++- src/linux/tools/minimize.py | 11 ++++++++++- src/linux/tools/minimizer_plot.py | 11 ++++++++++- src/linux/tools/mtsp_enum.py | 11 ++++++++++- src/linux/tools/repro.py | 11 ++++++++++- src/windows/tools/clean_windows.py | 12 +++++++++++- src/windows/tools/copycrashers.py | 11 ++++++++++- src/windows/tools/drillresults.py | 11 ++++++++++- src/windows/tools/minimize.py | 11 ++++++++++- src/windows/tools/mtsp_enum.py | 11 ++++++++++- src/windows/tools/quickstats.py | 11 ++++++++++- src/windows/tools/repro.py | 12 +++++++++++- src/windows/tools/zipdiff.py | 11 ++++++++++- 19 files changed, 181 insertions(+), 41 deletions(-) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index ba7cf01..80cd204 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -133,9 +133,7 @@ def main(): if options.outdir: outdir = options.outdir else: - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - outdir = os.path.abspath(os.path.join(parentdir, 'minimizer_out')) + outdir = "./minimizer_out" filetools.make_directories(outdir) @@ -181,11 +179,11 @@ def main(): logger.debug('x character substitution') length = len(minimize.fuzzed) if options.prefer_x_target: - #We minimized to 'x', so we attempt to get metasploit as a freebie + # We minimized to 'x', so we attempt to get metasploit as a freebie targetstring = list(text.metasploit_pattern_orig(length)) filename_modifier = '-mtsp' else: - #We minimized to metasploit, so we attempt to get 'x' as a freebie + # We minimized to metasploit, so we attempt to get 'x' as a freebie targetstring = list('x' * length) filename_modifier = '-x' diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index 60e4f6d..32d01da 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -9,25 +9,12 @@ import re from subprocess import Popen -try: - from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file - from certfuzz import debuggers - from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.runners.runner_base import get_command_args_list - from certfuzz.campaign.config.config_windows import Config - from certfuzz.debuggers import msec # @UnusedImport -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - import sys - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file - from certfuzz import debuggers - from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.runners.runner_base import get_command_args_list - from certfuzz.campaign.config.config_windows import Config - from certfuzz.debuggers import msec # @UnusedImport +from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file +from certfuzz import debuggers +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.campaign.config.config_windows import Config, get_command_args_list +from certfuzz.debuggers import msec # @UnusedImport + logger = logging.getLogger() logger.setLevel(logging.WARNING) diff --git a/src/linux/tools/bff_stats.py b/src/linux/tools/bff_stats.py index 24410f2..87e9e1e 100755 --- a/src/linux/tools/bff_stats.py +++ b/src/linux/tools/bff_stats.py @@ -5,7 +5,16 @@ @organization: cert.org ''' -from certfuzz.tools.linux.bff_stats import main +import os +import sys +try: + from certfuzz.tools.linux.bff_stats import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.bff_stats import main if __name__ == '__main__': diff --git a/src/linux/tools/callsim.py b/src/linux/tools/callsim.py index d64a848..14ac497 100755 --- a/src/linux/tools/callsim.py +++ b/src/linux/tools/callsim.py @@ -4,7 +4,16 @@ @organization: cert.org ''' -from certfuzz.tools.linux.callsim import main +import os +import sys +try: + from from certfuzz.tools.linux.callsim import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from from certfuzz.tools.linux.callsim import main if __name__ == '__main__': main() diff --git a/src/linux/tools/create_crasher_script.py b/src/linux/tools/create_crasher_script.py index c33d70f..cdfe062 100755 --- a/src/linux/tools/create_crasher_script.py +++ b/src/linux/tools/create_crasher_script.py @@ -5,7 +5,16 @@ @organization: cert.org ''' -from certfuzz.tools.linux.create_crasher_script import main +import os +import sys +try: + from certfuzz.tools.linux.create_crasher_script import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.create_crasher_script import main if __name__ == '__main__': main() diff --git a/src/linux/tools/debuggerfile.py b/src/linux/tools/debuggerfile.py index e08d882..7b4a2fe 100755 --- a/src/linux/tools/debuggerfile.py +++ b/src/linux/tools/debuggerfile.py @@ -4,7 +4,16 @@ @organization: cert.org ''' -from certfuzz.tools.linux.debuggerfile import main +import os +import sys +try: + from certfuzz.tools.linux.debuggerfile import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.debuggerfile import main if __name__ == '__main__': main() diff --git a/src/linux/tools/drillresults.py b/src/linux/tools/drillresults.py index 10adb73..414d1aa 100755 --- a/src/linux/tools/drillresults.py +++ b/src/linux/tools/drillresults.py @@ -2,7 +2,16 @@ ''' This script looks for interesting crashes and rate them by potential exploitability ''' -from certfuzz.tools.linux.drillresults import main +import os +import sys +try: + from certfuzz.tools.linux.drillresults import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.drillresults import main if __name__ == '__main__': main() diff --git a/src/linux/tools/minimize.py b/src/linux/tools/minimize.py index 7391e0a..a1f4790 100755 --- a/src/linux/tools/minimize.py +++ b/src/linux/tools/minimize.py @@ -4,7 +4,16 @@ @organization: cert.org ''' -from certfuzz.tools.linux.minimize import main +import os +import sys +try: + from certfuzz.tools.linux.minimize import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.minimize import main if __name__ == '__main__': main() diff --git a/src/linux/tools/minimizer_plot.py b/src/linux/tools/minimizer_plot.py index 2cd3a2c..3ec2590 100755 --- a/src/linux/tools/minimizer_plot.py +++ b/src/linux/tools/minimizer_plot.py @@ -4,7 +4,16 @@ @organization: cert.org ''' -from certfuzz.tools.linux.minimizer_plot import main +import os +import sys +try: + from certfuzz.tools.linux.minimizer_plot import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.minimizer_plot import main if __name__ == '__main__': main() diff --git a/src/linux/tools/mtsp_enum.py b/src/linux/tools/mtsp_enum.py index f2a1cc1..534ecfa 100755 --- a/src/linux/tools/mtsp_enum.py +++ b/src/linux/tools/mtsp_enum.py @@ -4,7 +4,16 @@ @organization: cert.org ''' -from certfuzz.tools.common.mtsp_enum import main +import os +import sys +try: + from certfuzz.tools.common.mtsp_enum import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.common.mtsp_enum import main if __name__ == '__main__': main() diff --git a/src/linux/tools/repro.py b/src/linux/tools/repro.py index 3a07df6..a2e3312 100755 --- a/src/linux/tools/repro.py +++ b/src/linux/tools/repro.py @@ -4,7 +4,16 @@ @organization: cert.org ''' -from certfuzz.tools.linux.repro import main +import os +import sys +try: + from certfuzz.tools.linux.repro import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.repro import main if __name__ == '__main__': main() diff --git a/src/windows/tools/clean_windows.py b/src/windows/tools/clean_windows.py index 1959190..9a16dd4 100644 --- a/src/windows/tools/clean_windows.py +++ b/src/windows/tools/clean_windows.py @@ -3,7 +3,17 @@ @author: adh ''' -from certfuzz.tools.windows.clean_windows import main +import os +import sys +try: + from certfuzz.tools.windows.clean_windows import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.windows.clean_windows import main + if __name__ == '__main__': main() diff --git a/src/windows/tools/copycrashers.py b/src/windows/tools/copycrashers.py index cb82389..914c021 100644 --- a/src/windows/tools/copycrashers.py +++ b/src/windows/tools/copycrashers.py @@ -2,7 +2,16 @@ Created on Jun 14, 2013 ''' -from certfuzz.tools.windows.copycrashers import main +import os +import sys +try: + from certfuzz.tools.windows.copycrashers import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.windows.copycrashers import main if __name__ == '__main__': main() diff --git a/src/windows/tools/drillresults.py b/src/windows/tools/drillresults.py index 1b12f05..2014444 100644 --- a/src/windows/tools/drillresults.py +++ b/src/windows/tools/drillresults.py @@ -1,7 +1,16 @@ ''' This script looks for interesting crashes and rate them by potential exploitability ''' -from certfuzz.tools.windows.drillresults import main +import os +import sys +try: + from certfuzz.tools.windows.drillresults import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.windows.drillresults import main if __name__ == '__main__': main() diff --git a/src/windows/tools/minimize.py b/src/windows/tools/minimize.py index b60d159..e046830 100644 --- a/src/windows/tools/minimize.py +++ b/src/windows/tools/minimize.py @@ -3,7 +3,16 @@ @organization: cert.org ''' -from certfuzz.tools.windows.minimize import main +import os +import sys +try: + from certfuzz.tools.windows.minimize import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.windows.minimize import main if __name__ == '__main__': main() diff --git a/src/windows/tools/mtsp_enum.py b/src/windows/tools/mtsp_enum.py index f2d597a..5af035b 100644 --- a/src/windows/tools/mtsp_enum.py +++ b/src/windows/tools/mtsp_enum.py @@ -3,7 +3,16 @@ @organization: cert.org ''' -from certfuzz.tools.common.mtsp_enum import main +import os +import sys +try: + from certfuzz.tools.common.mtsp_enum import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.common.mtsp_enum import main if __name__ == '__main__': main() diff --git a/src/windows/tools/quickstats.py b/src/windows/tools/quickstats.py index 27e79c3..d11669b 100644 --- a/src/windows/tools/quickstats.py +++ b/src/windows/tools/quickstats.py @@ -2,7 +2,16 @@ Created on Jun 14, 2013 ''' -from certfuzz.tools.windows.quickstats import main +import os +import sys +try: + from certfuzz.tools.windows.quickstats import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.windows.quickstats import main if __name__ == '__main__': main() diff --git a/src/windows/tools/repro.py b/src/windows/tools/repro.py index af03c8b..a991c35 100644 --- a/src/windows/tools/repro.py +++ b/src/windows/tools/repro.py @@ -3,7 +3,17 @@ @organization: cert.org ''' -from certfuzz.tools.windows.repro import main +import os +import sys + +try: + from certfuzz.tools.windows.repro import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.windows.repro import main if __name__ == '__main__': main() diff --git a/src/windows/tools/zipdiff.py b/src/windows/tools/zipdiff.py index a33827e..56e40f8 100644 --- a/src/windows/tools/zipdiff.py +++ b/src/windows/tools/zipdiff.py @@ -3,7 +3,16 @@ @organization: cert.org ''' -from certfuzz.tools.windows.zipdiff import main +import os +import sys +try: + from certfuzz.tools.windows.zipdiff import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.windows.zipdiff import main if __name__ == '__main__': main() From 75c3580bc2a4f804f91dbed1d113b51877fc42bf Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 18 Jun 2014 10:38:37 -0400 Subject: [PATCH 0295/1169] Copy bff.yaml to configs directory on make_dev build. Don't copy seedfiles\examples to seefiles on NSIS install (just have yaml file point to examples subdirectory) --- build/dev/windows/windows_build.py | 15 ++++++++++++++- build/dist/windows/nsis/nsis_header.txt | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/build/dev/windows/windows_build.py b/build/dev/windows/windows_build.py index e4c67bd..d46e092 100644 --- a/build/dev/windows/windows_build.py +++ b/build/dev/windows/windows_build.py @@ -4,8 +4,21 @@ @author: adh ''' from .. import Build - +import os +import sys +mydir = os.path.dirname(os.path.abspath(__file__)) +parentdir = os.path.abspath(os.path.join(mydir, '..')) +sys.path.append(parentdir) +from dev.misc import copyfile class WindowsBuild(Build): _name = 'BFF' _platform = 'windows' + + def _copy_platform(self): + target_path = self.target_path + Build._copy_platform(self) + # Copy example bff.yaml file to configs directory + f_src = os.path.join(target_path, 'configs', 'examples', 'bff.yaml') + f_dst = os.path.join(target_path, 'configs') + copyfile(f_src, f_dst) diff --git a/build/dist/windows/nsis/nsis_header.txt b/build/dist/windows/nsis/nsis_header.txt index c21107d..2515ec7 100644 --- a/build/dist/windows/nsis/nsis_header.txt +++ b/build/dist/windows/nsis/nsis_header.txt @@ -120,7 +120,7 @@ FunctionEnd Function LaunchBFF ${If} $FUZZ_IMAGEMAGICK == "Y" - CopyFiles "$INSTDIR\seedfiles\examples\*.*" "$INSTDIR\seedfiles" + #CopyFiles "$INSTDIR\seedfiles\examples\*.*" "$INSTDIR\seedfiles" ExecShell "open" "results\convert v5.5.7\" sleep 2000 BringToFront From b702383ef222cfb46cfb6e1c47b0bba1a1a8d038 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 18 Jun 2014 11:01:01 -0400 Subject: [PATCH 0296/1169] You can't mix './" and '\\' directory specifiers with windows. Reverting to old path specification. --- src/certfuzz/tools/windows/minimize.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 80cd204..1baba89 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -133,7 +133,9 @@ def main(): if options.outdir: outdir = options.outdir else: - outdir = "./minimizer_out" + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..', '..', '..')) + outdir = os.path.abspath(os.path.join(parentdir, 'minimizer_out')) filetools.make_directories(outdir) From 3a19852982638f70943dfdf70146926fecc610ef Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 18 Jun 2014 11:12:45 -0400 Subject: [PATCH 0297/1169] Reverting last change to the path. "./" is fine with windows, even mixed. Removing references to FOE in bff.yaml. --- src/certfuzz/tools/windows/minimize.py | 4 +--- src/windows/configs/examples/bff.yaml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 1baba89..b2b863a 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -133,9 +133,7 @@ def main(): if options.outdir: outdir = options.outdir else: - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..', '..', '..')) - outdir = os.path.abspath(os.path.join(parentdir, 'minimizer_out')) + outdir = './minimizer_out' filetools.make_directories(outdir) diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 6c6f337..ff39ecf 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -19,11 +19,11 @@ campaign: # the target ##################################################################### target: - program: C:\FOE2\imagemagick\convert.exe + program: C:\BFF\imagemagick\convert.exe cmdline_template: $PROGRAM $SEEDFILE NUL # With the default ImageMagick fuzz run, the above target options # will result in the following invocation of ImageMagick: - # C:\FOE2\imagemagick\convert.exe NUL + # C:\BFF\imagemagick\convert.exe NUL # This exercises ImageMagick's image decoding, while also outputting # to the Windows NUL device, minimizing I/O. # When choosing a fuzzing target, modify the cmdline_template line to @@ -34,7 +34,7 @@ target: # file name, you will probably just use: # cmdline_template: $PROGRAM $SEEDFILE # - # NOTE: FOE uses python's shlex.split() method to parse the command + # NOTE: BFF uses python's shlex.split() method to parse the command # line template after substituting in the program and seedfile values. # For this reason, it is required that if any other items in the # cmdline_template involve windows paths, you need either use @@ -43,16 +43,16 @@ target: # cmdline_template: $PROGRAM -in $SEEDFILE -out "c:\some path\to file" ##################################################################### -# Directories used by FOE +# Directories used by BFF # -# seedfile_dir: Location of seed files (relative to foe2.py) -# working_dir: Temporary directory used by FOE. Use a ramdisk to +# seedfile_dir: Location of seed files (relative to bff.py) +# working_dir: Temporary directory used by BFF. Use a ramdisk to # reduce disk activity -# results_dir: Location of fuzzing results (relative to foe2.py) +# results_dir: Location of fuzzing results (relative to bff.py) ##################################################################### directories: seedfile_dir: seedfiles\examples - working_dir: C:\FOE2\fuzzdir + working_dir: C:\BFF\fuzzdir results_dir: results ##################################################################### @@ -68,7 +68,7 @@ directories: # if not present. # minimize: Create a file that is minimally-different than the seed # file, yet crashes with the same hash -# minimizer_timeout: The maximum amount of time that FOE will spend on +# minimizer_timeout: The maximum amount of time that BFF will spend on # a minimization run before giving up # keep_unique_faddr: Consider the Exception Faulting Address value as # part of the crash hash From 870efecdfd3c8533f982c1d8c5ce93a55e46f1b4 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 20 Jun 2014 13:06:42 -0400 Subject: [PATCH 0298/1169] Consolidate license files BFF-744 --- build/dev/build_base.py | 10 +++++- build/dev/misc.py | 19 +++++++++++ build/dev/windows/windows_build.py | 1 + src/LICENSE.txt | 46 --------------------------- src/linux/COPYING.txt | 51 ------------------------------ src/windows/COPYING.txt | 46 --------------------------- 6 files changed, 29 insertions(+), 144 deletions(-) delete mode 100644 src/LICENSE.txt delete mode 100644 src/linux/COPYING.txt delete mode 100644 src/windows/COPYING.txt diff --git a/build/dev/build_base.py b/build/dev/build_base.py index 4cd030f..fe96668 100644 --- a/build/dev/build_base.py +++ b/build/dev/build_base.py @@ -7,7 +7,7 @@ import logging import shutil -from dev.misc import copydir, copyfile, onerror +from dev.misc import copydir, copyfile, onerror, mdtotextfile logger = logging.getLogger(__name__) @@ -34,6 +34,8 @@ def __init__(self, name=None, platform=None): self.dev_builds_path = os.path.abspath(os.path.join(self.src_path, '..', 'dev_builds')) self.target_path = os.path.abspath(os.path.join(self.dev_builds_path, self.name)) self.platform_path = os.path.join(self.src_path, self.platform) + self.license_md_path = os.path.join(self.src_path, '..', 'LICENSE.md') + self.license_txt_path = os.path.join(self.target_path, 'COPYING.txt') def __enter__(self): return self @@ -48,6 +50,9 @@ def build(self): logger.info('Set up build dir') self._create_target_path() + logger.info('Converting markdown files') + self._convert_md_files() + logger.info('Copy platform-specific files to build dir') self._copy_platform() @@ -60,6 +65,9 @@ def build(self): logger.info('Clean up build dir') self._clean_up(self.target_path, remove_blacklist=False) + def _convert_md_files(self): + mdtotextfile(self.license_md_path, self.license_txt_path) + def _create_target_path(self): # create base build path if it doesn't already exist if not os.path.exists(self.target_path): diff --git a/build/dev/misc.py b/build/dev/misc.py index 1a49069..a9693bf 100644 --- a/build/dev/misc.py +++ b/build/dev/misc.py @@ -39,3 +39,22 @@ def copydir(src, dst): def copyfile(src, dst): logger.info('Copy file %s -> %s', src, dst) shutil.copy(src, dst) + + +def stripmarkdown(markdown): + replacements = {'©': '(C)', '®': '(R)'} + for k, v in replacements.iteritems(): + markdown = markdown.replace(k, v) + return markdown + + +def mdtotextfile(markdownfile, plaintextfile): + logger.info('Converting markdown file %s to plain text %s' % + (markdownfile, plaintextfile)) + f = open(markdownfile, 'r') + markdown = f.read() + f.close + plaintext = stripmarkdown(markdown) + f = open(plaintextfile, 'w') + f.write(plaintext) + f.close diff --git a/build/dev/windows/windows_build.py b/build/dev/windows/windows_build.py index d46e092..dd911d8 100644 --- a/build/dev/windows/windows_build.py +++ b/build/dev/windows/windows_build.py @@ -11,6 +11,7 @@ sys.path.append(parentdir) from dev.misc import copyfile + class WindowsBuild(Build): _name = 'BFF' _platform = 'windows' diff --git a/src/LICENSE.txt b/src/LICENSE.txt deleted file mode 100644 index b0eda2f..0000000 --- a/src/LICENSE.txt +++ /dev/null @@ -1,46 +0,0 @@ -Use of the CERT Basic Fuzzing Framework (BFF) system and related source code -is subject to the terms of the license below. Please note that winprocess.py -and killableprocess.py are subject to different licenses; see those files for -their respective licenses. - ------------------------------------------------------------------------- -Copyright (C) 2013 Carnegie Mellon University. All Rights Reserved. ------------------------------------------------------------------------- -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following acknowledgments - and disclaimers. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. The names "Department of Homeland Security," "Carnegie Mellon - University," "CERT" and/or "Software Engineering Institute" shall - not be used to endorse or promote products derived from this software - without prior written permission. For written permission, please - contact permission@sei.cmu.edu. - -4. Products derived from this software may not be called "CERT" nor - may "CERT" appear in their names without prior written permission of - permission@sei.cmu.edu. - -5. Redistributions of any form whatsoever must retain the following - acknowledgment: - - "This product includes software developed by CERT with funding - and support from the Department of Homeland Security under - Contract No. FA 8721-05-C-0003." - -THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND -CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER -EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE -EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, -CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND -RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, -RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND -COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. \ No newline at end of file diff --git a/src/linux/COPYING.txt b/src/linux/COPYING.txt deleted file mode 100644 index 1f7bd39..0000000 --- a/src/linux/COPYING.txt +++ /dev/null @@ -1,51 +0,0 @@ -Use of the CERT Basic Fuzzing Framework and related source code -is subject to the terms of the following licenses: - -GNU Public License (GPL) Rights pursuant to Version 2, June 1991 -Government Purpose License Rights (GPLR) pursuant to DFARS -252.227.7013 - -NO WARRANTY - -ANY INFORMATION, MATERIALS, SERVICES, INTELLECTUAL -PROPERTY OR OTHER PROPERTY OR RIGHTS GRANTED OR -PROVIDED BY CARNEGIE MELLON UNIVERSITY PURSUANT TO -THIS LICENSE (HEREINAFTER THE "DELIVERABLES") ARE ON -AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES -NO WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED -AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, -WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABILITY, INFORMATIONAL CONTENT, -NONINFRINGEMENT, OR ERROR-FREE OPERATION. CARNEGIE -MELLON UNIVERSITY SHALL NOT BE LIABLE FOR INDIRECT, -SPECIAL OR CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF -PROFITS OR INABILITY TO USE SAID INTELLECTUAL -PROPERTY, UNDER THIS LICENSE, REGARDLESS OF WHETHER -SUCH PARTY WAS AWARE OF THE POSSIBILITY OF SUCH -DAMAGES. LICENSEE AGREES THAT IT WILL NOT MAKE ANY -WARRANTY ON BEHALF OF CARNEGIE MELLON UNIVERSITY, -EXPRESS OR IMPLIED, TO ANY PERSON CONCERNING THE -APPLICATION OF OR THE RESULTS TO BE OBTAINED WITH -THE DELIVERABLES UNDER THIS LICENSE. - -Licensee hereby agrees to defend, indemnify, and hold harmless -Carnegie Mellon University, its trustees, officers, employees, -and agents from all claims or demands made against them (and any -related losses, expenses, or attorney's fees) arising out of, or -relating to Licensee's and/or its sub licensees' negligent use or -willful misuse of or negligent conduct or willful misconduct -regarding the Software, facilities, or other rights or assistance -granted by Carnegie Mellon University under this License, -including, but not limited to, any claims of product liability, -personal injury, death, damage to property, or violation of any -laws or regulations. - -Carnegie Mellon University Software Engineering Institute -authored documents are sponsored by the U.S. Department of -Defense under Contract F19628-00-C-0003. Carnegie Mellon -University retains copyrights in all material produced under this -contract. The U.S. Government retains a non-exclusive, -royalty-free license to publish or reproduce these documents, or -allow others to do so, for U.S. Government purposes only -pursuant to the copyright license under the contract clause at -252.227.7013. diff --git a/src/windows/COPYING.txt b/src/windows/COPYING.txt deleted file mode 100644 index 5d7819c..0000000 --- a/src/windows/COPYING.txt +++ /dev/null @@ -1,46 +0,0 @@ -Use of the FOE system and related source code is subject to the terms -of the license below. Please note that winprocess.py and -killableprocess.py are subject to different licenses; see those files for -their respective licenses. - ------------------------------------------------------------------------- -Copyright (C) 2013 Carnegie Mellon University. All Rights Reserved. ------------------------------------------------------------------------- -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following acknowledgments - and disclaimers. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. The names "Department of Homeland Security," "Carnegie Mellon - University," "CERT" and/or "Software Engineering Institute" shall - not be used to endorse or promote products derived from this software - without prior written permission. For written permission, please - contact permission@sei.cmu.edu. - -4. Products derived from this software may not be called "CERT" nor - may "CERT" appear in their names without prior written permission of - permission@sei.cmu.edu. - -5. Redistributions of any form whatsoever must retain the following - acknowledgment: - - "This product includes software developed by CERT with funding - and support from the Department of Homeland Security under - Contract No. FA 8721-05-C-0003." - -THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND -CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER -EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE -EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, -CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND -RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, -RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND -COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. \ No newline at end of file From d8bf323bac73718e6b38b1b99a2b4b934573ce04 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 20 Jun 2014 14:16:53 -0400 Subject: [PATCH 0299/1169] consolidate license file for make_dist as well. BFF-744 --- build/dist/build_base2.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/build/dist/build_base2.py b/build/dist/build_base2.py index f126e3d..f13e2db 100644 --- a/build/dist/build_base2.py +++ b/build/dist/build_base2.py @@ -9,7 +9,7 @@ import logging import zipfile -from dev.misc import copydir, copyfile, onerror +from dev.misc import copydir, copyfile, onerror, mdtotextfile from .prepend_license import main as _prepend_license @@ -82,6 +82,10 @@ def build(self): self.prepend_license() self.package() + def _convert_md_files(self): + licensemd = os.path.join('..', 'LICENSE.md') + mdtotextfile(licensemd, os.path.join(self.platform_path, self._license_file)) + def export(self): logger.info('Exporting') logger.info('Copy platform-specific files to tmp_dir') @@ -145,6 +149,9 @@ def _copy_platform(self): platform_path = os.path.join(self.src_path, 'linux') logger.info('Defaulting to %s', platform_path) + logger.info('Converting markdown files') + self._convert_md_files() + # copy platform-specific content for f in os.listdir(platform_path): f_src = os.path.join(platform_path, f) @@ -163,6 +170,7 @@ def _copy_platform(self): else: logger.warning("Not sure what to do with %s", f_src) + def _copy_common_dirs(self): # copy other dirs for d in self._common_dirs: From 105a11227a2758b2a95afa171913b633b01cd8d2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 20 Jun 2014 14:46:06 -0400 Subject: [PATCH 0300/1169] BFF-744 cleanup --- build/dist/build_base2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build/dist/build_base2.py b/build/dist/build_base2.py index f13e2db..0c3febc 100644 --- a/build/dist/build_base2.py +++ b/build/dist/build_base2.py @@ -53,6 +53,8 @@ def __init__(self, platform=None, distpath=None, srcpath=None): self._filename_pfx = 'BFF-{}'.format(self.platform) self.zipfile = '{}.zip'.format(self._filename_pfx) self.target = os.path.join(self.base_path, self.zipfile) + self.license_md_path = os.path.join(self.src_path, '..', 'LICENSE.md') + self.license_txt_path = os.path.join(self.platform_path, self._license_file) def __enter__(self): logger.debug('Entering Build context') @@ -83,8 +85,7 @@ def build(self): self.package() def _convert_md_files(self): - licensemd = os.path.join('..', 'LICENSE.md') - mdtotextfile(licensemd, os.path.join(self.platform_path, self._license_file)) + mdtotextfile(self.license_md_path, self.license_txt_path) def export(self): logger.info('Exporting') From 70901b411f3b3e1299c90d117b62f69bc5668a10 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 20 Jun 2014 14:58:41 -0400 Subject: [PATCH 0301/1169] Fix for OSX BFF-744. platform_path gets overriden, but the license file was copied to the non-overridden path. --- build/dist/build_base2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/dist/build_base2.py b/build/dist/build_base2.py index 0c3febc..fff57d5 100644 --- a/build/dist/build_base2.py +++ b/build/dist/build_base2.py @@ -149,6 +149,8 @@ def _copy_platform(self): logger.info('No platform-specific info found at %s', self.platform_path) platform_path = os.path.join(self.src_path, 'linux') logger.info('Defaulting to %s', platform_path) + # Set license text path since we're overriding it above + self.license_txt_path = os.path.join(platform_path, self._license_file) logger.info('Converting markdown files') self._convert_md_files() From 2ea8e49410394c4fd001b274ff896edbe8aea8df Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 20 Jun 2014 16:09:12 -0400 Subject: [PATCH 0302/1169] Change dates from 2013 to 2014. Build nsi from parent directory, not the BFF directory. (So that extra files aren't distributed) --- LICENSE.md | 4 ++-- build/dist/windows/nsis/buildnsi.py | 2 +- build/dist/windows/windows_build2.py | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 0f51ad5..6f1500d 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,7 +3,7 @@ subject to the following terms: # LICENSE # -Copyright © 2013 Carnegie Mellon University. All Rights Reserved. +Copyright © 2014 Carnegie Mellon University. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: 3. Products derived from this software may not include "Carnegie Mellon University," "SEI" and/or "Software Engineering Institute" in the name of such derived product, nor shall "Carnegie Mellon University," "SEI" and/or "Software Engineering Institute" be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact permission@sei.cmu.edu. # ACKNOWLEDGMENTS AND DISCLAIMERS: # -Copyright © 2013 Carnegie Mellon University +Copyright © 2014 Carnegie Mellon University This material is based upon work funded and supported by the Department of Homeland Security under Contract No. FA8721-05-C-0003 with Carnegie Mellon diff --git a/build/dist/windows/nsis/buildnsi.py b/build/dist/windows/nsis/buildnsi.py index ddd3224..45154ee 100644 --- a/build/dist/windows/nsis/buildnsi.py +++ b/build/dist/windows/nsis/buildnsi.py @@ -29,7 +29,7 @@ def main(version_string='', outfile=None, build_dir=None): fp.write(toptext) fp.write('!define VERSION "%s"\n' % version_string) - fp.write('!define COPYRIGHT "CERT 2013"\n') + fp.write('!define COPYRIGHT "CERT 2014"\n') fp.write('!define DESCRIPTION "BFF %s"\n' % version_string) fp.write('!define LICENSE_TXT "%s\..\COPYING.txt"\n' % distpath) fp.write('!define INSTALLER_NAME "%s\..\..\BFF-%s-setup.exe"\n' % (distpath, version_string)) diff --git a/build/dist/windows/windows_build2.py b/build/dist/windows/windows_build2.py index 9c171e3..9a7834c 100644 --- a/build/dist/windows/windows_build2.py +++ b/build/dist/windows/windows_build2.py @@ -26,13 +26,14 @@ def package(self): logger.debug('Import buildnsi') from .nsis import buildnsi + nsidir = os.path.join(self.build_dir, '..') # Copy files required by nsis for f in ['cert.ico', 'EnvVarUpdate.nsh', 'vmwarning.txt']: src = os.path.join(basedir, 'nsis', f) - logger.debug('Copy %s -> %s', src, self.build_dir) - shutil.copy(src, self.build_dir) + logger.debug('Copy %s -> %s', src, nsidir) + shutil.copy(src, nsidir) - nsifile = os.path.join(self.build_dir, 'bff.nsi') + nsifile = os.path.join(nsidir, 'bff.nsi') logger.debug('nsi file is %s', nsifile) version_string = "02.01.00.99" From 9f9c920587f4417b57f1b3b7ba33e7f613c5a22d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 20 Jun 2014 17:13:01 -0400 Subject: [PATCH 0303/1169] Add git revision number and hash to filename and details for BFF Windows. --- build/dist/build_base2.py | 6 ++++++ build/dist/windows/nsis/buildnsi.py | 4 ++-- build/dist/windows/windows_build2.py | 7 ++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/build/dist/build_base2.py b/build/dist/build_base2.py index fff57d5..b6c7407 100644 --- a/build/dist/build_base2.py +++ b/build/dist/build_base2.py @@ -8,6 +8,7 @@ import tempfile import logging import zipfile +from git import git_rev, git_hash from dev.misc import copydir, copyfile, onerror, mdtotextfile @@ -95,6 +96,11 @@ def export(self): logger.info('Copy common files to tmp_dir') self._copy_common_dirs() + logger.info('Getting git revision') + self.git_rev = git_rev() + self.git_hash = git_hash() + logger.info('%s : %s' % (self.git_rev, self.git_hash)) + def prune(self): logger.info('Pruning') logger.debug('nothing really happened here') diff --git a/build/dist/windows/nsis/buildnsi.py b/build/dist/windows/nsis/buildnsi.py index 45154ee..a90ecc8 100644 --- a/build/dist/windows/nsis/buildnsi.py +++ b/build/dist/windows/nsis/buildnsi.py @@ -8,7 +8,7 @@ import string -def main(version_string='', outfile=None, build_dir=None): +def main(version_string='', git_hash='', outfile=None, build_dir=None): distpath = 'BFF-windows-export' if build_dir: @@ -32,7 +32,7 @@ def main(version_string='', outfile=None, build_dir=None): fp.write('!define COPYRIGHT "CERT 2014"\n') fp.write('!define DESCRIPTION "BFF %s"\n' % version_string) fp.write('!define LICENSE_TXT "%s\..\COPYING.txt"\n' % distpath) - fp.write('!define INSTALLER_NAME "%s\..\..\BFF-%s-setup.exe"\n' % (distpath, version_string)) + fp.write('!define INSTALLER_NAME "%s\..\..\BFF-%s-%s-setup.exe"\n' % (distpath, version_string, git_hash)) headerfile = open("nsis_header.txt", "r") headertext = headerfile.read() diff --git a/build/dist/windows/windows_build2.py b/build/dist/windows/windows_build2.py index 9a7834c..7bf55ae 100644 --- a/build/dist/windows/windows_build2.py +++ b/build/dist/windows/windows_build2.py @@ -36,9 +36,10 @@ def package(self): nsifile = os.path.join(nsidir, 'bff.nsi') logger.debug('nsi file is %s', nsifile) - version_string = "02.01.00.99" + version_string = "2.8.0.%s" % self.git_rev + git_hash = self.git_hash # generate the nsi file - buildnsi.main(version_string=version_string, outfile=nsifile, build_dir=self.build_dir) + buildnsi.main(version_string=version_string, git_hash=git_hash, outfile=nsifile, build_dir=self.build_dir) # invoke makensis on the file we just made logger.debug('invoking makensis on %s', nsifile) @@ -48,7 +49,7 @@ def package(self): if self.build_dir: distpath = os.path.join(self.build_dir, distpath) - exename = 'BFF-%s-setup.exe' % version_string + exename = 'BFF-%s-%s-setup.exe' % (version_string, git_hash) exefile = '%s\..\..\%s' % (distpath, exename) self.target = os.path.join(os.path.dirname(self.target), exename) self._move_to_target(exefile) From 2f7c2a1c04362d34630142922a37a835a5259cb7 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 20 Jun 2014 17:29:03 -0400 Subject: [PATCH 0304/1169] For some reason, git.py wasn't picked up as a new file. Adding manually. --- build/dist/git.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 build/dist/git.py diff --git a/build/dist/git.py b/build/dist/git.py new file mode 100644 index 0000000..ba9ab83 --- /dev/null +++ b/build/dist/git.py @@ -0,0 +1,26 @@ +''' +Created on Jun 20, 2014 + +@author: wd +''' +import subprocess + + +def git_hash(): + args = ['git', 'rev-parse', 'HEAD'] + p = subprocess.Popen(args, stdout=subprocess.PIPE) + for l in p.communicate()[0].splitlines(): + if not l: + continue + return l[:7] + + +def git_rev(): + args = ['git', 'rev-list', 'HEAD'] + revcount = 0 + p = subprocess.Popen(args, stdout=subprocess.PIPE) + for l in p.communicate()[0].splitlines(): + if not l: + continue + revcount += 1 + return revcount From 73098b68c79d5a46f13ca8a71f7707cb562d7f37 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 24 Jun 2014 10:37:58 -0400 Subject: [PATCH 0305/1169] Update CrashWrangler version. BFF-483 --- build/dist/osx/darwin_build2.py | 31 ++-- .../1144497_crashwrangler-contents.xml | 6 +- .../1144497_crashwrangler.xml | 32 +++- .../BFF_installer.pmdoc/12exc-contents.xml | 6 +- .../installer/BFF_installer.pmdoc/12exc.xml | 30 +++- .../BFF_installer.pmdoc/13exc-contents.xml | 2 +- .../installer/BFF_installer.pmdoc/13exc.xml | 21 ++- .../BFF_installer.pmdoc/14exc-contents.xml | 2 +- .../installer/BFF_installer.pmdoc/14exc.xml | 21 ++- .../installer/BFF_installer.pmdoc/index.xml | 156 +++++++++++++++++- src/certfuzz/debuggers/crashwrangler.py | 2 + 11 files changed, 284 insertions(+), 25 deletions(-) diff --git a/build/dist/osx/darwin_build2.py b/build/dist/osx/darwin_build2.py index 1da6438..192f6f1 100644 --- a/build/dist/osx/darwin_build2.py +++ b/build/dist/osx/darwin_build2.py @@ -6,7 +6,7 @@ import os import shutil import subprocess -#import string +# import string import re import logging @@ -114,36 +114,36 @@ def refine(self): os.chdir(oldcwd) def _build_sparseimage(self): - #${SPARSE_DMG}: clean_sparseimage convert_template mount_sparseimage package copy_pkg unmount_sparseimage + # ${SPARSE_DMG}: clean_sparseimage convert_template mount_sparseimage package copy_pkg unmount_sparseimage - #DMG_TEMPLATE=${INSTALLER_BASE}/BFF-template.dmg + # DMG_TEMPLATE=${INSTALLER_BASE}/BFF-template.dmg self.dmg_template = os.path.join(self.INSTALLER_BASE, 'BFF-template.dmg') - #SPARSE_DMG=${base_path}/BFF-sparse.sparseimage + # SPARSE_DMG=${base_path}/BFF-sparse.sparseimage self.sparse_image = os.path.join(self.base_path, 'BFF-sparse.sparseimage') - #clean_sparseimage: + # clean_sparseimage: if os.path.exists(self.sparse_image): # ${RM} ${SPARSE_DMG} logger.debug('Deleting old sparseimage', self.sparse_image) os.remove(self.sparse_image) - #convert_template: + # convert_template: # hdiutil convert ${DMG_TEMPLATE} -format UDSP -o ${SPARSE_DMG} hdiutil('convert', self.dmg_template, '-format', 'UDSP', '-o', self.sparse_image) - #unmount_old_dmg: + # unmount_old_dmg: # ls -1d /Volumes/CERT\ BFF* | tr '\n' '\0' | xargs -0 -n1 -Ixxx hdiutil detach "xxx" for d in os.listdir('/Volumes'): if d.startswith('CERT BFF'): volume_to_detach = os.path.join('/Volumes', d) hdiutil('detach', volume_to_detach) - #mount_sparseimage: unmount_old_dmg + # mount_sparseimage: unmount_old_dmg # hdiutil mount ${SPARSE_DMG} hdiutil('mount', self.sparse_image) - #package: + # package: # cd ${INSTALLER_BASE} && /Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker \ # -d BFF_installer.pmdoc -v -o "/Volumes/CERT BFF/Install CERT BFF.pkg" packagemaker(self.INSTALLER_BASE, @@ -152,7 +152,7 @@ def _build_sparseimage(self): '-o', '/Volumes/CERT BFF/Install CERT BFF.pkg' ) - #copy_pkg: + # copy_pkg: # cp -a ${INSTALLER_BASE}/build/pkgs/* /Volumes/CERT\ BFF/pkgs/ srcdir = os.path.join(self.INSTALLER_BASE, 'build', 'pkgs') dstdir = '/Volumes/CERT BFF/pkgs' @@ -163,26 +163,26 @@ def _build_sparseimage(self): logger.debug('Copy %s -> %s', srcdir, dstdir) subprocess.call('cp -a %s %s' % (os.path.join(srcdir, '*'), re.escape(dstdir)), shell=True) - #unmount_sparseimage: + # unmount_sparseimage: # hdiutil detach "/Volumes/CERT BFF" hdiutil('detach', '/Volumes/CERT BFF') def _convert_sparseimage_to_dmg(self): - #FINAL_DMG=${base_path}/BFF.dmg + # FINAL_DMG=${base_path}/BFF.dmg self.final_dmg = os.path.join(self.base_path, 'BFF.dmg') - #clean_dmg: + # clean_dmg: # ${RM} ${FINAL_DMG} if os.path.exists(self.final_dmg): logger.debug('Deleting old dmg %s', self.final_dmg) os.remove(self.final_dmg) - #${FINAL_DMG}: clean_dmg ${SPARSE_DMG} + # ${FINAL_DMG}: clean_dmg ${SPARSE_DMG} # hdiutil convert ${SPARSE_DMG} -format UDBZ -o ${FINAL_DMG} hdiutil('convert', self.sparse_image, '-format', 'UDBZ', '-o', self.final_dmg) - #rename_dmg: ${FINAL_DMG} + # rename_dmg: ${FINAL_DMG} # SVN_REV=`cd ${LINUX_DIST_BASE} && ${SVN} info | grep Revision | cut -d' ' -f2`; \ # VERSION=`cd ${BUILD_BASE} && grep __version__ bff.py | cut -d'=' -f2 | sed -e "s/ //g" -e "s/\'//g"`; \ if self.dmg_file: @@ -197,7 +197,6 @@ def _convert_sparseimage_to_dmg(self): def _sync_dependencies(self): # Retrieve binary dependecies for building OSX installer - # rsync -EaxSv /Volumes/xcat/build/bff/osx/ installer/ # TODO: What if rsync fails? subprocess.call(['rsync', '-EaxSv', self.SHARED_DEPS, self.LOCAL_DEPS]) subprocess.call(['rsync', '-EaxSv', self.LOCAL_DEPS, self.INSTALLER_BASE]) diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml b/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml index 382fc02..0bc2b3a 100644 --- a/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml +++ b/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml @@ -1 +1,5 @@ - \ No newline at end of file + + + diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml b/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml index 814e9d6..ea36305 100644 --- a/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml +++ b/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml @@ -1 +1,31 @@ -com.cert.cc.certBff.44947_crashwrangler.pkg1.044947_crashwrangler.zip/Applications/BFF.app/Contents/Resources/packagesinstallTo.pathinstallFrom.isRelativeTypeinstallFrom.pathparentinstallTo1144497_crashwrangler-contents.xml/CVS$/\.svn$/\.cvsignore$/\.cvspass$/\.DS_Store$ + + + + com.cert.cc.certBff.52894_crashwrangler.pkg + 1.0 + + + + + 52894_crashwrangler.zip + + /Applications/BFF.app/Contents/Resources/packages + + + + + installTo.path + installFrom.isRelativeType + installFrom.path + parent + installTo + + + 1144497_crashwrangler-contents.xml + /CVS$ + /\.svn$ + /\.cvsignore$ + /\.cvspass$ + /\.DS_Store$ + + diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/12exc-contents.xml b/build/dist/osx/installer/BFF_installer.pmdoc/12exc-contents.xml index 2d0aba6..ee97d79 100644 --- a/build/dist/osx/installer/BFF_installer.pmdoc/12exc-contents.xml +++ b/build/dist/osx/installer/BFF_installer.pmdoc/12exc-contents.xml @@ -1 +1,5 @@ - \ No newline at end of file + + + diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/12exc.xml b/build/dist/osx/installer/BFF_installer.pmdoc/12exc.xml index fadb43b..2cf1843 100644 --- a/build/dist/osx/installer/BFF_installer.pmdoc/12exc.xml +++ b/build/dist/osx/installer/BFF_installer.pmdoc/12exc.xml @@ -1 +1,29 @@ -com.cert.cc.certBff.exc_handler_mountain_lion.pkg1.0crashwrangler/binaries/exc_handler_mountain_lion/usr/local/bininstallFrom.isRelativeTypeinstallFrom.pathparentinstallTo12exc-contents.xml/CVS$/\.svn$/\.cvsignore$/\.cvspass$/\.DS_Store$ \ No newline at end of file + + + + com.cert.cc.certBff.exc_handler_mountain_lion.pkg + 1.0 + + + + + crashwrangler/binaries/exc_handler_mountain_lion + /usr/local/bin + + + + + installFrom.isRelativeType + installFrom.path + parent + installTo + + + 12exc-contents.xml + /CVS$ + /\.svn$ + /\.cvsignore$ + /\.cvspass$ + /\.DS_Store$ + + diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/13exc-contents.xml b/build/dist/osx/installer/BFF_installer.pmdoc/13exc-contents.xml index bc1e5a7..69b34ea 100644 --- a/build/dist/osx/installer/BFF_installer.pmdoc/13exc-contents.xml +++ b/build/dist/osx/installer/BFF_installer.pmdoc/13exc-contents.xml @@ -1 +1 @@ - \ No newline at end of file + diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/13exc.xml b/build/dist/osx/installer/BFF_installer.pmdoc/13exc.xml index fb5a926..8c40e1f 100644 --- a/build/dist/osx/installer/BFF_installer.pmdoc/13exc.xml +++ b/build/dist/osx/installer/BFF_installer.pmdoc/13exc.xml @@ -1 +1,20 @@ -com.cert.cc.certBff.exc_handler_lion.pkg1.0crashwrangler/binaries/exc_handler_lion/usr/local/binparentinstallFrom.isRelativeTypeinstallTo \ No newline at end of file + + + + com.cert.cc.certBff.exc_handler_lion.pkg + 1.0 + + + + + crashwrangler/binaries/exc_handler_lion + /usr/local/bin + + + + + parent + installFrom.isRelativeType + installTo + + diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/14exc-contents.xml b/build/dist/osx/installer/BFF_installer.pmdoc/14exc-contents.xml index bc1e5a7..69b34ea 100644 --- a/build/dist/osx/installer/BFF_installer.pmdoc/14exc-contents.xml +++ b/build/dist/osx/installer/BFF_installer.pmdoc/14exc-contents.xml @@ -1 +1 @@ - \ No newline at end of file + diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/14exc.xml b/build/dist/osx/installer/BFF_installer.pmdoc/14exc.xml index e981c1f..d57bfba 100644 --- a/build/dist/osx/installer/BFF_installer.pmdoc/14exc.xml +++ b/build/dist/osx/installer/BFF_installer.pmdoc/14exc.xml @@ -1 +1,20 @@ -com.cert.cc.certBff.exc_handler_snowleopard.pkg1.0crashwrangler/binaries/exc_handler_snowleopard/usr/local/binparentinstallFrom.isRelativeTypeinstallTo \ No newline at end of file + + + + com.cert.cc.certBff.exc_handler_snowleopard.pkg + 1.0 + + + + + crashwrangler/binaries/exc_handler_snowleopard + /usr/local/bin + + + + + parent + installFrom.isRelativeType + installTo + + diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/index.xml b/build/dist/osx/installer/BFF_installer.pmdoc/index.xml index 3269bbf..8efb834 100644 --- a/build/dist/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/dist/osx/installer/BFF_installer.pmdoc/index.xml @@ -1 +1,155 @@ -CERT BFF/Users/wd/bff_trunk/installer/build/Install CERT BFF.pkgcom.cert.ccCERT_SEI_CMU_large_trans.pngLicense.txtReadme.txt01bff.xml02bff.xml03bff.xml04bff.xml05bff.xml06setuptools.xml07python.xml08hcluster.xml09zzuf.xml10libcaca.xml1144497_crashwrangler.xml12exc.xml13exc.xml14exc.xml15convert.xml16libzzuf.xml17imagemagick.xml18bff.xml19bff.xml20valgrind.xml21zzuf.xml22libcaca.xml23valgrind.xml24imagemagick.xml25valgrind.xml26callgrind.xml27pyyaml.xmlproperties.customizeOptionproperties.title + + + CERT BFF + /Users/wd/bff_trunk/installer/build/Install CERT + BFF.pkg + com.cert.cc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CERT_SEI_CMU_large_trans.png + + License.txt + + Readme.txt + + + + 01bff.xml + 02bff.xml + 03bff.xml + 04bff.xml + 05bff.xml + 06setuptools.xml + 07python.xml + 08hcluster.xml + 09zzuf.xml + 10libcaca.xml + 1144497_crashwrangler.xml + 12exc.xml + 13exc.xml + 14exc.xml + 15convert.xml + 16libzzuf.xml + 17imagemagick.xml + 18bff.xml + 19bff.xml + 20valgrind.xml + 21zzuf.xml + 22libcaca.xml + 23valgrind.xml + 24imagemagick.xml + 25valgrind.xml + 26callgrind.xml + 27pyyaml.xml + 28exc.xml + properties.customizeOption + properties.title + diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index 35b02f7..0cad996 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -28,6 +28,8 @@ cwapp = 'exc_handler_mountain_lion' elif re.match('Darwin-13', myplatform): cwapp = 'exc_handler_mavericks' +elif re.match('Darwin-14', myplatform): + cwapp = 'exc_handler_yosemite' else: cwapp = 'exc_handler' From 557d442853bdfd2424a7d094c3bf3bbe684f71a4 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 24 Jun 2014 14:58:46 -0400 Subject: [PATCH 0306/1169] Rename dist and dev module directories. Commit missing 28*.xml files for OSX installer. --- build/{dev => devmods}/__init__.py | 0 build/{dev => devmods}/build_base.py | 2 +- build/{dev => devmods}/linux/__init__.py | 0 build/{dev => devmods}/linux/linux_build.py | 0 build/{dev => devmods}/misc.py | 0 build/{dev => devmods}/osx/__init__.py | 0 build/{dev => devmods}/osx/darwin_build.py | 0 build/{dev => devmods}/windows/__init__.py | 0 .../{dev => devmods}/windows/windows_build.py | 2 +- build/{dist => distmods}/__init__.py | 0 build/{dist => distmods}/build2.py | 0 build/{dist => distmods}/build_base2.py | 2 +- build/{dist => distmods}/errors.py | 0 build/{dist => distmods}/git.py | 0 build/{dist => distmods}/linux/__init__.py | 0 .../{dist => distmods}/linux/linux_build2.py | 0 build/{dist => distmods}/misc.py | 0 build/{dist => distmods}/osx/__init__.py | 0 build/{dist => distmods}/osx/darwin_build2.py | 0 .../osx/installer/BFF.app/Contents/Info.plist | 0 .../Contents/MacOS/runterminal.applescript | Bin .../Contents/Resources/AppSettings.plist | 0 .../Resources/English.lproj/InfoPlist.strings | 0 .../Resources/English.lproj/MainMenu.nib | Bin .../BFF.app/Contents/Resources/appIcon.icns | Bin .../BFF.app/Contents/Resources/script | 0 .../BFF_installer.pmdoc/01bff-contents.xml | 0 .../installer/BFF_installer.pmdoc/01bff.xml | 0 .../BFF_installer.pmdoc/02bff-contents.xml | 0 .../installer/BFF_installer.pmdoc/02bff.xml | 0 .../BFF_installer.pmdoc/03bff-contents.xml | 0 .../installer/BFF_installer.pmdoc/03bff.xml | 0 .../BFF_installer.pmdoc/04bff-contents.xml | 0 .../installer/BFF_installer.pmdoc/04bff.xml | 0 .../BFF_installer.pmdoc/05bff-contents.xml | 0 .../installer/BFF_installer.pmdoc/05bff.xml | 0 .../06setuptools-contents.xml | 0 .../BFF_installer.pmdoc/06setuptools.xml | 0 .../BFF_installer.pmdoc/07python-contents.xml | 0 .../BFF_installer.pmdoc/07python.xml | 0 .../08hcluster-contents.xml | 0 .../BFF_installer.pmdoc/08hcluster.xml | 0 .../BFF_installer.pmdoc/09zzuf-contents.xml | 0 .../installer/BFF_installer.pmdoc/09zzuf.xml | 0 .../10libcaca-contents.xml | 0 .../BFF_installer.pmdoc/10libcaca.xml | 0 .../1144497_crashwrangler-contents.xml | 0 .../1144497_crashwrangler.xml | 0 .../BFF_installer.pmdoc/12exc-contents.xml | 0 .../installer/BFF_installer.pmdoc/12exc.xml | 0 .../BFF_installer.pmdoc/13exc-contents.xml | 0 .../installer/BFF_installer.pmdoc/13exc.xml | 0 .../BFF_installer.pmdoc/14exc-contents.xml | 0 .../installer/BFF_installer.pmdoc/14exc.xml | 0 .../15convert-contents.xml | 0 .../BFF_installer.pmdoc/15convert.xml | 0 .../16libzzuf-contents.xml | 0 .../BFF_installer.pmdoc/16libzzuf.xml | 0 .../17imagemagick-contents.xml | 0 .../BFF_installer.pmdoc/17imagemagick.xml | 0 .../BFF_installer.pmdoc/18bff-contents.xml | 0 .../installer/BFF_installer.pmdoc/18bff.xml | 0 .../BFF_installer.pmdoc/19bff-contents.xml | 0 .../installer/BFF_installer.pmdoc/19bff.xml | 0 .../20valgrind-contents.xml | 0 .../BFF_installer.pmdoc/20valgrind.xml | 0 .../BFF_installer.pmdoc/21zzuf-contents.xml | 0 .../installer/BFF_installer.pmdoc/21zzuf.xml | 0 .../22libcaca-contents.xml | 0 .../BFF_installer.pmdoc/22libcaca.xml | 0 .../23valgrind-contents.xml | 0 .../BFF_installer.pmdoc/23valgrind.xml | 0 .../24imagemagick-contents.xml | 0 .../BFF_installer.pmdoc/24imagemagick.xml | 0 .../25valgrind-contents.xml | 0 .../BFF_installer.pmdoc/25valgrind.xml | 0 .../26callgrind-contents.xml | 0 .../BFF_installer.pmdoc/26callgrind.xml | 0 .../BFF_installer.pmdoc/27pyyaml-contents.xml | 0 .../BFF_installer.pmdoc/27pyyaml.xml | 0 .../BFF_installer.pmdoc/28exc-contents.xml | 1 + .../installer/BFF_installer.pmdoc/28exc.xml | 20 ++++++++++++++++++ .../installer/BFF_installer.pmdoc/index.xml | 0 .../{dist => distmods}/osx/installer/build.sh | 0 .../osx/installer/scripts/chmod.sh | 0 .../osx/installer/scripts/hcluster-post.sh | 0 .../installer/scripts/imagemagick_chmod.sh | 0 .../installer/scripts/imagemagick_symlink.sh | 0 .../osx/installer/scripts/memcached-post.sh | 0 .../installer/scripts/python27-matplotlib.sh | 0 .../osx/installer/scripts/python27-numpy.sh | 0 .../osx/installer/scripts/python27-scipy.sh | 0 .../osx/installer/scripts/python27.sh | 0 .../osx/installer/scripts/pyyaml-post.sh | 0 .../installer/scripts/runterminal.applescript | 0 .../osx/installer/scripts/setuptools-post.sh | 0 .../osx/installer/scripts/symlink.sh | 0 .../osx/installer/scripts/tcl.sh | 0 .../installer/scripts/updateshellprofile.sh | 0 .../osx/installer/scripts/zzuf_chmod.sh | 0 build/{dist => distmods}/prepend_license.py | 0 build/{dist => distmods}/windows/__init__.py | 0 .../windows/nsis/EnvVarUpdate.nsh | 0 .../windows/nsis/__init__.py | 0 .../windows/nsis/buildnsi-refactor.py | 0 .../windows/nsis/buildnsi.py | 0 .../{dist => distmods}/windows/nsis/cert.ico | Bin .../{dist => distmods}/windows/nsis/inetc.dll | Bin .../windows/nsis/nsis_footer.txt | 0 .../windows/nsis/nsis_header.txt | 0 .../windows/nsis/nsis_mid.txt | 0 .../windows/nsis/nsis_top.txt | 0 .../windows/nsis/test/__init__.py | 0 .../windows/nsis/test/test_buildnsi.py | 0 .../windows/nsis/test/test_prepend_license.py | 0 .../windows/nsis/vmwarning.txt | 0 .../windows/windows_build2.py | 0 build/make_dev.py | 6 +++--- build/make_dist2.py | 6 +++--- 119 files changed, 30 insertions(+), 9 deletions(-) rename build/{dev => devmods}/__init__.py (100%) rename build/{dev => devmods}/build_base.py (98%) rename build/{dev => devmods}/linux/__init__.py (100%) rename build/{dev => devmods}/linux/linux_build.py (100%) rename build/{dev => devmods}/misc.py (100%) rename build/{dev => devmods}/osx/__init__.py (100%) rename build/{dev => devmods}/osx/darwin_build.py (100%) rename build/{dev => devmods}/windows/__init__.py (100%) rename build/{dev => devmods}/windows/windows_build.py (94%) rename build/{dist => distmods}/__init__.py (100%) rename build/{dist => distmods}/build2.py (100%) rename build/{dist => distmods}/build_base2.py (99%) rename build/{dist => distmods}/errors.py (100%) rename build/{dist => distmods}/git.py (100%) rename build/{dist => distmods}/linux/__init__.py (100%) rename build/{dist => distmods}/linux/linux_build2.py (100%) rename build/{dist => distmods}/misc.py (100%) rename build/{dist => distmods}/osx/__init__.py (100%) rename build/{dist => distmods}/osx/darwin_build2.py (100%) rename build/{dist => distmods}/osx/installer/BFF.app/Contents/Info.plist (100%) rename build/{dist => distmods}/osx/installer/BFF.app/Contents/MacOS/runterminal.applescript (100%) rename build/{dist => distmods}/osx/installer/BFF.app/Contents/Resources/AppSettings.plist (100%) rename build/{dist => distmods}/osx/installer/BFF.app/Contents/Resources/English.lproj/InfoPlist.strings (100%) rename build/{dist => distmods}/osx/installer/BFF.app/Contents/Resources/English.lproj/MainMenu.nib (100%) rename build/{dist => distmods}/osx/installer/BFF.app/Contents/Resources/appIcon.icns (100%) rename build/{dist => distmods}/osx/installer/BFF.app/Contents/Resources/script (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/01bff-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/01bff.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/02bff-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/02bff.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/03bff-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/03bff.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/04bff-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/04bff.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/05bff-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/05bff.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/06setuptools-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/06setuptools.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/07python-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/07python.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/08hcluster-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/08hcluster.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/09zzuf-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/09zzuf.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/10libcaca-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/10libcaca.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/12exc-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/12exc.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/13exc-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/13exc.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/14exc-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/14exc.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/15convert-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/15convert.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/16libzzuf-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/16libzzuf.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/17imagemagick-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/17imagemagick.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/18bff-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/18bff.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/19bff-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/19bff.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/20valgrind-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/20valgrind.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/21zzuf-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/21zzuf.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/22libcaca-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/22libcaca.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/23valgrind-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/23valgrind.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/24imagemagick-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/24imagemagick.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/25valgrind-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/25valgrind.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/26callgrind-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/26callgrind.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/27pyyaml-contents.xml (100%) rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/27pyyaml.xml (100%) create mode 100644 build/distmods/osx/installer/BFF_installer.pmdoc/28exc-contents.xml create mode 100644 build/distmods/osx/installer/BFF_installer.pmdoc/28exc.xml rename build/{dist => distmods}/osx/installer/BFF_installer.pmdoc/index.xml (100%) rename build/{dist => distmods}/osx/installer/build.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/chmod.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/hcluster-post.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/imagemagick_chmod.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/imagemagick_symlink.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/memcached-post.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/python27-matplotlib.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/python27-numpy.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/python27-scipy.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/python27.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/pyyaml-post.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/runterminal.applescript (100%) rename build/{dist => distmods}/osx/installer/scripts/setuptools-post.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/symlink.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/tcl.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/updateshellprofile.sh (100%) rename build/{dist => distmods}/osx/installer/scripts/zzuf_chmod.sh (100%) rename build/{dist => distmods}/prepend_license.py (100%) rename build/{dist => distmods}/windows/__init__.py (100%) rename build/{dist => distmods}/windows/nsis/EnvVarUpdate.nsh (100%) rename build/{dist => distmods}/windows/nsis/__init__.py (100%) rename build/{dist => distmods}/windows/nsis/buildnsi-refactor.py (100%) rename build/{dist => distmods}/windows/nsis/buildnsi.py (100%) rename build/{dist => distmods}/windows/nsis/cert.ico (100%) rename build/{dist => distmods}/windows/nsis/inetc.dll (100%) rename build/{dist => distmods}/windows/nsis/nsis_footer.txt (100%) rename build/{dist => distmods}/windows/nsis/nsis_header.txt (100%) rename build/{dist => distmods}/windows/nsis/nsis_mid.txt (100%) rename build/{dist => distmods}/windows/nsis/nsis_top.txt (100%) rename build/{dist => distmods}/windows/nsis/test/__init__.py (100%) rename build/{dist => distmods}/windows/nsis/test/test_buildnsi.py (100%) rename build/{dist => distmods}/windows/nsis/test/test_prepend_license.py (100%) rename build/{dist => distmods}/windows/nsis/vmwarning.txt (100%) rename build/{dist => distmods}/windows/windows_build2.py (100%) diff --git a/build/dev/__init__.py b/build/devmods/__init__.py similarity index 100% rename from build/dev/__init__.py rename to build/devmods/__init__.py diff --git a/build/dev/build_base.py b/build/devmods/build_base.py similarity index 98% rename from build/dev/build_base.py rename to build/devmods/build_base.py index fe96668..6630c2b 100644 --- a/build/dev/build_base.py +++ b/build/devmods/build_base.py @@ -7,7 +7,7 @@ import logging import shutil -from dev.misc import copydir, copyfile, onerror, mdtotextfile +from devmods.misc import copydir, copyfile, onerror, mdtotextfile logger = logging.getLogger(__name__) diff --git a/build/dev/linux/__init__.py b/build/devmods/linux/__init__.py similarity index 100% rename from build/dev/linux/__init__.py rename to build/devmods/linux/__init__.py diff --git a/build/dev/linux/linux_build.py b/build/devmods/linux/linux_build.py similarity index 100% rename from build/dev/linux/linux_build.py rename to build/devmods/linux/linux_build.py diff --git a/build/dev/misc.py b/build/devmods/misc.py similarity index 100% rename from build/dev/misc.py rename to build/devmods/misc.py diff --git a/build/dev/osx/__init__.py b/build/devmods/osx/__init__.py similarity index 100% rename from build/dev/osx/__init__.py rename to build/devmods/osx/__init__.py diff --git a/build/dev/osx/darwin_build.py b/build/devmods/osx/darwin_build.py similarity index 100% rename from build/dev/osx/darwin_build.py rename to build/devmods/osx/darwin_build.py diff --git a/build/dev/windows/__init__.py b/build/devmods/windows/__init__.py similarity index 100% rename from build/dev/windows/__init__.py rename to build/devmods/windows/__init__.py diff --git a/build/dev/windows/windows_build.py b/build/devmods/windows/windows_build.py similarity index 94% rename from build/dev/windows/windows_build.py rename to build/devmods/windows/windows_build.py index dd911d8..0133ab6 100644 --- a/build/dev/windows/windows_build.py +++ b/build/devmods/windows/windows_build.py @@ -9,7 +9,7 @@ mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) -from dev.misc import copyfile +from devmods.misc import copyfile class WindowsBuild(Build): diff --git a/build/dist/__init__.py b/build/distmods/__init__.py similarity index 100% rename from build/dist/__init__.py rename to build/distmods/__init__.py diff --git a/build/dist/build2.py b/build/distmods/build2.py similarity index 100% rename from build/dist/build2.py rename to build/distmods/build2.py diff --git a/build/dist/build_base2.py b/build/distmods/build_base2.py similarity index 99% rename from build/dist/build_base2.py rename to build/distmods/build_base2.py index b6c7407..48c9b23 100644 --- a/build/dist/build_base2.py +++ b/build/distmods/build_base2.py @@ -10,7 +10,7 @@ import zipfile from git import git_rev, git_hash -from dev.misc import copydir, copyfile, onerror, mdtotextfile +from devmods.misc import copydir, copyfile, onerror, mdtotextfile from .prepend_license import main as _prepend_license diff --git a/build/dist/errors.py b/build/distmods/errors.py similarity index 100% rename from build/dist/errors.py rename to build/distmods/errors.py diff --git a/build/dist/git.py b/build/distmods/git.py similarity index 100% rename from build/dist/git.py rename to build/distmods/git.py diff --git a/build/dist/linux/__init__.py b/build/distmods/linux/__init__.py similarity index 100% rename from build/dist/linux/__init__.py rename to build/distmods/linux/__init__.py diff --git a/build/dist/linux/linux_build2.py b/build/distmods/linux/linux_build2.py similarity index 100% rename from build/dist/linux/linux_build2.py rename to build/distmods/linux/linux_build2.py diff --git a/build/dist/misc.py b/build/distmods/misc.py similarity index 100% rename from build/dist/misc.py rename to build/distmods/misc.py diff --git a/build/dist/osx/__init__.py b/build/distmods/osx/__init__.py similarity index 100% rename from build/dist/osx/__init__.py rename to build/distmods/osx/__init__.py diff --git a/build/dist/osx/darwin_build2.py b/build/distmods/osx/darwin_build2.py similarity index 100% rename from build/dist/osx/darwin_build2.py rename to build/distmods/osx/darwin_build2.py diff --git a/build/dist/osx/installer/BFF.app/Contents/Info.plist b/build/distmods/osx/installer/BFF.app/Contents/Info.plist similarity index 100% rename from build/dist/osx/installer/BFF.app/Contents/Info.plist rename to build/distmods/osx/installer/BFF.app/Contents/Info.plist diff --git a/build/dist/osx/installer/BFF.app/Contents/MacOS/runterminal.applescript b/build/distmods/osx/installer/BFF.app/Contents/MacOS/runterminal.applescript similarity index 100% rename from build/dist/osx/installer/BFF.app/Contents/MacOS/runterminal.applescript rename to build/distmods/osx/installer/BFF.app/Contents/MacOS/runterminal.applescript diff --git a/build/dist/osx/installer/BFF.app/Contents/Resources/AppSettings.plist b/build/distmods/osx/installer/BFF.app/Contents/Resources/AppSettings.plist similarity index 100% rename from build/dist/osx/installer/BFF.app/Contents/Resources/AppSettings.plist rename to build/distmods/osx/installer/BFF.app/Contents/Resources/AppSettings.plist diff --git a/build/dist/osx/installer/BFF.app/Contents/Resources/English.lproj/InfoPlist.strings b/build/distmods/osx/installer/BFF.app/Contents/Resources/English.lproj/InfoPlist.strings similarity index 100% rename from build/dist/osx/installer/BFF.app/Contents/Resources/English.lproj/InfoPlist.strings rename to build/distmods/osx/installer/BFF.app/Contents/Resources/English.lproj/InfoPlist.strings diff --git a/build/dist/osx/installer/BFF.app/Contents/Resources/English.lproj/MainMenu.nib b/build/distmods/osx/installer/BFF.app/Contents/Resources/English.lproj/MainMenu.nib similarity index 100% rename from build/dist/osx/installer/BFF.app/Contents/Resources/English.lproj/MainMenu.nib rename to build/distmods/osx/installer/BFF.app/Contents/Resources/English.lproj/MainMenu.nib diff --git a/build/dist/osx/installer/BFF.app/Contents/Resources/appIcon.icns b/build/distmods/osx/installer/BFF.app/Contents/Resources/appIcon.icns similarity index 100% rename from build/dist/osx/installer/BFF.app/Contents/Resources/appIcon.icns rename to build/distmods/osx/installer/BFF.app/Contents/Resources/appIcon.icns diff --git a/build/dist/osx/installer/BFF.app/Contents/Resources/script b/build/distmods/osx/installer/BFF.app/Contents/Resources/script similarity index 100% rename from build/dist/osx/installer/BFF.app/Contents/Resources/script rename to build/distmods/osx/installer/BFF.app/Contents/Resources/script diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/01bff-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/01bff-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/01bff-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/01bff-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/01bff.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/01bff.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/01bff.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/01bff.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/02bff-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/02bff-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/02bff-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/02bff-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/02bff.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/02bff.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/02bff.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/02bff.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/03bff-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/03bff-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/03bff-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/03bff-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/03bff.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/03bff.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/03bff.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/03bff.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/04bff-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/04bff-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/04bff-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/04bff-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/04bff.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/04bff.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/04bff.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/04bff.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/05bff-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/05bff-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/05bff-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/05bff-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/05bff.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/05bff.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/05bff.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/05bff.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/06setuptools-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/06setuptools-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/06setuptools-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/06setuptools-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/06setuptools.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/06setuptools.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/06setuptools.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/06setuptools.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/07python-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/07python-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/07python-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/07python-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/07python.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/07python.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/07python.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/07python.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/08hcluster-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/08hcluster-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/08hcluster-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/08hcluster-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/08hcluster.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/08hcluster.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/08hcluster.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/08hcluster.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/09zzuf-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/09zzuf-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/09zzuf-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/09zzuf-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/09zzuf.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/09zzuf.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/09zzuf.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/09zzuf.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/10libcaca-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/10libcaca-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/10libcaca-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/10libcaca-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/10libcaca.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/10libcaca.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/10libcaca.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/10libcaca.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/12exc-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/12exc-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/12exc-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/12exc-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/12exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/12exc.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/12exc.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/12exc.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/13exc-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/13exc-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/13exc-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/13exc-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/13exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/13exc.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/13exc.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/13exc.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/14exc-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/14exc-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/14exc-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/14exc-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/14exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/14exc.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/14exc.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/14exc.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/15convert-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/15convert-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/15convert-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/15convert-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/15convert.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/15convert.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/15convert.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/15convert.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/16libzzuf-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/16libzzuf-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/16libzzuf-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/16libzzuf-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/16libzzuf.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/16libzzuf.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/16libzzuf.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/16libzzuf.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/17imagemagick-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/17imagemagick-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/17imagemagick-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/17imagemagick-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/17imagemagick.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/17imagemagick.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/17imagemagick.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/17imagemagick.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/18bff-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/18bff-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/18bff-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/18bff-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/18bff.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/18bff.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/18bff.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/18bff.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/19bff-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/19bff-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/19bff-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/19bff-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/19bff.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/19bff.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/19bff.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/19bff.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/20valgrind-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/20valgrind-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/20valgrind-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/20valgrind-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/20valgrind.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/20valgrind.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/20valgrind.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/20valgrind.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/21zzuf-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/21zzuf-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/21zzuf-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/21zzuf-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/21zzuf.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/21zzuf.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/21zzuf.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/21zzuf.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/22libcaca-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/22libcaca-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/22libcaca-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/22libcaca-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/22libcaca.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/22libcaca.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/22libcaca.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/22libcaca.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/23valgrind-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/23valgrind-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/23valgrind-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/23valgrind-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/23valgrind.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/23valgrind.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/23valgrind.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/23valgrind.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/24imagemagick-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/24imagemagick-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/24imagemagick-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/24imagemagick-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/24imagemagick.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/24imagemagick.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/24imagemagick.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/24imagemagick.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/25valgrind-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/25valgrind-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/25valgrind-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/25valgrind-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/25valgrind.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/25valgrind.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/25valgrind.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/25valgrind.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/26callgrind-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/26callgrind-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/26callgrind-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/26callgrind-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/26callgrind.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/26callgrind.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/26callgrind.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/26callgrind.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/27pyyaml-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/27pyyaml-contents.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/27pyyaml-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/27pyyaml-contents.xml diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/27pyyaml.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/27pyyaml.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/27pyyaml.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/27pyyaml.xml diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/28exc-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/28exc-contents.xml new file mode 100644 index 0000000..69b34ea --- /dev/null +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/28exc-contents.xml @@ -0,0 +1 @@ + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/28exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/28exc.xml new file mode 100644 index 0000000..d57bfba --- /dev/null +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/28exc.xml @@ -0,0 +1,20 @@ + + + + com.cert.cc.certBff.exc_handler_snowleopard.pkg + 1.0 + + + + + crashwrangler/binaries/exc_handler_snowleopard + /usr/local/bin + + + + + parent + installFrom.isRelativeType + installTo + + diff --git a/build/dist/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml similarity index 100% rename from build/dist/osx/installer/BFF_installer.pmdoc/index.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/index.xml diff --git a/build/dist/osx/installer/build.sh b/build/distmods/osx/installer/build.sh similarity index 100% rename from build/dist/osx/installer/build.sh rename to build/distmods/osx/installer/build.sh diff --git a/build/dist/osx/installer/scripts/chmod.sh b/build/distmods/osx/installer/scripts/chmod.sh similarity index 100% rename from build/dist/osx/installer/scripts/chmod.sh rename to build/distmods/osx/installer/scripts/chmod.sh diff --git a/build/dist/osx/installer/scripts/hcluster-post.sh b/build/distmods/osx/installer/scripts/hcluster-post.sh similarity index 100% rename from build/dist/osx/installer/scripts/hcluster-post.sh rename to build/distmods/osx/installer/scripts/hcluster-post.sh diff --git a/build/dist/osx/installer/scripts/imagemagick_chmod.sh b/build/distmods/osx/installer/scripts/imagemagick_chmod.sh similarity index 100% rename from build/dist/osx/installer/scripts/imagemagick_chmod.sh rename to build/distmods/osx/installer/scripts/imagemagick_chmod.sh diff --git a/build/dist/osx/installer/scripts/imagemagick_symlink.sh b/build/distmods/osx/installer/scripts/imagemagick_symlink.sh similarity index 100% rename from build/dist/osx/installer/scripts/imagemagick_symlink.sh rename to build/distmods/osx/installer/scripts/imagemagick_symlink.sh diff --git a/build/dist/osx/installer/scripts/memcached-post.sh b/build/distmods/osx/installer/scripts/memcached-post.sh similarity index 100% rename from build/dist/osx/installer/scripts/memcached-post.sh rename to build/distmods/osx/installer/scripts/memcached-post.sh diff --git a/build/dist/osx/installer/scripts/python27-matplotlib.sh b/build/distmods/osx/installer/scripts/python27-matplotlib.sh similarity index 100% rename from build/dist/osx/installer/scripts/python27-matplotlib.sh rename to build/distmods/osx/installer/scripts/python27-matplotlib.sh diff --git a/build/dist/osx/installer/scripts/python27-numpy.sh b/build/distmods/osx/installer/scripts/python27-numpy.sh similarity index 100% rename from build/dist/osx/installer/scripts/python27-numpy.sh rename to build/distmods/osx/installer/scripts/python27-numpy.sh diff --git a/build/dist/osx/installer/scripts/python27-scipy.sh b/build/distmods/osx/installer/scripts/python27-scipy.sh similarity index 100% rename from build/dist/osx/installer/scripts/python27-scipy.sh rename to build/distmods/osx/installer/scripts/python27-scipy.sh diff --git a/build/dist/osx/installer/scripts/python27.sh b/build/distmods/osx/installer/scripts/python27.sh similarity index 100% rename from build/dist/osx/installer/scripts/python27.sh rename to build/distmods/osx/installer/scripts/python27.sh diff --git a/build/dist/osx/installer/scripts/pyyaml-post.sh b/build/distmods/osx/installer/scripts/pyyaml-post.sh similarity index 100% rename from build/dist/osx/installer/scripts/pyyaml-post.sh rename to build/distmods/osx/installer/scripts/pyyaml-post.sh diff --git a/build/dist/osx/installer/scripts/runterminal.applescript b/build/distmods/osx/installer/scripts/runterminal.applescript similarity index 100% rename from build/dist/osx/installer/scripts/runterminal.applescript rename to build/distmods/osx/installer/scripts/runterminal.applescript diff --git a/build/dist/osx/installer/scripts/setuptools-post.sh b/build/distmods/osx/installer/scripts/setuptools-post.sh similarity index 100% rename from build/dist/osx/installer/scripts/setuptools-post.sh rename to build/distmods/osx/installer/scripts/setuptools-post.sh diff --git a/build/dist/osx/installer/scripts/symlink.sh b/build/distmods/osx/installer/scripts/symlink.sh similarity index 100% rename from build/dist/osx/installer/scripts/symlink.sh rename to build/distmods/osx/installer/scripts/symlink.sh diff --git a/build/dist/osx/installer/scripts/tcl.sh b/build/distmods/osx/installer/scripts/tcl.sh similarity index 100% rename from build/dist/osx/installer/scripts/tcl.sh rename to build/distmods/osx/installer/scripts/tcl.sh diff --git a/build/dist/osx/installer/scripts/updateshellprofile.sh b/build/distmods/osx/installer/scripts/updateshellprofile.sh similarity index 100% rename from build/dist/osx/installer/scripts/updateshellprofile.sh rename to build/distmods/osx/installer/scripts/updateshellprofile.sh diff --git a/build/dist/osx/installer/scripts/zzuf_chmod.sh b/build/distmods/osx/installer/scripts/zzuf_chmod.sh similarity index 100% rename from build/dist/osx/installer/scripts/zzuf_chmod.sh rename to build/distmods/osx/installer/scripts/zzuf_chmod.sh diff --git a/build/dist/prepend_license.py b/build/distmods/prepend_license.py similarity index 100% rename from build/dist/prepend_license.py rename to build/distmods/prepend_license.py diff --git a/build/dist/windows/__init__.py b/build/distmods/windows/__init__.py similarity index 100% rename from build/dist/windows/__init__.py rename to build/distmods/windows/__init__.py diff --git a/build/dist/windows/nsis/EnvVarUpdate.nsh b/build/distmods/windows/nsis/EnvVarUpdate.nsh similarity index 100% rename from build/dist/windows/nsis/EnvVarUpdate.nsh rename to build/distmods/windows/nsis/EnvVarUpdate.nsh diff --git a/build/dist/windows/nsis/__init__.py b/build/distmods/windows/nsis/__init__.py similarity index 100% rename from build/dist/windows/nsis/__init__.py rename to build/distmods/windows/nsis/__init__.py diff --git a/build/dist/windows/nsis/buildnsi-refactor.py b/build/distmods/windows/nsis/buildnsi-refactor.py similarity index 100% rename from build/dist/windows/nsis/buildnsi-refactor.py rename to build/distmods/windows/nsis/buildnsi-refactor.py diff --git a/build/dist/windows/nsis/buildnsi.py b/build/distmods/windows/nsis/buildnsi.py similarity index 100% rename from build/dist/windows/nsis/buildnsi.py rename to build/distmods/windows/nsis/buildnsi.py diff --git a/build/dist/windows/nsis/cert.ico b/build/distmods/windows/nsis/cert.ico similarity index 100% rename from build/dist/windows/nsis/cert.ico rename to build/distmods/windows/nsis/cert.ico diff --git a/build/dist/windows/nsis/inetc.dll b/build/distmods/windows/nsis/inetc.dll similarity index 100% rename from build/dist/windows/nsis/inetc.dll rename to build/distmods/windows/nsis/inetc.dll diff --git a/build/dist/windows/nsis/nsis_footer.txt b/build/distmods/windows/nsis/nsis_footer.txt similarity index 100% rename from build/dist/windows/nsis/nsis_footer.txt rename to build/distmods/windows/nsis/nsis_footer.txt diff --git a/build/dist/windows/nsis/nsis_header.txt b/build/distmods/windows/nsis/nsis_header.txt similarity index 100% rename from build/dist/windows/nsis/nsis_header.txt rename to build/distmods/windows/nsis/nsis_header.txt diff --git a/build/dist/windows/nsis/nsis_mid.txt b/build/distmods/windows/nsis/nsis_mid.txt similarity index 100% rename from build/dist/windows/nsis/nsis_mid.txt rename to build/distmods/windows/nsis/nsis_mid.txt diff --git a/build/dist/windows/nsis/nsis_top.txt b/build/distmods/windows/nsis/nsis_top.txt similarity index 100% rename from build/dist/windows/nsis/nsis_top.txt rename to build/distmods/windows/nsis/nsis_top.txt diff --git a/build/dist/windows/nsis/test/__init__.py b/build/distmods/windows/nsis/test/__init__.py similarity index 100% rename from build/dist/windows/nsis/test/__init__.py rename to build/distmods/windows/nsis/test/__init__.py diff --git a/build/dist/windows/nsis/test/test_buildnsi.py b/build/distmods/windows/nsis/test/test_buildnsi.py similarity index 100% rename from build/dist/windows/nsis/test/test_buildnsi.py rename to build/distmods/windows/nsis/test/test_buildnsi.py diff --git a/build/dist/windows/nsis/test/test_prepend_license.py b/build/distmods/windows/nsis/test/test_prepend_license.py similarity index 100% rename from build/dist/windows/nsis/test/test_prepend_license.py rename to build/distmods/windows/nsis/test/test_prepend_license.py diff --git a/build/dist/windows/nsis/vmwarning.txt b/build/distmods/windows/nsis/vmwarning.txt similarity index 100% rename from build/dist/windows/nsis/vmwarning.txt rename to build/distmods/windows/nsis/vmwarning.txt diff --git a/build/dist/windows/windows_build2.py b/build/distmods/windows/windows_build2.py similarity index 100% rename from build/dist/windows/windows_build2.py rename to build/distmods/windows/windows_build2.py diff --git a/build/make_dev.py b/build/make_dev.py index b9b15b5..7eca6a3 100644 --- a/build/make_dev.py +++ b/build/make_dev.py @@ -4,9 +4,9 @@ @organization: cert.org ''' import logging -from dev.linux.linux_build import LinuxBuild -from dev.windows.windows_build import WindowsBuild -#from dev.osx import DarwinBuild +from devmods.linux.linux_build import LinuxBuild +from devmods.windows.windows_build import WindowsBuild +# from dev.osx import DarwinBuild logger = logging.getLogger(__name__) diff --git a/build/make_dist2.py b/build/make_dist2.py index a4d9054..d98329e 100644 --- a/build/make_dist2.py +++ b/build/make_dist2.py @@ -5,9 +5,9 @@ ''' import argparse import logging -from dist.build2 import builder_for -from dist.build2 import SUPPORTED_PLATFORMS as builders -from dist.errors import BuildError +from distmods.build2 import builder_for +from distmods.build2 import SUPPORTED_PLATFORMS as builders +from distmods.errors import BuildError logger = logging.getLogger(__name__) From 6d546ae3bf98293cc288d36d40aa7810a43d861b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 24 Jun 2014 14:59:09 -0400 Subject: [PATCH 0307/1169] Fix up .gitignore --- .gitignore | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 8b03148..c151224 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,12 @@ doc/html doc/pdf *.bak *.pyc -build CERT_Basic_Fuzzing_Framework.egg-info -dev_builds +dist_builds doc setup_env/bff.env -src/dist +src/linux/COPYING.txt +src/windows/COPYING.txt +build/distmods/osx/installer/bff/ +build/distmods/osx/installer/*.txt +*.stackdump From 372d43935005ab8231df222ef214791913d233e4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 15:04:30 -0400 Subject: [PATCH 0308/1169] use absolute imports --- src/certfuzz/debuggers/crashwrangler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index 0cad996..4d1ad32 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -10,7 +10,8 @@ import platform import re -from certfuzz.debuggers import Debugger, register +from certfuzz.debuggers.debugger_base import Debugger +from certfuzz.debuggers.registration import register from certfuzz.debuggers.output_parsers.cwfile import CWfile from certfuzz.fuzztools import subprocess_helper as subp From 6abc073c41ce0611efe0f775e9768ddbbbad7844 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 15:32:16 -0400 Subject: [PATCH 0309/1169] rename campaign_base -> campaign_meta Fix up resulting imports, and have campaign_linux inherit from CampaignMeta --- src/certfuzz/campaign/__init__.py | 3 -- src/certfuzz/campaign/campaign.py | 7 +++-- src/certfuzz/campaign/campaign_linux.py | 5 ++-- .../{campaign_base.py => campaign_meta.py} | 2 +- ...campaign_base.py => test_campaign_meta.py} | 0 .../test/campaign/test_campaign_pkg.py | 30 ------------------- 6 files changed, 7 insertions(+), 40 deletions(-) rename src/certfuzz/campaign/{campaign_base.py => campaign_meta.py} (98%) rename src/certfuzz/test/campaign/{test_campaign_base.py => test_campaign_meta.py} (100%) delete mode 100644 src/certfuzz/test/campaign/test_campaign_pkg.py diff --git a/src/certfuzz/campaign/__init__.py b/src/certfuzz/campaign/__init__.py index d07d5d5..e69de29 100644 --- a/src/certfuzz/campaign/__init__.py +++ b/src/certfuzz/campaign/__init__.py @@ -1,3 +0,0 @@ -from certfuzz import __version__ -from certfuzz.campaign.campaign_base import import_module_by_name -from certfuzz.campaign.campaign_base import CampaignBase diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index ea446be..0353110 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -12,7 +12,8 @@ import tempfile import traceback -from certfuzz.campaign import CampaignBase, __version__, import_module_by_name +from certfuzz.campaign.campaign_meta import CampaignMeta, import_module_by_name +from certfuzz.version import __version__ from certfuzz.campaign.config.config_windows import Config from certfuzz.campaign.errors import CampaignError from certfuzz.debuggers import registration @@ -35,7 +36,7 @@ } -class Campaign(CampaignBase): +class Campaign(CampaignMeta): ''' Provides a fuzzing campaign object. ''' @@ -196,7 +197,7 @@ def _set_debugger(self): self.dbg_class = registration.debug_class def _write_version(self): - CampaignBase._write_version(self) + CampaignMeta._write_version(self) def _setup_output(self): # construct run output directory diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index ab88858..a0c31bb 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -13,6 +13,7 @@ import time import traceback +from certfuzz.campaign.campaign_meta import CampaignMeta from certfuzz.campaign.config import bff_config as cfg_helper from certfuzz.campaign.errors import CampaignScriptError from certfuzz.debuggers import crashwrangler # @UnusedImport @@ -25,7 +26,6 @@ from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.fuzztools.watchdog import WatchDog - from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.linux import Iteration @@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) -class LinuxCampaign(object): +class LinuxCampaign(CampaignMeta): def __init__(self, config_file=None, result_dir=None, debug=False): # Read the cfg file self.cfg_path = config_file @@ -234,7 +234,6 @@ def _do_interval(self, s1, s2, first_chunk=False): rf=sf.rangefinder) as iteration: iteration.go() - def go(self): # campaign.go cfg = self.cfg diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_meta.py similarity index 98% rename from src/certfuzz/campaign/campaign_base.py rename to src/certfuzz/campaign/campaign_meta.py index ec09447..82ef005 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_meta.py @@ -22,7 +22,7 @@ def import_module_by_name(name, logger=None): return module -class CampaignBase(object): +class CampaignMeta(object): __metaclass__ = abc.ABCMeta @abc.abstractmethod diff --git a/src/certfuzz/test/campaign/test_campaign_base.py b/src/certfuzz/test/campaign/test_campaign_meta.py similarity index 100% rename from src/certfuzz/test/campaign/test_campaign_base.py rename to src/certfuzz/test/campaign/test_campaign_meta.py diff --git a/src/certfuzz/test/campaign/test_campaign_pkg.py b/src/certfuzz/test/campaign/test_campaign_pkg.py deleted file mode 100644 index cd3bebc..0000000 --- a/src/certfuzz/test/campaign/test_campaign_pkg.py +++ /dev/null @@ -1,30 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.campaign - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.campaign - api_list = ['__version__', - 'import_module_by_name', - 'CampaignBase', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From f52305de7e908df331d5a743b1d02970fbad6b95 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 16:07:59 -0400 Subject: [PATCH 0310/1169] add stub methods to match metaclass --- src/certfuzz/campaign/campaign_linux.py | 15 +++++++++++++++ src/certfuzz/test/campaign/test_campaign_linux.py | 2 ++ 2 files changed, 17 insertions(+) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index a0c31bb..8a3aa09 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -212,6 +212,12 @@ def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): logger.debug('%s was found, not unique', crash_id) return False + def __getstate__(self): + pass + + def __setstate__(self): + pass + def _do_interval(self, s1, s2, first_chunk=False): # interval.go logger.debug('Starting interval %d-%d', s1, s2) @@ -234,6 +240,15 @@ def _do_interval(self, s1, s2, first_chunk=False): rf=sf.rangefinder) as iteration: iteration.go() + def _do_iteration(self): + pass + + def _keep_going(self): + pass + + def _write_version(self): + pass + def go(self): # campaign.go cfg = self.cfg diff --git a/src/certfuzz/test/campaign/test_campaign_linux.py b/src/certfuzz/test/campaign/test_campaign_linux.py index fba8510..9e7b6bc 100644 --- a/src/certfuzz/test/campaign/test_campaign_linux.py +++ b/src/certfuzz/test/campaign/test_campaign_linux.py @@ -4,11 +4,13 @@ @organization: cert.org ''' import unittest +from certfuzz.campaign.campaign_linux import LinuxCampaign class Test(unittest.TestCase): def setUp(self): + self.campaign = LinuxCampaign() pass def tearDown(self): From 8f5db9808698873a5dc8ceeef853edc26e0a7ea3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 3 Jun 2014 11:25:19 -0400 Subject: [PATCH 0311/1169] rename campaign -> campaign_base, Campaign -> CampaignBase --- .../campaign/{campaign.py => campaign_base.py} | 4 ++-- src/certfuzz/campaign/campaign_windows.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename src/certfuzz/campaign/{campaign.py => campaign_base.py} (99%) diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign_base.py similarity index 99% rename from src/certfuzz/campaign/campaign.py rename to src/certfuzz/campaign/campaign_base.py index 0353110..6459dde 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -36,7 +36,7 @@ } -class Campaign(CampaignMeta): +class CampaignBase(CampaignMeta): ''' Provides a fuzzing campaign object. ''' @@ -45,7 +45,7 @@ def __init__(self, config_file, result_dir=None, campaign_cache=None, ''' Typically one would invoke a campaign as follows: - with Campaign(params) as campaign: + with CampaignBase(params) as campaign: campaign.go() This will ensure that the runtime context is established properly, and diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 05f552f..6b024aa 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -9,16 +9,16 @@ from threading import Timer import platform -from certfuzz.campaign.campaign import Campaign +from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.runners.killableprocess import Popen logger = logging.getLogger(__name__) -class WindowsCampaign(Campaign): +class WindowsCampaign(CampaignBase): ''' - Extends Campaign to add windows-specific features like ButtonClicker + Extends CampaignBase to add windows-specific features like ButtonClicker ''' def __enter__(self): if sys.platform == 'win32': @@ -28,14 +28,14 @@ def __enter__(self): if hook_incompat and self.runner_module_name == 'certfuzz.runners.winrun': logger.debug('winrun is not compatible with Windows %s %s. Overriding.', winver, machine) self.runner_module_name = None - self = Campaign.__enter__(self) + self = CampaignBase.__enter__(self) self._start_buttonclicker() self._cache_app() return self def __exit__(self, etype, value, mytraceback): self._stop_buttonclicker() - return Campaign.__exit__(self, etype, value, mytraceback) + return CampaignBase.__exit__(self, etype, value, mytraceback) def _cache_app(self): logger.debug('Caching application %s and determining if we need to watch the CPU...', self.prog) From 2deb125ff3252ed85c15aa11d118fa962f1776a1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 08:56:57 -0400 Subject: [PATCH 0312/1169] mixup unit tests --- .../{test_campaign.py => test_campaign_base.py} | 10 +++++----- src/certfuzz/test/campaign/test_campaign_linux.py | 2 +- src/certfuzz/test/test_version.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/certfuzz/test/campaign/{test_campaign.py => test_campaign_base.py} (89%) diff --git a/src/certfuzz/test/campaign/test_campaign.py b/src/certfuzz/test/campaign/test_campaign_base.py similarity index 89% rename from src/certfuzz/test/campaign/test_campaign.py rename to src/certfuzz/test/campaign/test_campaign_base.py index 1795cc3..69868bc 100644 --- a/src/certfuzz/test/campaign/test_campaign.py +++ b/src/certfuzz/test/campaign/test_campaign_base.py @@ -5,7 +5,7 @@ ''' import unittest -from certfuzz.campaign import campaign +import certfuzz.campaign.campaign_base from tempfile import mkstemp, mkdtemp import yaml import shutil @@ -54,7 +54,7 @@ def setUp(self): self.tmpdir = mkdtemp() self.program = mkstemp(dir=self.tmpdir)[1] self._dump_test_config() - self.campaign = campaign.Campaign(self.cfg_file) + self.campaign = certfuzz.campaign.campaign_base.CampaignBase(self.cfg_file) fd, f = tempfile.mkstemp() os.close(fd) @@ -65,11 +65,11 @@ def tearDown(self): shutil.rmtree(self.tmpdir, ignore_errors=True) def test_init(self): - self.assertEqual('%s.%s' % (campaign.packages['fuzzers'], 'fuzzermodule'), + self.assertEqual('%s.%s' % (certfuzz.campaign.campaign_base.packages['fuzzers'], 'fuzzermodule'), self.campaign.fuzzer_module_name) - self.assertEqual('%s.%s' % (campaign.packages['runners'], 'runnermodule'), + self.assertEqual('%s.%s' % (certfuzz.campaign.campaign_base.packages['runners'], 'runnermodule'), self.campaign.runner_module_name) - self.assertEqual('%s.%s' % (campaign.packages['debuggers'], 'debuggermodule'), + self.assertEqual('%s.%s' % (certfuzz.campaign.campaign_base.packages['debuggers'], 'debuggermodule'), self.campaign.debugger_module_name) # def test_getstate(self): diff --git a/src/certfuzz/test/campaign/test_campaign_linux.py b/src/certfuzz/test/campaign/test_campaign_linux.py index 9e7b6bc..9d3ca70 100644 --- a/src/certfuzz/test/campaign/test_campaign_linux.py +++ b/src/certfuzz/test/campaign/test_campaign_linux.py @@ -10,7 +10,7 @@ class Test(unittest.TestCase): def setUp(self): - self.campaign = LinuxCampaign() +# self.campaign = LinuxCampaign() pass def tearDown(self): diff --git a/src/certfuzz/test/test_version.py b/src/certfuzz/test/test_version.py index 24b1f77..c94a119 100644 --- a/src/certfuzz/test/test_version.py +++ b/src/certfuzz/test/test_version.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz import version +import certfuzz.version as version class Test(unittest.TestCase): From 7f60eac61313a8c3015f50e32007463aa5ea1844 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 May 2014 15:30:08 -0400 Subject: [PATCH 0313/1169] add test suite line to setup.py this will allow for "python setup.py test" --- src/experimental/android/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/experimental/android/setup.py b/src/experimental/android/setup.py index 21ef1d0..9c1d793 100644 --- a/src/experimental/android/setup.py +++ b/src/experimental/android/setup.py @@ -89,6 +89,7 @@ def _scripts(): scripts=_scripts(), entry_points=_entry_points(), include_package_data=True, + test_suite='certfuzz.test', license='See LICENSE.txt', data_files=[ ('', ['LICENSE.txt']) From 85d307cc2473bcaffc48214912887d0848849dbc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 9 May 2014 09:59:24 -0400 Subject: [PATCH 0314/1169] put nosetests config into the setup.cfg file --- src/setup.cfg | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/setup.cfg diff --git a/src/setup.cfg b/src/setup.cfg new file mode 100644 index 0000000..e108a56 --- /dev/null +++ b/src/setup.cfg @@ -0,0 +1,10 @@ +[nosetests] +ignore-files=android +exclude=test_probability +verbosity=2 +detailed-errors=1 +# with-coverage=1 +# cover-package=certfuzz +# debug=nose.loader +# pdb=1 +# pdb-failures=1 \ No newline at end of file From 7b658a0e0daf5f69b16a81089495856178e5bed4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 09:38:15 -0400 Subject: [PATCH 0315/1169] put setup.py back where it belongs --- src/{experimental/android => }/setup.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{experimental/android => }/setup.py (100%) diff --git a/src/experimental/android/setup.py b/src/setup.py similarity index 100% rename from src/experimental/android/setup.py rename to src/setup.py From 0ca12c31ce826e632ddc18fc8fd49f1d7c0f00d6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 09:50:02 -0400 Subject: [PATCH 0316/1169] fix unit test --- .../test/campaign/config/test_bff_config.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/test/campaign/config/test_bff_config.py b/src/certfuzz/test/campaign/config/test_bff_config.py index 9ed8999..f11a468 100644 --- a/src/certfuzz/test/campaign/config/test_bff_config.py +++ b/src/certfuzz/test/campaign/config/test_bff_config.py @@ -10,6 +10,7 @@ from certfuzz.campaign.config.bff_config import MINIMIZED_EXT import tempfile from certfuzz.campaign.config.bff_config import read_config_options +import shutil class Test(unittest.TestCase): @@ -18,6 +19,7 @@ def delete_file(self, f): self.assertFalse(os.path.exists(f)) def setUp(self): + self.tmpdir = tempfile.mkdtemp() # build a config self.config = ConfigParser.RawConfigParser() @@ -71,7 +73,7 @@ def setUp(self): self.cfg = ConfigHelper(self.config) def tearDown(self): - pass + shutil.rmtree(self.tmpdir) def test_init(self): self.assertEqual(self.cfg.killprocname, 'killprocname') @@ -91,11 +93,10 @@ def test_program_is_script(self): pass def test_check_program_file_type(self): - f = os.path.abspath(__file__) - if f.endswith('pyc'): - # trim the last char ('c') - f = f[:-1] - self.cfg.program = f + fd, fname = tempfile.mkstemp(dir=self.tmpdir, text=True) + os.write(fd, 'sometext') + os.close(fd) + self.cfg.program = fname self.assertTrue(self.cfg.program_is_script()) def test_get_minimized_file(self): From 4e8e16718c131defb2d537e4f3fcb6aeaac63962 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 15:04:30 -0400 Subject: [PATCH 0317/1169] use absolute imports --- src/certfuzz/debuggers/crashwrangler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index 0cad996..4d1ad32 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -10,7 +10,8 @@ import platform import re -from certfuzz.debuggers import Debugger, register +from certfuzz.debuggers.debugger_base import Debugger +from certfuzz.debuggers.registration import register from certfuzz.debuggers.output_parsers.cwfile import CWfile from certfuzz.fuzztools import subprocess_helper as subp From d953bbf5f81e529879aab19b190ca49db3fcd13e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 15:32:16 -0400 Subject: [PATCH 0318/1169] rename campaign_base -> campaign_meta Fix up resulting imports, and have campaign_linux inherit from CampaignMeta --- src/certfuzz/campaign/__init__.py | 3 -- src/certfuzz/campaign/campaign.py | 7 +++-- src/certfuzz/campaign/campaign_linux.py | 5 ++-- .../{campaign_base.py => campaign_meta.py} | 2 +- ...campaign_base.py => test_campaign_meta.py} | 0 .../test/campaign/test_campaign_pkg.py | 30 ------------------- 6 files changed, 7 insertions(+), 40 deletions(-) rename src/certfuzz/campaign/{campaign_base.py => campaign_meta.py} (98%) rename src/certfuzz/test/campaign/{test_campaign_base.py => test_campaign_meta.py} (100%) delete mode 100644 src/certfuzz/test/campaign/test_campaign_pkg.py diff --git a/src/certfuzz/campaign/__init__.py b/src/certfuzz/campaign/__init__.py index d07d5d5..e69de29 100644 --- a/src/certfuzz/campaign/__init__.py +++ b/src/certfuzz/campaign/__init__.py @@ -1,3 +0,0 @@ -from certfuzz import __version__ -from certfuzz.campaign.campaign_base import import_module_by_name -from certfuzz.campaign.campaign_base import CampaignBase diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign.py index ea446be..0353110 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign.py @@ -12,7 +12,8 @@ import tempfile import traceback -from certfuzz.campaign import CampaignBase, __version__, import_module_by_name +from certfuzz.campaign.campaign_meta import CampaignMeta, import_module_by_name +from certfuzz.version import __version__ from certfuzz.campaign.config.config_windows import Config from certfuzz.campaign.errors import CampaignError from certfuzz.debuggers import registration @@ -35,7 +36,7 @@ } -class Campaign(CampaignBase): +class Campaign(CampaignMeta): ''' Provides a fuzzing campaign object. ''' @@ -196,7 +197,7 @@ def _set_debugger(self): self.dbg_class = registration.debug_class def _write_version(self): - CampaignBase._write_version(self) + CampaignMeta._write_version(self) def _setup_output(self): # construct run output directory diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index ab88858..a0c31bb 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -13,6 +13,7 @@ import time import traceback +from certfuzz.campaign.campaign_meta import CampaignMeta from certfuzz.campaign.config import bff_config as cfg_helper from certfuzz.campaign.errors import CampaignScriptError from certfuzz.debuggers import crashwrangler # @UnusedImport @@ -25,7 +26,6 @@ from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.fuzztools.watchdog import WatchDog - from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.linux import Iteration @@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) -class LinuxCampaign(object): +class LinuxCampaign(CampaignMeta): def __init__(self, config_file=None, result_dir=None, debug=False): # Read the cfg file self.cfg_path = config_file @@ -234,7 +234,6 @@ def _do_interval(self, s1, s2, first_chunk=False): rf=sf.rangefinder) as iteration: iteration.go() - def go(self): # campaign.go cfg = self.cfg diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_meta.py similarity index 98% rename from src/certfuzz/campaign/campaign_base.py rename to src/certfuzz/campaign/campaign_meta.py index ec09447..82ef005 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_meta.py @@ -22,7 +22,7 @@ def import_module_by_name(name, logger=None): return module -class CampaignBase(object): +class CampaignMeta(object): __metaclass__ = abc.ABCMeta @abc.abstractmethod diff --git a/src/certfuzz/test/campaign/test_campaign_base.py b/src/certfuzz/test/campaign/test_campaign_meta.py similarity index 100% rename from src/certfuzz/test/campaign/test_campaign_base.py rename to src/certfuzz/test/campaign/test_campaign_meta.py diff --git a/src/certfuzz/test/campaign/test_campaign_pkg.py b/src/certfuzz/test/campaign/test_campaign_pkg.py deleted file mode 100644 index cd3bebc..0000000 --- a/src/certfuzz/test/campaign/test_campaign_pkg.py +++ /dev/null @@ -1,30 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.campaign - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.campaign - api_list = ['__version__', - 'import_module_by_name', - 'CampaignBase', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From 300223ab5dca9fee2777cfe0c0872eebdd08062b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 2 Jun 2014 16:07:59 -0400 Subject: [PATCH 0319/1169] add stub methods to match metaclass --- src/certfuzz/campaign/campaign_linux.py | 15 +++++++++++++++ src/certfuzz/test/campaign/test_campaign_linux.py | 2 ++ 2 files changed, 17 insertions(+) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index a0c31bb..8a3aa09 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -212,6 +212,12 @@ def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): logger.debug('%s was found, not unique', crash_id) return False + def __getstate__(self): + pass + + def __setstate__(self): + pass + def _do_interval(self, s1, s2, first_chunk=False): # interval.go logger.debug('Starting interval %d-%d', s1, s2) @@ -234,6 +240,15 @@ def _do_interval(self, s1, s2, first_chunk=False): rf=sf.rangefinder) as iteration: iteration.go() + def _do_iteration(self): + pass + + def _keep_going(self): + pass + + def _write_version(self): + pass + def go(self): # campaign.go cfg = self.cfg diff --git a/src/certfuzz/test/campaign/test_campaign_linux.py b/src/certfuzz/test/campaign/test_campaign_linux.py index fba8510..9e7b6bc 100644 --- a/src/certfuzz/test/campaign/test_campaign_linux.py +++ b/src/certfuzz/test/campaign/test_campaign_linux.py @@ -4,11 +4,13 @@ @organization: cert.org ''' import unittest +from certfuzz.campaign.campaign_linux import LinuxCampaign class Test(unittest.TestCase): def setUp(self): + self.campaign = LinuxCampaign() pass def tearDown(self): From 220c66e3e71b0a05632d45b47191ed2647aeb192 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 3 Jun 2014 11:25:19 -0400 Subject: [PATCH 0320/1169] rename campaign -> campaign_base, Campaign -> CampaignBase --- .../campaign/{campaign.py => campaign_base.py} | 4 ++-- src/certfuzz/campaign/campaign_windows.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename src/certfuzz/campaign/{campaign.py => campaign_base.py} (99%) diff --git a/src/certfuzz/campaign/campaign.py b/src/certfuzz/campaign/campaign_base.py similarity index 99% rename from src/certfuzz/campaign/campaign.py rename to src/certfuzz/campaign/campaign_base.py index 0353110..6459dde 100644 --- a/src/certfuzz/campaign/campaign.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -36,7 +36,7 @@ } -class Campaign(CampaignMeta): +class CampaignBase(CampaignMeta): ''' Provides a fuzzing campaign object. ''' @@ -45,7 +45,7 @@ def __init__(self, config_file, result_dir=None, campaign_cache=None, ''' Typically one would invoke a campaign as follows: - with Campaign(params) as campaign: + with CampaignBase(params) as campaign: campaign.go() This will ensure that the runtime context is established properly, and diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 05f552f..6b024aa 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -9,16 +9,16 @@ from threading import Timer import platform -from certfuzz.campaign.campaign import Campaign +from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.runners.killableprocess import Popen logger = logging.getLogger(__name__) -class WindowsCampaign(Campaign): +class WindowsCampaign(CampaignBase): ''' - Extends Campaign to add windows-specific features like ButtonClicker + Extends CampaignBase to add windows-specific features like ButtonClicker ''' def __enter__(self): if sys.platform == 'win32': @@ -28,14 +28,14 @@ def __enter__(self): if hook_incompat and self.runner_module_name == 'certfuzz.runners.winrun': logger.debug('winrun is not compatible with Windows %s %s. Overriding.', winver, machine) self.runner_module_name = None - self = Campaign.__enter__(self) + self = CampaignBase.__enter__(self) self._start_buttonclicker() self._cache_app() return self def __exit__(self, etype, value, mytraceback): self._stop_buttonclicker() - return Campaign.__exit__(self, etype, value, mytraceback) + return CampaignBase.__exit__(self, etype, value, mytraceback) def _cache_app(self): logger.debug('Caching application %s and determining if we need to watch the CPU...', self.prog) From ab693533b8f4cc2279fc4db2b1dcccde8a75883b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 08:56:57 -0400 Subject: [PATCH 0321/1169] mixup unit tests --- .../{test_campaign.py => test_campaign_base.py} | 10 +++++----- src/certfuzz/test/campaign/test_campaign_linux.py | 2 +- src/certfuzz/test/test_version.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/certfuzz/test/campaign/{test_campaign.py => test_campaign_base.py} (89%) diff --git a/src/certfuzz/test/campaign/test_campaign.py b/src/certfuzz/test/campaign/test_campaign_base.py similarity index 89% rename from src/certfuzz/test/campaign/test_campaign.py rename to src/certfuzz/test/campaign/test_campaign_base.py index 1795cc3..69868bc 100644 --- a/src/certfuzz/test/campaign/test_campaign.py +++ b/src/certfuzz/test/campaign/test_campaign_base.py @@ -5,7 +5,7 @@ ''' import unittest -from certfuzz.campaign import campaign +import certfuzz.campaign.campaign_base from tempfile import mkstemp, mkdtemp import yaml import shutil @@ -54,7 +54,7 @@ def setUp(self): self.tmpdir = mkdtemp() self.program = mkstemp(dir=self.tmpdir)[1] self._dump_test_config() - self.campaign = campaign.Campaign(self.cfg_file) + self.campaign = certfuzz.campaign.campaign_base.CampaignBase(self.cfg_file) fd, f = tempfile.mkstemp() os.close(fd) @@ -65,11 +65,11 @@ def tearDown(self): shutil.rmtree(self.tmpdir, ignore_errors=True) def test_init(self): - self.assertEqual('%s.%s' % (campaign.packages['fuzzers'], 'fuzzermodule'), + self.assertEqual('%s.%s' % (certfuzz.campaign.campaign_base.packages['fuzzers'], 'fuzzermodule'), self.campaign.fuzzer_module_name) - self.assertEqual('%s.%s' % (campaign.packages['runners'], 'runnermodule'), + self.assertEqual('%s.%s' % (certfuzz.campaign.campaign_base.packages['runners'], 'runnermodule'), self.campaign.runner_module_name) - self.assertEqual('%s.%s' % (campaign.packages['debuggers'], 'debuggermodule'), + self.assertEqual('%s.%s' % (certfuzz.campaign.campaign_base.packages['debuggers'], 'debuggermodule'), self.campaign.debugger_module_name) # def test_getstate(self): diff --git a/src/certfuzz/test/campaign/test_campaign_linux.py b/src/certfuzz/test/campaign/test_campaign_linux.py index 9e7b6bc..9d3ca70 100644 --- a/src/certfuzz/test/campaign/test_campaign_linux.py +++ b/src/certfuzz/test/campaign/test_campaign_linux.py @@ -10,7 +10,7 @@ class Test(unittest.TestCase): def setUp(self): - self.campaign = LinuxCampaign() +# self.campaign = LinuxCampaign() pass def tearDown(self): diff --git a/src/certfuzz/test/test_version.py b/src/certfuzz/test/test_version.py index 24b1f77..c94a119 100644 --- a/src/certfuzz/test/test_version.py +++ b/src/certfuzz/test/test_version.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz import version +import certfuzz.version as version class Test(unittest.TestCase): From 097d0191c91a3c74090c9621fc5984b884e23bd6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 May 2014 15:30:08 -0400 Subject: [PATCH 0322/1169] add test suite line to setup.py this will allow for "python setup.py test" --- src/experimental/android/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/experimental/android/setup.py b/src/experimental/android/setup.py index 21ef1d0..9c1d793 100644 --- a/src/experimental/android/setup.py +++ b/src/experimental/android/setup.py @@ -89,6 +89,7 @@ def _scripts(): scripts=_scripts(), entry_points=_entry_points(), include_package_data=True, + test_suite='certfuzz.test', license='See LICENSE.txt', data_files=[ ('', ['LICENSE.txt']) From e8d97bed825f82f6ceb28fd9df296fe85107e87e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 9 May 2014 09:59:24 -0400 Subject: [PATCH 0323/1169] put nosetests config into the setup.cfg file --- src/setup.cfg | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/setup.cfg diff --git a/src/setup.cfg b/src/setup.cfg new file mode 100644 index 0000000..e108a56 --- /dev/null +++ b/src/setup.cfg @@ -0,0 +1,10 @@ +[nosetests] +ignore-files=android +exclude=test_probability +verbosity=2 +detailed-errors=1 +# with-coverage=1 +# cover-package=certfuzz +# debug=nose.loader +# pdb=1 +# pdb-failures=1 \ No newline at end of file From d472cafcff67346a9dcd8be7caf976ba65bd0970 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 09:38:15 -0400 Subject: [PATCH 0324/1169] put setup.py back where it belongs --- src/{experimental/android => }/setup.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{experimental/android => }/setup.py (100%) diff --git a/src/experimental/android/setup.py b/src/setup.py similarity index 100% rename from src/experimental/android/setup.py rename to src/setup.py From 9d68b2da3f131bb80020913811c6ee437fe4cceb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 09:50:02 -0400 Subject: [PATCH 0325/1169] fix unit test --- .../test/campaign/config/test_bff_config.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/test/campaign/config/test_bff_config.py b/src/certfuzz/test/campaign/config/test_bff_config.py index 9ed8999..f11a468 100644 --- a/src/certfuzz/test/campaign/config/test_bff_config.py +++ b/src/certfuzz/test/campaign/config/test_bff_config.py @@ -10,6 +10,7 @@ from certfuzz.campaign.config.bff_config import MINIMIZED_EXT import tempfile from certfuzz.campaign.config.bff_config import read_config_options +import shutil class Test(unittest.TestCase): @@ -18,6 +19,7 @@ def delete_file(self, f): self.assertFalse(os.path.exists(f)) def setUp(self): + self.tmpdir = tempfile.mkdtemp() # build a config self.config = ConfigParser.RawConfigParser() @@ -71,7 +73,7 @@ def setUp(self): self.cfg = ConfigHelper(self.config) def tearDown(self): - pass + shutil.rmtree(self.tmpdir) def test_init(self): self.assertEqual(self.cfg.killprocname, 'killprocname') @@ -91,11 +93,10 @@ def test_program_is_script(self): pass def test_check_program_file_type(self): - f = os.path.abspath(__file__) - if f.endswith('pyc'): - # trim the last char ('c') - f = f[:-1] - self.cfg.program = f + fd, fname = tempfile.mkstemp(dir=self.tmpdir, text=True) + os.write(fd, 'sometext') + os.close(fd) + self.cfg.program = fname self.assertTrue(self.cfg.program_is_script()) def test_get_minimized_file(self): From 48136d650a7f9dba522c388cf9bd795dc06792d8 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 25 Jun 2014 10:22:14 -0400 Subject: [PATCH 0326/1169] Add --delete to rsync to prevent local BFF build machine changes from affecting the build. --- build/distmods/osx/darwin_build2.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/build/distmods/osx/darwin_build2.py b/build/distmods/osx/darwin_build2.py index 192f6f1..d60a819 100644 --- a/build/distmods/osx/darwin_build2.py +++ b/build/distmods/osx/darwin_build2.py @@ -70,25 +70,6 @@ def __exit__(self, etype, value, traceback): except: print "Failed to remove %s" % self.sparse_image -# def process_args(self): -# ''' -# Process the other arguments passed to the build object -# ''' -# super(self.__class__, self).process_args() -# -# if self.buildtype == 'tag': -# self.dmg_file = '%s-%s.dmg' % (self.PROJECT, self.tag) -# elif self.buildtype == 'branch': -# self.dmg_file_template = '%s-%s-r$SVN_REV.dmg' % (self.PROJECT, self.branch) -# elif self.buildtype == 'trunk': -# self.dmg_file_template = '%s-trunk-r$SVN_REV.dmg' % (self.PROJECT) -# else: -# raise BuildError('Unknown buildtype: %s' % self.buildtype) - -# def export(self): -# # export the linux code -# super(self.__class__, self).export() - def refine(self): Build.refine(self) @@ -198,7 +179,7 @@ def _convert_sparseimage_to_dmg(self): def _sync_dependencies(self): # Retrieve binary dependecies for building OSX installer # TODO: What if rsync fails? - subprocess.call(['rsync', '-EaxSv', self.SHARED_DEPS, self.LOCAL_DEPS]) + subprocess.call(['rsync', '-EaxSv', '--delete', self.SHARED_DEPS, self.LOCAL_DEPS]) subprocess.call(['rsync', '-EaxSv', self.LOCAL_DEPS, self.INSTALLER_BASE]) def package(self): From e42525b656caf88195d2ab18abbb668341664829 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 12:47:36 -0400 Subject: [PATCH 0327/1169] rip out android code The rate of bitrot on the android code is pretty high as we are making sweeping changes to linux and windows code. Since we don't have any plans to maintain it we should remove the android stuff from the main development branch. --- src/certfuzz/android/__init__.py | 1 - src/certfuzz/android/api/__init__.py | 5 - src/certfuzz/android/api/aapt.py | 37 --- src/certfuzz/android/api/activity_manager.py | 140 ----------- src/certfuzz/android/api/adb_cmd.py | 176 -------------- src/certfuzz/android/api/android_cmd.py | 31 --- src/certfuzz/android/api/android_emulator.py | 196 --------------- src/certfuzz/android/api/android_manifest.py | 63 ----- src/certfuzz/android/api/defaults.py | 61 ----- src/certfuzz/android/api/errors.py | 24 -- src/certfuzz/android/api/intent.py | 222 ----------------- src/certfuzz/android/api/log_helper.py | 27 -- src/certfuzz/android/avd_mgr/__init__.py | 5 - src/certfuzz/android/avd_mgr/cloner.py | 194 --------------- src/certfuzz/android/avd_mgr/errors.py | 22 -- src/certfuzz/android/avd_mgr/main.py | 119 --------- .../android/avd_mgr/orphan_catcher.py | 74 ------ src/certfuzz/android/config.yaml | 122 ---------- src/certfuzz/android/controller/__init__.py | 1 - src/certfuzz/android/controller/errors.py | 10 - src/certfuzz/android/errors.py | 9 - src/certfuzz/android/worker/__init__.py | 2 - src/certfuzz/android/worker/errors.py | 10 - src/certfuzz/android/worker/tasks.py | 11 - src/certfuzz/android/worker/worker.py | 138 ----------- src/certfuzz/bff/android.py | 85 ------- src/certfuzz/campaign/campaign_android.py | 230 ------------------ .../campaign/config/android_config.py | 80 ------ src/certfuzz/crash/android_testcase.py | 106 -------- src/certfuzz/db/__init__.py | 1 - src/certfuzz/db/couchdb/__init__.py | 2 - src/certfuzz/db/couchdb/datatypes/__init__.py | 4 - .../db/couchdb/datatypes/campaign_doc.py | 27 -- src/certfuzz/db/couchdb/datatypes/errors.py | 9 - src/certfuzz/db/couchdb/datatypes/file_doc.py | 18 -- .../db/couchdb/datatypes/testcase_doc.py | 20 -- src/certfuzz/db/couchdb/db.py | 200 --------------- src/certfuzz/db/couchdb/errors.py | 12 - src/certfuzz/db/errors.py | 9 - src/certfuzz/iteration/iteration_android.py | 204 ---------------- src/certfuzz/runners/android_runner.py | 151 ------------ src/certfuzz/test/android/__init__.py | 0 src/certfuzz/test/android/api/__init__.py | 0 src/certfuzz/test/android/api/test_aapt.py | 21 -- .../test/android/api/test_activity_manager.py | 22 -- src/certfuzz/test/android/api/test_adb_cmd.py | 22 -- .../test/android/api/test_android_cmd.py | 22 -- .../test/android/api/test_android_emulator.py | 22 -- .../test/android/api/test_android_manifest.py | 21 -- src/certfuzz/test/android/api/test_api_pkg.py | 22 -- .../test/android/api/test_defaults.py | 22 -- src/certfuzz/test/android/api/test_intent.py | 45 ---- .../test/android/api/test_log_helper.py | 22 -- src/certfuzz/test/android/avd_mgr/__init__.py | 0 .../test/android/avd_mgr/test_avd_mgr_pkg.py | 22 -- .../test/android/avd_mgr/test_cloner.py | 175 ------------- .../test/android/avd_mgr/test_main.py | 22 -- .../android/avd_mgr/test_orphan_catcher.py | 22 -- .../test/android/controller/__init__.py | 0 .../android/controller/test_controller_pkg.py | 21 -- src/certfuzz/test/android/test_android_pkg.py | 22 -- src/certfuzz/test/android/worker/__init__.py | 0 .../test/android/worker/test_tasks.py | 22 -- .../test/android/worker/test_worker.py | 22 -- .../test/android/worker/test_worker_pkg.py | 21 -- src/certfuzz/test/bff/test_android.py | 21 -- .../campaign/config/test_android_config.py | 21 -- .../test/campaign/test_campaign_android.py | 22 -- .../test/crash/test_android_testcase.py | 21 -- src/certfuzz/test/db/__init__.py | 0 src/certfuzz/test/db/couchdb/__init__.py | 0 .../test/db/couchdb/datatypes/__init__.py | 0 .../db/couchdb/datatypes/test_campaign_doc.py | 32 --- .../couchdb/datatypes/test_datatypes_pkg.py | 21 -- .../db/couchdb/datatypes/test_file_doc.py | 23 -- .../db/couchdb/datatypes/test_testcase_doc.py | 23 -- .../test/db/couchdb/test_couchdb_pkg.py | 21 -- src/certfuzz/test/db/couchdb/test_db.py | 24 -- src/certfuzz/test/db/test_db_pkg.py | 21 -- .../test/iteration/test_iteration_android.py | 23 -- .../test/runners/test_android_runner.py | 21 -- src/certfuzz/test/tools/android/__init__.py | 0 .../test/tools/android/test_apk_dumper.py | 21 -- .../test/tools/android/test_config_tools.py | 21 -- src/certfuzz/tools/android/__init__.py | 0 src/certfuzz/tools/android/apk_dumper.py | 51 ---- src/certfuzz/tools/android/config_tools.py | 79 ------ src/experimental/README.md | 4 - src/experimental/android/MANIFEST.in | 3 - src/experimental/android/readme.txt | 7 - .../android/refresh-virtualenv.sh | 7 - .../android/scripts/reset_bff_android.sh | 74 ------ .../android/scripts/start_bff_android.sh | 4 - .../scripts/ubufuzz_first_time_setup.sh | 18 -- src/experimental/android/setup-virtualenv.sh | 12 - 95 files changed, 4043 deletions(-) delete mode 100644 src/certfuzz/android/__init__.py delete mode 100644 src/certfuzz/android/api/__init__.py delete mode 100644 src/certfuzz/android/api/aapt.py delete mode 100644 src/certfuzz/android/api/activity_manager.py delete mode 100644 src/certfuzz/android/api/adb_cmd.py delete mode 100644 src/certfuzz/android/api/android_cmd.py delete mode 100644 src/certfuzz/android/api/android_emulator.py delete mode 100644 src/certfuzz/android/api/android_manifest.py delete mode 100644 src/certfuzz/android/api/defaults.py delete mode 100644 src/certfuzz/android/api/errors.py delete mode 100644 src/certfuzz/android/api/intent.py delete mode 100644 src/certfuzz/android/api/log_helper.py delete mode 100644 src/certfuzz/android/avd_mgr/__init__.py delete mode 100644 src/certfuzz/android/avd_mgr/cloner.py delete mode 100644 src/certfuzz/android/avd_mgr/errors.py delete mode 100755 src/certfuzz/android/avd_mgr/main.py delete mode 100644 src/certfuzz/android/avd_mgr/orphan_catcher.py delete mode 100644 src/certfuzz/android/config.yaml delete mode 100644 src/certfuzz/android/controller/__init__.py delete mode 100644 src/certfuzz/android/controller/errors.py delete mode 100644 src/certfuzz/android/errors.py delete mode 100644 src/certfuzz/android/worker/__init__.py delete mode 100644 src/certfuzz/android/worker/errors.py delete mode 100644 src/certfuzz/android/worker/tasks.py delete mode 100644 src/certfuzz/android/worker/worker.py delete mode 100644 src/certfuzz/bff/android.py delete mode 100644 src/certfuzz/campaign/campaign_android.py delete mode 100644 src/certfuzz/campaign/config/android_config.py delete mode 100644 src/certfuzz/crash/android_testcase.py delete mode 100644 src/certfuzz/db/__init__.py delete mode 100644 src/certfuzz/db/couchdb/__init__.py delete mode 100644 src/certfuzz/db/couchdb/datatypes/__init__.py delete mode 100644 src/certfuzz/db/couchdb/datatypes/campaign_doc.py delete mode 100644 src/certfuzz/db/couchdb/datatypes/errors.py delete mode 100644 src/certfuzz/db/couchdb/datatypes/file_doc.py delete mode 100644 src/certfuzz/db/couchdb/datatypes/testcase_doc.py delete mode 100644 src/certfuzz/db/couchdb/db.py delete mode 100644 src/certfuzz/db/couchdb/errors.py delete mode 100644 src/certfuzz/db/errors.py delete mode 100644 src/certfuzz/iteration/iteration_android.py delete mode 100644 src/certfuzz/runners/android_runner.py delete mode 100644 src/certfuzz/test/android/__init__.py delete mode 100644 src/certfuzz/test/android/api/__init__.py delete mode 100644 src/certfuzz/test/android/api/test_aapt.py delete mode 100644 src/certfuzz/test/android/api/test_activity_manager.py delete mode 100644 src/certfuzz/test/android/api/test_adb_cmd.py delete mode 100644 src/certfuzz/test/android/api/test_android_cmd.py delete mode 100644 src/certfuzz/test/android/api/test_android_emulator.py delete mode 100644 src/certfuzz/test/android/api/test_android_manifest.py delete mode 100644 src/certfuzz/test/android/api/test_api_pkg.py delete mode 100644 src/certfuzz/test/android/api/test_defaults.py delete mode 100644 src/certfuzz/test/android/api/test_intent.py delete mode 100644 src/certfuzz/test/android/api/test_log_helper.py delete mode 100644 src/certfuzz/test/android/avd_mgr/__init__.py delete mode 100644 src/certfuzz/test/android/avd_mgr/test_avd_mgr_pkg.py delete mode 100644 src/certfuzz/test/android/avd_mgr/test_cloner.py delete mode 100644 src/certfuzz/test/android/avd_mgr/test_main.py delete mode 100644 src/certfuzz/test/android/avd_mgr/test_orphan_catcher.py delete mode 100644 src/certfuzz/test/android/controller/__init__.py delete mode 100644 src/certfuzz/test/android/controller/test_controller_pkg.py delete mode 100644 src/certfuzz/test/android/test_android_pkg.py delete mode 100644 src/certfuzz/test/android/worker/__init__.py delete mode 100644 src/certfuzz/test/android/worker/test_tasks.py delete mode 100644 src/certfuzz/test/android/worker/test_worker.py delete mode 100644 src/certfuzz/test/android/worker/test_worker_pkg.py delete mode 100644 src/certfuzz/test/bff/test_android.py delete mode 100644 src/certfuzz/test/campaign/config/test_android_config.py delete mode 100644 src/certfuzz/test/campaign/test_campaign_android.py delete mode 100644 src/certfuzz/test/crash/test_android_testcase.py delete mode 100644 src/certfuzz/test/db/__init__.py delete mode 100644 src/certfuzz/test/db/couchdb/__init__.py delete mode 100644 src/certfuzz/test/db/couchdb/datatypes/__init__.py delete mode 100644 src/certfuzz/test/db/couchdb/datatypes/test_campaign_doc.py delete mode 100644 src/certfuzz/test/db/couchdb/datatypes/test_datatypes_pkg.py delete mode 100644 src/certfuzz/test/db/couchdb/datatypes/test_file_doc.py delete mode 100644 src/certfuzz/test/db/couchdb/datatypes/test_testcase_doc.py delete mode 100644 src/certfuzz/test/db/couchdb/test_couchdb_pkg.py delete mode 100644 src/certfuzz/test/db/couchdb/test_db.py delete mode 100644 src/certfuzz/test/db/test_db_pkg.py delete mode 100644 src/certfuzz/test/iteration/test_iteration_android.py delete mode 100644 src/certfuzz/test/runners/test_android_runner.py delete mode 100644 src/certfuzz/test/tools/android/__init__.py delete mode 100644 src/certfuzz/test/tools/android/test_apk_dumper.py delete mode 100644 src/certfuzz/test/tools/android/test_config_tools.py delete mode 100644 src/certfuzz/tools/android/__init__.py delete mode 100644 src/certfuzz/tools/android/apk_dumper.py delete mode 100644 src/certfuzz/tools/android/config_tools.py delete mode 100644 src/experimental/android/MANIFEST.in delete mode 100644 src/experimental/android/readme.txt delete mode 100755 src/experimental/android/refresh-virtualenv.sh delete mode 100755 src/experimental/android/scripts/reset_bff_android.sh delete mode 100755 src/experimental/android/scripts/start_bff_android.sh delete mode 100755 src/experimental/android/scripts/ubufuzz_first_time_setup.sh delete mode 100755 src/experimental/android/setup-virtualenv.sh diff --git a/src/certfuzz/android/__init__.py b/src/certfuzz/android/__init__.py deleted file mode 100644 index c99f543..0000000 --- a/src/certfuzz/android/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .errors import AndroidError diff --git a/src/certfuzz/android/api/__init__.py b/src/certfuzz/android/api/__init__.py deleted file mode 100644 index e9ed90e..0000000 --- a/src/certfuzz/android/api/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .adb_cmd import AdbCmd -from .log_helper import pfunc -from .errors import Android_API_Error -from .errors import AdbCmdError, AndroidCmdError, AndroidEmulatorError -from .android_emulator import AndroidEmulator diff --git a/src/certfuzz/android/api/aapt.py b/src/certfuzz/android/api/aapt.py deleted file mode 100644 index 442456f..0000000 --- a/src/certfuzz/android/api/aapt.py +++ /dev/null @@ -1,37 +0,0 @@ -''' -Created on Feb 28, 2013 - -@organization: cert.org -''' -import logging - -from certfuzz.android.api.defaults import sdk_platform_tool -from certfuzz.android.api.errors import AaptError -from certfuzz.fuzztools.command_line_callable import CommandLineCallable - - -logger = logging.getLogger(__name__) - - -class Aapt(CommandLineCallable): - def __init__(self, sdk_path='~/android-sdk'): - CommandLineCallable.__init__(self, ignore_result=False) - self.arg_pfx = [sdk_platform_tool('aapt')] - - def dump(self, what, path_to_apk, assets=None, values=False): - args = [] - if values: - args.append('--values') - if not what in ['badging', 'permissions', 'resources', - 'configurations', 'xmltree', 'xmlstrings']: - raise AaptError('Unknown dump option: %s', what) - args.append('d') - args.append(what) - args.append(path_to_apk) - if assets: - args.extend(assets) - self.call(args) - - def get_manifest(self, path_to_apk): - logger.debug('get manifest from %s', path_to_apk) - self.dump(what='xmltree', path_to_apk=path_to_apk, assets=['AndroidManifest.xml']) diff --git a/src/certfuzz/android/api/activity_manager.py b/src/certfuzz/android/api/activity_manager.py deleted file mode 100644 index 92b62fa..0000000 --- a/src/certfuzz/android/api/activity_manager.py +++ /dev/null @@ -1,140 +0,0 @@ -''' -Created on Jan 25, 2013 - -@organization: cert.org -''' -from certfuzz.android.api.adb_cmd import AdbCmd -from certfuzz.android.api.errors import ActivityManagerError - - -def am(handle, args): - ActivityManager(handle).go(args) - - -class ActivityManager(object): - ''' - classdocs - ''' - def __init__(self, handle=None): - ''' - Constructor - ''' - self.handle = handle - self.result = None - - def go(self, args): - arg_pfx = ['am'] - self.result = AdbCmd(self.handle).shell(arg_pfx + args) - - def start(self, intent, debug=False, wait_for_launch=False, profiler=None, - profile_not_idle=None, repeat=None, force_stop=False, - opengl_trace=False): - if not intent: - raise ActivityManagerError('Intent not specified') - - args = ['start'] - if debug: - args.append('-D') - if wait_for_launch: - args.append('-W') - if profiler: - args.extend(['--start-profiler', profiler]) - if profile_not_idle: - args.extend(['-P', profile_not_idle]) - if repeat: - args.extend(['--R', repeat]) - if force_stop: - args.append('-S') - if opengl_trace: - args.append('--opengl-trace') - - args.extend(intent.as_args()) - self.go(args) - - def startservice(self, intent): - self.go(['startservice', intent]) - - def force_stop(self, package): - self.go(['force-stop', package]) - - def kill(self, package): - self.go(['kill', package]) - # check self.result.stdout or self.result.stderr here - - def kill_all(self): - self.go(['kill-all']) - - def broadcast(self, intent): - self.go(['broadcast', intent]) - - def instrument(self, component, *options): - raise NotImplementedError - - def profile_start(self, process, filepath): -# am profile start - self.go(['profile', 'start', process, filepath]) - - def profile_stop(self, process): -# am profile stop [] - self.go(['profile', 'stop', process]) - - def dumpheap(self, process, filepath, flags=None): -# am dumpheap [flags] - args = ['dumpheap'] - for flag in flags: - args.append(flag) - args.extend([process, filepath]) - self.go(args) - - def set_debug_app(self, package, w=False, persistent=False): -# am set-debug-app [-w] [--persistent] - args = ['set-debug-app'] - if w: - args.append('-w') - if persistent: - args.append('--persistent') - args.append(package) - self.go(args) - - def clear_debug_app(self): -# am clear-debug-app - self.go(['clear-debug-app']) - - def monitor(self, gdb_port=None): -# am monitor [--gdb ] - args = ['monitor'] - if gdb_port: - args.extend(['--gdb', gdb_port]) - self.go(args) - - def screen_compat(self, package, on=True,): -# am screen-compat [on|off] - args = ['screen-compat'] - if on: - args.append('on') - else: - args.append('off') - args.append(package) - - def display_size(self, reset=False, m=None, n=None): -# am display-size [reset|MxN] - args = ['display-size'] - if reset: - if m or n: - raise ActivityManagerError("Can't use both reset and MxN") - args.append('reset') - elif m: - if not n: - raise ActivityManagerError("Must specify both M and N") - args.append('%dx%d' % (m, n)) - else: - raise ActivityManagerError("specify either reset or m,n pair") - self.go(args) - - def to_uri(self, intent): - # am to-uri [INTENT] - self.go(['to-uri', intent]) - - def to_intent_uri(self, intent): - # am to-intent-uri [INTENT] - self.go(['to-intent-uri', intent]) diff --git a/src/certfuzz/android/api/adb_cmd.py b/src/certfuzz/android/api/adb_cmd.py deleted file mode 100644 index 865c51b..0000000 --- a/src/certfuzz/android/api/adb_cmd.py +++ /dev/null @@ -1,176 +0,0 @@ -''' -Created on Jan 4, 2013 - -@organization: cert.org -''' -import functools -import logging -import signal -import subprocess - -from certfuzz.android.api.defaults import sdk_platform_tool -from certfuzz.android.api.errors import AdbCmdError -from certfuzz.android.api.log_helper import pfunc -from certfuzz.fuzztools.command_line_callable import CommandLineCallable - - -adb = sdk_platform_tool('adb') - -logger = logging.getLogger(__name__) - - -def _terminate_and_raise(p, signum, frame): - ''' - signal handler for use when a process p times out - :param p: - :param signum: - :param frame: - ''' - p.terminate() - raise AdbCmdError() - - -class AdbCmd(CommandLineCallable): - @pfunc(logger=logger) - def __init__(self, handle=None): - CommandLineCallable.__init__(self, ignore_result=False) - - self.handle = None - if handle is not None: - self.handle = handle.strip() # make sure there's no newlines - arg_pfx = [adb] - if self.handle: - arg_pfx.extend(['-s', self.handle]) - self.arg_pfx = arg_pfx - - @pfunc(logger=logger) - def __enter__(self): - return self - - @pfunc(logger=logger) - def __exit__(self, etype, evalue, traceback): - if etype is AdbCmdError: - logger.warning('Caught AdbCmdError: %s', evalue) - - @pfunc(logger=logger) - def bugreport(self): - self.call(['bugreport']) - - @pfunc(logger=logger) - def devices(self): - self.call(['devices']) - devices = {} - for line in self.stdout.splitlines()[1:]: - chunks = line.split() - if len(chunks) == 2: - handle, state = chunks - devices[handle] = state - return devices - - @pfunc(logger=logger) - def emu_kill(self): - self.call(['emu', 'kill']) - if self.stderr: - raise AdbCmdError(self.stderr) - - @pfunc(logger=logger) - def get_serialno(self): - self.call('get-serialno') - - @pfunc(logger=logger) - def get_state(self): - self.call('get-state') - - @pfunc(logger=logger) - def install(self, path_to_apk): - self.call(['install', path_to_apk]) - - @pfunc(logger=logger) - def reinstall(self, path_to_apk): - self.call(['install', 'r', path_to_apk]) - - @pfunc(logger=logger) - def jdwp(self): - self.call(['jdwp']) - - @pfunc(logger=logger) - def kill_server(self): - self.call(['kill-server']) - - @pfunc(logger=logger) - def pull(self, remote, local): - self.call(['pull', remote, local]) - if self.stderr: - # normal result looks like - # 29 KB/s (10000 bytes in 0.332s) - if 'bytes in' in self.stderr: - logger.info(self.stderr.strip()) - else: - raise AdbCmdError(self.stderr) - - @pfunc(logger=logger) - def push(self, local, remote): - self.call(['push', local, remote]) - if self.stderr: - # normal result looks like - # 858 KB/s (10000 bytes in 0.011s) - if 'bytes in' in self.stderr: - logger.info(self.stderr.strip()) - else: - raise AdbCmdError(self.stderr) - - @pfunc(logger=logger) - def restart(self): - self.kill_server() - self.start_server() - - @pfunc(logger=logger) - def shell(self, args): - _args = ['shell'] - _args.extend(args) - self.call(_args) - logger.debug(self.stdout) - - @pfunc(logger=logger) - def clear_logs(self): - args = ['logcat', '-c'] - self.shell(args) - - @pfunc(logger=logger) - def start_server(self): - self.call(['start-server']) - - @pfunc(logger=logger) - def wait_for_device(self): - logger.info('Waiting for device %s', self.handle) - self.call(['wait-for-device']) - - @pfunc(logger=logger) - def wait_for_sdcard(self, sdcart_timeout=600): - # TODO: make sure the sdcard isn't already mounted and ready - logger.info('Waiting for sdcard to become available on %s', self.handle) - - args = self.arg_pfx + ['logcat', '-s', 'StorageNotification:I'] - - logger.debug(' '.join(args)) - p = subprocess.Popen(args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - alarm_handler = functools.partial(_terminate_and_raise, p) - signal.signal(signal.SIGALRM, alarm_handler) - signal.alarm(sdcart_timeout) - - # p.poll() returns None while p is still running. - while p.poll() is None: - while True: - line = p.stdout.readline().strip() - logger.debug('adb log: %s', line) - # we are watching for a log message like: - # I/StorageNotification( 256): Media {/mnt/sdcard} - # state changed from {checking} -> {mounted} - if line.endswith('{mounted}'): - # got a a match! - p.terminate() - signal.alarm(0) # unset alarm - return diff --git a/src/certfuzz/android/api/android_cmd.py b/src/certfuzz/android/api/android_cmd.py deleted file mode 100644 index 7887559..0000000 --- a/src/certfuzz/android/api/android_cmd.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Created on Jan 4, 2013 - -@organization: cert.org -''' -import logging -from certfuzz.android.api.log_helper import pfunc -from certfuzz.android.api.errors import AndroidCmdError -from certfuzz.fuzztools.command_line_callable import CommandLineCallable -from certfuzz.android.api.defaults import sdk_tool - -android = sdk_tool('android') - -logger = logging.getLogger(__name__) - - -class AndroidCmd(CommandLineCallable): - @pfunc(logger=logger) - def __init__(self): - CommandLineCallable.__init__(self, ignore_result=False) - self.arg_pfx = [android] - - @pfunc(logger=logger) - def delete(self, avd_name=None): - logger.debug('Deleting avd %s', avd_name) - if not avd_name: - raise AndroidCmdError('avd_name must be specified') - args = ['delete', 'avd', '--name', avd_name] - self.call(args) - for line in self.stdout.splitlines(): - logger.info(line) diff --git a/src/certfuzz/android/api/android_emulator.py b/src/certfuzz/android/api/android_emulator.py deleted file mode 100644 index 595d469..0000000 --- a/src/certfuzz/android/api/android_emulator.py +++ /dev/null @@ -1,196 +0,0 @@ -''' -Created on Jan 4, 2013 - -@organization: cert.org -''' -import logging -import os -import socket -import subprocess -import time - -from certfuzz.android.api.adb_cmd import AdbCmd, AdbCmdError -from certfuzz.android.api.android_cmd import AndroidCmd -from certfuzz.android.api.defaults import inifile, avddir, sdk_tool -from certfuzz.android.api.errors import AndroidEmulatorError -from certfuzz.android.api.log_helper import pfunc - - -# from .defaults import TIMERS -emulator = sdk_tool('emulator') - -# string formatters -avddir_basename = '{}.avd'.format -inifile_basename = '{}.ini'.format -socket_str = 'tcp:{:d}'.format -emu_handle = 'emulator-{:d}'.format - -logger = logging.getLogger(__name__) - - -class AndroidEmulator(object): - @pfunc(logger=logger) - def __init__(self, emu_opts, no_window=False): - self.avd = emu_opts['avd_name'] - self.avd_home = emu_opts['avd_home'] - self.timers = emu_opts['timers'] - - self.no_window = no_window - - self.handle = None - self.socket = None - self.port = None - self.args = None - - self._started = False - self._ready = False - - self.child_proc = None - self.destroy_on_kill = False - - @pfunc(logger=logger) - def __enter__(self): - logger.debug('Enter %s runtime context', self.__class__.__name__) - return self - - @pfunc(logger=logger) - def __exit__(self, etype, evalue, traceback): - logger.debug('Exit %s runtime context', self.__class__.__name__) - - if etype is not None: - logger.info('Caught exception %s: %s', etype.__name__, evalue) - logger.debug('Attempting to kill %s', self.handle) - try: - self.kill() - except AndroidEmulatorError as e: - logger.warning('Emulator kill failed: %s', e) - # no return value since we want to allow upstream contexts - # to do their thing if they so desire - - @pfunc(logger=logger) - def _bind_port(self): - # create socket - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(('127.0.0.1', 0)) - self.socket = s - self.port = self.socket.getsockname()[1] - logger.debug('listen on port %d', self.port) - - @pfunc(logger=logger) - def _construct_args(self): - args = [emulator, - '-no-boot-anim', - ] - if self.no_window: - args.append('-no-window') - if self.port: - args.extend(['-report-console', socket_str(self.port)]) - - args.extend(['-avd', self.avd]) - - self.args = args - logger.debug('args: %s', args) - - def check_child_is_running(self): - if self.child_proc.poll() is not None: - # child proc exited, time to die - raise AndroidEmulatorError('Child process terminated') - - @pfunc(logger=logger) - def _start(self): - # get a port number for the emulator to report back to - self._bind_port() - self._construct_args() - self.child_proc = subprocess.Popen(self.args) - self._get_handle() - - def _check_for_kill(self): - # killed handle should be gone from device list - expire = time.time() + self.timers['kill_timeout'] - while time.time() <= expire: - if self.handle in AdbCmd().devices().keys(): - time_remaining = expire - time.time() - logger.debug('%s still in device list, expires in %d', - self.handle, time_remaining) - time.sleep(self.timers['device_retry']) - else: - # it's gone, get out of here - return - else: - # if you got here, your timer expired and it's still in the list - raise AndroidEmulatorError('Failed to kill emulator %s' - % self.handle) - - @pfunc(logger=logger) - def kill(self): - if not self.handle: - raise AndroidEmulatorError('emu kill called when handle is undefined') - - try: - AdbCmd(self.handle).emu_kill() - except AdbCmdError as e: - logger.info('AdbCmdError: %s', e) - - self._check_for_kill() - - if self.destroy_on_kill: - self.delete() - - @pfunc(logger=logger) - def delete(self): - if not self.avd: - raise AndroidEmulatorError('android delete called when avd undefined') - - AndroidCmd().delete(self.avd) - - # check to see if ini file and avd dir have been removed - for f in (inifile(self.handle), avddir(self.handle)): - if os.path.exists(f): - raise AndroidEmulatorError('failed to remove %s' % f) - - @pfunc(logger=logger) - def start(self): - if not self.avd: - raise AndroidEmulatorError('Android Virtual Device not specified') - - logger.info('Starting emulator %s', self.avd) - self._start() - self._started = True - AdbCmd().restart() - AdbCmd(self.handle).wait_for_device() - self._ready = True - - @pfunc(logger=logger) - def _wait_for_handle(self): - expire = time.time() + self.timers['handle_timeout'] - while time.time() <= expire: - known_handles = AdbCmd().devices().keys() - logger.debug('known emulator handles: %s', known_handles) - if self.handle in known_handles: - logger.debug('found handle in list') - return - else: - time_left = int(expire - time.time()) - logger.debug('waiting for device, expire in %d', time_left) - time.sleep(self.timers['device_retry']) - AdbCmd().restart() - else: - raise AndroidEmulatorError('Handle [{}] not in device list'.format(self.handle)) - - @pfunc(logger=logger) - def _get_handle(self): - logger.debug('listen to socket %s', self.socket) - self.socket.listen(1) - conn, addr = self.socket.accept() - data = conn.recv(32) - logger.debug('read data %s', data) - conn.close() - self.socket.close() - logger.debug('socket closed') - self.socket = None - - emu_port = int(data) - self.handle = emu_handle(emu_port) - - logger.debug('Received emulator handle: %s', self.handle) - self._wait_for_handle() diff --git a/src/certfuzz/android/api/android_manifest.py b/src/certfuzz/android/api/android_manifest.py deleted file mode 100644 index de27001..0000000 --- a/src/certfuzz/android/api/android_manifest.py +++ /dev/null @@ -1,63 +0,0 @@ -''' -Created on Mar 1, 2013 - -@organization: cert.org -''' -import logging -import re - -logger = logging.getLogger(__name__) - -regex = { - 'mimetype': re.compile('A: android:mimeType[^=]*="([^"]+)"'), - 'package': re.compile('A: package[^=]*="([^"]+)"'), - 'version': re.compile('A: android:versionName[^=]*="([^"]+)"') - } -class AndroidManifest(object): - def __init__(self, text=None): - self.lines = text.splitlines() - self.parsed = {} - self.parsed['mimetypes'] = set() - - self.callbacks = [self._get_mimetypes, - self._log_line, - self._get_package, - self._get_version, - ] - self._parse() - - @property - def version_info(self): - return '{} {}'.format(self.parsed['package'], self.parsed['version']) - - @property - def mimetypes(self): - return self.parsed['mimetypes'] - - def _get_mimetypes(self, line): - m = re.match(regex['mimetype'], line) - if m: - self.parsed['mimetypes'].add(m.group(1)) - - def _get_package(self, line): - m = re.match(regex['package'], line) - if m: - self.parsed['package'] = m.group(1) - self.callbacks.remove(self._get_package) - - def _get_version(self, line): - m = re.match(regex['version'], line) - if m: - self.parsed['version'] = m.group(1) - self.callbacks.remove(self._get_version) - - - def _log_line(self, line): - logger.debug(line) - - def _parse(self): - - for line in self.lines: - l = line.strip() - for callback in self.callbacks: - callback(l) diff --git a/src/certfuzz/android/api/defaults.py b/src/certfuzz/android/api/defaults.py deleted file mode 100644 index ac8fe0a..0000000 --- a/src/certfuzz/android/api/defaults.py +++ /dev/null @@ -1,61 +0,0 @@ -''' -Created on Jan 14, 2013 - -@organization: cert.org -''' -import os -from functools import partial -from certfuzz.android.api.errors import Android_API_Error - - -_defaults = {'sdk_home': os.path.abspath(os.path.expanduser('~/android-sdk')), - 'avd_home': os.path.abspath(os.path.expanduser('~/.android/avd')) - } - -# set env vars to check (overrides config.yaml) -_env_key = lambda x: 'ANDROID_%s' % x.upper() -for key in ['sdk_home', 'avd_home']: - _defaults[key] = os.getenv(_env_key(key), _defaults[key]) - -# convert to expanded paths -_defaults['sdk_home'] = os.path.expanduser(_defaults['sdk_home']) -_defaults['avd_home'] = os.path.expanduser(_defaults['avd_home']) - -if not os.path.isdir(_defaults['sdk_home']): - raise Android_API_Error('No Android SDK found at %s, ' - 'try setting %s environment var' - % (_defaults['sdk_home'], _env_key('sdk_home'))) - -_sdk_relpath = partial(os.path.join, _defaults['sdk_home']) - -# usage: sdk_tool('android'), sdk_platform_tool('adb') etc. -sdk_tool = partial(_sdk_relpath, 'tools') -sdk_platform_tool = partial(_sdk_relpath, 'platform-tools') - -if not os.path.isdir(sdk_tool()): - raise Android_API_Error('SDK tool dir not found at %s. ' - 'Is %s really an Android SDK dir?' % (sdk_tool(), - _defaults['sdk_home'])) -if not os.path.isdir(sdk_platform_tool()): - raise Android_API_Error('SDK platform tool dir not found at %s. ' - 'Is %s really an Android SDK dir?' % (sdk_platform_tool(), - _defaults['sdk_home'])) - -if not os.path.isdir(_defaults['avd_home']): - raise Android_API_Error('No Android AVD dir found at %s,' - 'try setting %s environment var' - % (_defaults['avd_home'], _env_key('avd_home'))) -AVD_HOME = _defaults['avd_home'] - -# string formatters -avddir_basename = '{}.avd'.format -inifile_basename = '{}.ini'.format -avd_home = lambda x: os.path.join(_defaults['avd_home'], x) - -# convenience functions for fullpath versions -avddir = lambda x: avd_home(avddir_basename(x)) -inifile = lambda x: avd_home(inifile_basename(x)) - -# TIMERS = {} -# for k, v in _defaults['timers'].iteritems(): -# TIMERS[k.upper()] = v diff --git a/src/certfuzz/android/api/errors.py b/src/certfuzz/android/api/errors.py deleted file mode 100644 index cfc4af2..0000000 --- a/src/certfuzz/android/api/errors.py +++ /dev/null @@ -1,24 +0,0 @@ -''' -Created on Jan 15, 2013 - -@organization: cert.org -''' -from certfuzz.android import AndroidError - -class Android_API_Error(AndroidError): - pass - -class ActivityManagerError(Android_API_Error): - pass - -class AdbCmdError(Android_API_Error): - pass - -class AndroidCmdError(Android_API_Error): - pass - -class AndroidEmulatorError(Android_API_Error): - pass - -class AaptError(Android_API_Error): - pass diff --git a/src/certfuzz/android/api/intent.py b/src/certfuzz/android/api/intent.py deleted file mode 100644 index 52fbc84..0000000 --- a/src/certfuzz/android/api/intent.py +++ /dev/null @@ -1,222 +0,0 @@ -''' -Created on Feb 1, 2013 - -@organization: cert.org -''' -import logging - -logger = logging.getLogger(__name__) - -_bool_attrs = ["grant_read_uri_permission", - "grant_write_uri_permission", "debug_log_resolution", - "exclude_stopped_packages", "include_stopped_packages", - "activity_brought_to_front", "activity_clear_top", - "activity_clear_when_task_reset", - "activity_exclude_from_recents", - "activity_launched_from_history", - "activity_multiple_task", "activity_no_animation", - "activity_no_history", "activity_no_user_action", - "activity_previous_is_top", "activity_reorder_to_front", - "activity_reset_task_if_needed", "activity_single_top", - "activity_clear_task", "activity_task_on_home", - "receiver_registered_only", "receiver_replace_pending", - "selector", - ] -_str_attrs = ["action", "data_uri", "mime_type", "component", "uri", "package"] -_list_attrs = ["categories", "extras", "flags"] -_dict_attrs = ["extra_strings", "extra_booleans", "extra_ints", "extra_longs", - "extra_floats", "extra_uris", "extra_components", - "extra_int_array", "extra_long_array", "extra_float_array"] -_loadable_attrs = _bool_attrs + _str_attrs + _list_attrs + _dict_attrs - -def _attribute_to_option(a): - # 'foo_bar_baz' -> '--foo-bar-baz' - return '--%s' % a.replace('_', '-') - -class Intent(object): - ''' - Data object for constructing Intents (for use with activity manager) - ''' -#============================================================================== -# specifications include these flags and arguments: -# [-a ] [-d ] [-t ] - action = None - data_uri = None - mime_type = None -# [-c [-c ] ...] - categories = [] -# [-e|--es ...] - extra_strings = {} - -# [--esn ...] - extras = [] - -# [--ez ...] - extra_booleans = {} - -# [--ei ...] - extra_ints = {} - -# [--el ...] - extra_longs = {} - -# [--ef ...] - extra_floats = {} - -# [--eu ...] - extra_uris = {} - -# [--ecn ] - extra_components = {} - -# [--eia [, [, [,] [-f ] - component = None - flags = [] - -# [--grant-read-uri-permission] [--grant-write-uri-permission] - grant_read_uri_permission = False - grant_write_uri_permission = False -# [--debug-log-resolution] [--exclude-stopped-packages] - debug_log_resolution = False - exclude_stopped_packages = False -# [--include-stopped-packages] - include_stopped_packages = False -# [--activity-brought-to-front] [--activity-clear-top] - activity_brought_to_front = False - activity_clear_top = False -# [--activity-clear-when-task-reset] [--activity-exclude-from-recents] - activity_clear_when_task_reset = False - activity_exclude_from_recents = False -# [--activity-launched-from-history] [--activity-multiple-task] - activity_launched_from_history = False - activity_multiple_task = False -# [--activity-no-animation] [--activity-no-history] - activity_no_animation = False - activity_no_history = False -# [--activity-no-user-action] [--activity-previous-is-top] - activity_no_user_action = False - activity_previous_is_top = False -# [--activity-reorder-to-front] [--activity-reset-task-if-needed] - activity_reorder_to_front = False - activity_reset_task_if_needed = False -# [--activity-single-top] [--activity-clear-task] - activity_single_top = False - activity_clear_task = False -# [--activity-task-on-home] - activity_task_on_home = False -# [--receiver-registered-only] [--receiver-replace-pending] - receiver_registered_only = False - receiver_replace_pending = False -# [--selector] - selector = False -# [ | | ] - uri = None - package = None -# component = None -#============================================================================== - - def load_yaml(self, yaml_path): - ''' - Attempts to load data from a yaml file. - - :param yaml_path: - ''' - import yaml - try: - with open(yaml_path, 'r') as f: - d = yaml.safe_load(f.read()) - loaded_intent = d['intent'] - except IOError as e: - logger.warning('Unable to open %s: %s', yaml_path, e) - raise - except KeyError as e: - logger.warning('No intent found in %s: %s', yaml_path, e) - raise - - for k in _loadable_attrs: - try: - setattr(self, k, loaded_intent[k]) - except KeyError, e: - # missing attributes are ok - pass - - def as_args(self): - parts = [] - if self.action: - parts.extend(['-a', self.action]) - - if self.data_uri: - parts.extend(['-d', self.data_uri]) - - if self.mime_type: - parts.extend(['-t', self.mime_type]) - - def _l2list(pfx, inlist): - l = [] - for x in inlist: - l.extend([pfx, x]) - return l - - parts.extend(_l2list('-c', self.categories)) - - def _d2list(pfx, d): - l = [] - for k, v in d.iteritems(): - l.extend([pfx, k, str(v)]) - return l - - parts.extend(_d2list('--es', self.extra_strings)) - - parts.extend(_l2list('--esn', self.extras)) - - parts.extend(_d2list('--ez', self.extra_booleans)) - parts.extend(_d2list('--ei', self.extra_ints)) - parts.extend(_d2list('--el', self.extra_longs)) - parts.extend(_d2list('--ef', self.extra_floats)) - parts.extend(_d2list('--eu', self.extra_uris)) - parts.extend(_d2list('--ecn', self.extra_components)) - - def _darray2list(pfx, d): - l = [] - for k, v in d.iteritems(): - l.extend(pfx, k) - l.append(', '.join([str(x) for x in v])) - return l - - parts.extend(_darray2list('--eia', self.extra_int_array)) - parts.extend(_darray2list('--ela', self.extra_long_array)) - parts.extend(_darray2list('--efa', self.extra_float_array)) - - if self.component: - parts.extend(['-n', self.component]) - - if self.flags: - parts.extend(['-f', self.flags]) - - for a in _bool_attrs: - if hasattr(self, a): - val = getattr(self, a) - if val: - a_str = _attribute_to_option(a) - parts.append(a_str) - - if self.uri: - parts.append(self.uri) - elif self.package: - parts.append(self.package) -# elif self.component: -# parts.append(self.component) - - return parts - - def __repr__(self, *args, **kwargs): - return ' '.join(self.as_args()) diff --git a/src/certfuzz/android/api/log_helper.py b/src/certfuzz/android/api/log_helper.py deleted file mode 100644 index f8d9ddc..0000000 --- a/src/certfuzz/android/api/log_helper.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Created on Jan 14, 2013 - -@organization: cert.org -''' -import logging -import os - -_logger = logging.getLogger(__name__) - -def log_formatter(): - log_format = '%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s' - formatter = logging.Formatter(log_format) - return formatter - -def pfunc(logger=None): - if logger is None: - logger = _logger - - def real_decorator(function): - def wrapper(*args, **kwargs): - params = [str(x) for x in args] - params.extend(['{}={}'.format(k, v) for k, v in kwargs.iteritems()]) - logger.debug('%d %s(%s)', os.getpid(), function.__name__, ', '.join(params)) - return function(*args, **kwargs) - return wrapper - return real_decorator diff --git a/src/certfuzz/android/avd_mgr/__init__.py b/src/certfuzz/android/avd_mgr/__init__.py deleted file mode 100644 index a4414dd..0000000 --- a/src/certfuzz/android/avd_mgr/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .errors import AndroidEmulatorManagerError, AvdMgrError -from .errors import AvdClonerError, OrphanedProcessError -from .cloner import AvdCloner, clone_avd -from .orphan_catcher import OrphanCatcher -from ..api import AndroidEmulator, AndroidEmulatorError diff --git a/src/certfuzz/android/avd_mgr/cloner.py b/src/certfuzz/android/avd_mgr/cloner.py deleted file mode 100644 index 2aa8c5d..0000000 --- a/src/certfuzz/android/avd_mgr/cloner.py +++ /dev/null @@ -1,194 +0,0 @@ -''' -Created on Jan 4, 2013 - -@organization: cert.org -''' -import logging -import os -import shutil -import tempfile -import uuid - -from certfuzz.android.api.android_cmd import AndroidCmd -from certfuzz.android.api.defaults import inifile, avddir -from certfuzz.android.avd_mgr.errors import AvdClonerError - - -#from certfuzz.helpers import random_str -logger = logging.getLogger(__name__) -clone_name = '{}-clone-{}'.format - - -def clone_avd(src=None, dst=None, remove=False): - ''' - Given the name of an Android Virtual Device, clone it and return the - name of the new clone. - - :param src: - :param dst: - ''' - - with AvdCloner(src=src, dst=dst, remove=remove) as cloner: - cloner.clone() - return cloner.dst - - -class AvdCloner(object): - ''' - Provides ability to clone an Android Virtual Device. - ''' - def __init__(self, src=None, dst=None, remove=True): - self.src = src - self.remove = remove - if dst is not None: - self.dst = dst - else: - # TODO: Reconsider how to generate unique clone names. This - # is simply a trivial method, but may not be ideal. This - # was used in place of certfuzz.helpers.misc.random_str() - # because that method was returning the same string for - # each AvdCloner within a process. - uniq_id = str(uuid.uuid4()) - self.dst = clone_name(src, uniq_id[0:15]) - self._removables = [] - - self._src_avddir = None - self._dst_avddir = None - self._src_inifile = None - self._dst_inifile = None - - def __enter__(self): - if self.src is None: - raise AvdClonerError('src avd not specified') - - self._set_paths() - - return self - - def __exit__(self, etype, value, traceback): - if etype is not None: - logger.debug('caught %s: %s', etype, value) - - if etype is shutil.Error: - raise AvdClonerError(value) - - if self.remove: - self._remove() - - def _remove(self): - ''' - Removes the cloned avd dir and ini file - ''' - AndroidCmd().delete(self.dst) - - # forcibly remove whatever the above missed - for path in self._removables: - if os.path.isdir(path): - shutil.rmtree(path) - else: - try: - os.remove(path) - except OSError as e: - if not os.path.exists(path): - # remove failed, but the file is gone anyway so it's ok - pass - else: - raise e - - def _set_paths(self): - if not self.src: - raise AvdClonerError('Cannot set paths when src is not set') - self._src_avddir = avddir(self.src) - self._src_inifile = inifile(self.src) - - if not self.dst: - raise AvdClonerError('Cannot set paths when dst is not set') - self._dst_avddir = avddir(self.dst) - self._dst_inifile = inifile(self.dst) - - def _clone_avd_dir(self): - ''' - Copies the avd dir - :param src: - :param dst: - ''' - s = self._src_avddir - d = self._dst_avddir - logger.debug('Copy %s to %s', s, d) - shutil.copytree(s, d) - self._removables.append(d) - - def _clone_ini_file(self): - s = self._src_inifile - d = self._dst_inifile - logger.debug('Copy %s to %s', s, d) - shutil.copy(s, d) - self._removables.append(d) - - def _fix_ini(self): - ''' - Replaces the path contained in $AVD_HOME/.ini with $AVD_HOME/.avd - :param dst: - ''' - cfgpath = self._dst_inifile - logger.debug('Fixing %s', cfgpath) - with open(cfgpath, 'rb') as infp: - outfp, outname = tempfile.mkstemp(suffix='.ini', dir=self._dst_avddir, text=True) - for line in infp.readlines(): - if line.startswith('path='): - # replace path line - newpath = 'path={}\n'.format(self._dst_avddir) - logger.debug('...replacing %s', line.strip()) - logger.debug('...with %s', newpath.strip()) - os.write(outfp, newpath) - else: - os.write(outfp, line) - os.close(outfp) - logger.debug('Move %s -> %s', outname, cfgpath) - shutil.move(outname, cfgpath) - - def clone(self): - ''' - Clone the avd from src->dst. Hint: use in a - with AvdCloner(...) as c: - c.clone() - ''' - logger.debug('clone {} -> {}'.format(self.src, self.dst)) - self._clone_avd_dir() - self._clone_ini_file() - self._fix_ini() - -def main(): - import argparse - - logger = logging.getLogger() - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument('--debug', help='', action='store_true') - group.add_argument('-v', '--verbose', help='', action='store_true') - parser.add_argument('src_avd', - help='The short name of the Android Virtual Device ' - 'to clone from', default='new_demo') - parser.add_argument('dst_avd', - help='The short name of the Android Virtual Device ' - 'to copy to', default='demo_copy') - parser.add_argument('remove', help='Remove clone after creation', - action='store_true') - args = parser.parse_args() - - if args.debug: - logger.setLevel(logging.DEBUG) - elif args.verbose: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.WARNING) - - with AvdCloner(src=args.src_avd, dst=args.dst_avd, - remove=args.remove) as cloner: - cloner.clone() - -if __name__ == '__main__': - main() diff --git a/src/certfuzz/android/avd_mgr/errors.py b/src/certfuzz/android/avd_mgr/errors.py deleted file mode 100644 index 8a395a5..0000000 --- a/src/certfuzz/android/avd_mgr/errors.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 15, 2013 - -@organization: cert.org -''' -from certfuzz.android.errors import AndroidError - - -class AndroidEmulatorManagerError(AndroidError): - pass - - -class AvdMgrError(AndroidEmulatorManagerError): - pass - - -class AvdClonerError(AndroidEmulatorManagerError): - pass - - -class OrphanedProcessError(AndroidEmulatorManagerError): - pass diff --git a/src/certfuzz/android/avd_mgr/main.py b/src/certfuzz/android/avd_mgr/main.py deleted file mode 100755 index 2673b74..0000000 --- a/src/certfuzz/android/avd_mgr/main.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python -''' -Created on Jan 14, 2013 - -@organization: cert.org -''' -import logging -import os -import atexit - -from certfuzz.android.avd_mgr import AndroidEmulator -from certfuzz.android.avd_mgr import AvdCloner -from certfuzz.android.avd_mgr import OrphanCatcher - -logger = logging.getLogger(__name__) - -paths_to_clean_up = set() - - -def _on_exit(): - # make sure we clean up - for path in paths_to_clean_up: - try: - logger.debug('Removing %s', path) - os.remove(path) - except OSError as e: - # it's only a problem if we're leaving behind a file - if os.path.exists(path): - logger.error('Error removing %s: %s', path, e) - - -def _write_handle(handle, pipe): - msg = '%s\n' % handle - os.write(pipe, msg) - os.close(pipe) - - -def lifecycle(avd, pipe, poll_interval=5): - ''' - Starts an avd, then goes into a permanent polling mode awaiting - an exception to be thrown. - :param avd: - :param destroy_on_kill: - ''' - logger.debug('starting emulator lifecycle') -# handleable_exc = [AndroidEmulatorError, KeyboardInterrupt, SystemExit] - handleable_exc = None - with OrphanCatcher(avd, - interval=poll_interval, - handled_exceptions=handleable_exc) as orphan_catcher: - with avd: - avd.start() - logger.info('Started %s as %s', avd.avd, avd.handle) - if pipe: - _write_handle(avd.handle, pipe) - orphan_catcher.poll() - - -def avd_manager(emulator_opts, hide=False, pipe=None): - logger.debug('create emulator object') - emulator = AndroidEmulator(emulator_opts) - - clone = emulator_opts['clone'] - avd = emulator_opts['avd_name'] - - if clone: - logger.debug('cloning emulator %s', avd) - with AvdCloner(src=avd, remove=False) as cloner: - cloner.clone() - emulator.avd = cloner.dst - emulator.destroy_on_kill = True - else: - emulator.avd = avd - - if hide: - emulator.no_window = True - - # if we create a clone, destroy it - lifecycle(emulator, pipe, poll_interval=emulator_opts['orphan_check_interval']) - - -def main(): - ''' - Entry point for bff_avd_mgr - ''' - import argparse - atexit.register(_on_exit) - - logger = logging.getLogger() - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument('--debug', help='', action='store_true') - group.add_argument('-v', '--verbose', help='', action='store_true') - parser.add_argument('avd', - help='The short name of the Android Virtual Device', - type=str) - parser.add_argument('--hide', help='Hides emulator window', action='store_true', - default=False) - parser.add_argument('--clone', help='Clone the avd first, then run the clone', - action='store_true', default=False) - parser.add_argument('--emu_hdir', help='Directory to write emulator handles to', - type=str, default='.') - - args = parser.parse_args() - - if args.debug: - logger.setLevel(logging.DEBUG) - elif args.verbose: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.WARNING) - - avd_manager(args.avd, args.clone, args.hide, args.emu_hdir) - -if __name__ == '__main__': - main() diff --git a/src/certfuzz/android/avd_mgr/orphan_catcher.py b/src/certfuzz/android/avd_mgr/orphan_catcher.py deleted file mode 100644 index b24b9db..0000000 --- a/src/certfuzz/android/avd_mgr/orphan_catcher.py +++ /dev/null @@ -1,74 +0,0 @@ -''' -Created on Jan 15, 2013 - -@organization: cert.org -''' -import logging -import os -import time -from traceback import format_tb - -from certfuzz.android.avd_mgr.errors import OrphanedProcessError - - -# from .defaults import POLL_INTERVAL -# store pid and ppid at start up, we'll check them later -# to see if we are an orphan -logger = logging.getLogger(__name__) - -def _ping_process(pid): - try: - # kill -0 is a null signal - # it will throw an OSError if the process does not exist - os.kill(pid, 0) - return True - except OSError: - return False - -class OrphanCatcher(): - def __init__(self, avd, interval=5, handled_exceptions=None): - self.avd = avd - self.interval = interval - self.handled_errors = set() - self.handled_errors.add(OrphanedProcessError) - self.ppid = os.getppid() - - if handled_exceptions is not None: - for x in handled_exceptions: - self.handled_errors.add(x) - - def __enter__(self): - logger.debug('Enter %s runtime context, ppid=%s', self.__class__.__name__, self.ppid) - return self - - def _orphaned(self): - # Yeah, she'll tell you she's an orphan after you meet her family. - return not _ping_process(self.ppid) - - def _poll_once(self): - if self._orphaned(): - raise OrphanedProcessError('Parent process %d has disappeared' % self.ppid) - - # will raise an AndroidEmulatorError if it's not - self.avd.check_child_is_running() - - def poll(self): - logger.info('Waiting to be orphaned...') - while True: - self._poll_once() - time.sleep(self.interval) - - def __exit__(self, etype, value, tb): - logger.debug('Exit %s runtime context', self.__class__.__name__) - - handled = etype in self.handled_errors - - if etype: - if handled: - logger.debug('Handled...%s: %s', etype, value) - else: - logger.debug('Not handled...%s: %s', etype, value) - for line in format_tb(tb): - logger.warning(line) - - return handled diff --git a/src/certfuzz/android/config.yaml b/src/certfuzz/android/config.yaml deleted file mode 100644 index 5979050..0000000 --- a/src/certfuzz/android/config.yaml +++ /dev/null @@ -1,122 +0,0 @@ -##################################################################### -# Campaign options: -# -# id: used for identifying campaign, placement of results -##################################################################### -campaign: - id: client_demo -##################################################################### -# Fuzz target options: -# -# target: -# intent: a container for the necessary components to construct -# an intent -##################################################################### -target: - intent: - action: android.intent.action.VIEW - component: com.android.browser/.BrowserActivity - categories: - - android.intent.category.BROWSABLE - mime_type: text/html -##################################################################### -# Directories used by BFF for Android: -# -# directories: -# seedfile_dir: the directory containing the seedfiles -# apk_dir: the directory containing any APKs to install -# working_dir: -# results_dir: the directory in which to output results -# seedfile_cache: the location of the seedfile cache -##################################################################### -directories: - seedfile_dir: seedfiles - apk_dir: apks - working_dir: /tmp - results_dir: results - seedfile_cache: '~/seedfile_cache' -##################################################################### -# Fuzz run options -# -# first_iteration: The iteration number to begin with. Defaults to zero -# if not present. -# last_iteration: The iteration when a fuzzing campaign ends. If set -# to zero or not present, the campaign will continue -# until the fuzzer runs out of things to do. -# minimize: Create a file that is minimally-different than the seed -# file, yet crashes with the same hash -# minimizer_timeout: The maximum amount of time that BFF will spend on -# a minimization run before giving up -# keep_unique_faddr: Consider the Exception Faulting Address value as -# part of the crash hash -# keep_all_duplicates: Keep all duplicate crashing cases -##################################################################### -runoptions: - first_iteration: 0 - last_iteration: 1 - # minimize: true - # minimizer_timeout: 3600 - # keep_unique_faddr: False - # keep_all_duplicates: False -##################################################################### -# Fuzzer options -# -# fuzzer: -# bytemut: replace bytes with random values -##################################################################### -fuzzer: - fuzzer: bytemut -##################################################################### -# Runner options -# -# runner: -# runtimeout: TODO -##################################################################### -runner: - runtimeout: 2 -##################################################################### -# Database options: -# -# db: -# host: the host running the database service -# port: the port on which the database service is listening -# username: the username by which to authenticate -# password: the password by which to authenticate -# dbname: the database to store data within -##################################################################### -db: - host: localhost - port: 5984 - username: couchadmin - password: couchpassword - dbname: bff -##################################################################### -# Emulator options: -# -# number: the number of emulators that should run concurrently -# sdk_home: the location of the android SDK directories -# avd_home: the directory for android virtual devices -# tombstone_timeout: the number of attempts to collect a tombstone -# after an intent has been issued -# orphan_check_interval: the frequency to check if an emulator is -# orphaned -##################################################################### -emulator: - number: 1 - sdk_home: ~/android-sdk - avd_home: ~/.android/avd - avd_name: gold_4.1.2 - clone: true - tombstone_timeout: 5 - orphan_check_interval: 5 - timers: - # how long to wait for an emulator to be deleted before throwing an error - kill_timeout: 120 - # how long to wait for an emulator to write its handle to a file - handle_file_timeout: 300 - # how long to wait for a handle when booting an avd - handle_timeout: 300 - # how long to wait for the sdcard to mount after the devices is ready - sdcard_timeout: 600 - # time to nap between checking for device in device list - device_retry: 1 \ No newline at end of file diff --git a/src/certfuzz/android/controller/__init__.py b/src/certfuzz/android/controller/__init__.py deleted file mode 100644 index 02e3fc9..0000000 --- a/src/certfuzz/android/controller/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .errors import ControllerError diff --git a/src/certfuzz/android/controller/errors.py b/src/certfuzz/android/controller/errors.py deleted file mode 100644 index 04e07b5..0000000 --- a/src/certfuzz/android/controller/errors.py +++ /dev/null @@ -1,10 +0,0 @@ -''' -Created on Feb 11, 2013 - -@organization: cert.org -''' -from certfuzz.android.errors import AndroidError - - -class ControllerError(AndroidError): - pass diff --git a/src/certfuzz/android/errors.py b/src/certfuzz/android/errors.py deleted file mode 100644 index 79d0480..0000000 --- a/src/certfuzz/android/errors.py +++ /dev/null @@ -1,9 +0,0 @@ -''' -Created on Feb 11, 2013 - -@organization: cert.org -''' -from certfuzz.errors import CERTFuzzError - -class AndroidError(CERTFuzzError): - pass diff --git a/src/certfuzz/android/worker/__init__.py b/src/certfuzz/android/worker/__init__.py deleted file mode 100644 index 34ea489..0000000 --- a/src/certfuzz/android/worker/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# from .defaults import HANDLE_FILE_TIMEOUT -from .tasks import emu diff --git a/src/certfuzz/android/worker/errors.py b/src/certfuzz/android/worker/errors.py deleted file mode 100644 index 960b834..0000000 --- a/src/certfuzz/android/worker/errors.py +++ /dev/null @@ -1,10 +0,0 @@ -''' -Created on Feb 11, 2013 - -@organization: cert.org -''' -from certfuzz.android.errors import AndroidError - - -class WorkerError(AndroidError): - pass diff --git a/src/certfuzz/android/worker/tasks.py b/src/certfuzz/android/worker/tasks.py deleted file mode 100644 index f476d80..0000000 --- a/src/certfuzz/android/worker/tasks.py +++ /dev/null @@ -1,11 +0,0 @@ -''' -Created on Jan 17, 2013 - -@organization: cert.org -''' - - -class EmuHandle(object): - handle = None - -emu = EmuHandle() diff --git a/src/certfuzz/android/worker/worker.py b/src/certfuzz/android/worker/worker.py deleted file mode 100644 index 8be4e3f..0000000 --- a/src/certfuzz/android/worker/worker.py +++ /dev/null @@ -1,138 +0,0 @@ -''' -Created on Jan 10, 2013 - -@organization: cert.org -''' -import atexit -import logging -import os -import random -import signal -import sys -import time - -from certfuzz.android.api import AdbCmd -from certfuzz.android.api.log_helper import log_formatter -from certfuzz.android.avd_mgr.main import avd_manager - -from certfuzz.iteration.iteration_android import emu as iteration_emu - - -logger = logging.getLogger(__name__) - - -def _reset_root_logger(): - rl = logging.getLogger() - # remove the other handlers - rl.handlers = [] - logfilename = 'worker-%d.log' % os.getpid() - logfilepath = os.path.join('log', logfilename) - filehdlr = logging.FileHandler(filename=logfilepath, mode='w') - filehdlr.setFormatter(log_formatter()) - rl.addHandler(filehdlr) - - -def _setup_child(emu_opts, pipe_in, pipe_out): - # we are the child - _reset_root_logger() - logger.debug('%d child fork success', os.getpid()) - # reseed the random number generator for this process (avoids having - # all peer processes with the same PRNG state) - random.seed() - - os.close(pipe_in) - - # wait for a few secs before proceeding to avoid having many avds - # spinning up simultaneously - naptime = random.randint(0, 30) - logger.debug('Pausing for %d seconds before proceeding', naptime) - time.sleep(naptime) - avd_manager(emu_opts, hide=False, pipe=pipe_out) - - -def _setup_parent(pipe_in, pipe_out, child_pid, emu_opts, apk_dir): - # we are the parent - atexit.register(_on_exit) - signal.signal(signal.SIGTERM, _clean_exit) - signal.signal(signal.SIGINT, _clean_exit) - - os.close(pipe_out) - pipe_in = os.fdopen(pipe_in) - logger.debug('Waiting for emulator handle from child process %d', - child_pid) - handle = pipe_in.readline().strip() - logger.info('Received emulator handle from child process %d: %s', - child_pid, handle) - pipe_in.close() - - iteration_emu.handle = handle - - with AdbCmd(handle) as adbcmd: - adbcmd.wait_for_sdcard(emu_opts['timers']['sdcard_timeout']) - logger.info('%s (ppid=%d pid=%d) ready', - handle, os.getpid(), child_pid) - - # Install any APKS - _install_apks(handle, apk_dir) - - return handle - - -def start_emulator(emu_opts, apk_dir=None): - logger.info('starting emulator (pid=%d)', os.getpid()) - logger.debug('Opening pipe') - pipe_in, pipe_out = os.pipe() - logger.debug('forking child to spawn emulator') - child_pid = os.fork() - - if child_pid != 0: - # we are the parent - return _setup_parent(pipe_in, pipe_out, child_pid, emu_opts, apk_dir) - - # we must be the child - _setup_child(emu_opts, pipe_in, pipe_out) - - -def _stop_emulator(handle): - with AdbCmd() as adbcmd: - devices = adbcmd.devices() - - if handle in devices: - logger.warning('%d Emulator still in device list %s: %s', - os.getpid(), - handle, - devices[handle] - ) - # TODO: kill the emu here if it's still around - - -def _on_exit(*args, **kwargs): - ''' - Exit handler - ''' - # TODO: fix or delete. We are currently calling _stop_emulator in AndroidCampaign.__exit__() - # _stop_emulator() - pass - - -def _clean_exit(signum, stack_frame): - logger.info('Exiting on signal %d', signum) - logger.debug('Stack frame: %s', stack_frame) - sys.exit() - - -def _install_apks(handle, apk_dir=None): - # short circuit if no apk_dir given - if apk_dir == None: - return - - if not os.path.exists(apk_dir): - logger.warning('APK installation directory \'%s\' not found. ' % apk_dir + - 'Unable to install APKs.') - return - - # if we got here, apk_dir exists, so try to install stuff... - with AdbCmd(handle) as adbcmd: - apks = [apk for apk in os.listdir(apk_dir) if apk.lower().endswith('.apk')] - for apk in apks: - adbcmd.install('/'.join([apk_dir, apk])) diff --git a/src/certfuzz/bff/android.py b/src/certfuzz/bff/android.py deleted file mode 100644 index 8af0699..0000000 --- a/src/certfuzz/bff/android.py +++ /dev/null @@ -1,85 +0,0 @@ -''' -Created on Jan 17, 2013 - -@organization: cert.org -''' -import logging -import os - -from certfuzz.android.api.log_helper import log_formatter -from certfuzz.campaign.campaign_android import AndroidCampaign -from certfuzz.campaign.errors import AndroidCampaignError -from certfuzz.fuzztools import filetools - - -logger = logging.getLogger() - - -def setup_logging(log_level=logging.WARNING, - log_dir=None, - log_basename='bff.log'): - - formatter = log_formatter() - - log_handlers = [] - - # console logging - log_handlers.append(logging.StreamHandler()) - - # file logging - if log_dir is not None: - filetools.make_directories(log_dir) - logfile = os.path.join(log_dir, log_basename) - log_handlers.append(logging.FileHandler(logfile, mode='w')) - - for hdlr in log_handlers: - hdlr.setFormatter(formatter) - logger.addHandler(hdlr) - - logger.setLevel(log_level) - - -def main(): - log_level = logging.WARNING - - default_cfg = 'config/android_config.yaml' - import argparse - parser = argparse.ArgumentParser() - parser.add_argument('repeat_count', type=int, default=None) - parser.add_argument('--intent_yaml', type=str, default=None) - parser.add_argument('--config', type=str, default=default_cfg) - parser.add_argument('--verbose', action='store_true', default=False) - parser.add_argument('--debug', action='store_true', default=False) - args = parser.parse_args() - - if args.debug: - log_level = logging.DEBUG - elif args.verbose: - log_level = logging.INFO - - setup_logging(log_level, log_dir='log') - - if args.config != default_cfg: - if not os.path.exists(args.config): - logger.error('Could not find config %s' % args.config) - return - - if os.path.exists(args.config): - custom_cfg = args.config - logger.info('Using config file: %s' % custom_cfg) - else: - logger.error('Could not find %s' % args.config) - logger.warning('Using default config at %s' % default_cfg) - custom_cfg = default_cfg - - try: - with AndroidCampaign(config_file=custom_cfg, - intent_yaml=args.intent_yaml, - repeat_count=args.repeat_count) as c: - c.go() - except AndroidCampaignError as e: - logger.warning('Campaign terminated due to: %s', e) - - -if __name__ == '__main__': - main() diff --git a/src/certfuzz/campaign/campaign_android.py b/src/certfuzz/campaign/campaign_android.py deleted file mode 100644 index 6f904e4..0000000 --- a/src/certfuzz/campaign/campaign_android.py +++ /dev/null @@ -1,230 +0,0 @@ -''' -Created on Feb 7, 2013 - -@organization: cert.org -''' -import errno -import logging -import random -from socket import error as socket_error - -from certfuzz.android.api.intent import Intent -from certfuzz.android.worker import worker -from certfuzz.campaign import __version__ -from certfuzz.campaign.campaign import Campaign -from certfuzz.campaign.config.android_config import AndroidConfig -from certfuzz.campaign.errors import AndroidCampaignError -from certfuzz.db.couchdb.datatypes.campaign_doc import AndroidCampaignDoc -from certfuzz.db.couchdb.db import TestCaseDb, put_file -from certfuzz.file_handlers.directory import Directory - -from certfuzz.iteration.iteration_android import do_iteration - - -logger = logging.getLogger(__name__) - - -# TODO this should inherit from campaignbase? -class AndroidCampaign(Campaign): - def __init__(self, config_file='config/android_config.yaml', - intent_yaml=None, - repeat_count=None): - logger.debug('initialize %s', self.__class__.__name__) - self.config_file = config_file - self._version = __version__ - - cfgobj = AndroidConfig(self.config_file) - self.config = cfgobj.config - logger.debug('Config: %s', self.config) - - self.campaign_id = self.config['campaign']['id'] - self.fuzzopts = self.config['fuzzer'] - self.runopts = self.config['runner'] - self.apk_dir = self.config['directories']['apk_dir'] - self.emu_opts = self.config['emulator'] - self.task_timeout = 3600 # an hour should be plenty long enough - - self.intent = Intent() - if intent_yaml is None: - self.intent.__dict__.update(self.config['target']['intent']) - else: - self.intent.__dict__.update(intent_yaml) - - self.dbcfg = self.config['db'] - - # if the config doesn't specify otherwise... - self.current_seed = 0 - - try: - self.current_seed = self.config['runoptions']['first_iteration'] - except KeyError: - pass - - self.seed_interval = 10 - - if repeat_count != None: - self.stop_seed = repeat_count - else: - try: - self.stop_seed = self.config['runoptions']['last_iteration'] - except KeyError: - self.stop_seed = None - - self.results = [] - self.result_db = None - self.seedfiles = None - self.emu_handles = set() - self.handle_selector = 0 - - def _connect_db(self): - host = self.dbcfg['host'] - port = self.dbcfg['port'] - username = self.dbcfg['username'] - password = self.dbcfg['password'] - db = self.dbcfg['dbname'] - self.result_db = TestCaseDb(host, port, username, password, db) - - def _clear_db(self): - self.result_db = None - - def _store_seedfiles(self, sf_dir): - logger.info('loading seedfiles into database') - self.seedfiles = Directory(sf_dir).files - - if not len(self.seedfiles): - raise AndroidCampaignError('No seedfiles found') - - for basicfile in self.seedfiles: - put_file(basicfile, self.result_db.db) - - def _store_campaign_details(self): - # get or create the doc - try: - doc = AndroidCampaignDoc.load(self.result_db.db, - self.config['campaign']['id']) - except KeyError: - doc = None - - if doc is None: - doc = AndroidCampaignDoc() - try: - doc.id = self.config['campaign']['id'] - except KeyError: - logger.debug('Campaign id not set in config, will autogenerate') - - # add data to the doc - doc.target = self.config['target'] - doc.config = self.config['runoptions'] - doc.fuzzopts = self.config['fuzzer'] - doc.runopts = self.config['runner'] - - # store it - doc.store(self.result_db.db) - - # remember the id of this doc - self.campaign_id = doc.id - - def _store_campaign(self): - ''' - Stores information about the campaign to the db - ''' - self._store_campaign_details() - - sf_dir = self.config['directories']['seedfile_dir'] - self._store_seedfiles(sf_dir) - - def __enter__(self): - return self - - def __exit__(self, etype, value, traceback): - ''' - Kill workers... - :param etype: - :param value: - :param traceback: - ''' - handled = False - - self._clear_db() - - if etype == KeyboardInterrupt: - logger.warning('Campaign cancelled by ctrl-c') - handled = True - elif etype == AndroidCampaignError: - logger.warning('Campaign exiting due to: %s', value) - handled = True - elif etype == socket_error: - # we can handle socket error only if it's a connection refused - if etype.errno == errno.ECONNREFUSED: - logger.warning('Failed to connect to db: (%s) %s', etype, value) - handled = True - - logger.info('Campaign Complete') - return handled - - def __getstate__(self): - pass - - def __setstate__(self): - pass - - def _write_version(self): - Campaign._write_version(self) - - def _keep_going(self): - return (not self.stop_seed - or (self.current_seed < self.stop_seed)) - - def _pick_seedfile(self): - sf = random.choice(self.seedfiles) - return sf.sha1 - - def _do_interval(self): - interval_limit = self.current_seed + self.seed_interval - - # don't overshoot stop_seed - if self.stop_seed: - interval_limit = min(interval_limit, self.stop_seed) - - interval = xrange(self.current_seed, interval_limit) - - iter_args = {'campaign_id': 'campaign_id', - 'db_config': self.config['db'], - 'fuzzopts': self.fuzzopts, - 'runopts': self.runopts, - 'intent': self.intent, - 'sf': self._pick_seedfile(), - 'sf_dir': self.config['directories']['seedfile_dir'], - 'num': None, - } - - # asynchronously call each iteration for this interval - # iterations are thus passed out to the worker pool - for iteration_num in interval: - iter_args['num'] = iteration_num - logger.debug('queueing iteration %d', iteration_num) - - do_iteration(iter_args) - logger.debug('interval complete') - - # move the current_seed pointer to the next interval - self.current_seed = interval_limit - - def go(self): - ''' - 1. Connect to database - 2. Put campaign info into db - 3. Spawn workers - 4. Do stuff - ''' - logger.info('Starting Campaign') - self._connect_db() - self._store_campaign() - logger.info('Using db %s@%s for campaign %s', - self.result_db.db_name, - self.result_db.connection_string, - self.campaign_id) - - worker.start_emulator(self.emu_opts, self.apk_dir) - - Campaign.go(self) diff --git a/src/certfuzz/campaign/config/android_config.py b/src/certfuzz/campaign/config/android_config.py deleted file mode 100644 index fc2b57b..0000000 --- a/src/certfuzz/campaign/config/android_config.py +++ /dev/null @@ -1,80 +0,0 @@ -''' -Created on Mar 20, 2013 - -@organization: cert.org -''' -from certfuzz.campaign.config.config_base import Config as ConfigBase -#from ...android.controller.defaults import CONFIG as DEFAULT_CONFIG -import logging - -logger = logging.getLogger(__name__) - - -class AndroidConfig(ConfigBase): - - def __init__(self, config_file): - - super(AndroidConfig, self).__init__(config_file) - - self._set_derived_options() - self.validations = [] - self._add_validations() - self.validate() - - def _set_derived_options(self): - ConfigBase._set_derived_options(self) - - def _add_validations(self): - self.validations.append(self._validate_intent) - self.validations.append(self._validate_directories) - self.validations.append(self._validate_db_config) - - def _validate_intent(self): - # TODO validating the specified intent action, categories, and mime_type - # against the list of valid entries from the android spec could be - # a good future capability - return - - def _validate_directories(self): - # Make sure apk_dir exists if it is specified, otherwise it - # should be None - try: - self.config['directories']['apk_dir'] - except KeyError: - logger.warning('No APK installation directory specified. ' + - 'Make sure any needed APKs are already installed.') - self.config['directories']['apk_dir'] = None - - def _validate_db_config(self): - - # Validate username and password. If not found, set to None - for x in ['username', 'password']: - try: - x = self.config['db'][x] - except KeyError: - logger.warning('No %s specified in config' % x) - self.config['db'][x] = None - - # Validate host - try: - self.config['db']['host'] - except KeyError: - logger.warning('No host specified in config. Defaulting to localhost.') - self.config['db']['host'] = 'localhost' - - # Validate port - try: - db_port = self.config['db']['port'] - except KeyError: - logger.warning('No db port specified in config') - db_port = None - if db_port == None or db_port < 0 or db_port > 65535: - logger.warning('Invalid db port specified in config. Defaulting to 5984.') - self.config['db']['port'] = 5984 - - # Validate dbname - try: - self.config['db']['dbname'] - except KeyError: - logger.warning('No dbname specified in config. Defaulting to bff.') - self.config['db']['dbname'] = 'bff' diff --git a/src/certfuzz/crash/android_testcase.py b/src/certfuzz/crash/android_testcase.py deleted file mode 100644 index aef785e..0000000 --- a/src/certfuzz/crash/android_testcase.py +++ /dev/null @@ -1,106 +0,0 @@ -''' -Created on Apr 11, 2013 - -@organization: cert.org -''' -import logging -import os - -from certfuzz.android.api import AdbCmd -from certfuzz.android.api.log_helper import pfunc -from certfuzz.crash import TestCaseBase -from certfuzz.file_handlers.basicfile import BasicFile - - -logger = logging.getLogger(__name__) - - -class AndroidTestCase(TestCaseBase): - ''' - classdocs - ''' - _tmp_pfx = 'BFF_android_testcase_' - - def __init__(self, seedfile, fuzzedfile, workdir_base, handle, input_dir, - campaign_id=None): - ''' - Constructor - ''' - TestCaseBase.__init__(self, seedfile, fuzzedfile, workdir_base) - self.handle = handle - self.input_dir = input_dir - self.campaign_id = campaign_id - self._keep_local_copy = True - self.attachments = set() - - def __enter__(self): - TestCaseBase.__enter__(self) - self.collect_data() - return self - - @pfunc(logger=logger) - def collect_data(self): - # if you got here, it's because there was a tombstone - logger.info('Getting bugreport') - - with AdbCmd(handle=self.handle) as adbcmd: - adbcmd.bugreport() - bugreport_result = adbcmd - - if bugreport_result is not None: - self._write_bugreport(bugreport_result) - - @pfunc(logger=logger) - def _write_bugreport(self, bugreport): - ''' - Looks for bugreport.stdout or bugreport.stderr - :param bugreport: - ''' - outfile = 'bugreport-{}.txt'.format - for key in ['stdout', 'stderr']: - try: - value = getattr(bugreport, key) - except AttributeError: - # go to next key if bugreport doesn't have this one - continue - - if not value: - # go to next key if value is empty or None - continue - - of = os.path.join(self.working_dir, outfile(key)) - try: - with open(of, 'w') as f: - logger.debug('write to %s', of) - f.write(value) - self.attachments.add(of) - except EnvironmentError as e: - # parent of IOError, OSError, and WindowsError - # not fatal to the task, we just didn't get a bugreport - logger.warning('Write bugreport failed: %s', e) - - @pfunc(logger=logger) - def _store_attachments(self, doc, db): - for attachment_path in self.attachments: - f = BasicFile(attachment_path) - logger.debug('Attaching %s to record %s', f.path, doc.id) - with open(f.path, 'rb') as fp: - db.put_attachment(doc, fp, filename=f.basename) - - @pfunc(logger=logger) - def store(self, db): - ''' - Convert the data in this object to a couchdb record & send it to db - :param db: - ''' - doc = self.to_TestCaseDoc() - logger.debug('Store %s to db', doc.id) - doc.store(db) - self._store_attachments(doc, db) - - @pfunc(logger=logger) - def to_TestCaseDoc(self): - logger.debug('Convert test case to db doc') - doc = TestCaseBase.to_TestCaseDoc(self) - doc.campaign_id = self.campaign_id - return doc diff --git a/src/certfuzz/db/__init__.py b/src/certfuzz/db/__init__.py deleted file mode 100644 index 79fa053..0000000 --- a/src/certfuzz/db/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from errors import DbError diff --git a/src/certfuzz/db/couchdb/__init__.py b/src/certfuzz/db/couchdb/__init__.py deleted file mode 100644 index bee8aa5..0000000 --- a/src/certfuzz/db/couchdb/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .errors import CouchDbError, TestCaseDbError -from .db import TestCaseDb, put_file diff --git a/src/certfuzz/db/couchdb/datatypes/__init__.py b/src/certfuzz/db/couchdb/datatypes/__init__.py deleted file mode 100644 index 913b464..0000000 --- a/src/certfuzz/db/couchdb/datatypes/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .errors import DataTypeError -from .testcase_doc import TestCaseDoc -from .campaign_doc import CampaignDoc -from .file_doc import FileDoc diff --git a/src/certfuzz/db/couchdb/datatypes/campaign_doc.py b/src/certfuzz/db/couchdb/datatypes/campaign_doc.py deleted file mode 100644 index d9b7787..0000000 --- a/src/certfuzz/db/couchdb/datatypes/campaign_doc.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Created on Mar 15, 2013 - -@organization: cert.org -''' -try: - from couchdb.mapping import TextField, Document, DictField, DateTimeField -except ImportError as e: - pass - -from datetime import datetime - -class CampaignDoc(Document): - doctype = TextField(default='Campaign') - campaign_type = TextField(default='Base') - config = DictField() - added = DateTimeField(default=datetime.now()) - -class AndroidCampaignDoc(CampaignDoc): - campaign_type = TextField(default='Android') - fuzzopts = DictField() - runopts = DictField() - -class BFFCampaignDoc(CampaignDoc): - campaign_type = TextField(default='BFF') - fuzzer = TextField() - command = TextField() diff --git a/src/certfuzz/db/couchdb/datatypes/errors.py b/src/certfuzz/db/couchdb/datatypes/errors.py deleted file mode 100644 index fdb9113..0000000 --- a/src/certfuzz/db/couchdb/datatypes/errors.py +++ /dev/null @@ -1,9 +0,0 @@ -''' -Created on Mar 15, 2013 - -@organization: cert.org -''' -from .. import CouchDbError - -class DataTypeError(CouchDbError): - pass diff --git a/src/certfuzz/db/couchdb/datatypes/file_doc.py b/src/certfuzz/db/couchdb/datatypes/file_doc.py deleted file mode 100644 index 00a9daf..0000000 --- a/src/certfuzz/db/couchdb/datatypes/file_doc.py +++ /dev/null @@ -1,18 +0,0 @@ -''' -Created on Mar 15, 2013 - -@organization: cert.org -''' -try: - from couchdb.mapping import TextField, IntegerField, Document -except ImportError as e: - pass - -class FileDoc(Document): - doctype = TextField(default='File') - filename = TextField() - extension = TextField() - sha1 = TextField() - size_in_bytes = IntegerField() - derived_from_file_id = TextField() - # Attachment: file data diff --git a/src/certfuzz/db/couchdb/datatypes/testcase_doc.py b/src/certfuzz/db/couchdb/datatypes/testcase_doc.py deleted file mode 100644 index f345b9b..0000000 --- a/src/certfuzz/db/couchdb/datatypes/testcase_doc.py +++ /dev/null @@ -1,20 +0,0 @@ -''' -Created on Mar 15, 2013 - -@organization: cert.org -''' -try: - from couchdb.mapping import TextField, Document, IntegerField -except ImportError as e: - pass - -class TestCaseDoc(Document): - doctype = TextField(default='TestCase') - campaign_id = TextField() - crash_signature = TextField() - seed_file = TextField() # _id of the File document containing the seed - fuzzed_file = TextField() # _id of the File document containing the fuzzed file - minimized_file = TextField() # _id of the File document containing the minimized file - # Attachments: Any files that do not have the same extension as the associated seed file - bitwise_hd = IntegerField() - bytewise_hd = IntegerField() diff --git a/src/certfuzz/db/couchdb/db.py b/src/certfuzz/db/couchdb/db.py deleted file mode 100644 index bcaf76f..0000000 --- a/src/certfuzz/db/couchdb/db.py +++ /dev/null @@ -1,200 +0,0 @@ -import logging -import os - -try: - import couchdb - from .datatypes import FileDoc -except ImportError as e: - pass - -from .errors import TestCaseDbError - -# Logging initialization -logger = logging.getLogger(__name__) - -format_url = 'http://{}:{}/'.format - -def put_file(basicfile, db): - ''' - Checks for the existence of the file basicfile in the database. If not - found, add it to the database. Return the corresponding db document. - :param basicfile: - ''' - - logger.debug('Processing for insertion: %s', basicfile.path) - if not basicfile.exists(): - logger.warning('Skipping non-existent file %s', basicfile.path) - return - - # is it in the db already? - doc = FileDoc.load(db, basicfile.sha1) - if doc is not None: - logger.info('File %s already in db', basicfile.path) - return - - # new file... - doc = basicfile.to_FileDoc() - - # put it in the db - doc.store(db) - - # attach the content - logger.info('Uploading %s to db', basicfile.path) - with open(basicfile.path, 'rb') as fd: - db.put_attachment(doc, fd, filename=basicfile.basename) - -class TestCaseDb(): - - ''' - This class is a wrapper for the CouchDB client API. It creates a connection to the database - server and stores a handle to a Database object (self.db) which is used for - directly interacting with the CouchDB database. This class also contains helper methods - for common database interactions. - ''' - def __init__(self, host='localhost', port=5984, username=None, - password=None, dbname='bff', force_create=True): - self.host = host - self.port = port - self.username = username - self.password = password - self.db_name = dbname - self.force_create = force_create - - self.connection_string = format_url(self.host, self.port) - - self.server = None - self.db = None - - self.connect() - - def _check_dbname(self): - if self.db_name is None: - raise TestCaseDbError('db_name is not defined') - # couchdb requires lowercase db names - self.db_name = self.db_name.lower() - - def _set_server(self): - logger.debug('Connecting: %s', self.connection_string) - self.server = couchdb.Server(self.connection_string) - if self.username is not None and self.password is not None: - logger.debug('Setting username/password for db connection') - self.server.resource.credentials = (self.username, self.password) - - def _set_db(self): - self._check_dbname() - # Check if DB exists. If not, create it. - logger.debug('Checking to see if we need to create db %s', self.db_name) - if not self.db_name in self.server: - if self.force_create: - self.create() - else: - raise TestCaseDbError('The specified db \'%s\' does not exist' % self.db_name) - logger.debug('Setting db to %s', self.db_name) - self.db = self.server[self.db_name] - - def connect(self): - self._set_server() - self._set_db() - - def create(self): - ''' - Creates a database with the given name on the server specified during the TestCaseDb - object's initialization. - ''' - logger.debug('Creating db %s', self.db_name) - self._check_dbname() - - try: - self.server.create(self.db_name) - logger.debug("Created database with name %s", self.db_name) - except couchdb.http.PreconditionFailed: - logger.debug('Skipping create on %s: db exists' % self.db_name) - - def destroy(self, reason='unspecified'): - ''' - DANGEROUS: Destroys the database and all of its associated data, optionally allowing - for a reason to be given and logged. - ''' - del self.server[self.db_name] - logger.warning("Database '" + self.db_name + "' destroyed with reason: " + reason) - - def wipe(self, reason='unspecified'): - ''' - Wipes a database by deleting it and then recreating it. - ''' - logger.info("Database is being wiped with reason: " + reason) - self.destroy(reason) - self.create(self.db_name) - - def is_connected(self): - ''' - Returns True if the connection to the database is established, and False if it is not. - - @rtype: boolean - ''' - if self.db_name in self.server: - return True - else: - return False - - def info(self): - ''' - Returns a dictionary containing information about the database. - - @rtype: dictionary of strings - ''' - if self.is_connected(): - return self.db.info() - else: - return None - - def print_dump(self): - ''' - Prints every document in the database. Useful for testing. - ''' - if self.is_connected(): - for x in self.db: - print str(type(self.db[x])) - print self.db[x] - else: - return None - - def add_docs(self, docs, campaign_id): - for doc in docs: - self.add(doc, campaign_id) - - def add(self, doc): - ''' - Takes a Doc object (crashdb.data.db_structs.Doc) which consists of a Document (couchdb.mapping.Document) - as 'document' and a list of associated files as 'attachments'. The document is first inserted into - the database, and then attachments are subsequently attached to it and inserted one at a time. - ''' - try: - document = doc.document.store(self.db) - logger.debug('Adding: ' + str(document) + '\n') - for attachment in doc.attachments: - a = open(attachment, 'rb').read() - self.db.put_attachment(document, a, filename=os.path.basename(attachment)) - except couchdb.ResourceConflict: - # Log it; may not be entirely necessary since it's just telling us that the document is already in the DB - logging.error('A couchdb.ResourceConflict was raised for the following document with id ' - + document._id + ': \n\t' + str(doc)) - return doc.document['_id'] - - def bulk_insert(self, docs): - _list = [doc.document for doc in docs] - - ids = self.db.update(_list) - - counter = 0 - for _id in ids: - for attachment in docs[counter].attachments: - a = open(attachment, 'rb').read() - self.db.put_attachment(self.db[_id[1]], a, filename=os.path.basename(attachment)) - counter += 1 - - return ids - - def get(self, doc_id): - return self.db[doc_id] - diff --git a/src/certfuzz/db/couchdb/errors.py b/src/certfuzz/db/couchdb/errors.py deleted file mode 100644 index 276442e..0000000 --- a/src/certfuzz/db/couchdb/errors.py +++ /dev/null @@ -1,12 +0,0 @@ -''' -Created on Mar 15, 2013 - -@organization: cert.org -''' -from .. import DbError - -class CouchDbError(DbError): - pass - -class TestCaseDbError(CouchDbError): - pass diff --git a/src/certfuzz/db/errors.py b/src/certfuzz/db/errors.py deleted file mode 100644 index c63f9a4..0000000 --- a/src/certfuzz/db/errors.py +++ /dev/null @@ -1,9 +0,0 @@ -''' -Created on Mar 15, 2013 - -@organization: cert.org -''' -from .. import CERTFuzzError - -class DbError(CERTFuzzError): - pass diff --git a/src/certfuzz/iteration/iteration_android.py b/src/certfuzz/iteration/iteration_android.py deleted file mode 100644 index 44e3055..0000000 --- a/src/certfuzz/iteration/iteration_android.py +++ /dev/null @@ -1,204 +0,0 @@ -''' -Created on Feb 7, 2013 - -@organization: cert.org -''' -import logging -import os -import shutil -import tempfile - -from certfuzz.android.api.activity_manager import ActivityManagerError -from certfuzz.android.api.errors import AdbCmdError -from certfuzz.android.worker.errors import WorkerError -from certfuzz.crash.android_testcase import AndroidTestCase -from certfuzz.db.couchdb.datatypes import FileDoc -from certfuzz.db.couchdb.db import TestCaseDb, put_file -from certfuzz.file_handlers.fuzzedfile import FuzzedFile -from certfuzz.file_handlers.seedfile import SeedFile -from certfuzz.fuzzers.bytemut import ByteMutFuzzer -from certfuzz.fuzzers.errors import FuzzerExhaustedError -from certfuzz.fuzztools.filetools import find_or_create_dir -from certfuzz.runners.android_runner import AndroidRunner -from certfuzz.runners.errors import RunnerError - -from certfuzz.iteration.iteration_base import IterationBase2 - - -# from ..android.worker.defaults import TOMBSTONE_TIMEOUT, DBCFG, SF_CACHE_DIR -logger = logging.getLogger(__name__) - - -class EmuHandle(object): - handle = None - -emu = EmuHandle() - - -def _get_seedfile_by_id(tcdb, sf_dir, sfid): - # find the doc record by id - doc = FileDoc.load(tcdb.db, sfid) - if doc is None: - raise WorkerError('Seedfile not found in db, sfid=%s', sfid) - - sfpath = os.path.join(sf_dir, doc.filename) - if not os.path.exists(sfpath): - logger.info('Retrieving %s from db', doc.filename) - # pull content from db, write to disk - f = tcdb.db.get_attachment(doc, doc.filename) - if f is None: - raise WorkerError('Seedfile content not found in db, sfid=%s', sfid) - - logger.debug('...writing content to %s', sfpath) - # make sure we have a dir to drop it in - find_or_create_dir(sf_dir) - with open(sfpath, 'wb') as out: - out.write(f.read()) - else: - logger.debug('Found cached seedfile at %s', sfpath) - - sf = SeedFile(sf_dir, sfpath) - return sf - - -def do_iteration(iter_args): - iter_args['emu_handle'] = emu.handle - with AndroidIteration(**iter_args) as iteration: - try: - iteration.go() - except FuzzerExhaustedError: - # Some fuzzers run out of things to do. They should - # raise a FuzzerExhaustedError when that happens. - pass - # TODO: pass something more useful than 'True'? We just want to - # know that it returns, so this may suffice for now - # return iteration.result - return True - - -class AndroidIteration(IterationBase2): - def __init__(self, campaign_id=None, db_config=None, num=0, fuzzopts=None, - runopts=None, sf=None, emu_handle=None, sf_dir=None, intent=None): - self.campaign_id = campaign_id - self.db_config = db_config - self.current_seed = num - self.fuzzopts = fuzzopts - self.runopts = runopts - self.intent = intent - self.sf_dir = sf_dir - self.sf_id = sf - self.fuzzer = ByteMutFuzzer - self.runner = AndroidRunner - self.emu_handle = emu_handle - self.results = [] - self.minimizable = False - self.tcdb = None - self.sf = None - self.tmpdir = None - self.rng_seed = None - self.crashes = [] - self.iteration_tmpdir_pfx = 'iteration_' - - logger.info('New task for campaign %s ', self.campaign_id) - logger.debug('seedfile_id = %s', self.sf_id) - logger.debug('iteration_num = %d', self.current_seed) - logger.debug('fuzzopts = %s', self.fuzzopts) - logger.debug('runopts = %s', self.runopts) - - def __enter__(self): - logger.debug('Iteration %d start: %s', self.current_seed, self.sf_id) - - # create db conn - host = self.db_config['host'] - port = self.db_config['port'] - username = self.db_config['username'] - password = self.db_config['password'] - db = self.db_config['dbname'] - self.tcdb = TestCaseDb(host, port, username, password, db) - - # get the seedfile, caching if needed - self.sf = _get_seedfile_by_id(self.tcdb, self.sf_dir, self.sf_id) - - self.tmpdir = tempfile.mkdtemp(prefix='bff-fuzz-and-run-') - - return self - - def __exit__(self, etype, value, traceback): - logger.debug('Iteration %d complete', self.current_seed) - - # remove tempdir - shutil.rmtree(self.tmpdir, ignore_errors=True) - # clear db conn - self.tcdb = None - - if etype: - logger.warning('Iteration failed') - if etype is AdbCmdError: - logger.warning('ADB command failed') - elif etype is RunnerError: - logger.warning('Runner failed') - elif etype is ActivityManagerError: - logger.warning('Activity Manager failed') - - def _fuzz_and_run(self): - # # FUZZ - logger.info('...fuzzing') - fuzz_opts = self.fuzzopts - fuzz_args = self.sf, self.tmpdir, self.rng_seed, self.current_seed, fuzz_opts - with self.fuzzer(*fuzz_args) as fuzzer: - fuzzer.fuzz() - self.fuzzed = True -# self.r = fuzzer.range -# if self.r: -# logger.info('Selected r: %s', self.r) - - fuzzed_file_full_path = fuzzer.output_file_path - - dst_basename = '%s-fuzzed%s' % (self.sf.root, self.sf.ext) - dst_file = os.path.join('/', 'sdcard', dst_basename) - - # decide if we can minimize this case later - # do this here (and not sooner) because the fuzzer could - # decide at runtime whether it is or is not minimizable - # TODO: add runoptions/minimize to config file -# self.minimizable = fuzzer.is_minimizable and self.config['runoptions']['minimize'] - - # # RUN - logger.debug('...run') - - analysis_needed = False - if self.runner: - logger.info('...running %s', self.runner.__name__) - run_args = {'handle': self.emu_handle, - 'src_file': fuzzed_file_full_path, - 'dst_file': dst_file, - 'campaign_id': self.campaign_id, - 'intent': self.intent, - 'workingdir_base': self.tmpdir, - 'options': self.runopts, - } - with self.runner(**run_args) as runner: - runner.run() - analysis_needed = runner.saw_crash - - # is further analysis needed? - logger.debug('...check for crash') - if analysis_needed: - logger.info('...analyzing') - - # put fuzzed file in db - logger.info('...save fuzzed file to db') - fuzzedfile = FuzzedFile(path=fuzzed_file_full_path, derived_from=self.sf) - put_file(fuzzedfile, self.tcdb.db) - - # create testcase record - logger.info('...create test case object') - with AndroidTestCase(seedfile=self.sf, - fuzzedfile=fuzzedfile, - workdir_base=self.tmpdir, - handle=self.emu_handle, - input_dir=runner.result_dir, - campaign_id=self.campaign_id, - ) as testcase: - logger.info('...store object to db') - testcase.store(self.tcdb.db) diff --git a/src/certfuzz/runners/android_runner.py b/src/certfuzz/runners/android_runner.py deleted file mode 100644 index 0db946a..0000000 --- a/src/certfuzz/runners/android_runner.py +++ /dev/null @@ -1,151 +0,0 @@ -''' -Created on Apr 4, 2013 - -@organization: cert.org -''' -from certfuzz.runners.runner_base import Runner -from certfuzz.runners.errors import AndroidRunnerError -from certfuzz.android.api.adb_cmd import AdbCmd -from certfuzz.android.api.activity_manager import ActivityManager -from certfuzz.android.api.errors import AdbCmdError -# from ..db.couchdb.datatypes import TestCaseDoc -# from ..db.couchdb.db import TestCaseDb -# from ..helpers.misc import random_str -# from ..fuzztools.filetools import find_or_create_dir - -import logging -import os -import tempfile -import shutil -import time - -logger = logging.getLogger(__name__) - - -class AndroidRunner(Runner): - ''' - Runner object for Android platform - ''' - def __init__(self, handle, src_file, dst_file, campaign_id, intent=None, - workingdir_base=None, options=None): - Runner.__init__(self, options, workingdir_base) - self.handle = handle - self.src = src_file - self.dst = dst_file - self.campaign_id = campaign_id - self.outdir = os.path.expanduser('~/bff-results') - self.intent = intent - - if self.intent is None: - raise AndroidRunnerError('Cannot run task without intent') - else: - self.intent.data_uri = self._fuzzed_file_uri - - self.testcases = [] - - def __enter__(self): - Runner.__enter__(self) - return self - - def _copy_file_to_device(self): - with AdbCmd(handle=self.handle) as adbcmd: - adbcmd.push(self.src, self.dst) - - def _clean_tombstones(self): - cmd = ['rm', '-r', '/data/tombstones'] - with AdbCmd(handle=self.handle) as adbcmd: - adbcmd.shell(cmd) - - def _clean_log(self): - with AdbCmd(handle=self.handle) as adbcmd: - adbcmd.clear_logs() - - def _spoof_result(self): - ''' - Intended for debugging/testing purposes only. - - Places a dummy tombstone file into the /data/tombstones dir of the - emulator given by . Gives _wait_for_result_dir something to find. - :param handle: - ''' - # write a tempfile with some dummy text - fd, fname = tempfile.mkstemp(prefix='fake_tombstone_', text=True) - for c in 'ABCDE': - line = c * 80 + '\n' - os.write(fd, line) - os.close(fd) - - # copy the tempfile to the emulator - basename = os.path.basename(fname) - with AdbCmd(handle=self.handle) as adbcmd: - adbcmd.push(fname, '/data/tombstones/' + basename) - - def _wait_for_result_dir(self): - ''' - 1. Create a temp dir inside work_dir. - 2. Set a timer - 3. Until the timer expires, attempt to pull tombstone data from the - emulator specified in handle - 4. If anything is found, short circuit and return. - 5. If the timer expires, clean up and return nothing. - ''' - self.result_dir = None - check_for_result_dir_naptime = 1 - d = tempfile.mkdtemp(dir=self.workingdir) - expire_at = time.time() + self.runtimeout - while time.time() <= expire_at: - try: - with AdbCmd(handle=self.handle) as adbcmd: - adbcmd.pull('/data/tombstones', d) - except AdbCmdError as e: - # errors are okay, we'll just try again - logger.debug('Caught error getting tombstone, will retry: %s', - e) - - if len(os.listdir(d)): - self.result_dir = d - self.saw_crash = True - # shortcut if we got something - return - else: - time.sleep(check_for_result_dir_naptime) - - # if you got here, then the timer ran out and there's nothing in d - shutil.rmtree(d, ignore_errors=True) - - def _check_result(self): - ''' - Returns result dir if tombstone found, None otherwise. - :param handle: - :param work_dir: - ''' - # TODO: remove call to _spoof_result once we know check_result works - #self._spoof_result() - - self._wait_for_result_dir() - - if self.saw_crash: - logger.warning('Found crash tombstone') - else: - logger.debug('No tombstones found') - - @property - def _fuzzed_file_uri(self): - return 'file://{}'.format(self.dst) - - def _prerun(self): - self._copy_file_to_device() - self._clean_tombstones() - self._clean_log() - - def _run(self): - logger.debug('runner %d: %s', os.getpid(), self.handle) - logger.debug('intent: %s', self.intent) - ActivityManager(self.handle).start(self.intent, wait_for_launch=True) - - def _postrun(self): - self._check_result() - ActivityManager(self.handle).force_stop(self.intent.component) - # TODO: kill_all may not be necessary - ActivityManager(self.handle).kill_all() - logger.debug('Stopped %s' % self.intent.component) diff --git a/src/certfuzz/test/android/__init__.py b/src/certfuzz/test/android/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/test/android/api/__init__.py b/src/certfuzz/test/android/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/test/android/api/test_aapt.py b/src/certfuzz/test/android/api/test_aapt.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/android/api/test_aapt.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/api/test_activity_manager.py b/src/certfuzz/test/android/api/test_activity_manager.py deleted file mode 100644 index e8850d2..0000000 --- a/src/certfuzz/test/android/api/test_activity_manager.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.api import activity_manager - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/api/test_adb_cmd.py b/src/certfuzz/test/android/api/test_adb_cmd.py deleted file mode 100644 index 3147fd3..0000000 --- a/src/certfuzz/test/android/api/test_adb_cmd.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.api import adb_cmd - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/api/test_android_cmd.py b/src/certfuzz/test/android/api/test_android_cmd.py deleted file mode 100644 index 785b725..0000000 --- a/src/certfuzz/test/android/api/test_android_cmd.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.api import android_cmd - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/api/test_android_emulator.py b/src/certfuzz/test/android/api/test_android_emulator.py deleted file mode 100644 index d722828..0000000 --- a/src/certfuzz/test/android/api/test_android_emulator.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.api import android_emulator - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/api/test_android_manifest.py b/src/certfuzz/test/android/api/test_android_manifest.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/android/api/test_android_manifest.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/api/test_api_pkg.py b/src/certfuzz/test/android/api/test_api_pkg.py deleted file mode 100644 index 01e2296..0000000 --- a/src/certfuzz/test/android/api/test_api_pkg.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android import api - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/api/test_defaults.py b/src/certfuzz/test/android/api/test_defaults.py deleted file mode 100644 index 5a9f78f..0000000 --- a/src/certfuzz/test/android/api/test_defaults.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.api import defaults - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/api/test_intent.py b/src/certfuzz/test/android/api/test_intent.py deleted file mode 100644 index dce25d0..0000000 --- a/src/certfuzz/test/android/api/test_intent.py +++ /dev/null @@ -1,45 +0,0 @@ -# ''' -# Created on Feb 1, 2013 -# -# @organization: cert.org -# ''' -# import unittest -# import certfuzz.android.api.intent -# from certfuzz.android.api.intent import _attribute_to_option -# -# class Test(unittest.TestCase): -# -# def setUp(self): -# self.intent = certfuzz.android.api.intent.Intent() -# -# def tearDown(self): -# pass -# -# def test_as_args(self): -# for attribute in ('action', 'data_uri', 'mime_type'): -# self.assertTrue(hasattr(self.intent, attribute)) -# setattr(self.intent, attribute, attribute) -# self.assertIn(attribute, self.intent.as_args()) -# -# for opt in ('-a', '-d', '-t'): -# self.assertIn(opt, self.intent.as_args()) -# -# for attribute in certfuzz.android.api.intent._bool_attrs: -# self.assertTrue(hasattr(self.intent, attribute)) -# self.assertFalse(getattr(self.intent, attribute)) -# self.assertNotIn(attribute, self.intent.as_args()) -# setattr(self.intent, attribute, True) -# self.assertTrue(getattr(self.intent, attribute)) -# -# self.assertIn(_attribute_to_option(attribute), -# self.intent.as_args()) -# -# def test_attribute_to_option(self): -# for x in xrange(30): -# in_str = '_'.join('a' * x) -# out_str = '--' + '-'.join('a' * x) -# self.assertEqual(out_str, _attribute_to_option(in_str)) -# -# if __name__ == "__main__": -# # import sys;sys.argv = ['', 'Test.testName'] -# unittest.main() diff --git a/src/certfuzz/test/android/api/test_log_helper.py b/src/certfuzz/test/android/api/test_log_helper.py deleted file mode 100644 index 8af9255..0000000 --- a/src/certfuzz/test/android/api/test_log_helper.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.api import log_helper - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/avd_mgr/__init__.py b/src/certfuzz/test/android/avd_mgr/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/test/android/avd_mgr/test_avd_mgr_pkg.py b/src/certfuzz/test/android/avd_mgr/test_avd_mgr_pkg.py deleted file mode 100644 index 18d67b6..0000000 --- a/src/certfuzz/test/android/avd_mgr/test_avd_mgr_pkg.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android import avd_mgr - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/avd_mgr/test_cloner.py b/src/certfuzz/test/android/avd_mgr/test_cloner.py deleted file mode 100644 index 460334b..0000000 --- a/src/certfuzz/test/android/avd_mgr/test_cloner.py +++ /dev/null @@ -1,175 +0,0 @@ -# ''' -# Created on Jan 18, 2013 -# -# @organization: cert.org -# ''' -# import unittest -# from certfuzz.android.avd_mgr import cloner -# from certfuzz.android.avd_mgr.errors import AvdClonerError -# import tempfile -# import shutil -# import os -# from certfuzz.android.api.defaults import avddir, avddir_basename -# -# class Test(unittest.TestCase): -# -# def setUp(self): -# self.c = cloner.AvdCloner() -# self.tmpdir = tempfile.mkdtemp() -# -# def tearDown(self): -# shutil.rmtree(self.tmpdir) -# -# def test_init(self): -# c = self.c -# self.assertNotEqual(None, c.dst) -# self.assertTrue(c.remove) -# self.assertEqual(0, len(c._removables)) -# -# def test_enter(self): -# c = self.c -# self.assertEqual(None, c.src) -# self.assertRaises(AvdClonerError, c.__enter__) -# c.src = 'foo' -# self.assertEqual(c, c.__enter__()) -# -# def test_remove(self): -# c = self.c -# # add a file -# fd, f = tempfile.mkstemp(dir=self.tmpdir) -# os.close(fd) -# c._removables.append(f) -# # add a dir -# d = tempfile.mkdtemp(dir=self.tmpdir) -# c._removables.append(d) -# # test it -# self.assertTrue(os.path.exists(f)) -# self.assertTrue(os.path.exists(d)) -# c._remove() -# self.assertFalse(os.path.exists(f)) -# self.assertFalse(os.path.exists(d)) -# -# # make sure it doesn't choke on nonexistent files/dirs -# c._remove() -# -# def test_set_paths(self): -# c = self.c -# -# # raise error if src not set -# c.src = None -# c.dst = 'bar' -# self.assertRaises(AvdClonerError, c._set_paths) -# -# # raise error if neither src nor dst set -# c.src = None -# c.dst = None -# self.assertRaises(AvdClonerError, c._set_paths) -# -# # raise error if just dst not set -# c.src = 'foo' -# c.dst = None -# self.assertRaises(AvdClonerError, c._set_paths) -# -# c.src = 'foo' -# c.dst = 'bar' -# c._set_paths() -# self.assertEqual('foo.avd', os.path.basename(c._src_avddir)) -# self.assertEqual('bar.avd', os.path.basename(c._dst_avddir)) -# self.assertEqual('foo.ini', os.path.basename(c._src_inifile)) -# self.assertEqual('bar.ini', os.path.basename(c._dst_inifile)) -# -# def test_clone_avd_dir(self): -# c = self.c -# c.src = tempfile.mkdtemp(dir=self.tmpdir) -# c._set_paths() -# -# # raise exception if src doesn't exist -# self.assertEqual(0, len(c._removables)) -# self.assertFalse(os.path.exists(c._src_avddir)) -# self.assertRaises(OSError, c._clone_avd_dir) -# self.assertEqual(0, len(c._removables)) -# -# # raise exception if dst already exists -# c._src_avddir = tempfile.mkdtemp(dir=self.tmpdir) -# c._dst_avddir = tempfile.mkdtemp(dir=self.tmpdir) -# self.assertTrue(os.path.exists(c._src_avddir)) -# self.assertTrue(os.path.exists(c._dst_avddir)) -# self.assertRaises(OSError, c._clone_avd_dir) -# self.assertEqual(0, len(c._removables)) -# -# # create a file in src -# fd, f = tempfile.mkstemp(dir=c._src_avddir) -# basename = os.path.basename(f) -# dstfile = os.path.join(c._dst_avddir, basename) -# # make sure target does not exist -# shutil.rmtree(c._dst_avddir) -# self.assertFalse(os.path.exists(c._dst_avddir)) -# self.assertFalse(os.path.exists(dstfile)) -# self.assertEqual(0, len(c._removables)) -# c._clone_avd_dir() -# self.assertTrue(os.path.exists(c._dst_avddir)) -# self.assertTrue(os.path.exists(dstfile)) -# self.assertNotEqual(0, len(c._removables)) -# self.assertTrue(c._dst_avddir in c._removables) -# -# def test_clone_ini_file(self): -# c = self.c -# c.src = tempfile.mkdtemp(dir=self.tmpdir) -# c._set_paths() -# self.assertEqual(0, len(c._removables)) -# -# fd, f = tempfile.mkstemp(dir=self.tmpdir) -# os.close(fd) -# c._src_inifile = f -# -# fd, f = tempfile.mkstemp(dir=self.tmpdir) -# os.close(fd) -# os.remove(f) -# c._dst_inifile = f -# -# # src exists, dst doesn't, should succeed -# self.assertFalse(os.path.exists(c._dst_inifile)) -# c._removables = [] -# c._clone_ini_file() -# self.assertTrue(os.path.exists(c._dst_inifile)) -# self.assertTrue(c._dst_inifile in c._removables) -# -# # src exists, dst exists, should succeed -# c._removables = [] -# c._clone_ini_file() -# self.assertTrue(os.path.exists(c._dst_inifile)) -# self.assertTrue(c._dst_inifile in c._removables) -# -# # src does not exist, should fail -# os.remove(c._src_inifile) -# self.assertRaises(IOError, c._clone_ini_file) -# os.remove(c._dst_inifile) -# self.assertRaises(IOError, c._clone_ini_file) -# -# def test_fix_ini(self): -# c = self.c -# # write an ini file -# fd, f = tempfile.mkstemp(dir=self.tmpdir, text=True) -# doc = "AAAAA\npath=foo\nBBBBB\n" -# os.write(fd, doc) -# os.close(fd) -# c._dst_inifile = f -# -# c._dst_avddir = self.tmpdir -# -# # expected: path=foo replaced by path=bar, all other lines untouched -# c._fix_ini() -# self.assertTrue(os.path.exists(c._dst_inifile)) -# with open(c._dst_inifile, 'r') as f: -# content = f.read() -# self.assertFalse('foo' in content) -# -# lines = content.splitlines() -# self.assertEqual('AAAAA', lines[0]) -# self.assertEqual('path=%s' % self.tmpdir, lines[1]) -# self.assertEqual('BBBBB', lines[2]) -# self.assertEqual(3, len(lines)) -# -# if __name__ == "__main__": -# # import sys;sys.argv = ['', 'Test.testName'] -# unittest.main() diff --git a/src/certfuzz/test/android/avd_mgr/test_main.py b/src/certfuzz/test/android/avd_mgr/test_main.py deleted file mode 100644 index 15e7a99..0000000 --- a/src/certfuzz/test/android/avd_mgr/test_main.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.avd_mgr import main - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/avd_mgr/test_orphan_catcher.py b/src/certfuzz/test/android/avd_mgr/test_orphan_catcher.py deleted file mode 100644 index 7ce8a36..0000000 --- a/src/certfuzz/test/android/avd_mgr/test_orphan_catcher.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.avd_mgr import orphan_catcher - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/controller/__init__.py b/src/certfuzz/test/android/controller/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/test/android/controller/test_controller_pkg.py b/src/certfuzz/test/android/controller/test_controller_pkg.py deleted file mode 100644 index 952e484..0000000 --- a/src/certfuzz/test/android/controller/test_controller_pkg.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/test_android_pkg.py b/src/certfuzz/test/android/test_android_pkg.py deleted file mode 100644 index 641f2fe..0000000 --- a/src/certfuzz/test/android/test_android_pkg.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz import android - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/worker/__init__.py b/src/certfuzz/test/android/worker/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/test/android/worker/test_tasks.py b/src/certfuzz/test/android/worker/test_tasks.py deleted file mode 100644 index 15884eb..0000000 --- a/src/certfuzz/test/android/worker/test_tasks.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.android.worker import tasks - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/worker/test_worker.py b/src/certfuzz/test/android/worker/test_worker.py deleted file mode 100644 index e4b06fd..0000000 --- a/src/certfuzz/test/android/worker/test_worker.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.android.worker import worker - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/android/worker/test_worker_pkg.py b/src/certfuzz/test/android/worker/test_worker_pkg.py deleted file mode 100644 index 952e484..0000000 --- a/src/certfuzz/test/android/worker/test_worker_pkg.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/bff/test_android.py b/src/certfuzz/test/bff/test_android.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/bff/test_android.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/campaign/config/test_android_config.py b/src/certfuzz/test/campaign/config/test_android_config.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/campaign/config/test_android_config.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/campaign/test_campaign_android.py b/src/certfuzz/test/campaign/test_campaign_android.py deleted file mode 100644 index 826d4c7..0000000 --- a/src/certfuzz/test/campaign/test_campaign_android.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.campaign import campaign_android - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/crash/test_android_testcase.py b/src/certfuzz/test/crash/test_android_testcase.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/crash/test_android_testcase.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/db/__init__.py b/src/certfuzz/test/db/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/test/db/couchdb/__init__.py b/src/certfuzz/test/db/couchdb/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/test/db/couchdb/datatypes/__init__.py b/src/certfuzz/test/db/couchdb/datatypes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/test/db/couchdb/datatypes/test_campaign_doc.py b/src/certfuzz/test/db/couchdb/datatypes/test_campaign_doc.py deleted file mode 100644 index 3bd4a46..0000000 --- a/src/certfuzz/test/db/couchdb/datatypes/test_campaign_doc.py +++ /dev/null @@ -1,32 +0,0 @@ -# ''' -# Created on Mar 15, 2013 -# -# @organization: cert.org -# ''' -# import unittest -# from certfuzz.db.couchdb.datatypes import campaign_doc -# -# class Test(unittest.TestCase): -# -# def setUp(self): -# self.doc = campaign_doc.CampaignDoc() -# -# def tearDown(self): -# pass -# -# def test_campaign_doc(self): -# print dir(self.doc) -# -# try: -# from couchdb.mapping import Document -# self.assertIsInstance(self.doc, Document) -# except ImportError: -# pass -# -# self.assertEqual('Campaign', self.doc.doctype) -# for field in ['doctype', 'campaign_type', 'config', 'added']: -# self.assertTrue(hasattr(self.doc, field)) -# -# if __name__ == "__main__": -# # import sys;sys.argv = ['', 'Test.testName'] -# unittest.main() diff --git a/src/certfuzz/test/db/couchdb/datatypes/test_datatypes_pkg.py b/src/certfuzz/test/db/couchdb/datatypes/test_datatypes_pkg.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/db/couchdb/datatypes/test_datatypes_pkg.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/db/couchdb/datatypes/test_file_doc.py b/src/certfuzz/test/db/couchdb/datatypes/test_file_doc.py deleted file mode 100644 index 7f9ee69..0000000 --- a/src/certfuzz/test/db/couchdb/datatypes/test_file_doc.py +++ /dev/null @@ -1,23 +0,0 @@ -''' -Created on Mar 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.db.couchdb.datatypes import file_doc - - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/db/couchdb/datatypes/test_testcase_doc.py b/src/certfuzz/test/db/couchdb/datatypes/test_testcase_doc.py deleted file mode 100644 index ec1af37..0000000 --- a/src/certfuzz/test/db/couchdb/datatypes/test_testcase_doc.py +++ /dev/null @@ -1,23 +0,0 @@ -''' -Created on Mar 18, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.db.couchdb.datatypes import testcase_doc - - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/db/couchdb/test_couchdb_pkg.py b/src/certfuzz/test/db/couchdb/test_couchdb_pkg.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/db/couchdb/test_couchdb_pkg.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/db/couchdb/test_db.py b/src/certfuzz/test/db/couchdb/test_db.py deleted file mode 100644 index 13115f5..0000000 --- a/src/certfuzz/test/db/couchdb/test_db.py +++ /dev/null @@ -1,24 +0,0 @@ -''' -Created on Mar 18, 2013 - -@organization: cert.org -''' -import unittest -from certfuzz.db.couchdb import db - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_format_url(self): - import itertools - for host, port in itertools.product(['foo', 'bar', 'baz', 'quux'], range(20)): - self.assertEqual('http://' + host + ':' + str(port) + '/', db.format_url(host, port)) - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/db/test_db_pkg.py b/src/certfuzz/test/db/test_db_pkg.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/db/test_db_pkg.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/iteration/test_iteration_android.py b/src/certfuzz/test/iteration/test_iteration_android.py deleted file mode 100644 index 5c13ca6..0000000 --- a/src/certfuzz/test/iteration/test_iteration_android.py +++ /dev/null @@ -1,23 +0,0 @@ -''' -Created on Feb 22, 2013 - -@organization: cert.org -''' -import unittest -# from certfuzz.iteration import iteration_android - - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/runners/test_android_runner.py b/src/certfuzz/test/runners/test_android_runner.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/runners/test_android_runner.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/tools/android/__init__.py b/src/certfuzz/test/tools/android/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/test/tools/android/test_apk_dumper.py b/src/certfuzz/test/tools/android/test_apk_dumper.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/tools/android/test_apk_dumper.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/tools/android/test_config_tools.py b/src/certfuzz/test/tools/android/test_config_tools.py deleted file mode 100644 index 4134c48..0000000 --- a/src/certfuzz/test/tools/android/test_config_tools.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/tools/android/__init__.py b/src/certfuzz/tools/android/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/certfuzz/tools/android/apk_dumper.py b/src/certfuzz/tools/android/apk_dumper.py deleted file mode 100644 index 6f9245b..0000000 --- a/src/certfuzz/tools/android/apk_dumper.py +++ /dev/null @@ -1,51 +0,0 @@ -''' -Created on Feb 28, 2013 - -@organization: cert.org -''' -import logging -import argparse -from certfuzz.android.api.aapt import Aapt -from certfuzz.android.api.android_manifest import AndroidManifest -import os - - -def main(): - logger = logging.getLogger() - hdlr = logging.StreamHandler() - logger.addHandler(hdlr) - - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument('--debug', help='', action='store_true') - group.add_argument('-v', '--verbose', help='', action='store_true') - parser.add_argument('apk', - help='Path to an apk', - type=str) - - args = parser.parse_args() - - if args.debug: - logger.setLevel(logging.DEBUG) - elif args.verbose: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.WARNING) - - aapt = Aapt() - aapt.get_manifest(os.path.expanduser(args.apk)) - manifest_text = aapt.stdout - - manifest = AndroidManifest(manifest_text) - - vstr = '{} {}'.format(os.path.basename(args.apk), manifest.version_info) - print '#' * (len(vstr) + 4) - print '# {} #'.format(vstr) - print '#' * (len(vstr) + 4) - print - for mimetype in manifest.mimetypes: - print mimetype - print - -if __name__ == '__main__': - main() diff --git a/src/certfuzz/tools/android/config_tools.py b/src/certfuzz/tools/android/config_tools.py deleted file mode 100644 index d1d196e..0000000 --- a/src/certfuzz/tools/android/config_tools.py +++ /dev/null @@ -1,79 +0,0 @@ -''' -Created on Jun 21, 2013 - -@organization: cert.org -''' - -import logging -import os -import shutil -import yaml -import argparse -from certfuzz.android.api.log_helper import log_formatter - -logger = logging.getLogger() -hdlr = logging.StreamHandler() -hdlr.setFormatter(log_formatter()) -logger.addHandler(hdlr) - - -def main(): - parser = argparse.ArgumentParser(description='Create a configuration file with the default options') - parser.add_argument('--force', '-f', - help='Overwrite the config file if it already exists', - action='store_true', default=False) - parser.add_argument('--directory', '--dir', '-d', - help='The directory in which to store the config file', - default='config') - group = parser.add_mutually_exclusive_group() - group.add_argument('--debug', help='', action='store_true') - group.add_argument('--verbose', help='', action='store_true') - args = parser.parse_args() - - if args.debug: - logger.setLevel(logging.DEBUG) - elif args.verbose: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.WARNING) - - populate_config_dir(force=args.force, directory=args.directory) - - -def populate_config_dir(force=False, directory='config'): - - prefix = 'android' - config_files = ['certfuzz/android/config.yaml'] - - # TODO should there be some check in place to ensure that this is running - # from the correct working directory? (i.e. ~/android/src/). Otherwise - # it will break when trying to find the config files. - - if not os.path.exists(directory): - os.mkdir(directory) - - for f in config_files: - cp_path = '%s/%s_%s' % (directory, prefix, os.path.basename(f)) - config_exists = os.path.exists(cp_path) - - if config_exists: - logger.warning('%s already exists' % cp_path) - if not force: - logger.warning('Please specify --overwrite if you wish to overwrite %s' % cp_path) - - if force or not config_exists: - logger.info('Copying %s to %s' % (f, cp_path)) - shutil.copy(f, cp_path) - - -def load_config(cfg_file): - - if os.path.exists(cfg_file): - logging.info('Loading config: %s' % cfg_file) - return yaml.safe_load(open(cfg_file, 'r')) - else: - logger.error('Could not find config: %s' % cfg_file) - return None - -if __name__ == "__main__": - main() diff --git a/src/experimental/README.md b/src/experimental/README.md index 6f26d74..1ab773e 100644 --- a/src/experimental/README.md +++ b/src/experimental/README.md @@ -2,10 +2,6 @@ These are things that were spinoffs from the main BFF and FOE development effort. They worked at a point in time, but aren't actively supported and we have no plans to develop them further at this time. Although they're not in a fully releasable form, we figured somebody might find some of it useful if they're willing to dig a bit. -## android ## - -In 2013 we put some effort into porting BFF to work on Android. The idea was that you'd start with the BFF linux VM and install the Android SDK. The basic program flow is as follows: BFF for Android runs in the linux machine. It clones AVDs, fuzzes seed files, copies fuzzed files into the AVD's SD card, injects an explicit intent to tell an app to open the fuzzed file, and checks to see if the app crashed by looking for a tombstone. If it finds one, it collects data about the crash and dumps it into a couchdb instance. We got approximately that far, but the project ended and we haven't revisited it since. See also the code in `certfuzz\android` for implementation details. However, our subsequent experience is that file parsing vulnerabilities are pretty far down the list of ways you can attack Android apps -- there are any number of lower hanging fruit. - ## aws ## This was a student project by Shaun Blackburn. We gave him a copy of BFF 2.6 and asked him to make it work in AWS, which he did. The resulting cloudinit script is here, along with some slides and a readme for background. It worked in the spring of 2013, but we are not actively maintaining it. diff --git a/src/experimental/android/MANIFEST.in b/src/experimental/android/MANIFEST.in deleted file mode 100644 index d003902..0000000 --- a/src/experimental/android/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include *.txt -recursive-include certfuzz *.py *.yaml -recursive-include examples *.txt *.py *.yaml diff --git a/src/experimental/android/readme.txt b/src/experimental/android/readme.txt deleted file mode 100644 index a7445d8..0000000 --- a/src/experimental/android/readme.txt +++ /dev/null @@ -1,7 +0,0 @@ -You should try this out using virtualenv: - -virtualenv testenv -source testenv/bin/activate -python setup.py install -bff_avd_mgr -deactivate \ No newline at end of file diff --git a/src/experimental/android/refresh-virtualenv.sh b/src/experimental/android/refresh-virtualenv.sh deleted file mode 100755 index ccc4299..0000000 --- a/src/experimental/android/refresh-virtualenv.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -ENVNAME=virtualenv-certfuzz - -svn update -source $ENVNAME/bin/activate -python setup.py install diff --git a/src/experimental/android/scripts/reset_bff_android.sh b/src/experimental/android/scripts/reset_bff_android.sh deleted file mode 100755 index 42a3166..0000000 --- a/src/experimental/android/scripts/reset_bff_android.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash - -# Sometimes you need to clean or nuke the VM in which BFF runs. -# This script will remove the output directories and -# local copy of the config file so that you can start -# fresh. Probably best to reboot afterwards - -ROOT= -AVD_DIR=~/.android/avd -RUN_SCRIPT=$ROOT/foo -CONFIG=$ROOT/config/android_config.yaml - -function get_config_param(){ - val=`grep -v '#' $CONFIG | grep $1: | tr -d ' ' | awk -F: '{ print \$2 }'` -} - - -if [ $# -lt 1 ] -then - echo "For your safety, this script won't run unless you provide an argument." - echo - echo "E.g.," $0 " [--reboot]" - echo - echo "With any argument other than --remove-results, this script will simply kill any" - echo "running BFF batch.sh, remove the local fuzzing directory, and reset" - echo "the local copy of memcached." - echo - echo "However, --remove-results will wipe out the remote results directory as well " - echo "so only use it if you really, really mean it." - echo - echo "If the second arg is --reboot, the machine will be rebooted." - exit 1 -fi - -# kill any running bff stuff -ps -ef| grep bff | awk '{print $2;}' | xargs kill -9 -ps -ef| grep android | awk '{print $2;}' | xargs kill -9 - -# wipe the local files -rm -rfv $ROOT/fuzzing -rm -rfv $ROOT/log/*.log -rm -rfv $AVD_DIR/*clone* - -# wipe the remote results too -if [ "$1" = "--remove-results" ]; then - DB_HOST=$(get_config_param 'host') - DB_PORT=$(get_config_param 'port') - DB_USER=$(get_config_param 'username') - DB_PASS=$(get_config_param 'password') - DB_NAME=$(get_config_param 'dbname') - DB_CREDS='' - if [[ ! -z "$DB_USER" ]] && [[ ! -z "$DB_PASS" ]]; then - DB_CREDS=$DB_USER:$DB_PASS - fi - - echo Attempting to delete results at $DB_HOST:$DB_PORT/$DB_NAME - curl -X DELETE http://$DB_CREDS@$DB_HOST:$DB_PORT/$DB_NAME -fi - -# wipe the config, if necessary -if [ "$2" = "--reset-config" ]; then - sudo rm -rf $ROOT/config - echo Calling $RESET_CONFIG - exec $RESET_CONFIG -fi - -if [ "$3" = "--reboot" ]; then - # reboot the vm - echo Rebooting machine... - sudo reboot -fi - - - diff --git a/src/experimental/android/scripts/start_bff_android.sh b/src/experimental/android/scripts/start_bff_android.sh deleted file mode 100755 index 217ab3b..0000000 --- a/src/experimental/android/scripts/start_bff_android.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -cd ~/android/src -source include virtualenv-certfuzz/bin/activate -xterm -e bff_android --debug 0 & diff --git a/src/experimental/android/scripts/ubufuzz_first_time_setup.sh b/src/experimental/android/scripts/ubufuzz_first_time_setup.sh deleted file mode 100755 index 5679d25..0000000 --- a/src/experimental/android/scripts/ubufuzz_first_time_setup.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# HINT: run me with sudo... -cd ~ -apt-get update -# per http://askubuntu.com/questions/318246/complete-installation-guide-for-android-sdk-on-ubuntu -apt-get install -y libgl1-mesa-dev openjdk-7-jdk -# install couchdb locally -apt-get install -y couchdb - -# get the android sdk -wget http://dl.google.com/android/android-sdk_r22.0.5-linux.tgz -tar zxvf android-sdk_r22.0.5-linux.tgz -# link it to a generic name -ln -s android-sdk-linux android-sdk - -# code goes into ~/android... -mkdir ~/android diff --git a/src/experimental/android/setup-virtualenv.sh b/src/experimental/android/setup-virtualenv.sh deleted file mode 100755 index 5f8f63c..0000000 --- a/src/experimental/android/setup-virtualenv.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -ENVNAME=virtualenv-certfuzz - -rm -rf $ENVNAME -virtualenv --system-site-packages $ENVNAME - -./refresh-virtualenv.sh - -deactivate - -echo "Activate virtualenv using 'source $ENVNAME/bin/activate'" From d2b84a98fd399f9a69c4f06d9f8f715f30501ad1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 12:56:06 -0400 Subject: [PATCH 0328/1169] clean out mentions of android in errors, setup etc. --- src/certfuzz/campaign/errors.py | 4 ---- src/certfuzz/crash/__init__.py | 1 - src/certfuzz/crash/errors.py | 3 --- src/certfuzz/runners/errors.py | 3 --- src/setup.cfg | 1 - src/setup.py | 13 +++++-------- 6 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/certfuzz/campaign/errors.py b/src/certfuzz/campaign/errors.py index 6bf3ec8..0173b26 100644 --- a/src/certfuzz/campaign/errors.py +++ b/src/certfuzz/campaign/errors.py @@ -10,9 +10,5 @@ class CampaignError(CERTFuzzError): pass -class AndroidCampaignError(CampaignError): - pass - - class CampaignScriptError(CampaignError): pass diff --git a/src/certfuzz/crash/__init__.py b/src/certfuzz/crash/__init__.py index f20efe4..bbc1ec0 100644 --- a/src/certfuzz/crash/__init__.py +++ b/src/certfuzz/crash/__init__.py @@ -3,4 +3,3 @@ from certfuzz.crash.bff_crash import BffCrash from certfuzz.crash.errors import CrashError from certfuzz.crash.errors import TestCaseError -from certfuzz.crash.errors import AndroidTestCaseError diff --git a/src/certfuzz/crash/errors.py b/src/certfuzz/crash/errors.py index f6aa42b..e85f0d7 100644 --- a/src/certfuzz/crash/errors.py +++ b/src/certfuzz/crash/errors.py @@ -13,6 +13,3 @@ class TestCaseError(CERTFuzzError): class CrashError(TestCaseError): pass - -class AndroidTestCaseError(TestCaseError): - pass diff --git a/src/certfuzz/runners/errors.py b/src/certfuzz/runners/errors.py index 1659a86..baf3890 100644 --- a/src/certfuzz/runners/errors.py +++ b/src/certfuzz/runners/errors.py @@ -21,6 +21,3 @@ class RunnerPlatformVersionError(RunnerError): class RunnerRegistryError(RunnerError): pass - -class AndroidRunnerError(RunnerError): - pass diff --git a/src/setup.cfg b/src/setup.cfg index e108a56..e3fc0d5 100644 --- a/src/setup.cfg +++ b/src/setup.cfg @@ -1,5 +1,4 @@ [nosetests] -ignore-files=android exclude=test_probability verbosity=2 detailed-errors=1 diff --git a/src/setup.py b/src/setup.py index 9c1d793..ca888e8 100644 --- a/src/setup.py +++ b/src/setup.py @@ -62,13 +62,10 @@ def _entry_points(): eps['console_scripts'] = console_scripts return eps -def _scripts(): - _s = [ -# 'scripts/start_bff_android.sh', -# 'scripts/reset_bff_android.sh', -# 'scripts/ubufuzz_first_time_setup.sh', - ] - return _s +#def _scripts(): +# _s = [ +# ] +# return _s setup(name="CERT_Basic_Fuzzing_Framework", version="3.0a", @@ -86,7 +83,7 @@ def _scripts(): 'numpy', # 'matplotlib', ], - scripts=_scripts(), +# scripts=_scripts(), entry_points=_entry_points(), include_package_data=True, test_suite='certfuzz.test', From e249ba8ae3a5ebe5cba28ffb69b78166f6f4066a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 25 Jun 2014 16:55:36 -0400 Subject: [PATCH 0329/1169] Fix typo in crashwrangler zip name in OSX installer --- .../BFF_installer.pmdoc/1144497_crashwrangler-contents.xml | 2 +- .../installer/BFF_installer.pmdoc/1144497_crashwrangler.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml index 0bc2b3a..be4f5b4 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml @@ -1,5 +1,5 @@ diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml index ea36305..208cc4c 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml @@ -1,13 +1,13 @@ - com.cert.cc.certBff.52894_crashwrangler.pkg + com.cert.cc.certBff.52854_crashwrangler.pkg 1.0 - 52894_crashwrangler.zip + 52854_crashwrangler.zip /Applications/BFF.app/Contents/Resources/packages From a83abbc87820c9ebf40f4a95daf39f0a921a34a3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 26 Jun 2014 10:26:09 -0400 Subject: [PATCH 0330/1169] Check return codes for dist build utilities (nsis, packagemaker, hdiutil, etc.) BFF-752 --- build/distmods/build_base2.py | 3 +++ build/distmods/osx/darwin_build2.py | 4 ++-- build/distmods/windows/windows_build2.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/build/distmods/build_base2.py b/build/distmods/build_base2.py index 48c9b23..11211cb 100644 --- a/build/distmods/build_base2.py +++ b/build/distmods/build_base2.py @@ -13,6 +13,7 @@ from devmods.misc import copydir, copyfile, onerror, mdtotextfile from .prepend_license import main as _prepend_license +from subprocess import CalledProcessError logger = logging.getLogger(__name__) @@ -68,6 +69,8 @@ def __enter__(self): def __exit__(self, etype, value, traceback): handled = False + if etype is CalledProcessError: + logger.debug('Error returned from called processs: %s' % value) logger.debug('Removing temp dir %s', self.tmp_dir) shutil.rmtree(self.tmp_dir) self._in_runtime_context = False diff --git a/build/distmods/osx/darwin_build2.py b/build/distmods/osx/darwin_build2.py index d60a819..0eb7fa3 100644 --- a/build/distmods/osx/darwin_build2.py +++ b/build/distmods/osx/darwin_build2.py @@ -22,7 +22,7 @@ def hdiutil(command, *parameters): args = ['hdiutil', command] args.extend(parameters) logger.debug(args) - subprocess.call(args) + subprocess.check_call(args) # mac-specific @@ -34,7 +34,7 @@ def packagemaker(working_dir='.', *parameters): os.chdir(working_dir) logger.debug('cd %s && %s', working_dir, args) - subprocess.call(args) + subprocess.check_call(args) # popd os.chdir(oldcwd) diff --git a/build/distmods/windows/windows_build2.py b/build/distmods/windows/windows_build2.py index 7bf55ae..e395fcd 100644 --- a/build/distmods/windows/windows_build2.py +++ b/build/distmods/windows/windows_build2.py @@ -43,7 +43,7 @@ def package(self): # invoke makensis on the file we just made logger.debug('invoking makensis on %s', nsifile) - subprocess.call(['makensis', nsifile]) + subprocess.check_call(['makensis', nsifile]) distpath = 'BFF-windows-export' if self.build_dir: From fa98a3267fe56e8ae482912f4757cf604b447106 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 26 Jun 2014 10:26:45 -0400 Subject: [PATCH 0331/1169] Update crashwrangler zip provided by BFF --- .../1144497_crashwrangler-contents.xml | 6 +- .../1144497_crashwrangler.xml | 55 +++++++++---------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml index be4f5b4..69b34ea 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml @@ -1,5 +1 @@ - - - + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml index 208cc4c..3a08b78 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml @@ -1,31 +1,28 @@ - - - com.cert.cc.certBff.52854_crashwrangler.pkg - 1.0 - - - - - 52854_crashwrangler.zip - - /Applications/BFF.app/Contents/Resources/packages - - - - - installTo.path - installFrom.isRelativeType - installFrom.path - parent - installTo - - - 1144497_crashwrangler-contents.xml - /CVS$ - /\.svn$ - /\.cvsignore$ - /\.cvspass$ - /\.DS_Store$ - + +com.cert.cc.certBff.44497_crashwrangler.pkg +1.0 + + + +52854_crashwrangler.zip +/Applications/BFF.app/Contents/Resources/packages + + + + +installTo.path +installFrom.isRelativeType +installFrom.path +parent +installTo + + +1144497_crashwrangler-contents.xml +/CVS$ +/\.svn$ +/\.cvsignore$ +/\.cvspass$ +/\.DS_Store$ + From c46e1d8887ea091b352f7502901a01b147133170 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 26 Jun 2014 10:27:25 -0400 Subject: [PATCH 0332/1169] Remove tidy indent and wrap, as they break PackageMaker --- .../BFF_installer.pmdoc/12exc-contents.xml | 4 +- .../installer/BFF_installer.pmdoc/12exc.xml | 52 ++-- .../installer/BFF_installer.pmdoc/13exc.xml | 34 ++- .../installer/BFF_installer.pmdoc/14exc.xml | 34 ++- .../installer/BFF_installer.pmdoc/28exc.xml | 36 ++- .../installer/BFF_installer.pmdoc/index.xml | 265 ++++++++---------- 6 files changed, 187 insertions(+), 238 deletions(-) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/12exc-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/12exc-contents.xml index ee97d79..3384988 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/12exc-contents.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/12exc-contents.xml @@ -1,5 +1,3 @@ - + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/12exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/12exc.xml index 2cf1843..471fd93 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/12exc.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/12exc.xml @@ -1,29 +1,27 @@ - - - com.cert.cc.certBff.exc_handler_mountain_lion.pkg - 1.0 - - - - - crashwrangler/binaries/exc_handler_mountain_lion - /usr/local/bin - - - - - installFrom.isRelativeType - installFrom.path - parent - installTo - - - 12exc-contents.xml - /CVS$ - /\.svn$ - /\.cvsignore$ - /\.cvspass$ - /\.DS_Store$ - + +com.cert.cc.certBff.exc_handler_mountain_lion.pkg +1.0 + + + +crashwrangler/binaries/exc_handler_mountain_lion +/usr/local/bin + + + + +installFrom.isRelativeType +installFrom.path +parent +installTo + + +12exc-contents.xml +/CVS$ +/\.svn$ +/\.cvsignore$ +/\.cvspass$ +/\.DS_Store$ + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/13exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/13exc.xml index 8c40e1f..22fa197 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/13exc.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/13exc.xml @@ -1,20 +1,18 @@ - - - com.cert.cc.certBff.exc_handler_lion.pkg - 1.0 - - - - - crashwrangler/binaries/exc_handler_lion - /usr/local/bin - - - - - parent - installFrom.isRelativeType - installTo - + +com.cert.cc.certBff.exc_handler_lion.pkg +1.0 + + + +crashwrangler/binaries/exc_handler_lion +/usr/local/bin + + + + +parent +installFrom.isRelativeType +installTo + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/14exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/14exc.xml index d57bfba..c10ee9f 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/14exc.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/14exc.xml @@ -1,20 +1,18 @@ - - - com.cert.cc.certBff.exc_handler_snowleopard.pkg - 1.0 - - - - - crashwrangler/binaries/exc_handler_snowleopard - /usr/local/bin - - - - - parent - installFrom.isRelativeType - installTo - + +com.cert.cc.certBff.exc_handler_snowleopard.pkg +1.0 + + + +crashwrangler/binaries/exc_handler_snowleopard +/usr/local/bin + + + + +parent +installFrom.isRelativeType +installTo + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/28exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/28exc.xml index d57bfba..a0a90f0 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/28exc.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/28exc.xml @@ -1,20 +1,18 @@ - - - - com.cert.cc.certBff.exc_handler_snowleopard.pkg - 1.0 - - - - - crashwrangler/binaries/exc_handler_snowleopard - /usr/local/bin - - - - - parent - installFrom.isRelativeType - installTo - + + +com.cert.cc.certBff.exc_handler_mavericks.pkg +1.0 + + + +crashwrangler/binaries/exc_handler_mavericks +/usr/local/bin + + + + +parent +installFrom.isRelativeType +installTo + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index 8efb834..af64abf 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -1,155 +1,114 @@ - - CERT BFF - /Users/wd/bff_trunk/installer/build/Install CERT - BFF.pkg - com.cert.cc - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CERT_SEI_CMU_large_trans.png - - License.txt - - Readme.txt - - - - 01bff.xml - 02bff.xml - 03bff.xml - 04bff.xml - 05bff.xml - 06setuptools.xml - 07python.xml - 08hcluster.xml - 09zzuf.xml - 10libcaca.xml - 1144497_crashwrangler.xml - 12exc.xml - 13exc.xml - 14exc.xml - 15convert.xml - 16libzzuf.xml - 17imagemagick.xml - 18bff.xml - 19bff.xml - 20valgrind.xml - 21zzuf.xml - 22libcaca.xml - 23valgrind.xml - 24imagemagick.xml - 25valgrind.xml - 26callgrind.xml - 27pyyaml.xml - 28exc.xml - properties.customizeOption - properties.title + +CERT BFF +/Users/wd/bff_trunk/installer/build/Install CERT BFF.pkg +com.cert.cc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +CERT_SEI_CMU_large_trans.png +License.txt +Readme.txt + + + +01bff.xml +02bff.xml +03bff.xml +04bff.xml +05bff.xml +06setuptools.xml +07python.xml +08hcluster.xml +09zzuf.xml +10libcaca.xml +1144497_crashwrangler.xml +12exc.xml +13exc.xml +14exc.xml +15convert.xml +16libzzuf.xml +17imagemagick.xml +18bff.xml +19bff.xml +20valgrind.xml +21zzuf.xml +22libcaca.xml +23valgrind.xml +24imagemagick.xml +25valgrind.xml +26callgrind.xml +27pyyaml.xml +28exc.xml +properties.customizeOption +properties.title From 37b9d2ca5a9faedd11bf84f24c2611314d53aa56 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 09:11:32 -0400 Subject: [PATCH 0333/1169] ignore eclipse builders --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c151224..1bd99b6 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ src/windows/COPYING.txt build/distmods/osx/installer/bff/ build/distmods/osx/installer/*.txt *.stackdump +.teeproject +.externalToolBuilders From 2e860fbb0994bfcab65812accbe0d12a886c972a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 10:40:40 -0400 Subject: [PATCH 0334/1169] eliminate run_without_timer method --- src/certfuzz/fuzztools/process_killer.py | 4 ++-- src/certfuzz/fuzztools/subprocess_helper.py | 7 ------ src/certfuzz/fuzztools/watchdog.py | 4 ++-- src/certfuzz/fuzztools/zzuf.py | 6 ++--- .../test/fuzztools/test_subprocess_helper.py | 22 ++++--------------- 5 files changed, 11 insertions(+), 32 deletions(-) diff --git a/src/certfuzz/fuzztools/process_killer.py b/src/certfuzz/fuzztools/process_killer.py index 319a790..0d58d22 100644 --- a/src/certfuzz/fuzztools/process_killer.py +++ b/src/certfuzz/fuzztools/process_killer.py @@ -3,8 +3,8 @@ @organization: cert.org ''' -from certfuzz.fuzztools import subprocess_helper as subp import logging +import subprocess logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -28,4 +28,4 @@ def go(self): ''' command = self._get_cmdline() logger.debug('Running [%s]', command) - subp.run_without_timer(command) + subprocess.call(command, shell=True) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 82bae3c..5bd7c57 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -83,13 +83,6 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): return p -def run_without_timer(command): - ''' - Runs command, returns return code - ''' - return subprocess.call(command, shell=True) - - def _kill(p, returncode, killprocname): #@UnusedVariable if (on_windows()): """_kill function for Win32""" diff --git a/src/certfuzz/fuzztools/watchdog.py b/src/certfuzz/fuzztools/watchdog.py index eefe1ad..d8d3e87 100644 --- a/src/certfuzz/fuzztools/watchdog.py +++ b/src/certfuzz/fuzztools/watchdog.py @@ -7,7 +7,7 @@ import logging import platform -from certfuzz.fuzztools import subprocess_helper as subp +import subprocess system = platform.system() @@ -41,4 +41,4 @@ def go(self): logger.warning('WatchDog does not support %s', system) return - subp.run_without_timer(self.cmdline) + subprocess.call(self.cmdline, shell=True) diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index cf9ce28..3f429b7 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -8,6 +8,7 @@ import logging from certfuzz.fuzztools import subprocess_helper as subp +import subprocess logger = logging.getLogger(__name__) @@ -33,8 +34,7 @@ def _set_cmdline(self): self.cmdline = 'cat %s | zzuf -s%s -r%s > %s' % (self.seedfile, self.seed, self.range, self.outfile) def generate(self): - subp.run_without_timer(self.cmdline) - + subprocess.call(self.cmdline, shell=True) class Zzuf: def __init__(self, dir, s1, s2, cmd, seedfile, file, copymode, ratiomin, ratiomax, timeout, quiet=True): @@ -83,7 +83,7 @@ def go(self): given parameters. ''' command = self._get_go_fuzz_cmdline() - retcode = subp.run_without_timer(command) + retcode = subprocess.call(command, shell=True) if retcode: self.saw_crash = True return self.saw_crash diff --git a/src/certfuzz/test/fuzztools/test_subprocess_helper.py b/src/certfuzz/test/fuzztools/test_subprocess_helper.py index 678237d..559f8f8 100644 --- a/src/certfuzz/test/fuzztools/test_subprocess_helper.py +++ b/src/certfuzz/test/fuzztools/test_subprocess_helper.py @@ -1,15 +1,14 @@ -import os -import tempfile -from certfuzz.fuzztools.subprocess_helper import run_with_timer -from certfuzz.fuzztools.subprocess_helper import run_without_timer - ''' Created on Apr 8, 2011 @organization: cert.org ''' +import os +import tempfile +from certfuzz.fuzztools.subprocess_helper import run_with_timer import unittest + class Test(unittest.TestCase): def delete_file(self, f): os.remove(f) @@ -40,19 +39,6 @@ def test_run_with_timer2(self): # clean up self.delete_file(f) - def test_run_without_timer(self): - # we just want a tempfile name, not the actual file - (fd, f) = tempfile.mkstemp(text=True) - os.close(fd) - self.delete_file(f) - - run_without_timer('touch %s' % f) - # if the file exists, we win! - self.assertTrue(os.path.exists(f)) - - # clean up - self.delete_file(f) - def test_killall(self): #TODO: how do you test this? pass From ea726b3b314999dd9c22de7d88138cfcf8694eea Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 10:50:23 -0400 Subject: [PATCH 0335/1169] use check_call instead of call --- src/certfuzz/fuzztools/zzuf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index 3f429b7..b80c924 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -34,7 +34,8 @@ def _set_cmdline(self): self.cmdline = 'cat %s | zzuf -s%s -r%s > %s' % (self.seedfile, self.seed, self.range, self.outfile) def generate(self): - subprocess.call(self.cmdline, shell=True) + subprocess.check_call(self.cmdline, shell=True) + class Zzuf: def __init__(self, dir, s1, s2, cmd, seedfile, file, copymode, ratiomin, ratiomax, timeout, quiet=True): From f86b5e5181864970f53aed62356c9f9ac9723bf4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 10:50:54 -0400 Subject: [PATCH 0336/1169] use check_call instead of call, but catch the exception so we know we saw a crash --- src/certfuzz/fuzztools/zzuf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index b80c924..dd13411 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -9,6 +9,7 @@ from certfuzz.fuzztools import subprocess_helper as subp import subprocess +from subprocess import CalledProcessError logger = logging.getLogger(__name__) @@ -84,9 +85,12 @@ def go(self): given parameters. ''' command = self._get_go_fuzz_cmdline() - retcode = subprocess.call(command, shell=True) - if retcode: + + try: + subprocess.check_call(command, shell=True) + except CalledProcessError: self.saw_crash = True + return self.saw_crash def generate_test_case(self, seedfile, seed, range, outfile): From 5e4bc6d6bffa8f875ba40d31d55af59c5806622b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 11:20:27 -0400 Subject: [PATCH 0337/1169] convert process killer to a runtime context syntax --- src/certfuzz/campaign/campaign_linux.py | 6 ++-- src/certfuzz/fuzztools/process_killer.py | 38 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 8a3aa09..fc861d8 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -147,10 +147,8 @@ def _set_unbuffered_stdout(self): def _start_process_killer(self): logger.debug('start process killer') - # set up and spawn the process killer - killscript = os.path.abspath(os.path.expanduser('~/bff/killproc.sh')) - ProcessKiller(killscript, self.cfg.killprocname, self.cfg.killproctimeout).go() - logger.debug("Process killer started: %s %s %d", killscript, self.cfg.killprocname, self.cfg.killproctimeout) + with ProcessKiller(self.cfg.killprocname, self.cfg.killproctimeout) as pk: + pk.go() def _cache_prg(self): logger.debug('cache program') diff --git a/src/certfuzz/fuzztools/process_killer.py b/src/certfuzz/fuzztools/process_killer.py index 0d58d22..4b1fec2 100644 --- a/src/certfuzz/fuzztools/process_killer.py +++ b/src/certfuzz/fuzztools/process_killer.py @@ -5,27 +5,47 @@ ''' import logging import subprocess +import os logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) class ProcessKiller: - def __init__(self, script, killprocname, killproctimeout): - self.script = script + def __init__(self, killprocname, killproctimeout): + self.script = os.path.abspath(os.path.expanduser('~/bff/killproc.sh')) self.killprocname = killprocname self.killproctimeout = killproctimeout - def _get_cmdline(self): # if you don't have xterm... -# template = "bash %s %s %s &" etc - template = "xterm -geometry +0-0 -e bash %s %s %s &" - return template % (self.script, self.killprocname, self.killproctimeout) + # template = "bash %s %s %s &" etc + self.template = "xterm -geometry +0-0 -e bash {} {} {} &" + + self.cmdline = None + + def __enter__(self): + self._set_cmdline() + return self + + def __exit__(self, etype, value, traceback): + handled = False + + if etype is subprocess.CalledProcessError: + logger.warning('ProcessKiller startup failed: %s', value) + handled = True + elif etype is None: + logger.debug("Process killer started: %s %s %d", self.script, + self.cfg.killprocname, self.cfg.killproctimeout) + + return handled + + def _set_cmdline(self): + self.cmdline = self.template.format(self.script, self.killprocname, + self.killproctimeout) def go(self): ''' Spawns a separate process to kill out of control processes. ''' - command = self._get_cmdline() - logger.debug('Running [%s]', command) - subprocess.call(command, shell=True) + logger.debug('Running [%s]', self.cmdline) + subprocess.check_call(self.cmdline, shell=True) From e3b5494ccfcd11e1335b71eedafc425cc0917b48 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 11:21:26 -0400 Subject: [PATCH 0338/1169] convert watchdog to a runtime context syntax --- src/certfuzz/campaign/campaign_linux.py | 9 +++---- src/certfuzz/fuzztools/watchdog.py | 33 ++++++++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index fc861d8..5e19989 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -164,16 +164,15 @@ def _cache_prg(self): def _setup_watchdog(self): logger.debug('setup watchdog') - # set up the watchdog timeout within the VM and restart the daemon - watchdog = WatchDog(self.cfg.watchdogfile, - self.cfg.watchdogtimeout) # setup our watchdog file toucher TWDF.remote_d = self.cfg.remote_dir TWDF.wdf = self.cfg.watchdogfile TWDF.enable() - touch_watchdog_file() - watchdog.go() + + # set up the watchdog timeout within the VM and restart the daemon + with WatchDog(self.cfg.watchdogfile, self.cfg.watchdogtimeout) as watchdog: + watchdog.go() def _create_seedfile_set(self): logger.info('Building seedfile set') diff --git a/src/certfuzz/fuzztools/watchdog.py b/src/certfuzz/fuzztools/watchdog.py index d8d3e87..23137dc 100644 --- a/src/certfuzz/fuzztools/watchdog.py +++ b/src/certfuzz/fuzztools/watchdog.py @@ -19,18 +19,33 @@ class WatchDog: - def __init__(self, file, timeout): - self.file = file + def __init__(self, f, timeout): + self.file = f self.timeout = timeout - self.cmdline = self._get_cmdline() - def _get_cmdline(self): # we're just going to overwrite /etc/watchdog.conf # hope that's okay - template = 'sudo sh -c "echo file=%s > /etc/watchdog.conf' - template += ' && echo change=%s >> /etc/watchdog.conf' - template += ' && /etc/init.d/watchdog restart"' - return template % (self.file, self.timeout) + self.template = 'sudo sh -c "echo file={} > /etc/watchdog.conf' + self.template += ' && echo change={} >> /etc/watchdog.conf' + self.template += ' && /etc/init.d/watchdog restart"' + + self.cmdline = None + + def __enter__(self): + self._set_cmdline() + return self + + def __exit__(self, etype, value, traceback): + handled = False + + if etype is subprocess.CalledProcessError: + logger.warning('WatchDog startup failed: %s', value) + handled = True + + return handled + + def _set_cmdline(self): + self.cmdline = self.template.format(self.file, self.timeout) def go(self): ''' @@ -41,4 +56,4 @@ def go(self): logger.warning('WatchDog does not support %s', system) return - subprocess.call(self.cmdline, shell=True) + subprocess.check_call(self.cmdline, shell=True) From 8a67e758f1b6f263e1fbdd3305345924ad0c4098 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 11:26:31 -0400 Subject: [PATCH 0339/1169] fix unit tests --- src/certfuzz/test/fuzztools/test_process_killer.py | 9 +++++---- src/certfuzz/test/fuzztools/test_watchdog.py | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/test/fuzztools/test_process_killer.py b/src/certfuzz/test/fuzztools/test_process_killer.py index f36a9e0..5578083 100644 --- a/src/certfuzz/test/fuzztools/test_process_killer.py +++ b/src/certfuzz/test/fuzztools/test_process_killer.py @@ -1,22 +1,23 @@ -from certfuzz.fuzztools.process_killer import ProcessKiller - ''' Created on Apr 8, 2011 @organization: cert.org ''' import unittest +from certfuzz.fuzztools.process_killer import ProcessKiller + class Test(unittest.TestCase): def setUp(self): - self.pk = ProcessKiller(*tuple('abc')) + self.pk = ProcessKiller(*tuple('bc')) def tearDown(self): pass def test_get_cmdline(self): - self.assertTrue('a b c' in self.pk._get_cmdline()) + self.pk._set_cmdline() + self.assertTrue('b c' in self.pk.cmdline) def test_spawn_process_killer(self): # cannot test directly, see test_get_spawn_process_killer_cmdline() diff --git a/src/certfuzz/test/fuzztools/test_watchdog.py b/src/certfuzz/test/fuzztools/test_watchdog.py index 1e16505..3d5faa8 100644 --- a/src/certfuzz/test/fuzztools/test_watchdog.py +++ b/src/certfuzz/test/fuzztools/test_watchdog.py @@ -1,11 +1,11 @@ -from certfuzz.fuzztools.watchdog import WatchDog - ''' Created on Apr 8, 2011 @organization: cert.org ''' import unittest +from certfuzz.fuzztools.watchdog import WatchDog + class Test(unittest.TestCase): @@ -17,7 +17,8 @@ def tearDown(self): def test_get_watchdog_timeout_cmdline(self): expected = 'sudo sh -c "echo file=/tmp/foo > /etc/watchdog.conf && echo change=1234567890 >> /etc/watchdog.conf && /etc/init.d/watchdog restart"' - self.assertEqual(self.w._get_cmdline(), expected) + self.w._set_cmdline() + self.assertEqual(self.w.cmdline, expected) def test_go(self): # cannot test directly, see test_get_watchdog_timeout_cmdline() From ceac02ee48273bd1b77d5bdd28932e77aa2fac8b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 12:44:04 -0400 Subject: [PATCH 0340/1169] bug fix --- src/certfuzz/fuzztools/process_killer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/fuzztools/process_killer.py b/src/certfuzz/fuzztools/process_killer.py index 4b1fec2..f059c43 100644 --- a/src/certfuzz/fuzztools/process_killer.py +++ b/src/certfuzz/fuzztools/process_killer.py @@ -35,7 +35,7 @@ def __exit__(self, etype, value, traceback): handled = True elif etype is None: logger.debug("Process killer started: %s %s %d", self.script, - self.cfg.killprocname, self.cfg.killproctimeout) + self.killprocname, self.killproctimeout) return handled From d89a3c3d90b4ec7f85cdc3fa236ad7d9935e2418 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 26 Jun 2014 13:20:56 -0400 Subject: [PATCH 0341/1169] Remove reference to crashwrangler-contents.xml --- .../BFF_installer.pmdoc/1144497_crashwrangler.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml index 3a08b78..b93c797 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml @@ -17,12 +17,4 @@ parent installTo - -1144497_crashwrangler-contents.xml -/CVS$ -/\.svn$ -/\.cvsignore$ -/\.cvspass$ -/\.DS_Store$ - From 6f20f7f62d23bd633cfb0a4e9f323ec78e68a7bb Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 26 Jun 2014 14:12:29 -0400 Subject: [PATCH 0342/1169] Get crashwrangler zip as part of CrashWrangler component in PackageMaker. BFF-754 --- ...gler-contents.xml => 1152854_crashwrangler-contents.xml} | 0 ...{1144497_crashwrangler.xml => 1152854_crashwrangler.xml} | 6 ++---- build/distmods/osx/installer/BFF_installer.pmdoc/index.xml | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) rename build/distmods/osx/installer/BFF_installer.pmdoc/{1144497_crashwrangler-contents.xml => 1152854_crashwrangler-contents.xml} (100%) rename build/distmods/osx/installer/BFF_installer.pmdoc/{1144497_crashwrangler.xml => 1152854_crashwrangler.xml} (69%) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler-contents.xml similarity index 100% rename from build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler-contents.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler-contents.xml diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml similarity index 69% rename from build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml rename to build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml index b93c797..fb03708 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/1144497_crashwrangler.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml @@ -1,19 +1,17 @@ -com.cert.cc.certBff.44497_crashwrangler.pkg +com.cert.cc.certBff.52854_crashwrangler.pkg 1.0 -52854_crashwrangler.zip +52854_crashwrangler.zip /Applications/BFF.app/Contents/Resources/packages -installTo.path installFrom.isRelativeType -installFrom.path parent installTo diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index af64abf..a7cd73c 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -44,7 +44,7 @@ - + @@ -91,7 +91,7 @@ 08hcluster.xml 09zzuf.xml 10libcaca.xml -1144497_crashwrangler.xml +1152854_crashwrangler.xml 12exc.xml 13exc.xml 14exc.xml From 69dfd109f32369a0f7cff77272b01cafc4482348 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 15:23:22 -0400 Subject: [PATCH 0343/1169] make sure trials is always at least as big as successes (fix BFF-521) --- .../scoring/multiarmed_bandit/arms/bayes_laplace.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py b/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py index 7263d8a..0924c29 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py +++ b/src/certfuzz/scoring/multiarmed_bandit/arms/bayes_laplace.py @@ -14,6 +14,11 @@ class BanditArmBayesLaplace(BanditArmBase): Uses Laplace's Law of Succession ''' - def _update_p(self, successes=0, trials=0): + def _update_p(self, *_unused_args): + # sometimes successes can get ahead of trials before catching up + # later in the same iteration. This line ensures that we never try + # to calculate a number that will end up >1.0 (fixes BFF-521) + trials = max(self.trials, self.successes) + # see Laplace's Law of Succession - self.probability = (self.successes + 1.0) / (self.trials + 2.0) + self.probability = (self.successes + 1.0) / (trials + 2.0) From 6c8bbcf0c19eb04e755b11cc665ff5eaf8545c51 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 15:28:26 -0400 Subject: [PATCH 0344/1169] update unit tests --- .../multiarmed_bandit/arms/test_bayes_laplace.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py b/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py index 3185e32..e54cf45 100644 --- a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py +++ b/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py @@ -15,7 +15,7 @@ def setUp(self): def tearDown(self): pass - def test_update_p(self): + def test_update(self): self.assertEqual(0, self.arm.trials) self.assertEqual(0, self.arm.successes) self.assertEqual(1.0 / 2.0, self.arm.probability) @@ -23,10 +23,19 @@ def test_update_p(self): self.assertEqual(2.0 / 3.0, self.arm.probability) self.arm.update(1, 1) self.assertEqual(3.0 / 4.0, self.arm.probability) + + def test_update_p(self): self.arm.successes = 0 - self.assertEqual(2, self.arm.trials) + self.arm.trials = 2 + self.arm._update_p() + self.assertEqual(1.0 / 4.0, self.arm.probability) + + # test for BFF-521 + # if success > trials, it should use successes as the trials value + self.arm.successes = 5 + self.arm.trials = 3 self.arm._update_p() - self.assertEqual(0.25, self.arm.probability) + self.assertEqual(6.0 / 7.0, self.arm.probability) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] From ff31ba23fe285718ad3bac13c240fbade1ae3e0f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 3 Jun 2014 11:28:37 -0400 Subject: [PATCH 0345/1169] use campaign meta methods --- src/certfuzz/campaign/campaign_linux.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 5e19989..bd0b87d 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -210,10 +210,10 @@ def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): return False def __getstate__(self): - pass + CampaignMeta.__getstate__(self) def __setstate__(self): - pass + CampaignMeta.__setstate__(self) def _do_interval(self, s1, s2, first_chunk=False): # interval.go @@ -238,13 +238,13 @@ def _do_interval(self, s1, s2, first_chunk=False): iteration.go() def _do_iteration(self): - pass + CampaignMeta._do_iteration(self) def _keep_going(self): - pass + CampaignMeta._keep_going(self) def _write_version(self): - pass + CampaignMeta._write_version(self) def go(self): # campaign.go From 593deb808e81d10cce8ad8aae8ccf453a3eb7943 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 14:03:57 -0400 Subject: [PATCH 0346/1169] reorder campaign_linux to better match campaign_base --- src/certfuzz/campaign/campaign_linux.py | 97 ++++++++++++++++--------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index bd0b87d..626a23f 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -54,16 +54,20 @@ def __init__(self, config_file=None, result_dir=None, debug=False): def __enter__(self): self._setup_dirs() - - # setup working dir - self.working_dir = tempfile.mkdtemp(prefix='campaign_', dir=self.workdirbase) - logger.debug('workdir=%s', self.working_dir) - self._check_for_script() self._copy_config() self._start_process_killer() self._set_unbuffered_stdout() + + # stuff that is handled in CampaignBase + self._setup_workdir() + self._set_fuzzer() + self._set_runner() + self._set_debugger() + self._setup_output() self._create_seedfile_set() + + if self.cfg.watchdogtimeout: self._setup_watchdog() @@ -103,17 +107,6 @@ def __exit__(self, etype, value, mytraceback): return handled - def _cleanup_workdir(self): - try: - shutil.rmtree(self.working_dir) - except: - pass - - if os.path.exists(self.working_dir): - logger.warning("Unable to remove campaign working dir: %s", self.working_dir) - else: - logger.debug('Removed campaign working dir: %s', self.working_dir) - def _setup_dirs(self): logger.debug('setup dirs') paths = [self.cfg.local_dir, @@ -174,6 +167,47 @@ def _setup_watchdog(self): with WatchDog(self.cfg.watchdogfile, self.cfg.watchdogtimeout) as watchdog: watchdog.go() + + def _check_for_script(self): + logger.debug('check for script') + if self.cfg.program_is_script(): + logger.warning("Target application is a shell script.") + raise CampaignScriptError() + #cfg.disable_verification() + #time.sleep(10) + + def _check_prog(self): + pass + + def _set_fuzzer(self): + pass + + def _set_runner(self): + pass + + def _set_debugger(self): + pass + + def _write_version(self): + CampaignMeta._write_version(self) + + def _setup_output(self): + pass + + def _setup_workdir(self): + pass + + def _cleanup_workdir(self): + try: + shutil.rmtree(self.working_dir) + except: + pass + + if os.path.exists(self.working_dir): + logger.warning("Unable to remove campaign working dir: %s", self.working_dir) + else: + logger.debug('Removed campaign working dir: %s', self.working_dir) + def _create_seedfile_set(self): logger.info('Building seedfile set') sfs_logfile = os.path.join(self.cfg.seedfile_output_dir, 'seedfile_set.log') @@ -185,13 +219,17 @@ def _create_seedfile_set(self): ) as sfset: self.seedfile_set = sfset - def _check_for_script(self): - logger.debug('check for script') - if self.cfg.program_is_script(): - logger.warning("Target application is a shell script.") - raise CampaignScriptError() - #cfg.disable_verification() - #time.sleep(10) + def __setstate__(self): + CampaignMeta.__setstate__(self) + + def _read_state(self): + pass + + def __getstate__(self): + CampaignMeta.__getstate__(self) + + def _save_state(self): + pass def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): ''' @@ -209,11 +247,8 @@ def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): logger.debug('%s was found, not unique', crash_id) return False - def __getstate__(self): - CampaignMeta.__getstate__(self) - - def __setstate__(self): - CampaignMeta.__setstate__(self) + def _keep_going(self): + CampaignMeta._keep_going(self) def _do_interval(self, s1, s2, first_chunk=False): # interval.go @@ -240,12 +275,6 @@ def _do_interval(self, s1, s2, first_chunk=False): def _do_iteration(self): CampaignMeta._do_iteration(self) - def _keep_going(self): - CampaignMeta._keep_going(self) - - def _write_version(self): - CampaignMeta._write_version(self) - def go(self): # campaign.go cfg = self.cfg From 69777ca27f9e5c465f38c6717dc6f1db06dd67d3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 14:57:29 -0400 Subject: [PATCH 0347/1169] add debug log msgs --- src/certfuzz/campaign/campaign_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 6459dde..cfaa261 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -307,7 +307,9 @@ def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): ''' if not crash_id in self.crashes_seen: self.crashes_seen.add(crash_id) + logger.debug("%s did not exist in cache, crash is unique", crash_id) return True + logger.debug('%s was found, not unique', crash_id) return False def _keep_going(self): From 79c58a0521db2ecee68cc074cd416cf55d744cee Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 15:01:15 -0400 Subject: [PATCH 0348/1169] more campaign_base - campaign_linux merging --- src/certfuzz/campaign/campaign_linux.py | 69 ++++++++++++++----------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 626a23f..33fc2fb 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -13,7 +13,7 @@ import time import traceback -from certfuzz.campaign.campaign_meta import CampaignMeta +from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.campaign.config import bff_config as cfg_helper from certfuzz.campaign.errors import CampaignScriptError from certfuzz.debuggers import crashwrangler # @UnusedImport @@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) -class LinuxCampaign(CampaignMeta): +class LinuxCampaign(CampaignBase): def __init__(self, config_file=None, result_dir=None, debug=False): # Read the cfg file self.cfg_path = config_file @@ -42,6 +42,11 @@ def __init__(self, config_file=None, result_dir=None, debug=False): self.debug = debug logger.info('Reading config from %s', config_file) self.cfg = cfg_helper.read_config_options(config_file) + + self.current_seed = self.cfg.start_seed + self.seed_interval = self.cfg.seed_interval + self.first_chunk = True + self.seedfile_set = None self.hashes = [] self.workdirbase = self.cfg.testscase_tmp_dir @@ -189,7 +194,7 @@ def _set_debugger(self): pass def _write_version(self): - CampaignMeta._write_version(self) + CampaignBase._write_version(self) def _setup_output(self): pass @@ -220,13 +225,13 @@ def _create_seedfile_set(self): self.seedfile_set = sfset def __setstate__(self): - CampaignMeta.__setstate__(self) + pass def _read_state(self): pass def __getstate__(self): - CampaignMeta.__getstate__(self) + pass def _save_state(self): pass @@ -248,40 +253,44 @@ def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): return False def _keep_going(self): - CampaignMeta._keep_going(self) + if self.stop_seed: + return self.current_seed < self.stop_seed + else: + return True - def _do_interval(self, s1, s2, first_chunk=False): - # interval.go - logger.debug('Starting interval %d-%d', s1, s2) + def _do_interval(self): # wipe the tmp dir clean to try to avoid filling the VM disk TmpReaper().clean_tmp() + # choose seedfile sf = self.seedfile_set.next_item() + logger.info('Selected seedfile: %s', sf.basename) + r = sf.rangefinder.next_item() - qf = not first_chunk + qf = not self.first_chunk logger.info(STATE_TIMER) - for s in xrange(s1, s2): - # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot - touch_watchdog_file() - with Iteration(cfg=self.cfg, seednum=s, seedfile=sf, r=r, - workdirbase=self.working_dir, quiet=qf, - uniq_func=self._crash_is_unique, - sf_set=self.seedfile_set, - rf=sf.rangefinder) as iteration: - iteration.go() + interval_limit = self.current_seed + self.seed_interval + logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) + for seednum in xrange(self.current_seed, interval_limit): + self._do_iteration(sf, r, qf, seednum) + + self.current_seed = interval_limit + self.first_chunk = False - def _do_iteration(self): - CampaignMeta._do_iteration(self) + def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): + # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot + touch_watchdog_file() + with Iteration(cfg=self.cfg, seednum=seednum, seedfile=seedfile, r=range_obj, workdirbase=self.working_dir, quiet=quiet_flag, + uniq_func=self._crash_is_unique, + sf_set=self.seedfile_set, + rf=seedfile.rangefinder) as iteration: + iteration.go() def go(self): - # campaign.go - cfg = self.cfg - - first_chunk = True - for s in itertools.count(start=cfg.start_seed, step=cfg.seed_interval): - s1 = s - s2 = s + cfg.seed_interval - self._do_interval(s1, s2, first_chunk) - first_chunk = False + ''' + Starts campaign + ''' + while self._keep_going(): + self._do_interval() From a5086c9d7d39ac11a9c391e7d412f7deaeee6f39 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 15:35:17 -0400 Subject: [PATCH 0349/1169] Bff doesn't have a stop seed anymore --- src/certfuzz/campaign/campaign_base.py | 11 +---------- src/windows/configs/examples/bff.yaml | 1 - 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index cfaa261..ee59234 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -78,9 +78,6 @@ def __init__(self, config_file, result_dir=None, campaign_cache=None, if not self.current_seed: # default to zero self.current_seed = 0 - # if stop_seed is zero or None, we'll keep going forever - # see self._keep_going() - self.stop_seed = self.config['runoptions'].get('last_iteration') self.seed_interval = self.config['runoptions'].get('seed_interval') if not self.seed_interval: @@ -313,10 +310,7 @@ def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): return False def _keep_going(self): - if self.stop_seed: - return self.current_seed < self.stop_seed - else: - return True + return CampaignMeta._keep_going(self) def _do_interval(self): # choose seedfile @@ -329,10 +323,7 @@ def _do_interval(self): # cache our current state self._save_state() - # don't overshoot stop_seed interval_limit = self.current_seed + self.seed_interval - if self.stop_seed: - interval_limit = min(interval_limit, self.stop_seed) # start an iteration interval # note that range does not include interval_limit diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index ff39ecf..fbe483f 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -78,7 +78,6 @@ directories: ##################################################################### runoptions: first_iteration: 0 - last_iteration: 0 seed_interval: 1 minimize: True minimizer_timeout: 3600 From d41e1a2558abf9324b0bfc4283d1be7dfe90622a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 15:40:00 -0400 Subject: [PATCH 0350/1169] remove dead code --- src/certfuzz/campaign/campaign_base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index ee59234..a4ff728 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -134,8 +134,6 @@ def __enter__(self): self._set_debugger() self._setup_output() self._create_seedfile_set() - # buttonclicker is os-specific, moved to subclass -# self._start_buttonclicker() return self def __exit__(self, etype, value, mytraceback): From acbbb372d553dee78b193da2410d53e346f495c8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 15:41:05 -0400 Subject: [PATCH 0351/1169] remove common code, move functions out of config object to campaign module --- src/certfuzz/campaign/campaign_linux.py | 110 +++++++++++---------- src/certfuzz/campaign/config/bff_config.py | 31 ------ 2 files changed, 56 insertions(+), 85 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 33fc2fb..dd140d9 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -29,11 +29,36 @@ from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.linux import Iteration +import subprocess logger = logging.getLogger(__name__) +def check_program_file_type(string, program): + ''' + @rtype: boolean + Runs the system "file" command on self.program + @return: True if appears in the output. + ''' + file_loc = subprocess.Popen("which %s" % program, stdout=subprocess.PIPE, shell=True).stdout.read().strip() + # maybe it's not on the path, but it still exists + if not file_loc: + if os.path.exists(program): + file_loc = program + + # we still can't find it, so give give up + if not os.path.exists(file_loc): + return False + + # get the 'file' results + ftype = subprocess.Popen("file -b -L %s" % file_loc, stdout=subprocess.PIPE, shell=True).stdout.read() + if string in ftype: + return True + else: + return False + + class LinuxCampaign(CampaignBase): def __init__(self, config_file=None, result_dir=None, debug=False): # Read the cfg file @@ -59,19 +84,11 @@ def __init__(self, config_file=None, result_dir=None, debug=False): def __enter__(self): self._setup_dirs() - self._check_for_script() self._copy_config() self._start_process_killer() self._set_unbuffered_stdout() - # stuff that is handled in CampaignBase - self._setup_workdir() - self._set_fuzzer() - self._set_runner() - self._set_debugger() - self._setup_output() - self._create_seedfile_set() - + CampaignBase.__enter__(self) if self.cfg.watchdogtimeout: self._setup_watchdog() @@ -172,47 +189,49 @@ def _setup_watchdog(self): with WatchDog(self.cfg.watchdogfile, self.cfg.watchdogtimeout) as watchdog: watchdog.go() - def _check_for_script(self): logger.debug('check for script') - if self.cfg.program_is_script(): + if check_program_file_type('text', self.cfg.program): logger.warning("Target application is a shell script.") raise CampaignScriptError() #cfg.disable_verification() #time.sleep(10) def _check_prog(self): - pass + self._check_for_script() + # TODO: we could also use the parent class to check if the prog is present +# CampaignBase._check_prog(self) def _set_fuzzer(self): + ''' + Overrides parent class + ''' pass def _set_runner(self): + ''' + Overrides parent class + ''' pass def _set_debugger(self): + ''' + Overrides parent class + ''' pass - def _write_version(self): - CampaignBase._write_version(self) - def _setup_output(self): + ''' + Overrides parent class + ''' pass def _setup_workdir(self): + ''' + Overrides parent class + ''' pass - def _cleanup_workdir(self): - try: - shutil.rmtree(self.working_dir) - except: - pass - - if os.path.exists(self.working_dir): - logger.warning("Unable to remove campaign working dir: %s", self.working_dir) - else: - logger.debug('Removed campaign working dir: %s', self.working_dir) - def _create_seedfile_set(self): logger.info('Building seedfile set') sfs_logfile = os.path.join(self.cfg.seedfile_output_dir, 'seedfile_set.log') @@ -225,38 +244,28 @@ def _create_seedfile_set(self): self.seedfile_set = sfset def __setstate__(self): + ''' + Overrides parent class + ''' pass def _read_state(self): + ''' + Overrides parent class + ''' pass def __getstate__(self): + ''' + Overrides parent class + ''' pass def _save_state(self): - pass - - def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): ''' - If crash_id represents a new crash, add the crash_id to crashes_seen - and return True. Otherwise return False. - - @param crash_id: the crash_id to look up - @param exploitability: not used at this time + Overrides parent class ''' - if not crash_id in self.crashes_seen: - self.crashes_seen.add(crash_id) - logger.debug("%s did not exist in cache, crash is unique", crash_id) - return True - - logger.debug('%s was found, not unique', crash_id) - return False - - def _keep_going(self): - if self.stop_seed: - return self.current_seed < self.stop_seed - else: - return True + pass def _do_interval(self): # wipe the tmp dir clean to try to avoid filling the VM disk @@ -287,10 +296,3 @@ def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): sf_set=self.seedfile_set, rf=seedfile.rangefinder) as iteration: iteration.go() - - def go(self): - ''' - Starts campaign - ''' - while self._keep_going(): - self._do_interval() diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index db46431..8f4fd95 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -13,7 +13,6 @@ import re import shlex import shutil -import subprocess from certfuzz.fuzztools import filetools @@ -168,36 +167,6 @@ def full_path_original(self, seedfile): ''' return os.path.join(self.full_path_local_fuzz_dir(seedfile), seedfile) - def program_is_script(self): - ''' - @rtype: boolean - @return: True if self.program is a file type of "text" as determined by check_program_file_type(). - ''' - return self.check_program_file_type('text') - - def check_program_file_type(self, string): - ''' - @rtype: boolean - Runs the system "file" command on self.program - @return: True if appears in the output. - ''' - file_loc = subprocess.Popen("which %s" % self.program, stdout=subprocess.PIPE, shell=True).stdout.read().strip() - # maybe it's not on the path, but it still exists - if not file_loc: - if os.path.exists(self.program): - file_loc = self.program - - # we still can't find it, so give give up - if not os.path.exists(file_loc): - return False - - # get the 'file' results - ftype = subprocess.Popen("file -b -L %s" % file_loc, stdout=subprocess.PIPE, shell=True).stdout.read() - if string in ftype: - return True - else: - return False - def get_minimized_file(self, outfile): ''' @rtype: string From 99f50a368119c903c66c2ead3aa4dce057580e82 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 16:20:51 -0400 Subject: [PATCH 0352/1169] merge _copy_config method into _setup_output method in parent class --- src/certfuzz/campaign/campaign_linux.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index dd140d9..29d4512 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -62,12 +62,13 @@ def check_program_file_type(string, program): class LinuxCampaign(CampaignBase): def __init__(self, config_file=None, result_dir=None, debug=False): # Read the cfg file - self.cfg_path = config_file + self.config_file = config_file self.result_dir = result_dir self.debug = debug - logger.info('Reading config from %s', config_file) - self.cfg = cfg_helper.read_config_options(config_file) + logger.info('Reading config from %s', self.config_file) + self.cfg = cfg_helper.read_config_options(self.config_file) + self.outdir = self.cfg.output_dir self.current_seed = self.cfg.start_seed self.seed_interval = self.cfg.seed_interval self.first_chunk = True @@ -82,9 +83,7 @@ def __init__(self, config_file=None, result_dir=None, debug=False): verify_supported_platform() def __enter__(self): - self._setup_dirs() - self._copy_config() self._start_process_killer() self._set_unbuffered_stdout() @@ -134,7 +133,6 @@ def _setup_dirs(self): paths = [self.cfg.local_dir, self.cfg.cached_objects_dir, self.cfg.seedfile_local_dir, - self.cfg.output_dir, self.cfg.seedfile_output_dir, self.cfg.crashers_dir, self.cfg.testscase_tmp_dir, @@ -145,11 +143,6 @@ def _setup_dirs(self): logger.debug('Creating dir %s', d) mkdir_p(d) - def _copy_config(self): - logger.debug('copy config') - - copy_file(self.cfg_path, self.cfg.output_dir) - def _set_unbuffered_stdout(self): ''' Reopens stdout with a buffersize of 0 (unbuffered) @@ -220,12 +213,6 @@ def _set_debugger(self): ''' pass - def _setup_output(self): - ''' - Overrides parent class - ''' - pass - def _setup_workdir(self): ''' Overrides parent class From 43fbb9d711ffe37a035a987099ef1724e041df93 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 25 Jun 2014 16:21:40 -0400 Subject: [PATCH 0353/1169] set first_chunk true when initializing object instead of when entering runtime context --- src/certfuzz/campaign/campaign_linux.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 29d4512..8de45e5 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -71,6 +71,8 @@ def __init__(self, config_file=None, result_dir=None, debug=False): self.outdir = self.cfg.output_dir self.current_seed = self.cfg.start_seed self.seed_interval = self.cfg.seed_interval + + # flag to indicate whether this is a fresh script start up or not self.first_chunk = True self.seedfile_set = None @@ -92,9 +94,6 @@ def __enter__(self): if self.cfg.watchdogtimeout: self._setup_watchdog() - # flag to indicate whether this is a fresh script start up or not - self.first_chunk = True - check_ppid() return self From e4cd22d67237e0f782f66ae1456a653bdd84800b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 07:53:20 -0400 Subject: [PATCH 0354/1169] remove dead code --- src/certfuzz/campaign/campaign_linux.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 8de45e5..d922431 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -186,8 +186,6 @@ def _check_for_script(self): if check_program_file_type('text', self.cfg.program): logger.warning("Target application is a shell script.") raise CampaignScriptError() - #cfg.disable_verification() - #time.sleep(10) def _check_prog(self): self._check_for_script() From 2694fb4d292b2d6ebec62bf4b679e5f7b80d2fa1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 08:43:01 -0400 Subject: [PATCH 0355/1169] logging cleanup --- src/certfuzz/iteration/linux.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/linux.py index 219579f..5a6f0b7 100644 --- a/src/certfuzz/iteration/linux.py +++ b/src/certfuzz/iteration/linux.py @@ -85,20 +85,6 @@ def record_tries(self): self.sf_set.record_tries(key=self.seedfile.md5, tries=1) self.rf.record_tries(key=self.r.id, tries=1) - def _log(self): -# # emit a log entry - crashcount = z.get_crashcount(self.cfg.crashers_dir) -# rate = get_rate(self.s1) -# seed_str = "seeds=%d-%d" % (self.s1, self.s2) -# range_str = "range=%.6f-%.6f" % (self.r.min, self.r.max) -# rate_str = "Rate=(%.2f/s %.1f/m %.0f/h %.0f/d)" % (rate, rate * 60, rate * 3600, rate * 86400) -# expected_density = self.seedfile_set.expected_crash_density -# xd_str = "expected=%.9f" % expected_density -# xr_str = 'expected_rate=%.6f uniq/day' % (expected_density * rate * 86400) -# logger.info('Fuzzing %s %s %s %s %s %s crash_count=%d', -# self.sf.path, seed_str, range_str, rate_str, xd_str, xr_str, crashcount) - logger.info('Fuzzing...crash_count=%d', crashcount) - def _setup_analyzers(self): self.analyzer_classes.append(stderr.StdErr) self.analyzer_classes.append(cw_gmalloc.CrashWranglerGmalloc) @@ -141,7 +127,7 @@ def _post_run(self): self.record_tries() if not self.zzuf.saw_crash: - self._log() + logger.debug('No crash seen') return # we must have seen a crash From 2dc078f5b68fd3bb8fa3d9d55aaead1adb4917b3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 08:49:22 -0400 Subject: [PATCH 0356/1169] rename some variables for consistency with campaign_linux --- src/certfuzz/campaign/campaign_base.py | 23 ++++++++++++++--------- src/certfuzz/campaign/campaign_windows.py | 6 +++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index a4ff728..b346b00 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -69,7 +69,9 @@ def __init__(self, config_file, result_dir=None, campaign_cache=None, self.config = cfgobj.config self.configdate = cfgobj.configdate - self.id = self.config['campaign']['id'] + self.campaign_id = self.config['campaign']['id'] + + # TODO: buttonclicker should move to campaign_windows self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker') if not self.use_buttonclicker: self.use_buttonclicker = False @@ -88,14 +90,14 @@ def __init__(self, config_file, result_dir=None, campaign_cache=None, else: self.outdir_base = os.path.abspath(self.config['directories']['results_dir']) - self.outdir = os.path.join(self.outdir_base, self.id) + self.outdir = os.path.join(self.outdir_base, self.campaign_id) logger.debug('outdir=%s', self.outdir) self.sf_set_out = os.path.join(self.outdir, 'seedfiles') self.work_dir_base = os.path.abspath(self.config['directories']['working_dir']) if not self.cached_state_file: - cachefile = 'campaign_%s.pkl' % re.sub('\W', '_', self.id) + cachefile = 'campaign_%s.pkl' % re.sub('\W', '_', self.campaign_id) self.cached_state_file = os.path.join(self.work_dir_base, cachefile) self.seed_dir_in = self.config['directories']['seedfile_dir'] @@ -107,7 +109,7 @@ def __init__(self, config_file, result_dir=None, campaign_cache=None, # TODO: consider making this configurable self.status_interval = 100 - self.prog = self.config['target']['program'] + self.program = self.config['target']['program'] self.cmd_template = self.config['target']['cmdline_template'] self.crashes_seen = set() @@ -170,8 +172,8 @@ def __exit__(self, etype, value, mytraceback): return handled def _check_prog(self): - if not os.path.exists(self.prog): - msg = 'Cannot find program "%s" (resolves to "%s")' % (self.prog, os.path.abspath(self.prog)) + if not os.path.exists(self.program): + msg = 'Cannot find program "%s" (resolves to "%s")' % (self.program, os.path.abspath(self.program)) raise CampaignError(msg) def _set_fuzzer(self): @@ -221,9 +223,12 @@ def _cleanup_workdir(self): logger.debug('Removed campaign working dir: %s', self.working_dir) def _create_seedfile_set(self): + logger.info('Building seedfile set') if self.seedfile_set is None: - with SeedfileSet(self.id, self.seed_dir_in, self.seed_dir_local, - self.sf_set_out) as sfset: + with SeedfileSet(campiagn_id=self.campaign_id, + origin_path=self.seed_dir_in, + localpath=self.seed_dir_local, + outputpath=self.sf_set_out) as sfset: self.seedfile_set = sfset def __setstate__(self, state): @@ -231,7 +236,7 @@ def __setstate__(self, state): state['crashes_seen'] = set(state['crashes_seen']) # reconstitute the seedfile set - with SeedfileSet(state['id'], state['seed_dir_in'], state['seed_dir_local'], + with SeedfileSet(state['campaign_id'], state['seed_dir_in'], state['seed_dir_local'], state['sf_set_out']) as sfset: new_sfset = sfset diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 6b024aa..0249efe 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -38,11 +38,11 @@ def __exit__(self, etype, value, mytraceback): return CampaignBase.__exit__(self, etype, value, mytraceback) def _cache_app(self): - logger.debug('Caching application %s and determining if we need to watch the CPU...', self.prog) - targetdir = os.path.dirname(self.prog) + logger.debug('Caching application %s and determining if we need to watch the CPU...', self.program) + targetdir = os.path.dirname(self.program) # Use overriden Popen that uses a job object to make sure that # child processes are killed - p = Popen(self.prog, cwd=targetdir) + p = Popen(self.program, cwd=targetdir) runtimeout = self.config['runner']['runtimeout'] logger.debug('...Timer: %f', runtimeout) t = Timer(runtimeout, self.kill, args=[p]) From 49925539459d4c36a0094fba98aa000ca08956bb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 09:12:35 -0400 Subject: [PATCH 0357/1169] refactor attributes, remove duplicative methods, remove superfluous config options Squashed commits: [15f19dd] refactor attribute names to match campaign_base better. Also removes some superfluous config options. [513b74d] remove call to now defunct method --- src/certfuzz/campaign/campaign_base.py | 2 +- src/certfuzz/campaign/campaign_linux.py | 57 ++++++------------- src/certfuzz/campaign/config/bff_config.py | 19 ++++--- .../test/campaign/config/test_bff_config.py | 9 --- .../test/campaign/test_campaign_linux.py | 17 ++++-- src/linux/conf.d/bff.cfg | 8 --- 6 files changed, 40 insertions(+), 72 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index b346b00..29d2cb6 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -226,7 +226,7 @@ def _create_seedfile_set(self): logger.info('Building seedfile set') if self.seedfile_set is None: with SeedfileSet(campiagn_id=self.campaign_id, - origin_path=self.seed_dir_in, + originpath=self.seed_dir_in, localpath=self.seed_dir_local, outputpath=self.sf_set_out) as sfset: self.seedfile_set = sfset diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index d922431..9d6d5dd 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -21,7 +21,7 @@ from certfuzz.debuggers.registration import verify_supported_platform from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.file_handlers.tmp_reaper import TmpReaper -from certfuzz.fuzztools import subprocess_helper as subp +from certfuzz.fuzztools import subprocess_helper as subp, filetools from certfuzz.fuzztools.filetools import mkdir_p, copy_file from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.state_timer import STATE_TIMER @@ -68,24 +68,36 @@ def __init__(self, config_file=None, result_dir=None, debug=False): logger.info('Reading config from %s', self.config_file) self.cfg = cfg_helper.read_config_options(self.config_file) - self.outdir = self.cfg.output_dir + self.campaign_id = self.cfg.campaign_id self.current_seed = self.cfg.start_seed self.seed_interval = self.cfg.seed_interval + self.seed_dir_in = self.cfg.seedfile_origin_dir + + self.outdir_base = os.path.abspath(self.cfg.output_dir) + + self.outdir = os.path.join(self.outdir_base, self.campaign_id) + logger.debug('outdir=%s', self.outdir) + self.sf_set_out = os.path.join(self.outdir, 'seedfiles') + + self.work_dir_base = self.cfg.local_dir + + self.program = self.cfg.program + # flag to indicate whether this is a fresh script start up or not self.first_chunk = True self.seedfile_set = None + self.hashes = [] - self.workdirbase = self.cfg.testscase_tmp_dir self.working_dir = None + self.seed_dir_local = None self.crashes_seen = set() # give up if we don't have a debugger verify_supported_platform() def __enter__(self): - self._setup_dirs() self._start_process_killer() self._set_unbuffered_stdout() @@ -127,21 +139,6 @@ def __exit__(self, etype, value, mytraceback): return handled - def _setup_dirs(self): - logger.debug('setup dirs') - paths = [self.cfg.local_dir, - self.cfg.cached_objects_dir, - self.cfg.seedfile_local_dir, - self.cfg.seedfile_output_dir, - self.cfg.crashers_dir, - self.cfg.testscase_tmp_dir, - ] - - for d in paths: - if not os.path.exists(d): - logger.debug('Creating dir %s', d) - mkdir_p(d) - def _set_unbuffered_stdout(self): ''' Reopens stdout with a buffersize of 0 (unbuffered) @@ -183,14 +180,13 @@ def _setup_watchdog(self): def _check_for_script(self): logger.debug('check for script') - if check_program_file_type('text', self.cfg.program): + if check_program_file_type('text', self.program): logger.warning("Target application is a shell script.") raise CampaignScriptError() def _check_prog(self): self._check_for_script() - # TODO: we could also use the parent class to check if the prog is present -# CampaignBase._check_prog(self) + CampaignBase._check_prog(self) def _set_fuzzer(self): ''' @@ -210,23 +206,6 @@ def _set_debugger(self): ''' pass - def _setup_workdir(self): - ''' - Overrides parent class - ''' - pass - - def _create_seedfile_set(self): - logger.info('Building seedfile set') - sfs_logfile = os.path.join(self.cfg.seedfile_output_dir, 'seedfile_set.log') - with SeedfileSet(campaign_id=self.cfg.campaign_id, - originpath=self.cfg.seedfile_origin_dir, - localpath=self.cfg.seedfile_local_dir, - outputpath=self.cfg.seedfile_output_dir, - logfile=sfs_logfile, - ) as sfset: - self.seedfile_set = sfset - def __setstate__(self): ''' Overrides parent class diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index 8f4fd95..c242b6b 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -113,12 +113,8 @@ def __init__(self, cfg): self.seedfile_origin_dir = os.path.expanduser(self.cfg.get('directories', 'seedfile_origin_dir')) self.debugger_template_dir = os.path.expanduser(self.cfg.get('directories', 'debugger_template_dir')) self.local_dir = os.path.expanduser(self.cfg.get('directories', 'local_dir')) - self.cached_objects_dir = os.path.expanduser(self.cfg.get('directories', 'cached_objects_dir')) - self.seedfile_local_dir = os.path.expanduser(self.cfg.get('directories', 'seedfile_local_dir')) self.output_dir = os.path.expanduser(self.cfg.get('directories', 'output_dir')) - self.seedfile_output_dir = os.path.expanduser(self.cfg.get('directories', 'seedfile_output_dir')) - self.crashers_dir = os.path.expanduser(self.cfg.get('directories', 'crashers_dir')) - self.testscase_tmp_dir = os.path.expanduser(self.cfg.get('directories', 'temp_working_dir')) + self.watchdogfile = os.path.expanduser(self.cfg.get('directories', 'watchdog_file')) # derived properties @@ -131,10 +127,11 @@ def __init__(self, cfg): self.tmpdir = None - self.cached_config_file = os.path.join(self.cached_objects_dir, CACHED_CONFIG_OBJECT_FILE) - self.cached_seedrange_file = os.path.join(self.cached_objects_dir, CACHED_SEEDRANGE_OBJECT_FILE) - self.cached_rangefinder_file = os.path.join(self.cached_objects_dir, CACHED_RANGEFINDER_OBJECT_FILE) - self.cached_seedfile_set = os.path.join(self.cached_objects_dir, CACHED_SEEDFILESET_OBJECT_FILE) + # derived cached paths +# self.cached_config_file = os.path.join(self.local_dir, CACHED_CONFIG_OBJECT_FILE) +# self.cached_seedrange_file = os.path.join(self.local_dir, CACHED_SEEDRANGE_OBJECT_FILE) +# self.cached_rangefinder_file = os.path.join(self.local_dir, CACHED_RANGEFINDER_OBJECT_FILE) +# self.cached_seedfile_set = os.path.join(self.local_dir, CACHED_SEEDFILESET_OBJECT_FILE) def get_command(self, filepath): return ' '.join(self.get_command_list(filepath)) @@ -196,13 +193,16 @@ def get_filenames(self, outfile, use_minimized_as_root=True): return files + def create_tmpdir(self): + # TODO: this should become part of campaign object if not self.tmpdir: self.tmpdir = filetools.mkdtemp(self.testscase_tmp_dir) logger.debug("Created temp dir %s", self.tmpdir) assert os.path.isdir(self.tmpdir) def clean_tmpdir(self): + # TODO: this should become part of campaign object if self.tmpdir is None: return @@ -214,6 +214,7 @@ def clean_tmpdir(self): self.create_tmpdir() def get_testcase_outfile(self, seedfile, s1): + # TODO: this should become part of campaign object ''' @rtype: string @return: the path to the output file for this seed: . diff --git a/src/certfuzz/test/campaign/config/test_bff_config.py b/src/certfuzz/test/campaign/config/test_bff_config.py index f11a468..dab9724 100644 --- a/src/certfuzz/test/campaign/config/test_bff_config.py +++ b/src/certfuzz/test/campaign/config/test_bff_config.py @@ -81,7 +81,6 @@ def test_init(self): self.assertEqual(self.cfg.watchdogtimeout, 2) self.assertEqual(self.cfg.copymode, 1) self.assertEqual(self.cfg.progtimeout, 3.4) - self.assertEqual(self.cfg.seedfile_local_dir, 'seedfile_local_dir') self.assertEqual(self.cfg.output_dir, 'output_dir') self.assertEqual(self.cfg.local_dir, 'local_dir') self.assertEqual(self.cfg.debugger_timeout, 4) @@ -92,13 +91,6 @@ def test_init(self): def test_program_is_script(self): pass - def test_check_program_file_type(self): - fd, fname = tempfile.mkstemp(dir=self.tmpdir, text=True) - os.write(fd, 'sometext') - os.close(fd) - self.cfg.program = fname - self.assertTrue(self.cfg.program_is_script()) - def test_get_minimized_file(self): self.assertEqual(self.cfg.get_minimized_file('foo.txt'), 'foo-%s.txt' % MINIMIZED_EXT) @@ -128,7 +120,6 @@ def test_read_config_options(self): self.assertEqual(cfg.watchdogtimeout, 2) self.assertEqual(cfg.copymode, 1) self.assertEqual(cfg.progtimeout, 3.4) - self.assertEqual(cfg.seedfile_local_dir, 'seedfile_local_dir') self.assertEqual(cfg.output_dir, 'output_dir') self.assertEqual(cfg.local_dir, 'local_dir') self.assertEqual(cfg.debugger_timeout, 4) diff --git a/src/certfuzz/test/campaign/test_campaign_linux.py b/src/certfuzz/test/campaign/test_campaign_linux.py index 9d3ca70..180af00 100644 --- a/src/certfuzz/test/campaign/test_campaign_linux.py +++ b/src/certfuzz/test/campaign/test_campaign_linux.py @@ -4,20 +4,25 @@ @organization: cert.org ''' import unittest -from certfuzz.campaign.campaign_linux import LinuxCampaign +from certfuzz.campaign.campaign_linux import LinuxCampaign, check_program_file_type +import tempfile +import os +import shutil class Test(unittest.TestCase): def setUp(self): -# self.campaign = LinuxCampaign() - pass + self.tmpdir = tempfile.mkdtemp() def tearDown(self): - pass + shutil.rmtree(self.tmpdir) - def testName(self): - pass + def test_check_program_file_type(self): + fd, fname = tempfile.mkstemp(dir=self.tmpdir, text=True) + os.write(fd, 'sometext') + os.close(fd) + self.assertTrue(check_program_file_type('text', fname)) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/linux/conf.d/bff.cfg b/src/linux/conf.d/bff.cfg index e047894..709eee2 100644 --- a/src/linux/conf.d/bff.cfg +++ b/src/linux/conf.d/bff.cfg @@ -53,18 +53,10 @@ debugger_template_dir=%(remote_dir)s/certfuzz/debuggers/templates # If results are stored in a shared location, # this directory needs to be unique for each fuzzing machine output_dir=~/results -crashers_dir=%(output_dir)s/crashers -seedfile_output_dir=%(output_dir)s/seeds # dirs local to the fuzzing machine # Local directory for fuzzing run mutated files. local_dir=~/fuzzing -seedfile_local_dir=%(local_dir)s/seeds - -# BFF stores cached objects to assist in recovering -# from fuzzing machine reboots -cached_objects_dir=%(local_dir)s -temp_working_dir=%(local_dir)s/tmp # Location of file used checked by Linux watchdog to determine # if fuzzer is still running From 51845625e5ab195e1aaf31ccbff8c825fff51faa Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 08:58:32 -0400 Subject: [PATCH 0358/1169] clean up unused imports --- src/certfuzz/campaign/campaign_linux.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 9d6d5dd..3e269db 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -4,14 +4,12 @@ @author: adh ''' -import itertools import logging import os -import shutil import sys -import tempfile import time import traceback +import subprocess from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.campaign.config import bff_config as cfg_helper @@ -19,17 +17,14 @@ from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.debuggers.registration import verify_supported_platform -from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.file_handlers.tmp_reaper import TmpReaper -from certfuzz.fuzztools import subprocess_helper as subp, filetools -from certfuzz.fuzztools.filetools import mkdir_p, copy_file +from certfuzz.fuzztools import subprocess_helper as subp from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.linux import Iteration -import subprocess logger = logging.getLogger(__name__) From fd4035594ab0cfbd74280f881cbd87047eb2b50a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 09:00:10 -0400 Subject: [PATCH 0359/1169] rename certfuzz.iteration.linux -> certfuzz.iteration.iteration_linux for consistency --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/iteration/{linux.py => iteration_linux.py} | 0 .../test/iteration/{test_linux.py => test_iteration_linux.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/certfuzz/iteration/{linux.py => iteration_linux.py} (100%) rename src/certfuzz/test/iteration/{test_linux.py => test_iteration_linux.py} (100%) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 3e269db..056616a 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -24,7 +24,7 @@ from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid -from certfuzz.iteration.linux import Iteration +from certfuzz.iteration.iteration_linux import Iteration logger = logging.getLogger(__name__) diff --git a/src/certfuzz/iteration/linux.py b/src/certfuzz/iteration/iteration_linux.py similarity index 100% rename from src/certfuzz/iteration/linux.py rename to src/certfuzz/iteration/iteration_linux.py diff --git a/src/certfuzz/test/iteration/test_linux.py b/src/certfuzz/test/iteration/test_iteration_linux.py similarity index 100% rename from src/certfuzz/test/iteration/test_linux.py rename to src/certfuzz/test/iteration/test_iteration_linux.py From 34be477fe7c02732394278b5cf47c26ab673ff03 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 09:16:24 -0400 Subject: [PATCH 0360/1169] bugfix --- src/certfuzz/campaign/campaign_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 29d2cb6..b3246da 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -225,7 +225,7 @@ def _cleanup_workdir(self): def _create_seedfile_set(self): logger.info('Building seedfile set') if self.seedfile_set is None: - with SeedfileSet(campiagn_id=self.campaign_id, + with SeedfileSet(campaign_id=self.campaign_id, originpath=self.seed_dir_in, localpath=self.seed_dir_local, outputpath=self.sf_set_out) as sfset: From c30a37cbe4d5d84363a9f2950256897d47fb0340 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 10:31:10 -0400 Subject: [PATCH 0361/1169] make ZzufTestCase more self contained --- src/certfuzz/fuzztools/zzuf.py | 33 +++++++++++++---------- src/certfuzz/iteration/iteration_linux.py | 14 +++++----- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index dd13411..5143bca 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -7,9 +7,9 @@ ''' import logging -from certfuzz.fuzztools import subprocess_helper as subp import subprocess from subprocess import CalledProcessError +import os logger = logging.getLogger(__name__) @@ -17,7 +17,7 @@ class ZzufTestCase: - def __init__(self, seedfile, seed, range, outfile): + def __init__(self, seedfile, seed, range, working_dir): ''' @param seedfile: The original seed file to use @param seed: The zzuf seed number to use @@ -27,12 +27,27 @@ def __init__(self, seedfile, seed, range, outfile): self.seedfile = seedfile self.seed = seed self.range = range - self.outfile = outfile + self.working_dir = working_dir + self.outfile = None + def __enter__(self): + self._set_outfile() self._set_cmdline() + return self + + def __exit__(self, etype, value, traceback): + return + + def _set_outfile(self): + # generate the test case file name + (root, ext) = os.path.splitext(self.seedfile.basename) + new_root = '{}-{}'.format(root, self.seed) + new_basename = new_root + ext + self.outfile = os.path.join(self.working_dir, new_basename) + logger.debug('Output file is %s', self.outfile) def _set_cmdline(self): - self.cmdline = 'cat %s | zzuf -s%s -r%s > %s' % (self.seedfile, self.seed, self.range, self.outfile) + self.cmdline = 'cat %s | zzuf -s%s -r%s > %s' % (self.seedfile.path, self.seed, self.range, self.outfile) def generate(self): subprocess.check_call(self.cmdline, shell=True) @@ -93,16 +108,6 @@ def go(self): return self.saw_crash - def generate_test_case(self, seedfile, seed, range, outfile): - ''' - Generates the test case for the given , , - and , storing the result in - ''' - - testcase = ZzufTestCase(seedfile, seed, range, outfile) - testcase.generate() - return testcase - def _get_zzuf_args(self): ''' Builds an argument string for zzuf based on the passed parameters. diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 5a6f0b7..6a7edbc 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -19,7 +19,7 @@ from certfuzz.file_handlers import seedfile_set from certfuzz.fuzztools import bff_helper as z, filetools from certfuzz.fuzztools.state_timer import STATE_TIMER -from certfuzz.fuzztools.zzuf import Zzuf +from certfuzz.fuzztools.zzuf import Zzuf, ZzufTestCase from certfuzz.fuzztools.zzuflog import ZzufLog from certfuzz.minimizer import MinimizerError, UnixMinimizer as Minimizer @@ -145,14 +145,14 @@ def _post_run(self): logger.info('Generating testcase for %s', zzuf_log.line) # a true crash zzuf_range = zzuf_log.range - # create the temp dir for the results - self.cfg.create_tmpdir() - outfile = self.cfg.get_testcase_outfile(self.seedfile.path, self.s1) - logger.debug('Output file is %s', outfile) - self.zzuf.generate_test_case(self.seedfile.path, self.s1, zzuf_range, outfile) + + with ZzufTestCase(seedfile=self.seedfile, seed=self.s1, + range=zzuf_range, + working_dir=self.working_dir) as testcase: + testcase.generate() # Do internal verification using GDB / Valgrind / Stderr - fuzzedfile = BasicFile(outfile) + fuzzedfile = BasicFile(testcase.outfile) testcase = BffCrash(cfg=self.cfg, seedfile=self.seedfile, From 3f69e586c3cd5a0c3c831f011bfda16882aeb80d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 14:50:52 -0400 Subject: [PATCH 0362/1169] refactor Iteration initialization call --- src/certfuzz/campaign/campaign_base.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index b3246da..5bc8c0b 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -340,15 +340,14 @@ def _do_interval(self): self.current_seed = interval_limit def _do_iteration(self, sf, rng_seed, seednum): - iter_args = (sf, rng_seed, seednum, self.config, self.fuzzer, - self.runner, self.debugger_module, self.dbg_class, - self.keep_heisenbugs, self.keep_duplicates, - self.cmd_template, self._crash_is_unique, - self.working_dir, self.outdir, self.debug) # use a with...as to ensure we always hit # the __enter__ and __exit__ methods of the # newly created Iteration() - with Iteration(*iter_args) as iteration: + with Iteration(sf, rng_seed, seednum, self.config, self.fuzzer, + self.runner, self.debugger_module, self.dbg_class, + self.keep_heisenbugs, self.keep_duplicates, + self.cmd_template, self._crash_is_unique, + self.working_dir, self.outdir, self.debug) as iteration: try: iteration.go() except FuzzerExhaustedError: From 26f5cdc54be05917b4813fc56e5952dc312be27c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 14:51:42 -0400 Subject: [PATCH 0363/1169] if crash_base_dir doesn't exist, just create it --- src/certfuzz/crash/bff_crash.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index 0eb51db..9f6688a 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -8,7 +8,7 @@ from certfuzz.crash.crash_base import Crash, CrashError from certfuzz.debuggers import registration -from certfuzz.fuzztools import hostinfo +from certfuzz.fuzztools import hostinfo, filetools try: @@ -159,8 +159,10 @@ def get_signature(self): return self.signature def _verify_crash_base_dir(self): - if not self.crash_base_dir or not os.path.exists(self.crash_base_dir): - raise CrashError('Crash has no base dir') + if not self.crash_base_dir: + raise CrashError('crash_base_dir not set') + if not os.path.exists(self.crash_base_dir): + filetools.make_directories(self.crash_base_dir) def get_result_dir(self): assert self.crash_base_dir From fc0fd99d0ec1dca99f5620888dcb9b37cc47c214 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 14:52:34 -0400 Subject: [PATCH 0364/1169] pass in the output directory from the campaign to the iteration --- src/certfuzz/campaign/campaign_linux.py | 3 ++- src/certfuzz/iteration/iteration_linux.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 056616a..faecdc2 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -252,5 +252,6 @@ def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): with Iteration(cfg=self.cfg, seednum=seednum, seedfile=seedfile, r=range_obj, workdirbase=self.working_dir, quiet=quiet_flag, uniq_func=self._crash_is_unique, sf_set=self.seedfile_set, - rf=seedfile.rangefinder) as iteration: + rf=seedfile.rangefinder, + outdir=self.outdir) as iteration: iteration.go() diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 6a7edbc..a067ee5 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -41,7 +41,7 @@ def get_uniq_logger(logfile): class Iteration(IterationBase3): def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None, - sf_set=None, rf=None): + sf_set=None, rf=None, outdir=None): IterationBase3.__init__(self, workdirbase) self.cfg = cfg self.seednum = seednum @@ -50,6 +50,9 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No self.quiet_flag = quiet self.sf_set = sf_set self.rf = rf + self.outdir = outdir + + self.testcase_base_dir = os.path.join(self.outdir, 'crashers') if uniq_func is None: self.uniq_func = lambda _tc_id: True @@ -161,7 +164,7 @@ def _post_run(self): debugger_timeout=self.cfg.debugger_timeout, killprocname=self.cfg.killprocname, backtrace_lines=self.cfg.backtracelevels, - crashers_dir=self.cfg.crashers_dir, + crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, seednum=self.s1, range=self.r) From 4bd5391a94c4b855296e96bdfebbcfa2e54dad71 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 26 Jun 2014 15:08:43 -0400 Subject: [PATCH 0365/1169] fix unit test --- src/certfuzz/test/fuzztools/test_zzuf.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/test/fuzztools/test_zzuf.py b/src/certfuzz/test/fuzztools/test_zzuf.py index 88c403f..a27c859 100644 --- a/src/certfuzz/test/fuzztools/test_zzuf.py +++ b/src/certfuzz/test/fuzztools/test_zzuf.py @@ -1,13 +1,14 @@ -from certfuzz.fuzztools.zzuf import Zzuf -from certfuzz.fuzztools.zzuf import ZzufTestCase -import re ''' Created on Apr 8, 2011 @organization: cert.org ''' - import unittest +import re + +from certfuzz.fuzztools.zzuf import Zzuf +from certfuzz.fuzztools.zzuf import ZzufTestCase + class Test(unittest.TestCase): @@ -24,8 +25,15 @@ def tearDown(self): pass def test_testcase_set_cmdline(self): - expected = "cat a | zzuf -sb -rc > d" - testcase = ZzufTestCase('a', 'b', 'c', 'd') + expected = "cat a | zzuf -sb -rc > x" + + class MockSf(object): + path = 'a' + sf = MockSf() + + testcase = ZzufTestCase(sf, 'b', 'c', 'd') + testcase.outfile = 'x' + testcase._set_cmdline() self.assertEqual(testcase.cmdline, expected) def test_generate_test_case(self): From 06057887d9e30072c6ed7571717cc553b423cf5d Mon Sep 17 00:00:00 2001 From: wd Date: Fri, 27 Jun 2014 10:02:55 -0400 Subject: [PATCH 0366/1169] Saving files before refreshing line endings --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..81224b1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +*.sh text eol=lf \ No newline at end of file From ba6495f87962b27028b1871edf02f2d96633af6a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 27 Jun 2014 10:07:19 -0400 Subject: [PATCH 0367/1169] Only change .sh files --- .gitattributes | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index 81224b1..526c8a3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1 @@ -# Set the default behavior, in case people don't have core.autocrlf set. -* text=auto - *.sh text eol=lf \ No newline at end of file From 196d25b680c349e5eb2e3d28f4022790d77fa1b7 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 27 Jun 2014 10:21:37 -0400 Subject: [PATCH 0368/1169] Allow batch.sh to take arguments BFF-756 --- src/linux/batch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index 8a7391f..06b2fc7 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -98,7 +98,7 @@ cd $scriptlocation echo "Using python interpreter: $mypython" if [[ -f "$scriptlocation/bff.py" ]]; then - $mypython $scriptlocation/bff.py --config=$scriptlocation/conf.d/bff.cfg + $mypython $scriptlocation/bff.py --config=$scriptlocation/conf.d/bff.cfg "$@" else read -p "Cannot find $scriptlocation/bff.py Please verify script locations." fi From 4ea73f61f38ad9b62c65432865e0a5969160c3ba Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 27 Jun 2014 11:02:49 -0400 Subject: [PATCH 0369/1169] refactor __enter__ and __exit__ methods to DRY up shared code --- src/certfuzz/campaign/campaign_base.py | 99 +++++++++++++++++++---- src/certfuzz/campaign/campaign_linux.py | 37 ++------- src/certfuzz/campaign/campaign_meta.py | 4 + src/certfuzz/campaign/campaign_windows.py | 11 ++- 4 files changed, 97 insertions(+), 54 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 5bc8c0b..f6b9258 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -124,10 +124,32 @@ def __init__(self, config_file, result_dir=None, campaign_cache=None, self.runner_module_name = '%s.%s' % (packages['runners'], self.config['runner']['runner']) self.debugger_module_name = '%s.%s' % (packages['debuggers'], self.config['debugger']['debugger']) + def _pre_enter(self): + ''' + Callback for class-specific tasks that happen before + CampaignBase.__enter__() does its work. If self is modified it must + return self, otherwise no return value is needed. + + @return: None or self + ''' + + def _post_enter(self): + ''' + Callback for class-specific tasks that happen after + CampaignBase.__enter__() does its work. If self is modified it must + return self, otherwise no return value is needed. + + @return: None or self + ''' + def __enter__(self): ''' Creates a runtime context for the campaign. ''' + _result = self._pre_enter() + if _result is not None: + self = _result + self._read_state() self._check_prog() self._setup_workdir() @@ -136,37 +158,80 @@ def __enter__(self): self._set_debugger() self._setup_output() self._create_seedfile_set() + + _result = self._post_enter() + if _result is not None: + self = _result + return self - def __exit__(self, etype, value, mytraceback): + def _handle_common_errors(self, etype, value, mytraceback): ''' - Handles known exceptions gracefully, attempts to clean up temp files - before exiting. + Handles errors common to this class and all its subclasses + :param etype: + :param value: ''' handled = False -# self._stop_buttonclicker() + # self._stop_buttonclicker() if etype is KeyboardInterrupt: logger.warning('Keyboard interrupt - exiting') handled = True elif etype is RunnerArchitectureError: logger.error('Unsupported architecture: %s', value) - logger.error('Set "verify_architecture=false" in the runner \ - section of your config to override this check') + logger.error('Set "verify_architecture=false" in the runner section of your config to override this check') handled = True elif etype is RunnerPlatformVersionError: logger.error('Unsupported platform: %s', value) handled = True - elif etype: - logger.debug('Unhandled exception:') - logger.debug(' type: %s', etype) - logger.debug(' value: %s', value) - for l in traceback.format_exception(etype, value, mytraceback): - logger.debug(l.rstrip()) - if self.debug and etype and not handled: - # leave it behind if we're in debug mode - # and there's a problem - logger.debug('Skipping cleanup since we are in debug mode.') - else: + return handled + + def _handle_errors(self, etype, value, mytraceback): + ''' + Callback to handle class-specific errors. If used, it should be + overridden by subclasses. Will be called after _handle_common_errors + + :param etype: + :param value: + :param mytraceback: + ''' + + def _log_unhandled_exception(self, etype, value, mytraceback): + logger.debug('Unhandled exception:') + logger.debug(' type: %s', etype) + logger.debug(' value: %s', value) + for l in traceback.format_exception(etype, value, mytraceback): + logger.debug(l.rstrip()) + + def _pre_exit(self): + ''' + Implements methods to be completed prior to handling errors in the + __exit__ method. No return value. + ''' + + def __exit__(self, etype, value, mytraceback): + ''' + Handles known exceptions gracefully, attempts to clean up temp files + before exiting. + ''' + self._pre_exit() + + # handle common errors + handled = self._handle_common_errors(etype, value, mytraceback) + + if etype and not handled: + # call the class-specific error handler + handled = self._handle_errors(etype, value, mytraceback) + + cleanup = True + if etype and not handled: + # if you got here, nothing has handled the error + # so log it and keep going + self._log_unhandled_exception(etype, value, mytraceback) + if self.debug: + cleanup = False + logger.debug('Skipping cleanup since we are in debug mode.') + + if cleanup: self._cleanup_workdir() return handled diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index faecdc2..a38631a 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -92,46 +92,21 @@ def __init__(self, config_file=None, result_dir=None, debug=False): # give up if we don't have a debugger verify_supported_platform() - def __enter__(self): + def _pre_enter(self): self._start_process_killer() self._set_unbuffered_stdout() - CampaignBase.__enter__(self) - + def _post_enter(self): if self.cfg.watchdogtimeout: self._setup_watchdog() - check_ppid() + self._cache_app() - return self - - def __exit__(self, etype, value, mytraceback): - handled = not etype - - if etype is KeyboardInterrupt: - logger.warning('Keyboard interrupt - exiting') - handled = True + def _handle_errors(self, etype, value, mytraceback): + handled = False if etype is CampaignScriptError: logger.warning("Please configure BFF to fuzz a binary. Exiting...") handled = True - - if handled: - self._cleanup_workdir() - elif self.debug: - # Not handled, debug set - - # leave it behind if we're in debug mode - # and there's a problem - logger.debug('Skipping cleanup since we are in debug mode.') - else: - # Not handled, debug not set - - logger.debug('Unhandled exception:') - logger.debug(' type: %s', etype) - logger.debug(' value: %s', value) - for l in traceback.format_exception(etype, value, mytraceback): - logger.debug(l.rstrip()) - return handled def _set_unbuffered_stdout(self): @@ -149,7 +124,7 @@ def _start_process_killer(self): with ProcessKiller(self.cfg.killprocname, self.cfg.killproctimeout) as pk: pk.go() - def _cache_prg(self): + def _cache_app(self): logger.debug('cache program') sf = self.seedfile_set.next_item() diff --git a/src/certfuzz/campaign/campaign_meta.py b/src/certfuzz/campaign/campaign_meta.py index 82ef005..9ac94ed 100644 --- a/src/certfuzz/campaign/campaign_meta.py +++ b/src/certfuzz/campaign/campaign_meta.py @@ -40,6 +40,10 @@ def __enter__(self): def __exit__(self, etype, value, mytraceback): pass + @abc.abstractmethod + def _handle_errors(self, etype, value, mytraceback): + raise NotImplementedError + @abc.abstractmethod def __getstate__(self): raise NotImplementedError diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 0249efe..4dcd46f 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -20,22 +20,21 @@ class WindowsCampaign(CampaignBase): ''' Extends CampaignBase to add windows-specific features like ButtonClicker ''' - def __enter__(self): + def _pre_enter(self): if sys.platform == 'win32': winver = sys.getwindowsversion().major machine = platform.machine() - hook_incompat = (winver > 5) or (machine == 'AMD64') + hook_incompat = winver > 5 or machine == 'AMD64' if hook_incompat and self.runner_module_name == 'certfuzz.runners.winrun': logger.debug('winrun is not compatible with Windows %s %s. Overriding.', winver, machine) self.runner_module_name = None - self = CampaignBase.__enter__(self) + + def _post_enter(self): self._start_buttonclicker() self._cache_app() - return self - def __exit__(self, etype, value, mytraceback): + def _pre_exit(self): self._stop_buttonclicker() - return CampaignBase.__exit__(self, etype, value, mytraceback) def _cache_app(self): logger.debug('Caching application %s and determining if we need to watch the CPU...', self.program) From ca40fb2be7eff075fbb17b68cf043ab66ef961a4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 27 Jun 2014 14:17:05 -0400 Subject: [PATCH 0370/1169] merge CampaignMeta into CampaignBase, DRY up campaign_linux and campaign_windows At this point, the main platform-specific parts of the code are isolated down to: __enter__ and __exit__ methods _do_interval _do_iteration I think there is still room to squeeze _do_interval and _do_iteration some more. --- src/certfuzz/campaign/campaign_base.py | 132 ++++++++-------------- src/certfuzz/campaign/campaign_linux.py | 17 +-- src/certfuzz/campaign/campaign_meta.py | 83 -------------- src/certfuzz/campaign/campaign_windows.py | 99 ++++++++++++++++ 4 files changed, 149 insertions(+), 182 deletions(-) delete mode 100644 src/certfuzz/campaign/campaign_meta.py diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index f6b9258..6acb008 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -4,7 +4,6 @@ @organization: cert.org ''' -import gc import logging import os import re @@ -12,20 +11,19 @@ import tempfile import traceback -from certfuzz.campaign.campaign_meta import CampaignMeta, import_module_by_name from certfuzz.version import __version__ from certfuzz.campaign.config.config_windows import Config from certfuzz.campaign.errors import CampaignError from certfuzz.debuggers import registration from certfuzz.file_handlers.seedfile_set import SeedfileSet -from certfuzz.fuzzers.errors import FuzzerExhaustedError from certfuzz.fuzztools import filetools from certfuzz.fuzztools.object_caching import dump_obj_to_file from certfuzz.runners.errors import RunnerArchitectureError, \ RunnerPlatformVersionError import cPickle as pickle -from certfuzz.iteration.iteration_windows import Iteration +import abc +import sys logger = logging.getLogger(__name__) @@ -36,10 +34,20 @@ } -class CampaignBase(CampaignMeta): +def import_module_by_name(name, logger=None): + if logger: + logger.debug('Importing module %s', name) + __import__(name) + module = sys.modules[name] + return module + + +class CampaignBase(object): ''' Provides a fuzzing campaign object. ''' + __metaclass__ = abc.ABCMeta + def __init__(self, config_file, result_dir=None, campaign_cache=None, debug=False): ''' @@ -124,6 +132,7 @@ def __init__(self, config_file, result_dir=None, campaign_cache=None, self.runner_module_name = '%s.%s' % (packages['runners'], self.config['runner']['runner']) self.debugger_module_name = '%s.%s' % (packages['debuggers'], self.config['debugger']['debugger']) + @abc.abstractmethod def _pre_enter(self): ''' Callback for class-specific tasks that happen before @@ -133,6 +142,7 @@ def _pre_enter(self): @return: None or self ''' + @abc.abstractmethod def _post_enter(self): ''' Callback for class-specific tasks that happen after @@ -185,6 +195,7 @@ def _handle_common_errors(self, etype, value, mytraceback): handled = True return handled + @abc.abstractmethod def _handle_errors(self, etype, value, mytraceback): ''' Callback to handle class-specific errors. If used, it should be @@ -202,6 +213,7 @@ def _log_unhandled_exception(self, etype, value, mytraceback): for l in traceback.format_exception(etype, value, mytraceback): logger.debug(l.rstrip()) + @abc.abstractmethod def _pre_exit(self): ''' Implements methods to be completed prior to handling errors in the @@ -241,15 +253,18 @@ def _check_prog(self): msg = 'Cannot find program "%s" (resolves to "%s")' % (self.program, os.path.abspath(self.program)) raise CampaignError(msg) + @abc.abstractmethod def _set_fuzzer(self): self.fuzzer_module = import_module_by_name(self.fuzzer_module_name, logger) self.fuzzer = self.fuzzer_module._fuzzer_class + @abc.abstractmethod def _set_runner(self): if self.runner_module_name: self.runner_module = import_module_by_name(self.runner_module_name, logger) self.runner = self.runner_module._runner_class + @abc.abstractmethod def _set_debugger(self): # this will import the module which registers the debugger self.debugger_module = import_module_by_name(self.debugger_module_name, logger) @@ -259,7 +274,9 @@ def _set_debugger(self): self.dbg_class = registration.debug_class def _write_version(self): - CampaignMeta._write_version(self) + version_file = os.path.join(self.outdir, 'version.txt') + version_string = 'Results produced by %s v%s' % (__name__, __version__) + filetools.write_file(version_string, version_file) def _setup_output(self): # construct run output directory @@ -296,20 +313,13 @@ def _create_seedfile_set(self): outputpath=self.sf_set_out) as sfset: self.seedfile_set = sfset - def __setstate__(self, state): - # turn the list into a set - state['crashes_seen'] = set(state['crashes_seen']) - - # reconstitute the seedfile set - with SeedfileSet(state['campaign_id'], state['seed_dir_in'], state['seed_dir_local'], - state['sf_set_out']) as sfset: - new_sfset = sfset - - new_sfset.__setstate__(state['seedfile_set']) - state['seedfile_set'] = new_sfset + @abc.abstractmethod + def __getstate__(self): + raise NotImplementedError - # update yourself - self.__dict__.update(state) + @abc.abstractmethod + def __setstate__(self): + raise NotImplementedError def _read_state(self, cache_file=None): if not cache_file: @@ -338,24 +348,6 @@ def _read_state(self, cache_file=None): else: logger.warning('Unable to reload campaign from %s, will use new campaign instead', cache_file) - def __getstate__(self): - state = self.__dict__.copy() - - state['crashes_seen'] = list(state['crashes_seen']) - if state['seedfile_set']: - state['seedfile_set'] = state['seedfile_set'].__getstate__() - - # for attributes that are modules, - # we can safely delete them as they will be - # reconstituted when we __enter__ a context - for key in ['fuzzer_module', 'fuzzer', - 'runner_module', 'runner', - 'debugger_module', 'dbg_class' - ]: - if key in state: - del state[key] - return state - def _save_state(self, cachefile=None): if not cachefile: cachefile = self.cached_state_file @@ -377,60 +369,24 @@ def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): logger.debug('%s was found, not unique', crash_id) return False + @abc.abstractmethod def _keep_going(self): - return CampaignMeta._keep_going(self) + ''' + Returns True if a campaign should proceed. False otherwise. + ''' + return True + @abc.abstractmethod def _do_interval(self): - # choose seedfile - sf = self.seedfile_set.next_item() - - logger.info('Selected seedfile: %s', sf.basename) - rng_seed = int(sf.md5, 16) - - if self.current_seed % self.status_interval == 0: - # cache our current state - self._save_state() - - interval_limit = self.current_seed + self.seed_interval - - # start an iteration interval - # note that range does not include interval_limit - for seednum in xrange(self.current_seed, interval_limit): - self._do_iteration(sf, rng_seed, seednum) - - del sf - # manually collect garbage - gc.collect() - - self.current_seed = interval_limit - - def _do_iteration(self, sf, rng_seed, seednum): - # use a with...as to ensure we always hit - # the __enter__ and __exit__ methods of the - # newly created Iteration() - with Iteration(sf, rng_seed, seednum, self.config, self.fuzzer, - self.runner, self.debugger_module, self.dbg_class, - self.keep_heisenbugs, self.keep_duplicates, - self.cmd_template, self._crash_is_unique, - self.working_dir, self.outdir, self.debug) as iteration: - try: - iteration.go() - except FuzzerExhaustedError: - # Some fuzzers run out of things to do. They should - # raise a FuzzerExhaustedError when that happens. - logger.info('Done with %s, removing from set', sf.basename) - # FIXME - # self.seedfile_set.del_item(sf.md5) - if not seednum % self.status_interval: - logger.info('Iteration: %d Crashes found: %d', self.current_seed, - len(self.crashes_seen)) - # FIXME - # self.seedfile_set.update_csv() - logger.info('Seedfile Set Status:') - logger.info('FIXME') - # for k, score, successes, tries, p in self.seedfile_set.status(): - # logger.info('%s %0.6f %d %d %0.6f', k, score, successes, - # tries, p) + ''' + Implements a loop over a set of iterations + ''' + + @abc.abstractmethod + def _do_iteration(self): + ''' + Implements a single iteration of the fuzzing process. + ''' def go(self): ''' diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index a38631a..e297278 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -92,9 +92,12 @@ def __init__(self, config_file=None, result_dir=None, debug=False): # give up if we don't have a debugger verify_supported_platform() + self._check_for_script() + def _pre_enter(self): self._start_process_killer() self._set_unbuffered_stdout() + self._check_for_script() def _post_enter(self): if self.cfg.watchdogtimeout: @@ -102,13 +105,6 @@ def _post_enter(self): check_ppid() self._cache_app() - def _handle_errors(self, etype, value, mytraceback): - handled = False - if etype is CampaignScriptError: - logger.warning("Please configure BFF to fuzz a binary. Exiting...") - handled = True - return handled - def _set_unbuffered_stdout(self): ''' Reopens stdout with a buffersize of 0 (unbuffered) @@ -154,10 +150,6 @@ def _check_for_script(self): logger.warning("Target application is a shell script.") raise CampaignScriptError() - def _check_prog(self): - self._check_for_script() - CampaignBase._check_prog(self) - def _set_fuzzer(self): ''' Overrides parent class @@ -214,6 +206,9 @@ def _do_interval(self): logger.info(STATE_TIMER) interval_limit = self.current_seed + self.seed_interval + + # start an iteration interval + # note that range does not include interval_limit logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) for seednum in xrange(self.current_seed, interval_limit): self._do_iteration(sf, r, qf, seednum) diff --git a/src/certfuzz/campaign/campaign_meta.py b/src/certfuzz/campaign/campaign_meta.py deleted file mode 100644 index 9ac94ed..0000000 --- a/src/certfuzz/campaign/campaign_meta.py +++ /dev/null @@ -1,83 +0,0 @@ -''' -Created on Oct 23, 2012 - -The certfuzz.campaign package provides modules to manage fuzzing campaigns, -configurations, and iterations. - -@organization: cert.org -''' -import abc -import sys -import os - -from certfuzz.version import __version__ -from certfuzz.fuzztools import filetools - - -def import_module_by_name(name, logger=None): - if logger: - logger.debug('Importing module %s', name) - __import__(name) - module = sys.modules[name] - return module - - -class CampaignMeta(object): - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def __init__(self, config_file, result_dir=None, campaign_cache=None, debug=False): - self.config_file = config_file - self.cached_state_file = campaign_cache - self.debug = debug - self._version = __version__ - - @abc.abstractmethod - def __enter__(self): - return self - - @abc.abstractmethod - def __exit__(self, etype, value, mytraceback): - pass - - @abc.abstractmethod - def _handle_errors(self, etype, value, mytraceback): - raise NotImplementedError - - @abc.abstractmethod - def __getstate__(self): - raise NotImplementedError - - @abc.abstractmethod - def __setstate__(self): - raise NotImplementedError - - @abc.abstractmethod - def _do_interval(self): - raise NotImplementedError - - @abc.abstractmethod - def _do_iteration(self): - raise NotImplementedError - - @abc.abstractmethod - def _keep_going(self): - ''' - Returns True if a campaign should proceed. False otherwise. - ''' - return True - - @abc.abstractmethod - def _write_version(self): - version_file = os.path.join(self.outdir, 'version.txt') - version_string = 'Results produced by %s v%s' % (__name__, __version__) - filetools.write_file(version_string, version_file) - - @abc.abstractmethod - def go(self): - ''' - Executes a fuzzing campaign. Will continue until either we run out of - iterations or the user issues a KeyboardInterrupt (ctrl-C). - ''' - while self._keep_going(): - self._do_interval() diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 4dcd46f..a50a752 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -12,6 +12,10 @@ from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.runners.killableprocess import Popen +from certfuzz.file_handlers.seedfile_set import SeedfileSet +from certfuzz.iteration.iteration_windows import Iteration +from certfuzz.fuzzers.errors import FuzzerExhaustedError +import gc logger = logging.getLogger(__name__) @@ -20,6 +24,39 @@ class WindowsCampaign(CampaignBase): ''' Extends CampaignBase to add windows-specific features like ButtonClicker ''' + def __getstate__(self): + state = self.__dict__.copy() + + state['crashes_seen'] = list(state['crashes_seen']) + if state['seedfile_set']: + state['seedfile_set'] = state['seedfile_set'].__getstate__() + + # for attributes that are modules, + # we can safely delete them as they will be + # reconstituted when we __enter__ a context + for key in ['fuzzer_module', 'fuzzer', + 'runner_module', 'runner', + 'debugger_module', 'dbg_class' + ]: + if key in state: + del state[key] + return state + + def __setstate__(self, state): + # turn the list into a set + state['crashes_seen'] = set(state['crashes_seen']) + + # reconstitute the seedfile set + with SeedfileSet(state['campaign_id'], state['seed_dir_in'], state['seed_dir_local'], + state['sf_set_out']) as sfset: + new_sfset = sfset + + new_sfset.__setstate__(state['seedfile_set']) + state['seedfile_set'] = new_sfset + + # update yourself + self.__dict__.update(state) + def _pre_enter(self): if sys.platform == 'win32': winver = sys.getwindowsversion().major @@ -99,3 +136,65 @@ def _start_buttonclicker(self): def _stop_buttonclicker(self): if self.use_buttonclicker: os.system('taskkill /im buttonclicker.exe') + + def _do_interval(self): + # choose seedfile + sf = self.seedfile_set.next_item() + + logger.info('Selected seedfile: %s', sf.basename) + rng_seed = int(sf.md5, 16) + + if self.current_seed % self.status_interval == 0: + # cache our current state + self._save_state() + + interval_limit = self.current_seed + self.seed_interval + + # start an iteration interval + # note that range does not include interval_limit + logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) + for seednum in xrange(self.current_seed, interval_limit): + self._do_iteration(sf, rng_seed, seednum) + + del sf + # manually collect garbage + gc.collect() + + self.current_seed = interval_limit + + def _do_iteration(self, sf, rng_seed, seednum): + # use a with...as to ensure we always hit + # the __enter__ and __exit__ methods of the + # newly created Iteration() + with Iteration(sf, rng_seed, seednum, self.config, self.fuzzer, + self.runner, self.debugger_module, self.dbg_class, + self.keep_heisenbugs, self.keep_duplicates, + self.cmd_template, self._crash_is_unique, + self.working_dir, self.outdir, self.debug) as iteration: + try: + iteration.go() + except FuzzerExhaustedError: + # Some fuzzers run out of things to do. They should + # raise a FuzzerExhaustedError when that happens. + logger.info('Done with %s, removing from set', sf.basename) + # FIXME + # self.seedfile_set.del_item(sf.md5) + if not seednum % self.status_interval: + logger.info('Iteration: %d Crashes found: %d', self.current_seed, + len(self.crashes_seen)) + # FIXME + # self.seedfile_set.update_csv() + logger.info('Seedfile Set Status:') + logger.info('FIXME') + # for k, score, successes, tries, p in self.seedfile_set.status(): + # logger.info('%s %0.6f %d %d %0.6f', k, score, successes, + # tries, p) + + def _set_fuzzer(self): + CampaignBase._set_fuzzer(self) + + def _set_runner(self): + CampaignBase._set_runner(self) + + def _set_debugger(self): + CampaignBase._set_debugger(self) From 7426ee1a928e3b6543a7173cc1258018c1cf5f2f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 27 Jun 2014 14:56:24 -0400 Subject: [PATCH 0371/1169] DRY up __init__ methods --- src/certfuzz/campaign/campaign_base.py | 88 +++++++++-------------- src/certfuzz/campaign/campaign_linux.py | 67 ++++++++--------- src/certfuzz/campaign/campaign_windows.py | 57 ++++++++++++++- 3 files changed, 116 insertions(+), 96 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 6acb008..cc1090f 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -6,35 +6,34 @@ import logging import os -import re import shutil import tempfile import traceback from certfuzz.version import __version__ -from certfuzz.campaign.config.config_windows import Config from certfuzz.campaign.errors import CampaignError from certfuzz.debuggers import registration from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.fuzztools import filetools -from certfuzz.fuzztools.object_caching import dump_obj_to_file from certfuzz.runners.errors import RunnerArchitectureError, \ RunnerPlatformVersionError import cPickle as pickle import abc import sys +import re logger = logging.getLogger(__name__) -packages = {'fuzzers': 'certfuzz.fuzzers', - 'runners': 'certfuzz.runners', - 'debuggers': 'certfuzz.debuggers', - } - def import_module_by_name(name, logger=None): + ''' + Imports a module at runtime given the pythonic name of the module + e.g., certfuzz.fuzzers.bytemut + :param name: + :param logger: + ''' if logger: logger.debug('Importing module %s', name) __import__(name) @@ -48,8 +47,8 @@ class CampaignBase(object): ''' __metaclass__ = abc.ABCMeta - def __init__(self, config_file, result_dir=None, campaign_cache=None, - debug=False): + @abc.abstractmethod + def __init__(self, config_file, result_dir=None, debug=False): ''' Typically one would invoke a campaign as follows: @@ -67,70 +66,47 @@ def __init__(self, config_file, result_dir=None, campaign_cache=None, @param debug: boolean indicating whether we are in debug mode ''' logger.debug('initialize %s', self.__class__.__name__) + # Read the cfg file self.config_file = config_file - self.cached_state_file = campaign_cache + self.cached_state_file = None self.debug = debug self._version = __version__ - self.gui_app = False - cfgobj = Config(self.config_file) - self.config = cfgobj.config - self.configdate = cfgobj.configdate + self.crashes_seen = set() - self.campaign_id = self.config['campaign']['id'] + self.runner_module_name = None + self.runner_module = None + self.runner = None - # TODO: buttonclicker should move to campaign_windows - self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker') - if not self.use_buttonclicker: - self.use_buttonclicker = False + self.seedfile_set = None + self.working_dir = None + self.seed_dir_local = None - self.current_seed = self.config['runoptions'].get('first_iteration') - if not self.current_seed: - # default to zero - self.current_seed = 0 + # flag to indicate whether this is a fresh script start up or not + self.first_chunk = True - self.seed_interval = self.config['runoptions'].get('seed_interval') - if not self.seed_interval: - self.seed_interval = 1 + # TODO: consider making this configurable + self.status_interval = 100 + self.outdir_base = None if result_dir: self.outdir_base = os.path.abspath(result_dir) - else: - self.outdir_base = os.path.abspath(self.config['directories']['results_dir']) + def _common_init(self): + ''' + Initializes some additional properties common to all platforms + ''' self.outdir = os.path.join(self.outdir_base, self.campaign_id) logger.debug('outdir=%s', self.outdir) - self.sf_set_out = os.path.join(self.outdir, 'seedfiles') - - self.work_dir_base = os.path.abspath(self.config['directories']['working_dir']) + self.sf_set_out = os.path.join(self.outdir, 'seedfiles') if not self.cached_state_file: cachefile = 'campaign_%s.pkl' % re.sub('\W', '_', self.campaign_id) self.cached_state_file = os.path.join(self.work_dir_base, cachefile) - - self.seed_dir_in = self.config['directories']['seedfile_dir'] - - self.keep_duplicates = self.config['runoptions']['keep_all_duplicates'] - self.keep_heisenbugs = self.config['campaign']['keep_heisenbugs'] - self.should_keep_u_faddr = self.config['runoptions']['keep_unique_faddr'] - - # TODO: consider making this configurable - self.status_interval = 100 - - self.program = self.config['target']['program'] - self.cmd_template = self.config['target']['cmdline_template'] - self.crashes_seen = set() - - self.runner_module_name = None - self.runner_module = None - self.runner = None - - self.seedfile_set = None - - self.fuzzer_module_name = '%s.%s' % (packages['fuzzers'], self.config['fuzzer']['fuzzer']) - if self.config['runner']['runner']: - self.runner_module_name = '%s.%s' % (packages['runners'], self.config['runner']['runner']) - self.debugger_module_name = '%s.%s' % (packages['debuggers'], self.config['debugger']['debugger']) + if not self.seed_interval: + self.seed_interval = 1 + if not self.current_seed: + self.current_seed = 0 @abc.abstractmethod def _pre_enter(self): diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index e297278..9c9365c 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -8,7 +8,6 @@ import os import sys import time -import traceback import subprocess from certfuzz.campaign.campaign_base import CampaignBase @@ -55,43 +54,35 @@ def check_program_file_type(string, program): class LinuxCampaign(CampaignBase): + ''' + Extends CampaignBase to add linux-specific features. + ''' def __init__(self, config_file=None, result_dir=None, debug=False): - # Read the cfg file - self.config_file = config_file - self.result_dir = result_dir - self.debug = debug - logger.info('Reading config from %s', self.config_file) - self.cfg = cfg_helper.read_config_options(self.config_file) - - self.campaign_id = self.cfg.campaign_id - self.current_seed = self.cfg.start_seed - self.seed_interval = self.cfg.seed_interval - - self.seed_dir_in = self.cfg.seedfile_origin_dir + CampaignBase.__init__(self, config_file, result_dir, debug) - self.outdir_base = os.path.abspath(self.cfg.output_dir) - - self.outdir = os.path.join(self.outdir_base, self.campaign_id) - logger.debug('outdir=%s', self.outdir) - self.sf_set_out = os.path.join(self.outdir, 'seedfiles') - - self.work_dir_base = self.cfg.local_dir + # read configs + logger.info('Reading config from %s', self.config_file) + self.config = cfg_helper.read_config_options(self.config_file) - self.program = self.cfg.program + # pull stuff out of configs + self.campaign_id = self.config.campaign_id + self.current_seed = self.config.start_seed + self.seed_interval = self.config.seed_interval + self.seed_dir_in = self.config.seedfile_origin_dir - # flag to indicate whether this is a fresh script start up or not - self.first_chunk = True + if self.outdir_base is None: + # it wasn't spec'ed on the command line so use the config + self.outdir_base = os.path.abspath(self.config.output_dir) - self.seedfile_set = None + self.work_dir_base = self.config.local_dir + self.program = self.config.program - self.hashes = [] - self.working_dir = None - self.seed_dir_local = None - self.crashes_seen = set() + # must occur after work_dir_base, outdir_base, and campaign_id are set + self._common_init() # give up if we don't have a debugger verify_supported_platform() - + # give up if prog is a script self._check_for_script() def _pre_enter(self): @@ -100,7 +91,7 @@ def _pre_enter(self): self._check_for_script() def _post_enter(self): - if self.cfg.watchdogtimeout: + if self.config.watchdogtimeout: self._setup_watchdog() check_ppid() self._cache_app() @@ -117,7 +108,7 @@ def _set_unbuffered_stdout(self): def _start_process_killer(self): logger.debug('start process killer') - with ProcessKiller(self.cfg.killprocname, self.cfg.killproctimeout) as pk: + with ProcessKiller(self.config.killprocname, self.config.killproctimeout) as pk: pk.go() def _cache_app(self): @@ -125,9 +116,9 @@ def _cache_app(self): sf = self.seedfile_set.next_item() # Run the program once to cache it into memory - fullpathorig = self.cfg.full_path_original(sf.path) - cmdargs = self.cfg.get_command_list(fullpathorig) - subp.run_with_timer(cmdargs, self.cfg.progtimeout * 8, self.cfg.killprocname, use_shell=True) + fullpathorig = self.config.full_path_original(sf.path) + cmdargs = self.config.get_command_list(fullpathorig) + subp.run_with_timer(cmdargs, self.config.progtimeout * 8, self.config.killprocname, use_shell=True) # Give target time to die time.sleep(1) @@ -135,13 +126,13 @@ def _cache_app(self): def _setup_watchdog(self): logger.debug('setup watchdog') # setup our watchdog file toucher - TWDF.remote_d = self.cfg.remote_dir - TWDF.wdf = self.cfg.watchdogfile + TWDF.remote_d = self.config.remote_dir + TWDF.wdf = self.config.watchdogfile TWDF.enable() touch_watchdog_file() # set up the watchdog timeout within the VM and restart the daemon - with WatchDog(self.cfg.watchdogfile, self.cfg.watchdogtimeout) as watchdog: + with WatchDog(self.config.watchdogfile, self.config.watchdogtimeout) as watchdog: watchdog.go() def _check_for_script(self): @@ -219,7 +210,7 @@ def _do_interval(self): def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() - with Iteration(cfg=self.cfg, seednum=seednum, seedfile=seedfile, r=range_obj, workdirbase=self.working_dir, quiet=quiet_flag, + with Iteration(cfg=self.config, seednum=seednum, seedfile=seedfile, r=range_obj, workdirbase=self.working_dir, quiet=quiet_flag, uniq_func=self._crash_is_unique, sf_set=self.seedfile_set, rf=seedfile.rangefinder, diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index a50a752..854b13a 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -8,22 +8,74 @@ import os from threading import Timer import platform +import gc from certfuzz.campaign.campaign_base import CampaignBase - +from certfuzz.campaign.config.config_windows import Config from certfuzz.runners.killableprocess import Popen from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.iteration.iteration_windows import Iteration from certfuzz.fuzzers.errors import FuzzerExhaustedError -import gc logger = logging.getLogger(__name__) +packages = {'fuzzers': 'certfuzz.fuzzers', + 'runners': 'certfuzz.runners', + 'debuggers': 'certfuzz.debuggers', + } + class WindowsCampaign(CampaignBase): ''' Extends CampaignBase to add windows-specific features like ButtonClicker ''' + def __init__(self, config_file, result_dir=None, debug=False): + CampaignBase.__init__(self, config_file, result_dir, debug) + + self.gui_app = False + + #read configs + logger.info('Reading config from %s', self.config_file) + cfgobj = Config(self.config_file) + self.config = cfgobj.config + self.configdate = cfgobj.configdate + + # pull stuff out of configs + self.campaign_id = self.config['campaign']['id'] + + self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker') + if not self.use_buttonclicker: + self.use_buttonclicker = False + + self.current_seed = self.config['runoptions'].get('first_iteration') + self.seed_interval = self.config['runoptions'].get('seed_interval') + + if self.outdir_base is None: + # it wasn't spec'ed on the command line so use the config + self.outdir_base = os.path.abspath(self.config['directories']['results_dir']) + + self.work_dir_base = os.path.abspath(self.config['directories']['working_dir']) + + self.seed_dir_in = self.config['directories']['seedfile_dir'] + + self.keep_duplicates = self.config['runoptions']['keep_all_duplicates'] + self.keep_heisenbugs = self.config['campaign']['keep_heisenbugs'] + self.should_keep_u_faddr = self.config['runoptions']['keep_unique_faddr'] + + self.program = self.config['target']['program'] + self.cmd_template = self.config['target']['cmdline_template'] + + self.fuzzer_module_name = '%s.%s' % (packages['fuzzers'], self.config['fuzzer']['fuzzer']) + if self.config['runner']['runner']: + self.runner_module_name = '%s.%s' % (packages['runners'], self.config['runner']['runner']) + self.debugger_module_name = '%s.%s' % (packages['debuggers'], self.config['debugger']['debugger']) + + + # must occur after work_dir_base, outdir_base, and campaign_id are set + self._common_init() + + + def __getstate__(self): state = self.__dict__.copy() @@ -161,6 +213,7 @@ def _do_interval(self): gc.collect() self.current_seed = interval_limit + self.first_chunk = False def _do_iteration(self, sf, rng_seed, seednum): # use a with...as to ensure we always hit From 958a97ad25b23725efa33304e1d161c71b1f4279 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 27 Jun 2014 16:01:16 -0400 Subject: [PATCH 0372/1169] improve unit tests on campaign_base --- src/certfuzz/campaign/campaign_base.py | 12 +- .../test/campaign/test_campaign_base.py | 199 ++++++++++-------- 2 files changed, 125 insertions(+), 86 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index cc1090f..f65900f 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -88,7 +88,12 @@ def __init__(self, config_file, result_dir=None, debug=False): # TODO: consider making this configurable self.status_interval = 100 + self.seed_interval = None + self.current_seed = None + self.outdir_base = None + self.outdir = None + self.sf_set_out = None if result_dir: self.outdir_base = os.path.abspath(result_dir) @@ -249,10 +254,13 @@ def _set_debugger(self): # now we have some class self.dbg_class = registration.debug_class + @property + def _version_file(self): + return os.path.join(self.outdir, 'version.txt') + def _write_version(self): - version_file = os.path.join(self.outdir, 'version.txt') version_string = 'Results produced by %s v%s' % (__name__, __version__) - filetools.write_file(version_string, version_file) + filetools.write_file(version_string, self._version_file) def _setup_output(self): # construct run output directory diff --git a/src/certfuzz/test/campaign/test_campaign_base.py b/src/certfuzz/test/campaign/test_campaign_base.py index 69868bc..da1ec06 100644 --- a/src/certfuzz/test/campaign/test_campaign_base.py +++ b/src/certfuzz/test/campaign/test_campaign_base.py @@ -5,108 +5,78 @@ ''' import unittest -import certfuzz.campaign.campaign_base -from tempfile import mkstemp, mkdtemp -import yaml +from tempfile import mkdtemp import shutil import os -import tempfile from certfuzz.fuzztools import filetools +from certfuzz.campaign.campaign_base import CampaignBase +import tempfile +from certfuzz.campaign.errors import CampaignError + +class UnimplementedCampaign(CampaignBase): + pass -class Mock(object): + +class ImplementedCampaign(CampaignBase): def __getstate__(self): - return dict(x=1, y=2, z=3) + pass + def __init__(self, config_file, result_dir=None): + return CampaignBase.__init__(self, config_file, result_dir) -class Test(unittest.TestCase): + def __setstate__(self): + pass + + def _do_interval(self): + pass + + def _do_iteration(self): + pass + + def _handle_errors(self): + pass + + def _keep_going(self): + return CampaignBase._keep_going(self) + + def _post_enter(self): + pass + + def _pre_enter(self): + pass + + def _pre_exit(self): + pass - def _dump_test_config(self): - cfg = {'campaign': { - 'id': 'campaign_id', - 'keep_heisenbugs': True, - 'cached_state_file': mkstemp(dir=self.tmpdir)[1], - }, - 'target': { - 'cmdline_template': 'cmdline_template', - 'program': self.program}, - 'runoptions': { - 'first_iteration': 0, - 'last_iteration': 100, - 'seed_interval': 5, - 'keep_all_duplicates': True, - 'keep_unique_faddr': True}, - 'directories': { - 'results_dir': mkdtemp(dir=self.tmpdir), - 'working_dir': mkdtemp(dir=self.tmpdir), - 'seedfile_dir': mkdtemp(dir=self.tmpdir)}, - 'fuzzer': { - 'fuzzer': 'fuzzermodule'}, - 'runner': { - 'runner': 'runnermodule'}, - 'debugger': { - 'debugger': 'debuggermodule'}} - self.cfg_file = mkstemp(dir=self.tmpdir)[1] - with open(self.cfg_file, 'wb') as output: - yaml.dump(cfg, stream=output) + def _set_debugger(self): + pass + def _set_fuzzer(self): + pass + + def _set_runner(self): + pass + + +class Test(unittest.TestCase): def setUp(self): self.tmpdir = mkdtemp() - self.program = mkstemp(dir=self.tmpdir)[1] - self._dump_test_config() - self.campaign = certfuzz.campaign.campaign_base.CampaignBase(self.cfg_file) - - fd, f = tempfile.mkstemp() - os.close(fd) - os.remove(f) - self.campaign.cached_state_file = f + _fd, cfgfile = tempfile.mkstemp(suffix=".cfg", dir=self.tmpdir, text=True) + try: + self.campaign = ImplementedCampaign(cfgfile) + except TypeError as e: + self.fail('ImplementedCampaign does not match requirements: {}'.format(e)) def tearDown(self): shutil.rmtree(self.tmpdir, ignore_errors=True) - def test_init(self): - self.assertEqual('%s.%s' % (certfuzz.campaign.campaign_base.packages['fuzzers'], 'fuzzermodule'), - self.campaign.fuzzer_module_name) - self.assertEqual('%s.%s' % (certfuzz.campaign.campaign_base.packages['runners'], 'runnermodule'), - self.campaign.runner_module_name) - self.assertEqual('%s.%s' % (certfuzz.campaign.campaign_base.packages['debuggers'], 'debuggermodule'), - self.campaign.debugger_module_name) - -# def test_getstate(self): -# self.campaign.seedfile_set = Mock() -# -# # get_state should return a pickleable result -# state = self.campaign.__getstate__() -# -# import pickle -# try: -# pickle.dumps(state) -# except Exception, e: -# self.fail(e) - - def counter(self, *args): - self.count += 1 - -# def test_save_state(self): -# -# # make sure we can write -# self.assertFalse(os.path.exists(self.campaign.cached_state_file)) -# self.campaign._save_state() -# self.assertTrue(os.path.exists(self.campaign.cached_state_file)) -# -# def test_set_state(self): -# state = {'crashes_seen': [1, 2, 3, 3], -# 'seedfile_set': {'things': {}}, -# 'id': 2134, -# 'seed_dir_in': mkdtemp(dir=self.tmpdir), -# 'seed_dir_local': mkdtemp(dir=self.tmpdir), -# 'sf_set_out': tempfile.mktemp(dir=self.tmpdir)[1], -# } -# self.campaign.__setstate__(state) -# self.assertEqual(self.campaign.crashes_seen, set([1, 2, 3])) + def test_metaclass(self): + self.assertRaises(TypeError, UnimplementedCampaign) def test_write_version(self): - vf = os.path.join(self.campaign.outdir, 'version.txt') + self.campaign.outdir = self.tmpdir + vf = self.campaign._version_file filetools.make_directories(self.campaign.outdir) self.assertTrue(os.path.isdir(self.campaign.outdir)) self.assertFalse(os.path.exists(vf)) @@ -114,6 +84,67 @@ def test_write_version(self): self.assertTrue(os.path.exists(vf)) self.assertTrue(os.path.getsize(vf) > 0) + def test_check_prog(self): + fd, f = tempfile.mkstemp(dir=self.tmpdir) + os.close(fd) + self.campaign.program = f + try: + self.assertTrue(os.path.exists(f)) + self.campaign._check_prog() + except CampaignError as e: + self.fail('File {} exists but _check_prog failed: {}'.format(f, e)) + + # now try it when the file is gone + os.remove(f) + self.assertFalse(os.path.exists(f)) + self.assertRaises(CampaignError, self.campaign._check_prog) + + def test_setup_output(self): + self.campaign.outdir = tempfile.mkdtemp(dir=self.tmpdir) + shutil.rmtree(self.campaign.outdir) + self.assertFalse(os.path.exists(self.campaign.outdir)) + self.campaign._setup_output() + self.assertTrue(os.path.isdir(self.campaign.outdir)) + self.assertTrue(os.path.isfile(self.campaign._version_file)) + + def test_setup_workdir(self): + self.campaign.work_dir_base = tempfile.mkdtemp(dir=self.tmpdir) + shutil.rmtree(self.campaign.work_dir_base) + self.assertFalse(os.path.exists(self.campaign.work_dir_base)) + self.assertEqual(None, self.campaign.working_dir) + self.assertEqual(None, self.campaign.seed_dir_local) + self.campaign._setup_workdir() + self.assertTrue(os.path.isdir(self.campaign.work_dir_base)) + self.assertTrue(os.path.isdir(self.campaign.working_dir)) + self.assertTrue(self.campaign.seed_dir_local.startswith(self.campaign.working_dir)) + self.assertTrue(self.campaign.seed_dir_local.endswith('seedfiles')) + + def test_cleanup_workdir(self): + self.campaign.work_dir_base = self.tmpdir + self.campaign._setup_workdir() + self.assertTrue(os.path.isdir(self.campaign.working_dir)) + self.campaign._cleanup_workdir() + self.assertFalse(os.path.exists(self.campaign.working_dir)) + + def test_crash_is_unique(self): + self.assertEqual(0, len(self.campaign.crashes_seen)) + self.assertTrue(self.campaign._crash_is_unique(1)) + self.assertEqual(1, len(self.campaign.crashes_seen)) + self.assertFalse(self.campaign._crash_is_unique(1)) + self.assertEqual(1, len(self.campaign.crashes_seen)) + self.assertTrue(self.campaign._crash_is_unique(2)) + self.assertEqual(2, len(self.campaign.crashes_seen)) + self.assertFalse(self.campaign._crash_is_unique(2)) + self.assertEqual(2, len(self.campaign.crashes_seen)) + + for x in [1, 2]: + self.assertTrue(x in self.campaign.crashes_seen) + + def test_keep_going(self): + for _x in range(100): + self.assertTrue(self.campaign._keep_going()) + + if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() From b0441bb1d554e4a547d7ef562fce3ec6925df637 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 27 Jun 2014 16:22:18 -0400 Subject: [PATCH 0373/1169] campaign_meta went away in a previous commit, remove its unit test file --- .../test/campaign/test_campaign_meta.py | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 src/certfuzz/test/campaign/test_campaign_meta.py diff --git a/src/certfuzz/test/campaign/test_campaign_meta.py b/src/certfuzz/test/campaign/test_campaign_meta.py deleted file mode 100644 index cfce113..0000000 --- a/src/certfuzz/test/campaign/test_campaign_meta.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From 3165a79365f942f46d5e61d6821e77754eec95ec Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 27 Jun 2014 16:27:34 -0400 Subject: [PATCH 0374/1169] improve unit tests --- src/certfuzz/campaign/campaign_base.py | 2 -- src/certfuzz/campaign/campaign_linux.py | 3 +++ src/certfuzz/campaign/campaign_windows.py | 5 ++--- src/certfuzz/campaign/config/config_base.py | 6 ++++++ .../test/campaign/config/test_config_base.py | 20 +++++++++++-------- .../test/campaign/test_campaign_base.py | 6 ------ .../test/campaign/test_campaign_linux.py | 12 +++++++++++ .../test/campaign/test_campaign_windows.py | 14 +++++++------ 8 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index f65900f..b9f7715 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -176,7 +176,6 @@ def _handle_common_errors(self, etype, value, mytraceback): handled = True return handled - @abc.abstractmethod def _handle_errors(self, etype, value, mytraceback): ''' Callback to handle class-specific errors. If used, it should be @@ -353,7 +352,6 @@ def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): logger.debug('%s was found, not unique', crash_id) return False - @abc.abstractmethod def _keep_going(self): ''' Returns True if a campaign should proceed. False otherwise. diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 9c9365c..f0e70d2 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -96,6 +96,9 @@ def _post_enter(self): check_ppid() self._cache_app() + def _pre_exit(self): + pass + def _set_unbuffered_stdout(self): ''' Reopens stdout with a buffersize of 0 (unbuffered) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 854b13a..5541ec3 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -38,6 +38,8 @@ def __init__(self, config_file, result_dir=None, debug=False): logger.info('Reading config from %s', self.config_file) cfgobj = Config(self.config_file) self.config = cfgobj.config + if self.config is None: + raise WindowsCampaignError('Config load failed, exiting') self.configdate = cfgobj.configdate # pull stuff out of configs @@ -70,12 +72,9 @@ def __init__(self, config_file, result_dir=None, debug=False): self.runner_module_name = '%s.%s' % (packages['runners'], self.config['runner']['runner']) self.debugger_module_name = '%s.%s' % (packages['debuggers'], self.config['debugger']['debugger']) - # must occur after work_dir_base, outdir_base, and campaign_id are set self._common_init() - - def __getstate__(self): state = self.__dict__.copy() diff --git a/src/certfuzz/campaign/config/config_base.py b/src/certfuzz/campaign/config/config_base.py index f8b19ea..b352bc9 100644 --- a/src/certfuzz/campaign/config/config_base.py +++ b/src/certfuzz/campaign/config/config_base.py @@ -7,6 +7,7 @@ import os.path import yaml +from certfuzz.campaign.config.errors import ConfigError logger = logging.getLogger(__name__) @@ -27,6 +28,7 @@ def __init__(self, config_file): self.configdate = None self.load() + self._verify_load() self._set_derived_options() self.validations = [] @@ -44,6 +46,10 @@ def load(self): if self.config: self.__dict__.update(self.config) + def _verify_load(self): + if self.config is None: + raise ConfigError('Config load failed for {}'.format(self.file)) + def validate(self): for validation in self.validations: validation() diff --git a/src/certfuzz/test/campaign/config/test_config_base.py b/src/certfuzz/test/campaign/config/test_config_base.py index 6895907..f166e28 100644 --- a/src/certfuzz/test/campaign/config/test_config_base.py +++ b/src/certfuzz/test/campaign/config/test_config_base.py @@ -11,6 +11,7 @@ import os from certfuzz.campaign import config import pprint +from certfuzz.campaign.config.errors import ConfigError _count = 0 def _counter(): @@ -63,21 +64,24 @@ def test_validate(self): c.validate() self.assertEqual(3, _count) - def test_load(self): + def test_init_fails_if_load_fails(self): dummy, f = self._write_yaml() os.remove(f) + self.assertRaises(ConfigError, config.Config, f) + + def test_verify_load(self): + # write another yaml file + _thing, f = self._write_yaml() + # sub the new file name c = config.Config(f) - self.assertEqual(None, c.config) - c.load() - # nothing should have happened - self.assertEqual(None, c.config) + c.config = None + self.assertRaises(ConfigError, c._verify_load) + def test_load(self): # write another yaml file thing, f = self._write_yaml() # sub the new file name - c.file = f - # load it - c.load() + c = config.Config(f) # we should get the thing back again self.assertEqual(thing, c.config) diff --git a/src/certfuzz/test/campaign/test_campaign_base.py b/src/certfuzz/test/campaign/test_campaign_base.py index da1ec06..668d7f8 100644 --- a/src/certfuzz/test/campaign/test_campaign_base.py +++ b/src/certfuzz/test/campaign/test_campaign_base.py @@ -34,12 +34,6 @@ def _do_interval(self): def _do_iteration(self): pass - def _handle_errors(self): - pass - - def _keep_going(self): - return CampaignBase._keep_going(self) - def _post_enter(self): pass diff --git a/src/certfuzz/test/campaign/test_campaign_linux.py b/src/certfuzz/test/campaign/test_campaign_linux.py index 180af00..06d33e2 100644 --- a/src/certfuzz/test/campaign/test_campaign_linux.py +++ b/src/certfuzz/test/campaign/test_campaign_linux.py @@ -8,16 +8,28 @@ import tempfile import os import shutil +from ConfigParser import NoSectionError class Test(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() + _fd, cfgfile = tempfile.mkstemp(suffix=".cfg", dir=self.tmpdir, text=True) + try: + self.campaign = LinuxCampaign(cfgfile) + except TypeError as e: + self.fail('LinuxCampaign does not match requirements: {}'.format(e)) + except NoSectionError as e: + pass def tearDown(self): shutil.rmtree(self.tmpdir) + def test_init_without_config(self): + _fd, cfgfile = tempfile.mkstemp(suffix=".cfg", dir=self.tmpdir, text=True) + self.assertRaises(NoSectionError, LinuxCampaign, cfgfile) + def test_check_program_file_type(self): fd, fname = tempfile.mkstemp(dir=self.tmpdir, text=True) os.write(fd, 'sometext') diff --git a/src/certfuzz/test/campaign/test_campaign_windows.py b/src/certfuzz/test/campaign/test_campaign_windows.py index 693062a..439c036 100644 --- a/src/certfuzz/test/campaign/test_campaign_windows.py +++ b/src/certfuzz/test/campaign/test_campaign_windows.py @@ -5,19 +5,21 @@ ''' import unittest from certfuzz.campaign.campaign_windows import WindowsCampaign +import tempfile +import shutil +from certfuzz.campaign.config.errors import ConfigError class Test(unittest.TestCase): - def setUp(self): - pass + self.tmpdir = tempfile.mkdtemp() def tearDown(self): - pass - - def testName(self): - pass + shutil.rmtree(self.tmpdir) + def test_init_without_config(self): + _fd, cfgfile = tempfile.mkstemp(suffix=".cfg", dir=self.tmpdir, text=True) + self.assertRaises(ConfigError, WindowsCampaign, cfgfile) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From f9d7d81e1ad10ae1620c84c6d8daf3079f32ff60 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 27 Jun 2014 16:39:02 -0400 Subject: [PATCH 0375/1169] we don't need to pass the logger in to import_module_by_name --- src/certfuzz/campaign/campaign_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index b9f7715..9bab2e2 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) -def import_module_by_name(name, logger=None): +def import_module_by_name(name): ''' Imports a module at runtime given the pythonic name of the module e.g., certfuzz.fuzzers.bytemut @@ -235,19 +235,19 @@ def _check_prog(self): @abc.abstractmethod def _set_fuzzer(self): - self.fuzzer_module = import_module_by_name(self.fuzzer_module_name, logger) + self.fuzzer_module = import_module_by_name(self.fuzzer_module_name) self.fuzzer = self.fuzzer_module._fuzzer_class @abc.abstractmethod def _set_runner(self): if self.runner_module_name: - self.runner_module = import_module_by_name(self.runner_module_name, logger) + self.runner_module = import_module_by_name(self.runner_module_name) self.runner = self.runner_module._runner_class @abc.abstractmethod def _set_debugger(self): # this will import the module which registers the debugger - self.debugger_module = import_module_by_name(self.debugger_module_name, logger) + self.debugger_module = import_module_by_name(self.debugger_module_name) # confirm that the registered debugger is compatible registration.verify_supported_platform() # now we have some class From 8f03be391d7d58884dbf04ca77e3dc516f20c95c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 27 Jun 2014 16:40:10 -0400 Subject: [PATCH 0376/1169] the config object will raise an error if it can't load. no need to check in windows campaign object --- src/certfuzz/campaign/campaign_windows.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 5541ec3..3266aa0 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -38,8 +38,6 @@ def __init__(self, config_file, result_dir=None, debug=False): logger.info('Reading config from %s', self.config_file) cfgobj = Config(self.config_file) self.config = cfgobj.config - if self.config is None: - raise WindowsCampaignError('Config load failed, exiting') self.configdate = cfgobj.configdate # pull stuff out of configs From 6ddb1bfe9024ee4f646f489d81c0b495031250bb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 08:17:01 -0400 Subject: [PATCH 0377/1169] _set_fuzzer, _set_runner, _set_debugger don't need to be abstract methods (although we do override them in campaign_linux) --- src/certfuzz/campaign/campaign_base.py | 3 --- src/certfuzz/campaign/campaign_windows.py | 9 --------- 2 files changed, 12 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 9bab2e2..74e70bd 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -233,18 +233,15 @@ def _check_prog(self): msg = 'Cannot find program "%s" (resolves to "%s")' % (self.program, os.path.abspath(self.program)) raise CampaignError(msg) - @abc.abstractmethod def _set_fuzzer(self): self.fuzzer_module = import_module_by_name(self.fuzzer_module_name) self.fuzzer = self.fuzzer_module._fuzzer_class - @abc.abstractmethod def _set_runner(self): if self.runner_module_name: self.runner_module = import_module_by_name(self.runner_module_name) self.runner = self.runner_module._runner_class - @abc.abstractmethod def _set_debugger(self): # this will import the module which registers the debugger self.debugger_module = import_module_by_name(self.debugger_module_name) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 3266aa0..97d5cb4 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -239,12 +239,3 @@ def _do_iteration(self, sf, rng_seed, seednum): # for k, score, successes, tries, p in self.seedfile_set.status(): # logger.info('%s %0.6f %d %d %0.6f', k, score, successes, # tries, p) - - def _set_fuzzer(self): - CampaignBase._set_fuzzer(self) - - def _set_runner(self): - CampaignBase._set_runner(self) - - def _set_debugger(self): - CampaignBase._set_debugger(self) From 89bc40c7854b82d525bc02cc0e1e4718aa8c0e02 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 08:19:21 -0400 Subject: [PATCH 0378/1169] organize imports --- src/certfuzz/campaign/campaign_base.py | 11 +++++------ src/certfuzz/campaign/campaign_linux.py | 6 +++--- src/certfuzz/campaign/campaign_windows.py | 13 +++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 74e70bd..623786c 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -4,24 +4,23 @@ @organization: cert.org ''' +import abc import logging import os +import re import shutil +import sys import tempfile import traceback +import cPickle as pickle -from certfuzz.version import __version__ from certfuzz.campaign.errors import CampaignError from certfuzz.debuggers import registration from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.fuzztools import filetools from certfuzz.runners.errors import RunnerArchitectureError, \ RunnerPlatformVersionError - -import cPickle as pickle -import abc -import sys -import re +from certfuzz.version import __version__ logger = logging.getLogger(__name__) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index f0e70d2..df140e7 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -6,15 +6,15 @@ import logging import os +import subprocess import sys import time -import subprocess from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.campaign.config import bff_config as cfg_helper from certfuzz.campaign.errors import CampaignScriptError -from certfuzz.debuggers import crashwrangler # @UnusedImport -from certfuzz.debuggers import gdb # @UnusedImport +from certfuzz.debuggers import crashwrangler #@UnusedImport +from certfuzz.debuggers import gdb #@UnusedImport from certfuzz.debuggers.registration import verify_supported_platform from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.fuzztools import subprocess_helper as subp diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 97d5cb4..6a04fa1 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -3,19 +3,20 @@ @author: adh ''' +import gc import logging -import sys import os -from threading import Timer import platform -import gc +import sys +from threading import Timer from certfuzz.campaign.campaign_base import CampaignBase -from certfuzz.campaign.config.config_windows import Config -from certfuzz.runners.killableprocess import Popen from certfuzz.file_handlers.seedfile_set import SeedfileSet -from certfuzz.iteration.iteration_windows import Iteration from certfuzz.fuzzers.errors import FuzzerExhaustedError +from certfuzz.runners.killableprocess import Popen +from certfuzz.campaign.config.config_windows import Config +from certfuzz.iteration.iteration_windows import Iteration + logger = logging.getLogger(__name__) From 212ba2587863c3a3fc87ba44792870ac659df1ce Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 08:23:28 -0400 Subject: [PATCH 0379/1169] use new style string formatting --- src/certfuzz/campaign/campaign_windows.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 6a04fa1..b802735 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -20,11 +20,6 @@ logger = logging.getLogger(__name__) -packages = {'fuzzers': 'certfuzz.fuzzers', - 'runners': 'certfuzz.runners', - 'debuggers': 'certfuzz.debuggers', - } - class WindowsCampaign(CampaignBase): ''' @@ -66,10 +61,10 @@ def __init__(self, config_file, result_dir=None, debug=False): self.program = self.config['target']['program'] self.cmd_template = self.config['target']['cmdline_template'] - self.fuzzer_module_name = '%s.%s' % (packages['fuzzers'], self.config['fuzzer']['fuzzer']) + self.fuzzer_module_name = 'certfuzz.fuzzers.{}'.format(self.config['fuzzer']['fuzzer']) if self.config['runner']['runner']: - self.runner_module_name = '%s.%s' % (packages['runners'], self.config['runner']['runner']) - self.debugger_module_name = '%s.%s' % (packages['debuggers'], self.config['debugger']['debugger']) + self.runner_module_name = 'certfuzz.runners.{}'.format(self.config['runner']['runner']) + self.debugger_module_name = 'certfuzz.debuggers.{}'.format(self.config['debugger']['debugger']) # must occur after work_dir_base, outdir_base, and campaign_id are set self._common_init() From 74f2dd08161d950e105439161e60868166d9263b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 3 Jul 2014 13:46:43 -0400 Subject: [PATCH 0380/1169] Remove unnecessary import of get_command_args_list --- src/certfuzz/tools/linux/repro.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index afa7265..4cc7690 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -12,7 +12,6 @@ from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.runners.runner_base import get_command_args_list from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options from certfuzz.debuggers import gdb # @UnusedImport except ImportError: @@ -24,7 +23,6 @@ from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.runners.runner_base import get_command_args_list from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options from certfuzz.debuggers import gdb # @UnusedImport From 034aa03fb954f24c25ec910949eba20923eed883 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 3 Jul 2014 14:18:50 -0400 Subject: [PATCH 0381/1169] Import crashwrangler module to allow repro.py to work on OS X. BFF-485 --- src/certfuzz/tools/linux/repro.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index 4cc7690..fa2e537 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -13,7 +13,7 @@ from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options - from certfuzz.debuggers import gdb # @UnusedImport + from certfuzz.debuggers import gdb, crashwrangler # @UnusedImport except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH import sys @@ -24,7 +24,7 @@ from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options - from certfuzz.debuggers import gdb # @UnusedImport + from certfuzz.debuggers import gdb, crashwrangler # @UnusedImport logger = logging.getLogger() logger.setLevel(logging.WARNING) From 1755980f1382ec04a23b1abac32220aa3b7e64da Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 3 Jul 2014 14:46:04 -0400 Subject: [PATCH 0382/1169] Fix remaining imports for minimizer_base. BFF-764 --- src/certfuzz/minimizer/minimizer_base.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 70056bd..a55dce1 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -19,14 +19,9 @@ from certfuzz.fuzztools import hamming, filetools, probability, text from certfuzz.fuzztools.filetools import delete_files, write_file from certfuzz.minimizer.errors import MinimizerError - - -try: - from ..analyzers import pin_calltrace - from ..analyzers import AnalyzerEmptyOutputError - from ..debuggers.output_parsers.calltracefile import Calltracefile -except ImportError: - pass +from certfuzz.analyzers import pin_calltrace +from certfuzz.analyzers.errors import AnalyzerEmptyOutputError +from certfuzz.debuggers.output_parsers.calltracefile import Calltracefile logger = logging.getLogger(__name__) From d496b027138a934c8074a8505d043881a05a9d1a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 10:29:28 -0400 Subject: [PATCH 0383/1169] refactor out common methods readfile carve carve2 score_reports --- src/certfuzz/tools/common/drillresults.py | 126 +++++++++++++++++++ src/certfuzz/tools/linux/drillresults.py | 133 ++------------------- src/certfuzz/tools/windows/drillresults.py | 128 +------------------- 3 files changed, 141 insertions(+), 246 deletions(-) create mode 100644 src/certfuzz/tools/common/drillresults.py diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py new file mode 100644 index 0000000..5548161 --- /dev/null +++ b/src/certfuzz/tools/common/drillresults.py @@ -0,0 +1,126 @@ +''' +Created on Jun 30, 2014 + +@organization: cert.org +''' + + +def readfile(textfile): + ''' + Read text file + ''' + with open(textfile, 'r') as f: + return f.read() + + +def carve(string, token1, token2): + startindex = string.find(token1) + if startindex == -1: + # can't find token1 + return "" + startindex = startindex + len(token1) + endindex = string.find(token2, startindex) + if endindex == -1: + # can't find token2 + return "" + return string[startindex:endindex] + + +# Todo: fix this up. Was added to bring gdb support +def carve2(string): + delims = [("Exception Faulting Address: ", "\n"), + ("si_addr:$2 = (void *)", "\n")] + for token1, token2 in delims: + startindex = string.find(token1) + if startindex == -1: + # can't find token1 + continue + startindex = startindex + len(token1) + endindex = string.find(token2, startindex) + return string[startindex:endindex] + + +def is_number(s): + try: + float(s) + return True + except ValueError: + return False + + +def score_crasher(crasher, details, ignorejit=False): + scores = [100] + if details['reallyexploitable'] == True: + # The crash summary is a very interesting one + for exception in details['exceptions']: + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not ignorejit: + # EIP is not in a loaded module + scores.append(20) + if details['exceptions'][exception]['shortdesc'] in re_set: + efa = '0x' + details['exceptions'][exception]['efa'] + if details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(30) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(20) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(20) + else: + # Faulting address has high entropy. Most exploitable. + scores.append(10) + else: + # The faulting address pattern is not in the fuzzed file + scores.append(40) + + else: + # The crash summary isn't necessarily interesting + for exception in details['exceptions']: + efa = '0x' + details['exceptions'][exception]['efa'] + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not ignorejit: + scores.append(20) + elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): + # likely heap corruption. Exploitable, but difficult + scores.append(45) + elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: + # non-continued potential stack buffer overflow + scores.append(40) + elif details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(70) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(60) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(60) + else: + # Faulting address has high entropy. + scores.append(50) + return min(scores) + +def score_reports(results, crashscores, ignorejit): + # Assign a ranking to each crash report. The lower the rank, the higher + # the exploitability + if results: + print "--- Interesting crashes ---\n" + # For each of the crash ids in the results dictionary, apply ranking + for crasher in results: + try: + crashscores[crasher] = score_crasher(crasher, results[crasher], ignorejit) + except KeyError: + print "Error scoring crash %s" % crasher + continue + +def main(): + pass + +if __name__ == '__main__': + main() diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index 3f79271..10de513 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -8,6 +8,9 @@ import re from optparse import OptionParser +from certfuzz.tools.common.drillresults import readfile, carve, carve2, \ + score_reports + regex = { 'gdb_report': re.compile(r'.+.gdb$'), 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), @@ -122,15 +125,6 @@ def fixefabug(reporttext, instraddr, faultaddr): return faultaddr -def readfile(textfile): - ''' - Read text file - ''' - f = open(textfile, 'r') - text = f.read() - return text - - def readbinfile(textfile): ''' Read binary file @@ -140,33 +134,6 @@ def readbinfile(textfile): return text -def carve(string, token1, token2): - startindex = string.find(token1) - if startindex == -1: - # can't find token1 - return "" - startindex = startindex + len(token1) - endindex = string.find(token2, startindex) - if endindex == -1: - # can't find token2 - return "" - return string[startindex:endindex] - - -# Todo: fix this up. Was added to bring gdb support -def carve2(string): - delims = [("Exception Faulting Address: ", "\n"), - ("si_addr:$2 = (void *)", "\n")] - for token1, token2 in delims: - startindex = string.find(token1) - if startindex == -1: - # can't find token1 - continue - startindex = startindex + len(token1) - endindex = string.find(token2, startindex) - return string[startindex:endindex] - - def getexnum(reporttext): ''' Get the exception number by counting the number of continues @@ -208,14 +175,11 @@ def getinstr(reporttext, instraddr): ''' Find the disassembly line for the current (crashing) instruction ''' - instructionline = '' - for line in reporttext.splitlines(): n = re.match(regex['current_instr'], line) if n: - instructionline = n.group(3) - break - return instructionline + return n.group(3) + return '' def formataddr(faultaddr): @@ -245,18 +209,6 @@ def formataddr(faultaddr): return faultaddr -def ratchetscore(crasher, score): - ''' - Only allow a crasher to increase score - ''' - try: - currentscore = scoredcrashes[crasher] - except KeyError: - currentscore = 100 - if score < currentscore: - scoredcrashes[crasher] = score - - def fixefaoffset(instructionline, faultaddr): ''' Adjust faulting address for instructions that use offsets @@ -333,10 +285,9 @@ def checkreport(reportfile, crasherfile, crash_hash): if shortdesc: # Set !exploitable Short Description for the exception crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc - if shortdesc in re_set: - # Flag the entire crash ID as really exploitable if this is a good - # exception - crashid['reallyexploitable'] = True + # Flag the entire crash ID as really exploitable if this is a good + # exception + crashid['reallyexploitable'] = shortdesc in re_set if not os.path.isfile(crasherfile): # Can't find the crasher file @@ -429,72 +380,6 @@ def parsegdbs(gdblist): checkreport(gdb['gdbfile'], gdb['crasherfile'], gdb['crash_hash']) -def score_reports(): - # Assign a ranking to each crash report. The lower the rank, - # the higher the exploitability - if results: - print "--- Interesting crashes ---\n" - # For each of the crash ids in the results dictionary, apply ranking - for crasher in results: - try: - if results[crasher]['reallyexploitable'] == True: - # The crash summery is a very interesting one - for exception in results[crasher]['exceptions']: - module = results[crasher]['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not ignorejit: - # EIP is not in a loaded module - ratchetscore(crasher, 20) - if results[crasher]['exceptions'][exception]['shortdesc'] in re_set: - efa = '0x' + results[crasher]['exceptions'][exception]['efa'] - if results[crasher]['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - ratchetscore(crasher, 30) - elif '0x0000' in efa: - # Faulting address is somewhat near null - ratchetscore(crasher, 20) - elif '0xffff' in efa: - # Faulting address is likely a negative number - ratchetscore(crasher, 20) - else: - # Faulting address has high entropy. Most exploitable. - ratchetscore(crasher, 10) - else: - # The faulting address pattern is not in the fuzzed file - ratchetscore(crasher, 40) - else: - # The crash summary isn't necessarily interesting - for exception in results[crasher]['exceptions']: - efa = '0x' + results[crasher]['exceptions'][exception]['efa'] - module = results[crasher]['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not ignorejit: - ratchetscore(crasher, 20) - elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): - #likely heap corruption. Exploitable, but difficult - ratchetscore(crasher, 45) - elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: - #non-continued potential stack buffer overflow - ratchetscore(crasher, 40) - elif results[crasher]['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - ratchetscore(crasher, 70) - elif '0x0000' in efa: - # Faulting address is somewhat near null - ratchetscore(crasher, 60) - elif '0xffff' in efa: - # Faulting address is likely a negative number - ratchetscore(crasher, 60) - else: - # Faulting address has high entropy. - ratchetscore(crasher, 50) - except KeyError: - #print "Error scoring crash %s" % crasher - continue - - def printreport(): sorted_crashes = sorted(scoredcrashes.iteritems(), key=lambda(k, v): (v, k)) @@ -541,7 +426,7 @@ def main(): tld = 'crashers' gdblist = findgdbs(tld) parsegdbs(gdblist) - score_reports() + score_reports(results, scoredcrashes, ignorejit) printreport() if __name__ == '__main__': diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 5a3b777..101a6ae 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -11,6 +11,9 @@ import zipfile import cPickle as pickle +from certfuzz.tools.common.drillresults import readfile, carve, carve2, \ + score_reports, is_number + regex = { 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]'), 'msec_report': re.compile('.+.msec$'), @@ -121,16 +124,6 @@ def fixefabug(reporttext, instraddr, faultaddr): return faultaddr -def readfile(textfile): - ''' - Read text file - ''' - f = open(textfile, 'r') - text = f.read() - f.close() - return text - - def readbinfile(inputfile): ''' Read binary file @@ -161,33 +154,6 @@ def readbinfile(inputfile): return filebytes -def carve(string, token1, token2): - startindex = string.find(token1) - if startindex == -1: - # can't find token1 - return "" - startindex = startindex + len(token1) - endindex = string.find(token2, startindex) - if endindex == -1: - # can't find token2 - return "" - return string[startindex:endindex] - - -# Todo: fix this up. Was added to bring gdb support -def carve2(string): - delims = [("Exception Faulting Address: ", "\n"), - ("si_addr:$2 = (void *)", "\n")] - for token1, token2 in delims: - startindex = string.find(token1) - if startindex == -1: - # can't find token1 - continue - startindex = startindex + len(token1) - endindex = string.find(token2, startindex) - return string[startindex:endindex] - - def getexnum(reporttext): ''' Get the exception number by counting the number of continues @@ -262,26 +228,6 @@ def formataddr(faultaddr): return faultaddr -def ratchetscore(crasher, score): - ''' - Only allow a crasher to increase score - ''' - try: - currentscore = scoredcrashes[crasher] - except KeyError: - currentscore = 100 - if score < currentscore: - scoredcrashes[crasher] = score - - -def is_number(s): - try: - float(s) - return True - except ValueError: - return False - - def fixefaoffset(instructionline, faultaddr): ''' Adjust faulting address for instructions that use offsets @@ -497,70 +443,8 @@ def parsemsecs(mseclist): checkreport(msec['msecfile'], msec['crasherfile'], msec['crash_hash']) -def score_reports(): - # Assign a ranking to each crash report. The lower the rank, the higher - # the exploitability - if results: - print "--- Interesting crashes ---\n" - # For each of the crash ids in the results dictionary, apply ranking - for crasher in results: - try: - if results[crasher]['reallyexploitable'] == True: - # The crash summery is a very interesting one - for exception in results[crasher]['exceptions']: - module = results[crasher]['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not ignorejit: - # EIP is not in a loaded module - ratchetscore(crasher, 20) - if results[crasher]['exceptions'][exception]['shortdesc'] in re_set: - efa = '0x' + results[crasher]['exceptions'][exception]['efa'] - if results[crasher]['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - ratchetscore(crasher, 30) - elif '0x0000' in efa: - # Faulting address is somewhat near null - ratchetscore(crasher, 20) - elif '0xffff' in efa: - # Faulting address is likely a negative number - ratchetscore(crasher, 20) - else: - # Faulting address has high entropy. Most exploitable. - ratchetscore(crasher, 10) - else: - # The faulting address pattern is not in the fuzzed file - ratchetscore(crasher, 40) - else: - # The crash summary isn't necessarily interesting - for exception in results[crasher]['exceptions']: - efa = '0x' + results[crasher]['exceptions'][exception]['efa'] - module = results[crasher]['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not ignorejit: - ratchetscore(crasher, 20) - elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): - # likely heap corruption. Exploitable, but difficult - ratchetscore(crasher, 45) - elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: - # non-continued potential stack buffer overflow - ratchetscore(crasher, 40) - elif results[crasher]['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - ratchetscore(crasher, 70) - elif '0x0000' in efa: - # Faulting address is somewhat near null - ratchetscore(crasher, 60) - elif '0xffff' in efa: - # Faulting address is likely a negative number - ratchetscore(crasher, 60) - else: - # Faulting address has high entropy. - ratchetscore(crasher, 50) - except KeyError: - print "Error scoring crash %s" % crasher - continue + + def printreport(): @@ -630,7 +514,7 @@ def main(): if not options.force: cached_results = loadcached(pickle_file) parsemsecs(mseclist) - score_reports() + score_reports(results, scoredcrashes, ignorejit) printreport() cache_results(pickle_file) From e07467254c22b38cea491847660813a8bc4a0365 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 10:33:45 -0400 Subject: [PATCH 0384/1169] pass in really exploitable set for each platform --- src/certfuzz/tools/common/drillresults.py | 6 +++--- src/certfuzz/tools/linux/drillresults.py | 2 +- src/certfuzz/tools/windows/drillresults.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 5548161..797fa97 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -48,7 +48,7 @@ def is_number(s): return False -def score_crasher(crasher, details, ignorejit=False): +def score_crasher(crasher, details, ignorejit, re_set): scores = [100] if details['reallyexploitable'] == True: # The crash summary is a very interesting one @@ -106,7 +106,7 @@ def score_crasher(crasher, details, ignorejit=False): scores.append(50) return min(scores) -def score_reports(results, crashscores, ignorejit): +def score_reports(results, crashscores, ignorejit, re_set): # Assign a ranking to each crash report. The lower the rank, the higher # the exploitability if results: @@ -114,7 +114,7 @@ def score_reports(results, crashscores, ignorejit): # For each of the crash ids in the results dictionary, apply ranking for crasher in results: try: - crashscores[crasher] = score_crasher(crasher, results[crasher], ignorejit) + crashscores[crasher] = score_crasher(crasher, results[crasher], ignorejit, re_set) except KeyError: print "Error scoring crash %s" % crasher continue diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index 10de513..c5096f3 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -426,7 +426,7 @@ def main(): tld = 'crashers' gdblist = findgdbs(tld) parsegdbs(gdblist) - score_reports(results, scoredcrashes, ignorejit) + score_reports(results, scoredcrashes, ignorejit, re_set) printreport() if __name__ == '__main__': diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 101a6ae..44b07e2 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -514,7 +514,7 @@ def main(): if not options.force: cached_results = loadcached(pickle_file) parsemsecs(mseclist) - score_reports(results, scoredcrashes, ignorejit) + score_reports(results, scoredcrashes, ignorejit, re_set) printreport() cache_results(pickle_file) From 69f9ffceea1ab1989fb17efc52f1d10f9bf8e472 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 10:39:39 -0400 Subject: [PATCH 0385/1169] move register sets to common module --- src/certfuzz/tools/common/drillresults.py | 12 ++++++++++++ src/certfuzz/tools/linux/drillresults.py | 12 +----------- src/certfuzz/tools/windows/drillresults.py | 12 ++---------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 797fa97..60a2358 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -4,6 +4,16 @@ @organization: cert.org ''' +registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', + 'edi', 'eip') + +registers64 = ('rax', 'rcx', 'rdx', 'rbx', 'rsp', 'rbp', 'rsi', + 'rdi', 'rip', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', + 'r14', 'r15') + +reg_set = set(registers) +reg64_set = set(registers64) + def readfile(textfile): ''' @@ -106,6 +116,7 @@ def score_crasher(crasher, details, ignorejit, re_set): scores.append(50) return min(scores) + def score_reports(results, crashscores, ignorejit, re_set): # Assign a ranking to each crash report. The lower the rank, the higher # the exploitability @@ -119,6 +130,7 @@ def score_reports(results, crashscores, ignorejit, re_set): print "Error scoring crash %s" % crasher continue + def main(): pass diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index c5096f3..db41503 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -9,7 +9,7 @@ from optparse import OptionParser from certfuzz.tools.common.drillresults import readfile, carve, carve2, \ - score_reports + score_reports, reg_set regex = { 'gdb_report': re.compile(r'.+.gdb$'), @@ -28,12 +28,6 @@ 'dbg_prompt': re.compile(r'^[0-9]:[0-9][0-9][0-9]> (.*)'), } -registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', - 'edi', 'eip') - -registers64 = ('rax', 'rcx', 'rdx', 'rbx', 'rsp', 'rbp', 'rsi', - 'rdi', 'rip', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', - 'r14', 'r15') # These !exploitable short descriptions indicate a very interesting crash really_exploitable = [ @@ -43,8 +37,6 @@ 'BadInstruction', 'ReturnAv', ] -reg_set = set(registers) -reg64_set = set(registers64) re_set = set(really_exploitable) results = {} @@ -214,8 +206,6 @@ def fixefaoffset(instructionline, faultaddr): Adjust faulting address for instructions that use offsets Currently only works for instructions like CALL [reg + offset] ''' - global reg_set - if '0x' not in faultaddr: faultaddr = '0x' + faultaddr instructionpieces = instructionline.split() diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 44b07e2..8206427 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -12,7 +12,7 @@ import cPickle as pickle from certfuzz.tools.common.drillresults import readfile, carve, carve2, \ - score_reports, is_number + score_reports, is_number, reg_set, reg64_set regex = { 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]'), @@ -28,12 +28,6 @@ 'wow64_dbg_prompt': re.compile('^[0-9]:[0-9][0-9][0-9]:x86> (.*)'), } -registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', - 'edi', 'eip') - -registers64 = ('rax', 'rcx', 'rdx', 'rbx', 'rsp', 'rbp', 'rsi', - 'rdi', 'rip', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', - 'r14', 'r15') # These !exploitable short descriptions indicate a very interesting crash really_exploitable = [ @@ -44,8 +38,7 @@ 'IllegalInstruction', 'PrivilegedInstruction', ] -reg_set = set(registers) -reg64_set = set(registers64) + re_set = set(really_exploitable) results = {} @@ -233,7 +226,6 @@ def fixefaoffset(instructionline, faultaddr): Adjust faulting address for instructions that use offsets Currently only works for instructions like CALL [reg + offset] ''' - global reg_set if _64bit_debugger and not wow64_app: reg_set = reg64_set From 3607c55b527f0e0d70df903f6ebf845608ef93b9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 11:00:27 -0400 Subject: [PATCH 0386/1169] refactor out printreport function --- src/certfuzz/tools/common/drillresults.py | 30 ++++++++++++++++ src/certfuzz/tools/linux/drillresults.py | 36 ++++--------------- src/certfuzz/tools/windows/drillresults.py | 40 ++++------------------ 3 files changed, 43 insertions(+), 63 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 60a2358..c9fc7d3 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -131,6 +131,36 @@ def score_reports(results, crashscores, ignorejit, re_set): continue +def print_crash_report(crasher, score, details, ignorejit): + print '\n%s - Exploitability rank: %s' % (crasher, score) + print 'Fuzzed file: %s' % details['fuzzedfile'] + for exception in details['exceptions']: + shortdesc = details['exceptions'][exception]['shortdesc'] + eiftext = '' + efa = '0x' + details['exceptions'][exception]['efa'] + if details['exceptions'][exception]['EIF']: + eiftext = " *** Byte pattern is in fuzzed file! ***" + print 'exception %s: %s accessing %s %s' % (exception, shortdesc, efa, eiftext) + if details['exceptions'][exception]['instructionline']: + print details['exceptions'][exception]['instructionline'] + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded': + if not ignorejit: + print 'Instruction pointer is not in a loaded module!' + else: + print 'Code executing in: %s' % module + + +def printreport(scoredcrashes, results, ignorejit): + sorted_crashes = sorted(scoredcrashes.iteritems(), key=lambda(k, v): (v, k)) + + for crashes in sorted_crashes: + crasher = crashes[0] + score = crashes[1] + details = results[crasher] + print_crash_report(crasher, score, details) + + def main(): pass diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index db41503..39f1aa5 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -9,11 +9,12 @@ from optparse import OptionParser from certfuzz.tools.common.drillresults import readfile, carve, carve2, \ - score_reports, reg_set + score_reports, reg_set, printreport + regex = { 'gdb_report': re.compile(r'.+.gdb$'), - 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), +# 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), 'frame0': re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+'), 'regs1': re.compile(r'^eax=.+'), 'regs2': re.compile(r'^eip=.+'), @@ -167,8 +168,9 @@ def getinstr(reporttext, instraddr): ''' Find the disassembly line for the current (crashing) instruction ''' + regex = re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)') for line in reporttext.splitlines(): - n = re.match(regex['current_instr'], line) + n = regex.match(line) if n: return n.group(3) return '' @@ -314,7 +316,6 @@ def checkreport(reportfile, crasherfile, crash_hash): if _64bit_debugger: # 64-bit target app faultaddr = faultaddr.zfill(16) - #print 'faultaddr: %s' % faultaddr efaptr = struct.unpack(' Date: Mon, 30 Jun 2014 11:14:37 -0400 Subject: [PATCH 0387/1169] partial fix for BFF-760 still need to make linux actually use the cached pickle file. Right now it will read and write it, but not use it. --- src/certfuzz/tools/common/drillresults.py | 19 ++++++++++++++++ src/certfuzz/tools/linux/drillresults.py | 12 ++++++++++- src/certfuzz/tools/windows/drillresults.py | 25 ++++------------------ 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index c9fc7d3..8fd31f5 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -3,6 +3,8 @@ @organization: cert.org ''' +import os +import cPickle as pickle registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', 'edi', 'eip') @@ -161,6 +163,23 @@ def printreport(scoredcrashes, results, ignorejit): print_crash_report(crasher, score, details) +def loadcached(pkl_filename): + try: + with open(pkl_filename, 'rb') as pkl_file: + return pickle.load(pkl_file) + except IOError: + # No cached results + pass + + +def cache_results(pkl_filename, results): + pkldir = os.path.dirname(pkl_filename) + if not os.path.exists(pkldir): + os.makedirs(pkldir) + with open(pkl_filename, 'wb') as pkl_file: + pickle.dump(results, pkl_file, -1) + + def main(): pass diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index 39f1aa5..906d81a 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -9,7 +9,7 @@ from optparse import OptionParser from certfuzz.tools.common.drillresults import readfile, carve, carve2, \ - score_reports, reg_set, printreport + score_reports, reg_set, printreport, loadcached, cache_results regex = { @@ -374,6 +374,9 @@ def parsegdbs(gdblist): def main(): # If user doesn't specify a directory to crawl, use "results" global ignorejit + global cached_results + pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') + usage = "usage: %prog [options]" parser = OptionParser(usage=usage) parser.add_option('-d', '--dir', @@ -382,6 +385,9 @@ def main(): parser.add_option('-j', '--ignorejit', dest='ignorejit', action='store_true', help='Ignore PC in unmapped module (JIT)') + parser.add_option('-f', '--force', dest='force', + action='store_true', + help='Force recalculation of results') (options, args) = parser.parse_args() ignorejit = options.ignorejit tld = options.resultsdir @@ -390,10 +396,14 @@ def main(): if not os.path.isdir(tld): # Probably using FOE 1.0, which defaults to "crashers" for output tld = 'crashers' + gdblist = findgdbs(tld) + if not options.force: + cached_results = loadcached(pickle_file) parsegdbs(gdblist) score_reports(results, scoredcrashes, ignorejit, re_set) printreport(results, scoredcrashes, ignorejit) + cache_results(pickle_file) if __name__ == '__main__': main() diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 8d8b109..eb9202d 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -9,10 +9,10 @@ from optparse import OptionParser import StringIO import zipfile -import cPickle as pickle from certfuzz.tools.common.drillresults import readfile, carve, carve2, \ - score_reports, is_number, reg_set, reg64_set, printreport + score_reports, is_number, reg_set, reg64_set, printreport, loadcached, \ + cache_results regex = { 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]'), @@ -49,7 +49,6 @@ _64bit_debugger = False wow64_app = False ignorejit = False -pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') def check_64bit(reporttext): @@ -435,28 +434,12 @@ def parsemsecs(mseclist): checkreport(msec['msecfile'], msec['crasherfile'], msec['crash_hash']) - -def loadcached(pkl_filename): - try: - with open(pkl_filename, 'rb') as pkl_file: - return pickle.load(pkl_file) - except IOError: - # No cached results - pass - - -def cache_results(pkl_filename): - pkldir = os.path.dirname(pkl_filename) - if not os.path.exists(pkldir): - os.makedirs(pkldir) - with open(pkl_filename, 'wb') as pkl_file: - pickle.dump(results, pkl_file, -1) - - def main(): # If user doesn't specify a directory to crawl, use "results" global ignorejit global cached_results + pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') + usage = "usage: %prog [options]" parser = OptionParser(usage=usage) parser.add_option('-d', '--dir', From 7fca18937ee249842c1f8edbae3033b2dcc41874 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 14:18:43 -0400 Subject: [PATCH 0388/1169] weeding out global vars _64bit_debugger and wow64_app --- src/certfuzz/tools/windows/drillresults.py | 105 ++++++++------------- 1 file changed, 39 insertions(+), 66 deletions(-) diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index eb9202d..358cf41 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -24,8 +24,6 @@ 'mapped_address': re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), 'mapped_address64': re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), 'syswow64': re.compile('ModLoad:.*syswow64.*', re.IGNORECASE), - 'dbg_prompt': re.compile('^[0-9]:[0-9][0-9][0-9]> (.*)'), - 'wow64_dbg_prompt': re.compile('^[0-9]:[0-9][0-9][0-9]:x86> (.*)'), } @@ -42,21 +40,14 @@ re_set = set(really_exploitable) results = {} -cached_results = {} scoredcrashes = {} regdict = {} -mseclist = [] -_64bit_debugger = False -wow64_app = False -ignorejit = False def check_64bit(reporttext): ''' Check if the debugger and target app are 64-bit ''' - global _64bit_debugger - global wow64_app for line in reporttext.splitlines(): n = re.match(regex['64bit_debugger'], line) if n: @@ -65,14 +56,13 @@ def check_64bit(reporttext): n = re.match(regex['syswow64'], line) if n: wow64_app = True + return (_64bit_debugger, wow64_app) -def pc_in_mapped_address(reporttext, instraddr): +def pc_in_mapped_address(reporttext, instraddr, _64bit_debugger): ''' Check if the instruction pointer is in a loaded module ''' - global _64bit_debugger - global wow64_app ma_regex = 'mapped_address' mapped_module = 'unloaded' if _64bit_debugger: @@ -146,25 +136,24 @@ def readbinfile(inputfile): return filebytes -def getexnum(reporttext): +def getexnum(reporttext, wow64_app): ''' Get the exception number by counting the number of continues ''' - global wow64_app - if wow64_app: - dbg_prompt = 'wow64_dbg_prompt' + pattern = re.compile('^[0-9]:[0-9][0-9][0-9]:x86> (.*)') else: - dbg_prompt = 'dbg_prompt' + pattern = re.compile('^[0-9]:[0-9][0-9][0-9]> (.*)') + exception = 0 + for line in reporttext.splitlines(): - n = re.match(regex[dbg_prompt], line) + n = re.match(pattern, line) if n: cdbcmd = n.group(1) cmds = cdbcmd.split(';') - for cmd in cmds: - if cmd == 'g': - exception = exception + 1 + exception += cmds.count('g') + return exception @@ -192,36 +181,32 @@ def getinstr(reporttext, instraddr): return line -def formataddr(faultaddr): +def formataddr(faultaddr, _64bit_debugger, wow64_app): ''' Format a 64- or 32-bit memory address to a fixed width ''' - global _64bit_debugger - global wow64_app - if not faultaddr: return - else: - faultaddr = faultaddr.strip() - faultaddr = faultaddr.replace('0x', '') + + faultaddr = faultaddr.strip().replace('0x', '') if _64bit_debugger and not wow64_app: # Due to a bug in !exploitable, the Exception Faulting Address is # often wrong with 64-bit targets if len(faultaddr) < 10: # pad faultaddr - faultaddr = faultaddr.zfill(16) - else: - if len(faultaddr) > 10: # 0x12345678 = 10 chars - faultaddr = faultaddr[-8:] - elif len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(8) + return faultaddr.zfill(16) - return faultaddr + if len(faultaddr) > 10: + # 0x12345678 = 10 chars + return faultaddr[-8:] + + if len(faultaddr) < 10: + # pad faultaddr + return faultaddr.zfill(8) -def fixefaoffset(instructionline, faultaddr): +def fixefaoffset(instructionline, faultaddr, _64bit_debugger, wow64_app): ''' Adjust faulting address for instructions that use offsets Currently only works for instructions like CALL [reg + offset] @@ -264,14 +249,10 @@ def fixefaoffset(instructionline, faultaddr): return faultaddr -def checkreport(reportfile, crasherfile, crash_hash): +def checkreport(reportfile, crasherfile, crash_hash, cached_results): ''' Parse the msec file ''' - global _64bit_debugger - global wow64_app - global cached_results - if cached_results: if cached_results.get(crash_hash): results[crash_hash] = cached_results[crash_hash] @@ -287,7 +268,7 @@ def checkreport(reportfile, crasherfile, crash_hash): reporttext = readfile(reportfile) getregs(reporttext) current_dir = os.path.dirname(reportfile) - exceptionnum = getexnum(reporttext) + exceptionnum = getexnum(reporttext, wow64_app) classification = carve(reporttext, "Exploitability Classification: ", "\n") try: if classification: @@ -340,11 +321,11 @@ def checkreport(reportfile, crasherfile, crash_hash): # Set the "fuzzedfile" property for the crash ID crashid['fuzzedfile'] = crasherfile # See if we're dealing with 64-bit debugger or target app - check_64bit(reporttext) + (_64bit_debugger, wow64_app) = check_64bit(reporttext) faultaddr = carve2(reporttext) instraddr = carve(reporttext, "Instruction Address:", "\n") - faultaddr = formataddr(faultaddr) - instraddr = formataddr(instraddr) + faultaddr = formataddr(faultaddr, _64bit_debugger, wow64_app) + instraddr = formataddr(instraddr, _64bit_debugger, wow64_app) # No faulting address means no crash. if not faultaddr or not instraddr: @@ -356,15 +337,14 @@ def checkreport(reportfile, crasherfile, crash_hash): if shortdesc != 'DEPViolation': faultaddr = fixefabug(reporttext, instraddr, faultaddr) - # pc_module = pc_in_mapped_address(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr) + crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr, _64bit_debugger) # Get the cdb line that contains the crashing instruction instructionline = getinstr(reporttext, instraddr) crashid['exceptions'][exceptionnum]['instructionline'] = instructionline if instructionline: - faultaddr = fixefaoffset(instructionline, faultaddr) + faultaddr = fixefaoffset(instructionline, faultaddr, _64bit_debugger, wow64_app) # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') @@ -394,7 +374,8 @@ def checkreport(reportfile, crasherfile, crash_hash): crashid['exceptions'][exceptionnum]['EIF'] = False -def findmsecs(tld): +def find_dbg_output(tld): + dbg_out_list = [] # Walk the results directory for root, dirs, files in os.walk(tld): crash_hash = os.path.basename(root) @@ -418,26 +399,16 @@ def findmsecs(tld): for current_file in files: # Go through all of the .msec files and parse them if regex['msec_report'].match(current_file): - msecdict = {} msecfile = os.path.join(root, current_file) if crasherfile and root not in crasherfile: crasherfile = os.path.join(root, crasherfile) - msecdict['msecfile'] = msecfile - msecdict['crasherfile'] = crasherfile - msecdict['crash_hash'] = crash_hash - mseclist.append(msecdict) - return mseclist - - -def parsemsecs(mseclist): - for msec in mseclist: - checkreport(msec['msecfile'], msec['crasherfile'], msec['crash_hash']) + dbg_tuple = (msecfile, crasherfile, crash_hash) + dbg_out_list.append(dbg_tuple) + return dbg_out_list def main(): # If user doesn't specify a directory to crawl, use "results" - global ignorejit - global cached_results pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') usage = "usage: %prog [options]" @@ -447,7 +418,8 @@ def main(): dest='resultsdir', default='../results') parser.add_option('-j', '--ignorejit', dest='ignorejit', action='store_true', - help='Ignore PC in unmapped module (JIT)') + help='Ignore PC in unmapped module (JIT)', + default=False) parser.add_option('-f', '--force', dest='force', action='store_true', help='Force recalculation of results') @@ -459,10 +431,11 @@ def main(): if not os.path.isdir(tld): # Probably using FOE 1.0, which defaults to "crashers" for output tld = 'crashers' - mseclist = findmsecs(tld) + dbg_out = find_dbg_output(tld) if not options.force: cached_results = loadcached(pickle_file) - parsemsecs(mseclist) + for dbg_file, crash_file, crash_hash in dbg_out: + checkreport(dbg_file, crash_file, crash_hash, cached_results) score_reports(results, scoredcrashes, ignorejit, re_set) printreport(results, scoredcrashes, ignorejit) cache_results(pickle_file) From a7b25eccfd535f41b2ad24bde5a42c50f4a9c3e4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 15:40:42 -0400 Subject: [PATCH 0389/1169] use underscores in function names --- src/certfuzz/tools/common/drillresults.py | 6 +- src/certfuzz/tools/linux/drillresults.py | 55 ++++++++--------- src/certfuzz/tools/windows/drillresults.py | 68 +++++++++++++--------- 3 files changed, 67 insertions(+), 62 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 8fd31f5..8e5afd7 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -17,7 +17,7 @@ reg64_set = set(registers64) -def readfile(textfile): +def read_file(textfile): ''' Read text file ''' @@ -153,7 +153,7 @@ def print_crash_report(crasher, score, details, ignorejit): print 'Code executing in: %s' % module -def printreport(scoredcrashes, results, ignorejit): +def print_report(scoredcrashes, results, ignorejit): sorted_crashes = sorted(scoredcrashes.iteritems(), key=lambda(k, v): (v, k)) for crashes in sorted_crashes: @@ -163,7 +163,7 @@ def printreport(scoredcrashes, results, ignorejit): print_crash_report(crasher, score, details) -def loadcached(pkl_filename): +def load_cached(pkl_filename): try: with open(pkl_filename, 'rb') as pkl_file: return pickle.load(pkl_file) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index 906d81a..e20fd01 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -8,8 +8,9 @@ import re from optparse import OptionParser -from certfuzz.tools.common.drillresults import readfile, carve, carve2, \ - score_reports, reg_set, printreport, loadcached, cache_results +from certfuzz.tools.common.drillresults import read_file, carve, carve2, \ + score_reports, reg_set, print_report, cache_results, load_cached, \ + ResultDriller regex = { @@ -42,20 +43,15 @@ results = {} scoredcrashes = {} -#regdict = {} -gdblist = [] -ignorejit = False -_64bit_debugger = False def check_64bit(reporttext): ''' Check if the debugger and target app are 64-bit ''' - global _64bit_debugger - - if _64bit_debugger: - return + # TODO: When would this ever happen? +# if _64bit_debugger: +# return for line in reporttext.splitlines(): m = re.match(regex['bt_addr'], line) @@ -63,9 +59,8 @@ def check_64bit(reporttext): start_addr = m.group(1) #print '%s length: %s'% (start_addr, len(start_addr)) if len(start_addr) > 10: - _64bit_debugger = True - #print 'Target process is 64-bit' - break + return True + return False def pc_in_mapped_address(reporttext, instraddr): @@ -104,12 +99,12 @@ def pc_in_mapped_address(reporttext, instraddr): return mapped_module -def fixefabug(reporttext, instraddr, faultaddr): +def fix_efa_bug(reporttext, instraddr, faultaddr): ''' !exploitable often reports an incorrect EFA for 64-bit targets. If we're dealing with a 64-bit target, we can second-guess the reported EFA ''' - instructionline = getinstr(reporttext, instraddr) + instructionline = get_instr(reporttext, instraddr) if not instructionline: return faultaddr ds = carve(instructionline, "ds:", "=") @@ -118,7 +113,7 @@ def fixefabug(reporttext, instraddr, faultaddr): return faultaddr -def readbinfile(textfile): +def read_bin_file(textfile): ''' Read binary file ''' @@ -127,7 +122,7 @@ def readbinfile(textfile): return text -def getexnum(reporttext): +def get_ex_num(reporttext): ''' Get the exception number by counting the number of continues ''' @@ -143,7 +138,7 @@ def getexnum(reporttext): return exception -def getinstraddr(reporttext): +def get_instr_addr(reporttext): ''' Find the address for the current (crashing) instruction ''' @@ -164,7 +159,7 @@ def getinstraddr(reporttext): return instraddr -def getinstr(reporttext, instraddr): +def get_instr(reporttext, instraddr): ''' Find the disassembly line for the current (crashing) instruction ''' @@ -176,7 +171,7 @@ def getinstr(reporttext, instraddr): return '' -def formataddr(faultaddr): +def format_addr(faultaddr, _64bit_debugger): ''' Format a 64- or 32-bit memory address to a fixed width ''' @@ -203,7 +198,7 @@ def formataddr(faultaddr): return faultaddr -def fixefaoffset(instructionline, faultaddr): +def fix_efa_offset(instructionline, faultaddr, _64bit_debugger): ''' Adjust faulting address for instructions that use offsets Currently only works for instructions like CALL [reg + offset] @@ -234,7 +229,7 @@ def fixefaoffset(instructionline, faultaddr): return faultaddr # Subtract offset to get actual interesting pattern faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = formataddr(faultaddr.replace('L', '')) + faultaddr = format_addr(faultaddr.replace('L', ''), _64bit_debugger) return faultaddr @@ -373,8 +368,6 @@ def parsegdbs(gdblist): def main(): # If user doesn't specify a directory to crawl, use "results" - global ignorejit - global cached_results pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') usage = "usage: %prog [options]" @@ -384,7 +377,8 @@ def main(): dest='resultsdir', default='../results') parser.add_option('-j', '--ignorejit', dest='ignorejit', action='store_true', - help='Ignore PC in unmapped module (JIT)') + help='Ignore PC in unmapped module (JIT)', + default=False) parser.add_option('-f', '--force', dest='force', action='store_true', help='Force recalculation of results') @@ -396,13 +390,14 @@ def main(): if not os.path.isdir(tld): # Probably using FOE 1.0, which defaults to "crashers" for output tld = 'crashers' - - gdblist = findgdbs(tld) if not options.force: - cached_results = loadcached(pickle_file) - parsegdbs(gdblist) + cached_results = load_cached(pickle_file) + + dbg_out = find_dbg_output(tld) + for dbg_file, crash_file, crash_hash in dbg_out: + check_report(dbg_file, crash_file, crash_hash, cached_results) score_reports(results, scoredcrashes, ignorejit, re_set) - printreport(results, scoredcrashes, ignorejit) + print_report(results, scoredcrashes, ignorejit) cache_results(pickle_file) if __name__ == '__main__': diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 358cf41..df4afe7 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -10,9 +10,9 @@ import StringIO import zipfile -from certfuzz.tools.common.drillresults import readfile, carve, carve2, \ - score_reports, is_number, reg_set, reg64_set, printreport, loadcached, \ - cache_results +from certfuzz.tools.common.drillresults import read_file, carve, carve2, \ + score_reports, is_number, reg_set, reg64_set, print_report, \ + cache_results, load_cached, ResultDriller regex = { 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]'), @@ -81,12 +81,12 @@ def pc_in_mapped_address(reporttext, instraddr, _64bit_debugger): return mapped_module -def fixefabug(reporttext, instraddr, faultaddr): +def fix_efa_bug(reporttext, instraddr, faultaddr): ''' !exploitable often reports an incorrect EFA for 64-bit targets. If we're dealing with a 64-bit target, we can second-guess the reported EFA ''' - instructionline = getinstr(reporttext, instraddr) + instructionline = get_instr(reporttext, instraddr) if not instructionline or "=" not in instructionline: # Nothing to fix return faultaddr @@ -106,7 +106,7 @@ def fixefabug(reporttext, instraddr, faultaddr): return faultaddr -def readbinfile(inputfile): +def read_bin_file(inputfile): ''' Read binary file ''' @@ -136,7 +136,7 @@ def readbinfile(inputfile): return filebytes -def getexnum(reporttext, wow64_app): +def get_ex_num(reporttext, wow64_app): ''' Get the exception number by counting the number of continues ''' @@ -157,7 +157,7 @@ def getexnum(reporttext, wow64_app): return exception -def getregs(reporttext): +def get_regs(reporttext): ''' Populate the register dictionary with register values at crash ''' @@ -170,7 +170,7 @@ def getregs(reporttext): regdict[splitreg[0]] = splitreg[1] -def getinstr(reporttext, instraddr): +def get_instr(reporttext, instraddr): ''' Find the disassembly line for the current (crashing) instruction ''' @@ -181,7 +181,7 @@ def getinstr(reporttext, instraddr): return line -def formataddr(faultaddr, _64bit_debugger, wow64_app): +def format_addr(faultaddr, _64bit_debugger, wow64_app): ''' Format a 64- or 32-bit memory address to a fixed width ''' @@ -206,7 +206,7 @@ def formataddr(faultaddr, _64bit_debugger, wow64_app): return faultaddr.zfill(8) -def fixefaoffset(instructionline, faultaddr, _64bit_debugger, wow64_app): +def fix_efa_offset(instructionline, faultaddr, _64bit_debugger, wow64_app): ''' Adjust faulting address for instructions that use offsets Currently only works for instructions like CALL [reg + offset] @@ -245,7 +245,7 @@ def fixefaoffset(instructionline, faultaddr, _64bit_debugger, wow64_app): return faultaddr # Subtract offset to get actual interesting pattern faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = formataddr(faultaddr.replace('L', '')) + faultaddr = format_addr(faultaddr.replace('L', '')) return faultaddr @@ -372,7 +372,26 @@ def checkreport(reportfile, crasherfile, crash_hash, cached_results): crashid['exceptions'][exceptionnum]['EIF'] = True else: crashid['exceptions'][exceptionnum]['EIF'] = False +def parse_args(): + import argparse + usage = "usage: %prog [options]" + parser = argparse.ArgumentParser(usage) + parser.add_option('-d', '--dir', + help='directory to look for results in. Default is "results"', + dest='resultsdir', + default='../results', + type=str) + parser.add_option('-j', '--ignorejit', dest='ignorejit', + action='store_true', + help='Ignore PC in unmapped module (JIT)', + default=False, + type=bool) + parser.add_option('-f', '--force', dest='force', + action='store_true', + help='Force recalculation of results', + type=bool) + return parser.parse_args() def find_dbg_output(tld): dbg_out_list = [] @@ -411,19 +430,8 @@ def main(): # If user doesn't specify a directory to crawl, use "results" pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') - usage = "usage: %prog [options]" - parser = OptionParser(usage=usage) - parser.add_option('-d', '--dir', - help='directory to look for results in. Default is "results"', - dest='resultsdir', default='../results') - parser.add_option('-j', '--ignorejit', dest='ignorejit', - action='store_true', - help='Ignore PC in unmapped module (JIT)', - default=False) - parser.add_option('-f', '--force', dest='force', - action='store_true', - help='Force recalculation of results') - (options, args) = parser.parse_args() + options = parse_args() + ignorejit = options.ignorejit tld = options.resultsdir if not os.path.isdir(tld): @@ -431,13 +439,15 @@ def main(): if not os.path.isdir(tld): # Probably using FOE 1.0, which defaults to "crashers" for output tld = 'crashers' - dbg_out = find_dbg_output(tld) + if not options.force: - cached_results = loadcached(pickle_file) + cached_results = load_cached(pickle_file) + + dbg_out = find_dbg_output(tld) for dbg_file, crash_file, crash_hash in dbg_out: - checkreport(dbg_file, crash_file, crash_hash, cached_results) + check_report(dbg_file, crash_file, crash_hash, cached_results) score_reports(results, scoredcrashes, ignorejit, re_set) - printreport(results, scoredcrashes, ignorejit) + print_report(results, scoredcrashes, ignorejit) cache_results(pickle_file) if __name__ == '__main__': From bea00bbb775208053bb801af0aec0d81374b3071 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 15:42:33 -0400 Subject: [PATCH 0390/1169] created ResultDriller object to start shift towards an OO architecture --- src/certfuzz/tools/common/drillresults.py | 84 ++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 8e5afd7..cd9e0f3 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -5,6 +5,7 @@ ''' import os import cPickle as pickle +import abc registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', 'edi', 'eip') @@ -180,8 +181,89 @@ def cache_results(pkl_filename, results): pickle.dump(results, pkl_file, -1) -def main(): +class DrillResultsError(Exception): pass + +class ResultDriller(object): + __metaclass__ = abc.ABCMeta + + def __init__(self, + ignore_jit=False, + base_dir='../results', + force_reload=False): + self.ignore_jit = ignore_jit + self.base_dir = base_dir + self.tld = None + self.force = force_reload + + self.pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') + self.cached_results = None + self.dbg_out = [] + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + return + + def _check_dirs(self): + check_dirs = [self.base_dir, 'results', 'crashers'] + for d in check_dirs: + if os.path.isdir(d): + self.tld = d + return + # if you got here, none of them exist + raise DrillResultsError('None of {} appears to be a dir'.format(check_dirs)) + + def load_cached(self): + try: + with open(self.pkl_filename, 'rb') as pkl_file: + self.cached_results = pickle.load(pkl_file) + except IOError: + # No cached results + pass + + @abc.abstractmethod + def check_64bit(self, reporttext): + ''' + Check if the debugger and target app are 64-bit + ''' + pass + + @abc.abstractmethod + def _platform_find_dbg_output(self, crash_hash): + pass + + def find_dbg_output(self): + ''' + Crawls self.tld looking for crash directories to process. Puts a list + of tuples into self.dbg_out. + ''' + # Walk the results directory + for root, dirs, files in os.walk(self.tld): + dir_basename = os.path.basename(root) + self._platform_find_dbg_output(dir_basename, files, root) + + @abc.abstractmethod + def _check_report(self, dbg_file, crash_file, crash_hash, cached_results): + pass + + def check_reports(self): + for dbg_file, crash_file, crash_hash in self.dbg_out: + self.check_report(dbg_file, crash_file, crash_hash, self.cached_results) + + def drill_results(self): + self._check_dirs() + + if not self.force: + self.load_cached() + + self.find_dbg_output() + self.check_reports() + self.score_reports() + self.print_reports() + self.cache_results() + if __name__ == '__main__': main() From 32815a1bc965470df3eef05ba8682c756541abc4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 15:44:03 -0400 Subject: [PATCH 0391/1169] get rid of if __name__ == '__main__' stuff --- src/certfuzz/tools/common/drillresults.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index cd9e0f3..10cb7d2 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -264,6 +264,3 @@ def drill_results(self): self.score_reports() self.print_reports() self.cache_results() - -if __name__ == '__main__': - main() From beb45fd172a4c284426a65e53c315e04164cfbfb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 15:46:10 -0400 Subject: [PATCH 0392/1169] add cache_result method to ResultDriller --- src/certfuzz/tools/common/drillresults.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 10cb7d2..8367239 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -253,6 +253,14 @@ def check_reports(self): for dbg_file, crash_file, crash_hash in self.dbg_out: self.check_report(dbg_file, crash_file, crash_hash, self.cached_results) + def cache_results(self): + pkldir = os.path.dirname(self.pkl_filename) + if not os.path.exists(pkldir): + os.makedirs(pkldir) + with open(self.pkl_filename, 'wb') as pkl_file: + pickle.dump(self.results, pkl_file, -1) + + def drill_results(self): self._check_dirs() From a470525076b3b1258e1a53d12cd58269ebb86da7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 08:15:02 -0400 Subject: [PATCH 0393/1169] move methods into objects linux: checkreport windows: check_64bit pc_in_mapped_address format_addr checkreport find_dbg_output common: score_reports print_reports score_crasher load_cached check_dirs --- src/certfuzz/tools/common/drillresults.py | 236 ++++++------ src/certfuzz/tools/linux/drillresults.py | 356 ++++++++++++------ src/certfuzz/tools/windows/drillresults.py | 401 +++++++++++---------- 3 files changed, 562 insertions(+), 431 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 8367239..40d6713 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -6,6 +6,10 @@ import os import cPickle as pickle import abc +import logging + +logger = logging.getLogger(__name__) + registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', 'edi', 'eip') @@ -60,82 +64,8 @@ def is_number(s): except ValueError: return False - -def score_crasher(crasher, details, ignorejit, re_set): - scores = [100] - if details['reallyexploitable'] == True: - # The crash summary is a very interesting one - for exception in details['exceptions']: - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not ignorejit: - # EIP is not in a loaded module - scores.append(20) - if details['exceptions'][exception]['shortdesc'] in re_set: - efa = '0x' + details['exceptions'][exception]['efa'] - if details['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - scores.append(30) - elif '0x0000' in efa: - # Faulting address is somewhat near null - scores.append(20) - elif '0xffff' in efa: - # Faulting address is likely a negative number - scores.append(20) - else: - # Faulting address has high entropy. Most exploitable. - scores.append(10) - else: - # The faulting address pattern is not in the fuzzed file - scores.append(40) - - else: - # The crash summary isn't necessarily interesting - for exception in details['exceptions']: - efa = '0x' + details['exceptions'][exception]['efa'] - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not ignorejit: - scores.append(20) - elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): - # likely heap corruption. Exploitable, but difficult - scores.append(45) - elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: - # non-continued potential stack buffer overflow - scores.append(40) - elif details['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - scores.append(70) - elif '0x0000' in efa: - # Faulting address is somewhat near null - scores.append(60) - elif '0xffff' in efa: - # Faulting address is likely a negative number - scores.append(60) - else: - # Faulting address has high entropy. - scores.append(50) - return min(scores) - - -def score_reports(results, crashscores, ignorejit, re_set): - # Assign a ranking to each crash report. The lower the rank, the higher - # the exploitability - if results: - print "--- Interesting crashes ---\n" - # For each of the crash ids in the results dictionary, apply ranking - for crasher in results: - try: - crashscores[crasher] = score_crasher(crasher, results[crasher], ignorejit, re_set) - except KeyError: - print "Error scoring crash %s" % crasher - continue - - -def print_crash_report(crasher, score, details, ignorejit): - print '\n%s - Exploitability rank: %s' % (crasher, score) +def print_crash_report(crash_key, score, details, ignorejit): + print '\n%s - Exploitability rank: %s' % (crash_key, score) print 'Fuzzed file: %s' % details['fuzzedfile'] for exception in details['exceptions']: shortdesc = details['exceptions'][exception]['shortdesc'] @@ -157,28 +87,9 @@ def print_crash_report(crasher, score, details, ignorejit): def print_report(scoredcrashes, results, ignorejit): sorted_crashes = sorted(scoredcrashes.iteritems(), key=lambda(k, v): (v, k)) - for crashes in sorted_crashes: - crasher = crashes[0] - score = crashes[1] - details = results[crasher] - print_crash_report(crasher, score, details) - - -def load_cached(pkl_filename): - try: - with open(pkl_filename, 'rb') as pkl_file: - return pickle.load(pkl_file) - except IOError: - # No cached results - pass - - -def cache_results(pkl_filename, results): - pkldir = os.path.dirname(pkl_filename) - if not os.path.exists(pkldir): - os.makedirs(pkldir) - with open(pkl_filename, 'wb') as pkl_file: - pickle.dump(results, pkl_file, -1) + for crash_key, score in sorted_crashes: + details = results[crash_key] + print_crash_report(crash_key, score, details) class DrillResultsError(Exception): @@ -200,6 +111,16 @@ def __init__(self, self.pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') self.cached_results = None self.dbg_out = [] + self.results = {} + self.scoredcrashes = {} + + self._64bit_debugger = False + + self.re_set = set(self.really_exploitable) + + @abc.abstractproperty + def really_exploitable(self): + return [] def __enter__(self): return self @@ -207,23 +128,6 @@ def __enter__(self): def __exit__(self, etype, value, traceback): return - def _check_dirs(self): - check_dirs = [self.base_dir, 'results', 'crashers'] - for d in check_dirs: - if os.path.isdir(d): - self.tld = d - return - # if you got here, none of them exist - raise DrillResultsError('None of {} appears to be a dir'.format(check_dirs)) - - def load_cached(self): - try: - with open(self.pkl_filename, 'rb') as pkl_file: - self.cached_results = pickle.load(pkl_file) - except IOError: - # No cached results - pass - @abc.abstractmethod def check_64bit(self, reporttext): ''' @@ -249,9 +153,103 @@ def find_dbg_output(self): def _check_report(self, dbg_file, crash_file, crash_hash, cached_results): pass + def _check_dirs(self): + check_dirs = [self.base_dir, 'results', 'crashers'] + for d in check_dirs: + if os.path.isdir(d): + self.tld = d + return + # if you got here, none of them exist + raise DrillResultsError('None of {} appears to be a dir'.format(check_dirs)) + + def load_cached(self): + if self.force: + logger.info('--force option used, ignoring cached results') + return + + try: + with open(self.pkl_filename, 'rb') as pkl_file: + self.cached_results = pickle.load(pkl_file) + except IOError: + # No cached results + pass + def check_reports(self): for dbg_file, crash_file, crash_hash in self.dbg_out: - self.check_report(dbg_file, crash_file, crash_hash, self.cached_results) + self._check_report(dbg_file, crash_file, crash_hash, self.cached_results) + + def _score_crasher(self, details): + scores = [100] + if details['reallyexploitable'] == True: + # The crash summary is a very interesting one + for exception in details['exceptions']: + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not self.ignorejit: + # EIP is not in a loaded module + scores.append(20) + if details['exceptions'][exception]['shortdesc'] in self.re_set: + efa = '0x' + details['exceptions'][exception]['efa'] + if details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(30) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(20) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(20) + else: + # Faulting address has high entropy. Most exploitable. + scores.append(10) + else: + # The faulting address pattern is not in the fuzzed file + scores.append(40) + else: + # The crash summary isn't necessarily interesting + for exception in details['exceptions']: + efa = '0x' + details['exceptions'][exception]['efa'] + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not self.ignorejit: + scores.append(20) + elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): + # likely heap corruption. Exploitable, but difficult + scores.append(45) + elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: + # non-continued potential stack buffer overflow + scores.append(40) + elif details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(70) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(60) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(60) + else: + # Faulting address has high entropy. + scores.append(50) + return min(scores) + + def score_reports(self, crashscores, ignorejit, re_set): + # Assign a ranking to each crash report. The lower the rank, the higher + # the exploitability + if self.results: + print "--- Interesting crashes ---\n" + # For each of the crash ids in the results dictionary, apply ranking + for crash_key, crash_details in self.results.iteritems(): + try: + crashscores[crash_key] = self._score_crasher(crash_details) + except KeyError: + print "Error scoring crash %s" % crash_key + continue + + def print_reports(self): + pass def cache_results(self): pkldir = os.path.dirname(self.pkl_filename) @@ -260,13 +258,9 @@ def cache_results(self): with open(self.pkl_filename, 'wb') as pkl_file: pickle.dump(self.results, pkl_file, -1) - def drill_results(self): self._check_dirs() - - if not self.force: - self.load_cached() - + self.load_cached() self.find_dbg_output() self.check_reports() self.score_reports() diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index e20fd01..a98deb1 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -233,111 +233,137 @@ def fix_efa_offset(instructionline, faultaddr, _64bit_debugger): return faultaddr -def checkreport(reportfile, crasherfile, crash_hash): - ''' - Parse the gdb file - ''' - - global _64bit_debugger - - #print('checking %s against %s: %s' % (reportfile, crasherfile, crash_hash)) - crashid = results[crash_hash] - - reporttext = readfile(reportfile) - current_dir = os.path.dirname(reportfile) - exceptionnum = 0 - classification = carve(reporttext, "Classification: ", "\n") - #print 'classification: %s' % classification - try: - if classification: - # Create a new exception dictionary to add to the crash - exception = {} - crashid['exceptions'][exceptionnum] = exception - except KeyError: - # Crash ID (crash_hash) not yet seen - # Default it to not being "really exploitable" - crashid['reallyexploitable'] = False - # Create a dictionary of exceptions for the crash id - exceptions = {} - crashid['exceptions'] = exceptions - # Create a dictionary for the exception - crashid['exceptions'][exceptionnum] = exception - - # Set !exploitable classification for the exception - if classification: - crashid['exceptions'][exceptionnum]['classification'] = classification - - shortdesc = carve(reporttext, "Short description: ", " (") - #print 'shortdesc: %s' % shortdesc - if shortdesc: - # Set !exploitable Short Description for the exception - crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc - # Flag the entire crash ID as really exploitable if this is a good - # exception - crashid['reallyexploitable'] = shortdesc in re_set - - if not os.path.isfile(crasherfile): - # Can't find the crasher file - #print "WTF! Cannot find %s" % crasherfile - return - # Set the "fuzzedfile" property for the crash ID - crashid['fuzzedfile'] = crasherfile - # See if we're dealing with 64-bit debugger or target app - check_64bit(reporttext) - faultaddr = carve2(reporttext) - #print 'faultaddr: %s' % faultaddr - instraddr = getinstraddr(reporttext) - #instraddr = carve(reporttext, "Instruction Address:", "\n") - faultaddr = formataddr(faultaddr) - instraddr = formataddr(instraddr) - #print 'instruction address: %s' % instraddr - - # No faulting address means no crash. - if not faultaddr: - return - - if instraddr: - crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr) - - # Get the cdb line that contains the crashing instruction - instructionline = getinstr(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['instructionline'] = instructionline - if instructionline: - faultaddr = fixefaoffset(instructionline, faultaddr) - - # Fix faulting pattern endian - faultaddr = faultaddr.replace('0x', '') - crashid['exceptions'][exceptionnum]['efa'] = faultaddr - if _64bit_debugger: - # 64-bit target app - faultaddr = faultaddr.zfill(16) - efaptr = struct.unpack(' 10: - # 0x12345678 = 10 chars - return faultaddr[-8:] - - if len(faultaddr) < 10: - # pad faultaddr - return faultaddr.zfill(8) def fix_efa_offset(instructionline, faultaddr, _64bit_debugger, wow64_app): @@ -248,130 +186,6 @@ def fix_efa_offset(instructionline, faultaddr, _64bit_debugger, wow64_app): faultaddr = format_addr(faultaddr.replace('L', '')) return faultaddr - -def checkreport(reportfile, crasherfile, crash_hash, cached_results): - ''' - Parse the msec file - ''' - if cached_results: - if cached_results.get(crash_hash): - results[crash_hash] = cached_results[crash_hash] - return - - crashid = results[crash_hash] - - if crasherfile == '': - # Old FOE version that didn't do multiple exceptions or rename msec - # file with exploitability - crasherfile, reportfileext = os.path.splitext(reportfile) - - reporttext = readfile(reportfile) - getregs(reporttext) - current_dir = os.path.dirname(reportfile) - exceptionnum = getexnum(reporttext, wow64_app) - classification = carve(reporttext, "Exploitability Classification: ", "\n") - try: - if classification: - # Create a new exception dictionary to add to the crash - exception = {} - crashid['exceptions'][exceptionnum] = exception - except KeyError: - # Crash ID (crash_hash) not yet seen - # Default it to not being "really exploitable" - crashid['reallyexploitable'] = False - # Create a dictionary of exceptions for the crash id - exceptions = {} - crashid['exceptions'] = exceptions - # Create a dictionary for the exception - crashid['exceptions'][exceptionnum] = exception - - # Set !exploitable classification for the exception - if classification: - crashid['exceptions'][exceptionnum]['classification'] = classification - - shortdesc = carve(reporttext, "Short Description: ", "\n") - if shortdesc: - # Set !exploitable Short Description for the exception - crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc - # Flag the entire crash ID as really exploitable if this is a good - # exception - crashid['reallyexploitable'] = shortdesc in re_set - # Check if the expected crasher file (fuzzed file) exists - if not os.path.isfile(crasherfile): - # It's not there, so try to extract the filename from the cdb - # commandline - commandline = carve(reporttext, "CommandLine: ", "\n") - args = commandline.split() - for arg in args: - if "sf_" in arg: - crasherfile = os.path.basename(arg) - if "-" in crasherfile: - # FOE 2.0 verify mode puts a '-' part on the - # filename when invoking cdb, however the resulting file - # is really just 'sf_.' - fileparts = crasherfile.split('-') - m = re.search('\..+', fileparts[1]) - # Recreate the original file name, minus the iteration - crasherfile = os.path.join(current_dir, fileparts[0] + m.group(0)) - else: - crasherfile = os.path.join(current_dir, crasherfile) - if not os.path.isfile(crasherfile): - # Can't find the crasher file - return - # Set the "fuzzedfile" property for the crash ID - crashid['fuzzedfile'] = crasherfile - # See if we're dealing with 64-bit debugger or target app - (_64bit_debugger, wow64_app) = check_64bit(reporttext) - faultaddr = carve2(reporttext) - instraddr = carve(reporttext, "Instruction Address:", "\n") - faultaddr = formataddr(faultaddr, _64bit_debugger, wow64_app) - instraddr = formataddr(instraddr, _64bit_debugger, wow64_app) - - # No faulting address means no crash. - if not faultaddr or not instraddr: - return - - if _64bit_debugger and not wow64_app and instraddr: - # Put backtick into instruction address for pattern matching - instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) - if shortdesc != 'DEPViolation': - faultaddr = fixefabug(reporttext, instraddr, faultaddr) - -# pc_module = pc_in_mapped_address(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr, _64bit_debugger) - - # Get the cdb line that contains the crashing instruction - instructionline = getinstr(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['instructionline'] = instructionline - if instructionline: - faultaddr = fixefaoffset(instructionline, faultaddr, _64bit_debugger, wow64_app) - - # Fix faulting pattern endian - faultaddr = faultaddr.replace('0x', '') - crashid['exceptions'][exceptionnum]['efa'] = faultaddr - if _64bit_debugger and not wow64_app: - # 64-bit target app - faultaddr = faultaddr.zfill(16) - efaptr = struct.unpack('' part on the + # filename when invoking cdb, however the resulting file + # is really just 'sf_.' + fileparts = crasherfile.split('-') + m = re.search('\..+', fileparts[1]) + # Recreate the original file name, minus the iteration + crasherfile = os.path.join(current_dir, fileparts[0] + m.group(0)) + else: + crasherfile = os.path.join(current_dir, crasherfile) + if not os.path.isfile(crasherfile): + # Can't find the crasher file + return + # Set the "fuzzedfile" property for the crash ID + crashid['fuzzedfile'] = crasherfile + # See if we're dealing with 64-bit debugger or target app + self.check_64bit(reporttext) + faultaddr = carve2(reporttext) + instraddr = carve(reporttext, "Instruction Address:", "\n") + faultaddr = self.format_addr(faultaddr) + instraddr = self.format_addr(instraddr) + + # No faulting address means no crash. + if not faultaddr or not instraddr: + return + + if self._64bit_debugger and not self.wow64_app and instraddr: + # Put backtick into instruction address for pattern matching + instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) + if shortdesc != 'DEPViolation': + faultaddr = fix_efa_bug(reporttext, instraddr, faultaddr) + + # pc_module = pc_in_mapped_address(reporttext, instraddr) + crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr, _64bit_debugger) + + # Get the cdb line that contains the crashing instruction + instructionline = get_instr(reporttext, instraddr) + crashid['exceptions'][exceptionnum]['instructionline'] = instructionline + if instructionline: + faultaddr = fix_efa_offset(instructionline, faultaddr, _64bit_debugger, wow64_app) + + # Fix faulting pattern endian + faultaddr = faultaddr.replace('0x', '') + crashid['exceptions'][exceptionnum]['efa'] = faultaddr + if _64bit_debugger and not wow64_app: + # 64-bit target app + faultaddr = faultaddr.zfill(16) + efaptr = struct.unpack(' 10: + # 0x12345678 = 10 chars + return faultaddr[-8:] + + if len(faultaddr) < 10: + # pad faultaddr + return faultaddr.zfill(8) + + def pc_in_mapped_address(self, reporttext, instraddr): + ''' + Check if the instruction pointer is in a loaded module + ''' + ma_regex = 'mapped_address' + mapped_module = 'unloaded' + if self._64bit_debugger: + ma_regex = 'mapped_address64' + + instraddr = instraddr.replace('`', '') + instraddr = int(instraddr, 16) + for line in reporttext.splitlines(): + n = re.match(regex[ma_regex], line) + if n: + # Strip out backticks present on 64-bit systems + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + if begin_address < instraddr < end_address: + mapped_module = n.group(3) + return mapped_module + def main(): @@ -450,5 +457,9 @@ def main(): print_report(results, scoredcrashes, ignorejit) cache_results(pickle_file) + with ResultDriller(ignore_jit=options.ignorejit, + base_dir=options.resultsdir) as rd: + rd.drill_results() + if __name__ == '__main__': main() From 335fce90c94bd095902622098bb5af30a86343ea Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 08:38:32 -0400 Subject: [PATCH 0394/1169] refactor error classes --- src/certfuzz/tools/common/drillresults.py | 1 + src/certfuzz/tools/common/errors.py | 10 ++++++++++ src/certfuzz/tools/errors.py | 10 ++++++++++ 3 files changed, 21 insertions(+) create mode 100644 src/certfuzz/tools/common/errors.py create mode 100644 src/certfuzz/tools/errors.py diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 40d6713..52673d6 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -7,6 +7,7 @@ import cPickle as pickle import abc import logging +from certfuzz.tools.common.errors import DrillResultsError logger = logging.getLogger(__name__) diff --git a/src/certfuzz/tools/common/errors.py b/src/certfuzz/tools/common/errors.py new file mode 100644 index 0000000..f5b7286 --- /dev/null +++ b/src/certfuzz/tools/common/errors.py @@ -0,0 +1,10 @@ +''' +Created on Jul 1, 2014 + +@author: adh +''' +from certfuzz.tools.errors import CERTFuzzToolError + + +class DrillResultsError(CERTFuzzToolError): + pass diff --git a/src/certfuzz/tools/errors.py b/src/certfuzz/tools/errors.py new file mode 100644 index 0000000..419b3bc --- /dev/null +++ b/src/certfuzz/tools/errors.py @@ -0,0 +1,10 @@ +''' +Created on Jul 1, 2014 + +@author: adh +''' +from certfuzz.errors import CERTFuzzError + + +class CERTFuzzToolError(CERTFuzzError): + pass From b43721ae5ebedc5d622612a83992ade290878a7f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 08:39:55 -0400 Subject: [PATCH 0395/1169] move parse_args to common module so both platforms have same command line options --- src/certfuzz/tools/common/drillresults.py | 21 +++++++++ src/certfuzz/tools/linux/drillresults.py | 41 +----------------- src/certfuzz/tools/windows/drillresults.py | 50 +--------------------- 3 files changed, 24 insertions(+), 88 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 52673d6..49451ef 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -7,6 +7,7 @@ import cPickle as pickle import abc import logging +import argparse from certfuzz.tools.common.errors import DrillResultsError logger = logging.getLogger(__name__) @@ -23,6 +24,26 @@ reg64_set = set(registers64) +def parse_args(): + usage = "usage: %prog [options]" + parser = argparse.ArgumentParser(usage) + parser.add_option('-d', '--dir', + help='directory to look for results in. Default is "results"', + dest='resultsdir', + default='../results', + type=str) + parser.add_option('-j', '--ignorejit', dest='ignorejit', + action='store_true', + help='Ignore PC in unmapped module (JIT)', + default=False, + type=bool) + parser.add_option('-f', '--force', dest='force', + action='store_true', + help='Force recalculation of results', + type=bool) + return parser.parse_args() + + def read_file(textfile): ''' Read text file diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index a98deb1..2bed40d 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -6,11 +6,9 @@ import struct import binascii import re -from optparse import OptionParser from certfuzz.tools.common.drillresults import read_file, carve, carve2, \ - score_reports, reg_set, print_report, cache_results, load_cached, \ - ResultDriller + reg_set, ResultDriller, parse_args regex = { @@ -484,43 +482,8 @@ def _check_report(self, reportfile, crasherfile, crash_hash, cached_results): crashid['exceptions'][exceptionnum]['EIF'] = False - - - def main(): - # If user doesn't specify a directory to crawl, use "results" - pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') - - usage = "usage: %prog [options]" - parser = OptionParser(usage=usage) - parser.add_option('-d', '--dir', - help='directory to look for results in. Default is "results"', - dest='resultsdir', default='../results') - parser.add_option('-j', '--ignorejit', dest='ignorejit', - action='store_true', - help='Ignore PC in unmapped module (JIT)', - default=False) - parser.add_option('-f', '--force', dest='force', - action='store_true', - help='Force recalculation of results') - (options, args) = parser.parse_args() - ignorejit = options.ignorejit - tld = options.resultsdir - if not os.path.isdir(tld): - tld = 'results' - if not os.path.isdir(tld): - # Probably using FOE 1.0, which defaults to "crashers" for output - tld = 'crashers' - if not options.force: - cached_results = load_cached(pickle_file) - - dbg_out = find_dbg_output(tld) - for dbg_file, crash_file, crash_hash in dbg_out: - check_report(dbg_file, crash_file, crash_hash, cached_results) - score_reports(results, scoredcrashes, ignorejit, re_set) - print_report(results, scoredcrashes, ignorejit) - cache_results(pickle_file) - + options = parse_args() with ResultDriller(ignore_jit=options.ignorejit, base_dir=options.resultsdir) as rd: rd.drill_results() diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index c546219..df122ee 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -6,13 +6,11 @@ import struct import binascii import re -from optparse import OptionParser import StringIO import zipfile from certfuzz.tools.common.drillresults import read_file, carve, carve2, \ - score_reports, is_number, reg_set, reg64_set, print_report, \ - cache_results, load_cached, ResultDriller + is_number, reg_set, reg64_set, ResultDriller, parse_args regex = { 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]'), @@ -40,8 +38,6 @@ re_set = set(really_exploitable) - - def fix_efa_bug(reporttext, instraddr, faultaddr): ''' !exploitable often reports an incorrect EFA for 64-bit targets. @@ -186,27 +182,6 @@ def fix_efa_offset(instructionline, faultaddr, _64bit_debugger, wow64_app): faultaddr = format_addr(faultaddr.replace('L', '')) return faultaddr -def parse_args(): - import argparse - - usage = "usage: %prog [options]" - parser = argparse.ArgumentParser(usage) - parser.add_option('-d', '--dir', - help='directory to look for results in. Default is "results"', - dest='resultsdir', - default='../results', - type=str) - parser.add_option('-j', '--ignorejit', dest='ignorejit', - action='store_true', - help='Ignore PC in unmapped module (JIT)', - default=False, - type=bool) - parser.add_option('-f', '--force', dest='force', - action='store_true', - help='Force recalculation of results', - type=bool) - return parser.parse_args() - def WindowsResultDriller(ResultDriller): # These !exploitable short descriptions indicate a very interesting crash really_exploitable = [ @@ -432,31 +407,8 @@ def pc_in_mapped_address(self, reporttext, instraddr): return mapped_module - def main(): - # If user doesn't specify a directory to crawl, use "results" - pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') - options = parse_args() - - ignorejit = options.ignorejit - tld = options.resultsdir - if not os.path.isdir(tld): - tld = 'results' - if not os.path.isdir(tld): - # Probably using FOE 1.0, which defaults to "crashers" for output - tld = 'crashers' - - if not options.force: - cached_results = load_cached(pickle_file) - - dbg_out = find_dbg_output(tld) - for dbg_file, crash_file, crash_hash in dbg_out: - check_report(dbg_file, crash_file, crash_hash, cached_results) - score_reports(results, scoredcrashes, ignorejit, re_set) - print_report(results, scoredcrashes, ignorejit) - cache_results(pickle_file) - with ResultDriller(ignore_jit=options.ignorejit, base_dir=options.resultsdir) as rd: rd.drill_results() From 8f3d58a94e47f1b804fc2dfefc4262114c00a731 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 08:40:42 -0400 Subject: [PATCH 0396/1169] move reporting methods into ResultDriller class --- src/certfuzz/tools/common/drillresults.py | 64 ++++++++++------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 49451ef..69c405d 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -86,37 +86,6 @@ def is_number(s): except ValueError: return False -def print_crash_report(crash_key, score, details, ignorejit): - print '\n%s - Exploitability rank: %s' % (crash_key, score) - print 'Fuzzed file: %s' % details['fuzzedfile'] - for exception in details['exceptions']: - shortdesc = details['exceptions'][exception]['shortdesc'] - eiftext = '' - efa = '0x' + details['exceptions'][exception]['efa'] - if details['exceptions'][exception]['EIF']: - eiftext = " *** Byte pattern is in fuzzed file! ***" - print 'exception %s: %s accessing %s %s' % (exception, shortdesc, efa, eiftext) - if details['exceptions'][exception]['instructionline']: - print details['exceptions'][exception]['instructionline'] - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded': - if not ignorejit: - print 'Instruction pointer is not in a loaded module!' - else: - print 'Code executing in: %s' % module - - -def print_report(scoredcrashes, results, ignorejit): - sorted_crashes = sorted(scoredcrashes.iteritems(), key=lambda(k, v): (v, k)) - - for crash_key, score in sorted_crashes: - details = results[crash_key] - print_crash_report(crash_key, score, details) - - -class DrillResultsError(Exception): - pass - class ResultDriller(object): __metaclass__ = abc.ABCMeta @@ -134,7 +103,7 @@ def __init__(self, self.cached_results = None self.dbg_out = [] self.results = {} - self.scoredcrashes = {} + self.crash_scores = {} self._64bit_debugger = False @@ -257,7 +226,7 @@ def _score_crasher(self, details): scores.append(50) return min(scores) - def score_reports(self, crashscores, ignorejit, re_set): + def score_reports(self): # Assign a ranking to each crash report. The lower the rank, the higher # the exploitability if self.results: @@ -265,13 +234,38 @@ def score_reports(self, crashscores, ignorejit, re_set): # For each of the crash ids in the results dictionary, apply ranking for crash_key, crash_details in self.results.iteritems(): try: - crashscores[crash_key] = self._score_crasher(crash_details) + self.crash_scores[crash_key] = self._score_crasher(crash_details) except KeyError: print "Error scoring crash %s" % crash_key continue + def print_crash_report(self, crash_key, score): + details = self.results[crash_key] + print '\n%s - Exploitability rank: %s' % (crash_key, score) + print 'Fuzzed file: %s' % details['fuzzedfile'] + for exception in details['exceptions']: + shortdesc = details['exceptions'][exception]['shortdesc'] + eiftext = '' + efa = '0x' + details['exceptions'][exception]['efa'] + if details['exceptions'][exception]['EIF']: + eiftext = " *** Byte pattern is in fuzzed file! ***" + print 'exception %s: %s accessing %s %s' % (exception, shortdesc, efa, eiftext) + if details['exceptions'][exception]['instructionline']: + print details['exceptions'][exception]['instructionline'] + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded': + if not self.ignorejit: + print 'Instruction pointer is not in a loaded module!' + else: + print 'Code executing in: %s' % module + + @property + def sorted_crashes(self): + return sorted(self.crash_scores.iteritems(), key=lambda(k, v): (v, k)) + def print_reports(self): - pass + for crash_key, score in self.sorted_crashes: + self.print_crash_report(crash_key, score) def cache_results(self): pkldir = os.path.dirname(self.pkl_filename) From de33eb1fc58ed0622fe95bd763edd3ee2d22792d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 09:15:52 -0400 Subject: [PATCH 0397/1169] fix argparse syntax --- src/certfuzz/tools/common/drillresults.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 69c405d..1de4e76 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -24,23 +24,25 @@ reg64_set = set(registers64) -def parse_args(): +def _build_arg_parser(): usage = "usage: %prog [options]" parser = argparse.ArgumentParser(usage) - parser.add_option('-d', '--dir', + parser.add_argument('-d', '--dir', help='directory to look for results in. Default is "results"', dest='resultsdir', default='../results', type=str) - parser.add_option('-j', '--ignorejit', dest='ignorejit', + parser.add_argument('-j', '--ignorejit', dest='ignorejit', action='store_true', help='Ignore PC in unmapped module (JIT)', - default=False, - type=bool) - parser.add_option('-f', '--force', dest='force', + default=False) + parser.add_argument('-f', '--force', dest='force', action='store_true', - help='Force recalculation of results', - type=bool) + help='Force recalculation of results') + return parser + +def parse_args(): + parser = _build_arg_parser() return parser.parse_args() From 3fcbc05cb8541310395990a3e0b7a981b9e2100d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 09:16:31 -0400 Subject: [PATCH 0398/1169] refactor out code common to carve and carve2 --- src/certfuzz/tools/common/drillresults.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 1de4e76..304de11 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -72,13 +72,12 @@ def carve2(string): delims = [("Exception Faulting Address: ", "\n"), ("si_addr:$2 = (void *)", "\n")] for token1, token2 in delims: - startindex = string.find(token1) - if startindex == -1: - # can't find token1 - continue - startindex = startindex + len(token1) - endindex = string.find(token2, startindex) - return string[startindex:endindex] + substring = carve(string, token1, token2) + if len(substring): + # returns the first matching substring + return substring + # if we got here, no match was found, just return empty string + return "" def is_number(s): From 7042ebfa51df68bc33d916c2cc406be65c7d2cc3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 09:17:10 -0400 Subject: [PATCH 0399/1169] use platform specific classes --- src/certfuzz/tools/linux/drillresults.py | 2 +- src/certfuzz/tools/windows/drillresults.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index 2bed40d..c9d8cf8 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -484,7 +484,7 @@ def _check_report(self, reportfile, crasherfile, crash_hash, cached_results): def main(): options = parse_args() - with ResultDriller(ignore_jit=options.ignorejit, + with LinuxResultDriller(ignore_jit=options.ignorejit, base_dir=options.resultsdir) as rd: rd.drill_results() diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index df122ee..1d72682 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -409,7 +409,7 @@ def pc_in_mapped_address(self, reporttext, instraddr): def main(): options = parse_args() - with ResultDriller(ignore_jit=options.ignorejit, + with WindowsResultDriller(ignore_jit=options.ignorejit, base_dir=options.resultsdir) as rd: rd.drill_results() From 3751a23234a65c85d4c6776634271ba9860367c2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 09:17:25 -0400 Subject: [PATCH 0400/1169] add unit tests for basic functions --- .../test/tools/common/test_drillresults.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/certfuzz/test/tools/common/test_drillresults.py diff --git a/src/certfuzz/test/tools/common/test_drillresults.py b/src/certfuzz/test/tools/common/test_drillresults.py new file mode 100644 index 0000000..3d1a6c7 --- /dev/null +++ b/src/certfuzz/test/tools/common/test_drillresults.py @@ -0,0 +1,82 @@ +''' +Created on Jul 1, 2014 + +@organization: cert.org +''' +import unittest +from certfuzz.tools.common import drillresults +import tempfile +import shutil +import os + + +class Test(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def test_all_registers_in_set(self): + for r in drillresults.registers: + self.assertTrue(r in drillresults.reg_set) + + def test_all_64b_registers_in_set(self): + for r in drillresults.registers64: + self.assertTrue(r in drillresults.reg64_set) + + def test_build_arg_parser(self): + p = drillresults._build_arg_parser() + + # not sure what else to test, so let's just make + # sure that the thing we get back at least has + # a parse_args method + self.assertTrue(hasattr(p, 'parse_args')) + self.assertTrue(hasattr(p.parse_args, '__call__')) + + def test_parse_args(self): + # not much to test here + pass + + def test_read_file(self): + fd, f = tempfile.mkstemp(text=True) + os.write(fd, 'fizzle') + os.close(fd) + result = drillresults.read_file(f) + self.assertEqual('fizzle', result) + os.remove(f) + + def test_carve(self): + s = 'redbluegreenyellowred' + r = drillresults.carve(s, 'red', 'red') + self.assertEqual('bluegreenyellow', r) + + r = drillresults.carve(s, 'blue', 'yellow') + self.assertEqual('green', r) + + def test_carve2(self): + # won't match + s = 'redbluegreenyellowred' + r = drillresults.carve2(s) + self.assertEqual('', r) + + # should match + for s in ['Exception Faulting Address: MATCHME\n', + 'si_addr:$2 = (void *)MATCHME\n']: + r = drillresults.carve2(s) + self.assertEqual('MATCHME', r) + + def test_is_number(self): + expect_true = ['1', '1e4', '10.1231'] + expect_false = ['abcdef', '0xfffe1010', '0b010001'] + for s in expect_true: + self.assertTrue(drillresults.is_number(s)) + for s in expect_false: + self.assertFalse(drillresults.is_number(s)) + + + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 6f943c77167dbacdcf3bb37fcdc0240db4dcad35 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 09:30:30 -0400 Subject: [PATCH 0401/1169] test metaclass behavior --- .../test/tools/common/test_drillresults.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/certfuzz/test/tools/common/test_drillresults.py b/src/certfuzz/test/tools/common/test_drillresults.py index 3d1a6c7..4389bbf 100644 --- a/src/certfuzz/test/tools/common/test_drillresults.py +++ b/src/certfuzz/test/tools/common/test_drillresults.py @@ -10,6 +10,20 @@ import os +class MockRd(drillresults.ResultDriller): + def _check_report(self): + pass + + def _platform_find_dbg_output(self): + pass + + def check_64bit(self): + pass + + def really_exploitable(self): + pass + + class Test(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() @@ -74,6 +88,8 @@ def test_is_number(self): for s in expect_false: self.assertFalse(drillresults.is_number(s)) + def test_rd_acts_as_metaclass(self): + self.assertRaises(TypeError, drillresults.ResultDriller) From b4ceacf8f371976d4e9f8f99396d94320190e382 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 09:43:22 -0400 Subject: [PATCH 0402/1169] test really_exploitable set creation --- src/certfuzz/test/tools/common/test_drillresults.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/test/tools/common/test_drillresults.py b/src/certfuzz/test/tools/common/test_drillresults.py index 4389bbf..77d7baf 100644 --- a/src/certfuzz/test/tools/common/test_drillresults.py +++ b/src/certfuzz/test/tools/common/test_drillresults.py @@ -9,8 +9,12 @@ import shutil import os +alphabet = 'abcdefghijklmnopqrstuvwxyz' class MockRd(drillresults.ResultDriller): + # really_exploitable expects a list + really_exploitable = list(alphabet) + def _check_report(self): pass @@ -20,9 +24,6 @@ def _platform_find_dbg_output(self): def check_64bit(self): pass - def really_exploitable(self): - pass - class Test(unittest.TestCase): def setUp(self): @@ -91,6 +92,10 @@ def test_is_number(self): def test_rd_acts_as_metaclass(self): self.assertRaises(TypeError, drillresults.ResultDriller) + def test_rd(self): + rd = MockRd() + for letter in alphabet: + self.assertTrue(letter in rd.re_set) if __name__ == "__main__": From 44694a1915733b65f410ec85e6e93ac06ed53b35 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 10:37:24 -0400 Subject: [PATCH 0403/1169] handle DrillResultsErrors gracefully --- src/certfuzz/tools/common/drillresults.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 304de11..0189827 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -118,7 +118,13 @@ def __enter__(self): return self def __exit__(self, etype, value, traceback): - return + handled = False + + if etype is DrillResultsError: + print "{}: {}".format(etype.__name__, value) + handled = True + + return handled @abc.abstractmethod def check_64bit(self, reporttext): From 9c55b1a8a39a730dcee1a408df08facb3ebe66a3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 10:38:12 -0400 Subject: [PATCH 0404/1169] ignore pickle files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1bd99b6..d109de3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ build/distmods/osx/installer/*.txt *.stackdump .teeproject .externalToolBuilders +*.pkl From 00bc33f9464750fb48692129cc3abdc6034da454 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 15:08:57 -0400 Subject: [PATCH 0405/1169] add TestCaseBundle class --- src/certfuzz/tools/common/drillresults.py | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 0189827..a214283 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -88,6 +88,48 @@ def is_number(s): return False +class TestCaseBundle(object): + __metaclass__ = abc.ABCMeta + + def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set): + self.dbg_outfile = dbg_outfile + self.testcase_file = testcase_file + self.crash_hash = crash_hash + self.re_set = re_set + + self.reporttext = read_file(self.dbg_outfile) + # Read in the fuzzed file + self.crasherdata = read_bin_file(self.testcase_file) + self.current_dir = os.path.dirname(self.dbg_outfile) + + self.details = {'reallyexploitable': False, + 'exceptions': {}} + self.score = 100 + self._64bit_debugger = False + + # See if we're dealing with 64-bit debugger or target app + self._check_64bit() + self._check_report() + self._score_testcase() + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + pass + + @abc.abstractmethod + def _check_64bit(self): + pass + + @abc.abstractmethod + def _check_report(self): + pass + + @abc.abstractmethod + def _score_testcase(self): + pass + class ResultDriller(object): __metaclass__ = abc.ABCMeta From 1c4ed28823b7bd8f46f21d1aebca4aed4db9e1c8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 15:09:37 -0400 Subject: [PATCH 0406/1169] option handling and convenience functions --- src/certfuzz/tools/common/drillresults.py | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index a214283..fa8dd86 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -27,6 +27,14 @@ def _build_arg_parser(): usage = "usage: %prog [options]" parser = argparse.ArgumentParser(usage) + + group = parser.add_mutually_exclusive_group() + group.add_argument('--debug', dest='debug', action='store_true', + help='Set logging to DEBUG and enable additional debuggers if available') + group.add_argument('-v', '--verbose', dest='verbose', action='store_true', + help='Set logging to INFO level') + + parser.add_argument('-d', '--dir', help='directory to look for results in. Default is "results"', dest='resultsdir', @@ -39,8 +47,29 @@ def _build_arg_parser(): parser.add_argument('-f', '--force', dest='force', action='store_true', help='Force recalculation of results') + return parser + +def root_logger_to_console(args): + root_logger = logging.getLogger() + hdlr = logging.StreamHandler() + root_logger.addHandler(hdlr) + + set_log_level(root_logger, args) + + +def set_log_level(log_obj, args): + if args.debug: + log_obj.setLevel(logging.DEBUG) + log_obj.debug('Log level = DEBUG') + elif args.verbose: + log_obj.setLevel(logging.INFO) + log_obj.info('Log level = INFO') + else: + log_obj.setLevel(logging.WARNING) + + def parse_args(): parser = _build_arg_parser() return parser.parse_args() @@ -54,6 +83,15 @@ def read_file(textfile): return f.read() +def read_bin_file(textfile): + ''' + Read binary file + ''' + f = open(textfile, 'rb') + text = f.read() + return text + + def carve(string, token1, token2): startindex = string.find(token1) if startindex == -1: From 612c239d2c0ec3950ca0e6fbcd4ffdd95d60f099 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 15:12:30 -0400 Subject: [PATCH 0407/1169] refactoring continues --- .../test/tools/common/test_drillresults.py | 2 +- src/certfuzz/tools/common/drillresults.py | 152 +++----- src/certfuzz/tools/linux/drillresults.py | 365 +++++++----------- 3 files changed, 190 insertions(+), 329 deletions(-) diff --git a/src/certfuzz/test/tools/common/test_drillresults.py b/src/certfuzz/test/tools/common/test_drillresults.py index 77d7baf..3fe52fe 100644 --- a/src/certfuzz/test/tools/common/test_drillresults.py +++ b/src/certfuzz/test/tools/common/test_drillresults.py @@ -18,7 +18,7 @@ class MockRd(drillresults.ResultDriller): def _check_report(self): pass - def _platform_find_dbg_output(self): + def _platform_find_testcases(self): pass def check_64bit(self): diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index fa8dd86..1fe8d45 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -181,13 +181,12 @@ def __init__(self, self.force = force_reload self.pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') - self.cached_results = None - self.dbg_out = [] - self.results = {} - self.crash_scores = {} + self.cached_testcases = None + self.testcase_bundles = [] +# self.results = {} +# self.crash_scores = {} self._64bit_debugger = False - self.re_set = set(self.really_exploitable) @abc.abstractproperty @@ -207,35 +206,31 @@ def __exit__(self, etype, value, traceback): return handled @abc.abstractmethod - def check_64bit(self, reporttext): - ''' - Check if the debugger and target app are 64-bit - ''' - pass - - @abc.abstractmethod - def _platform_find_dbg_output(self, crash_hash): + def _platform_find_testcases(self, crash_hash): pass - def find_dbg_output(self): + def process_testcases(self): ''' Crawls self.tld looking for crash directories to process. Puts a list - of tuples into self.dbg_out. + of tuples into self.testcase_bundles. ''' # Walk the results directory for root, dirs, files in os.walk(self.tld): + logger.debug('Looking for testcases in %s', root) dir_basename = os.path.basename(root) - self._platform_find_dbg_output(dir_basename, files, root) + self._platform_find_testcases(dir_basename, files, root) - @abc.abstractmethod - def _check_report(self, dbg_file, crash_file, crash_hash, cached_results): - pass + +# @abc.abstractmethod +# def _check_report(self, tcb): +# pass def _check_dirs(self): check_dirs = [self.base_dir, 'results', 'crashers'] for d in check_dirs: if os.path.isdir(d): self.tld = d + logger.debug('found dir: %s', self.tld) return # if you got here, none of them exist raise DrillResultsError('None of {} appears to be a dir'.format(check_dirs)) @@ -246,88 +241,32 @@ def load_cached(self): return try: - with open(self.pkl_filename, 'rb') as pkl_file: - self.cached_results = pickle.load(pkl_file) + with open(self.pickle_file, 'rb') as pkl_file: + self.cached_testcases = pickle.load(pkl_file) except IOError: # No cached results pass - def check_reports(self): - for dbg_file, crash_file, crash_hash in self.dbg_out: - self._check_report(dbg_file, crash_file, crash_hash, self.cached_results) - - def _score_crasher(self, details): - scores = [100] - if details['reallyexploitable'] == True: - # The crash summary is a very interesting one - for exception in details['exceptions']: - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignorejit: - # EIP is not in a loaded module - scores.append(20) - if details['exceptions'][exception]['shortdesc'] in self.re_set: - efa = '0x' + details['exceptions'][exception]['efa'] - if details['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - scores.append(30) - elif '0x0000' in efa: - # Faulting address is somewhat near null - scores.append(20) - elif '0xffff' in efa: - # Faulting address is likely a negative number - scores.append(20) - else: - # Faulting address has high entropy. Most exploitable. - scores.append(10) - else: - # The faulting address pattern is not in the fuzzed file - scores.append(40) - else: - # The crash summary isn't necessarily interesting - for exception in details['exceptions']: - efa = '0x' + details['exceptions'][exception]['efa'] - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignorejit: - scores.append(20) - elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): - # likely heap corruption. Exploitable, but difficult - scores.append(45) - elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: - # non-continued potential stack buffer overflow - scores.append(40) - elif details['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - scores.append(70) - elif '0x0000' in efa: - # Faulting address is somewhat near null - scores.append(60) - elif '0xffff' in efa: - # Faulting address is likely a negative number - scores.append(60) - else: - # Faulting address has high entropy. - scores.append(50) - return min(scores) - - def score_reports(self): - # Assign a ranking to each crash report. The lower the rank, the higher - # the exploitability - if self.results: - print "--- Interesting crashes ---\n" - # For each of the crash ids in the results dictionary, apply ranking - for crash_key, crash_details in self.results.iteritems(): - try: - self.crash_scores[crash_key] = self._score_crasher(crash_details) - except KeyError: - print "Error scoring crash %s" % crash_key - continue - - def print_crash_report(self, crash_key, score): - details = self.results[crash_key] +# def score_testcases(self): +# # Assign a ranking to each crash report. The lower the rank, the higher +# # the exploitability +# # For each of the crash ids in the results dictionary, apply ranking +# for tcb in self.testcase_bundles: +# testcase_key = tcb.crash_hash +# testcase_details = tcb.details +# logger.debug('Scoring testcase: %s', testcase_key) +# try: +# self.crash_scores[testcase_key] = self._score_crasher(testcase_details) +# except KeyError: +# logger.warning("Error scoring crash %s", testcase_key) +# continue + + @property + def crash_scores(self): + return dict([(tcb.crash_hash, tcb.score) for tcb in self.testcase_bundles]) + + def print_crash_report(self, crash_key, score, details): +# details = self.results[crash_key] print '\n%s - Exploitability rank: %s' % (crash_key, score) print 'Fuzzed file: %s' % details['fuzzedfile'] for exception in details['exceptions']: @@ -351,21 +290,26 @@ def sorted_crashes(self): return sorted(self.crash_scores.iteritems(), key=lambda(k, v): (v, k)) def print_reports(self): + results = dict([(tcb.crash_hash, tcb.details) for tcb in self.testcase_bundles]) + print "--- Interesting crashes ---\n" for crash_key, score in self.sorted_crashes: - self.print_crash_report(crash_key, score) + details = results[crash_key] + try: + self.print_crash_report(crash_key, score, details) + except KeyError as e: + logger.warning('Tescase %s is missing information: %s', crash_key, e) def cache_results(self): - pkldir = os.path.dirname(self.pkl_filename) + pkldir = os.path.dirname(self.pickle_file) if not os.path.exists(pkldir): os.makedirs(pkldir) - with open(self.pkl_filename, 'wb') as pkl_file: - pickle.dump(self.results, pkl_file, -1) + with open(self.pickle_file, 'wb') as pkl_file: + pickle.dump(self.testcase_bundles, pkl_file, -1) def drill_results(self): + logger.debug('drill_results') self._check_dirs() self.load_cached() - self.find_dbg_output() - self.check_reports() - self.score_reports() + self.process_testcases() self.print_reports() self.cache_results() diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index c9d8cf8..c922421 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -7,13 +7,19 @@ import binascii import re +import sys +import logging +sys.path.insert(0, '/Users/adh/git/bff/src') + from certfuzz.tools.common.drillresults import read_file, carve, carve2, \ - reg_set, ResultDriller, parse_args + reg_set, ResultDriller, parse_args, TestCaseBundle, set_log_level, \ + root_logger_to_console +logger = logging.getLogger(__name__) regex = { 'gdb_report': re.compile(r'.+.gdb$'), -# 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), + 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), 'frame0': re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+'), 'regs1': re.compile(r'^eax=.+'), 'regs2': re.compile(r'^eip=.+'), @@ -29,38 +35,6 @@ } -# These !exploitable short descriptions indicate a very interesting crash -really_exploitable = [ - 'SegFaultOnPc', - 'BranchAv', - 'StackCodeExection', - 'BadInstruction', - 'ReturnAv', - ] -re_set = set(really_exploitable) - -results = {} -scoredcrashes = {} - - -def check_64bit(reporttext): - ''' - Check if the debugger and target app are 64-bit - ''' - # TODO: When would this ever happen? -# if _64bit_debugger: -# return - - for line in reporttext.splitlines(): - m = re.match(regex['bt_addr'], line) - if m: - start_addr = m.group(1) - #print '%s length: %s'% (start_addr, len(start_addr)) - if len(start_addr) > 10: - return True - return False - - def pc_in_mapped_address(reporttext, instraddr): ''' Check if the instruction pointer is in a loaded module @@ -111,15 +85,6 @@ def fix_efa_bug(reporttext, instraddr, faultaddr): return faultaddr -def read_bin_file(textfile): - ''' - Read binary file - ''' - f = open(textfile, 'rb') - text = f.read() - return text - - def get_ex_num(reporttext): ''' Get the exception number by counting the number of continues @@ -161,9 +126,9 @@ def get_instr(reporttext, instraddr): ''' Find the disassembly line for the current (crashing) instruction ''' - regex = re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)') + rgx = regex['current_instr'] for line in reporttext.splitlines(): - n = regex.match(line) + n = rgx.match(line) if n: return n.group(3) return '' @@ -231,172 +196,26 @@ def fix_efa_offset(instructionline, faultaddr, _64bit_debugger): return faultaddr -#def check_report(reportfile, crasherfile, crash_hash, cached_results): -# ''' -# Parse the gdb file -# ''' -# -## global _64bit_debugger -# -# if cached_results: -# if cached_results.get(crash_hash): -# results[crash_hash] = cached_results[crash_hash] -# return -# -# -# #print('checking %s against %s: %s' % (reportfile, crasherfile, crash_hash)) -# crashid = results[crash_hash] -# -# reporttext = read_file(reportfile) -# current_dir = os.path.dirname(reportfile) -# exceptionnum = 0 -# classification = carve(reporttext, "Classification: ", "\n") -# #print 'classification: %s' % classification -# try: -# if classification: -# # Create a new exception dictionary to add to the crash -# exception = {} -# crashid['exceptions'][exceptionnum] = exception -# except KeyError: -# # Crash ID (crash_hash) not yet seen -# # Default it to not being "really exploitable" -# crashid['reallyexploitable'] = False -# # Create a dictionary of exceptions for the crash id -# exceptions = {} -# crashid['exceptions'] = exceptions -# # Create a dictionary for the exception -# crashid['exceptions'][exceptionnum] = exception -# -# # Set !exploitable classification for the exception -# if classification: -# crashid['exceptions'][exceptionnum]['classification'] = classification -# -# shortdesc = carve(reporttext, "Short description: ", " (") -# #print 'shortdesc: %s' % shortdesc -# if shortdesc: -# # Set !exploitable Short Description for the exception -# crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc -# # Flag the entire crash ID as really exploitable if this is a good -# # exception -# crashid['reallyexploitable'] = shortdesc in re_set -# -# if not os.path.isfile(crasherfile): -# # Can't find the crasher file -# #print "WTF! Cannot find %s" % crasherfile -# return -# # Set the "fuzzedfile" property for the crash ID -# crashid['fuzzedfile'] = crasherfile -# # See if we're dealing with 64-bit debugger or target app -# _64bit_debugger = check_64bit(reporttext) -# faultaddr = carve2(reporttext) -# instraddr = get_instr_addr(reporttext) -# faultaddr = format_addr(faultaddr, _64bit_debugger) -# instraddr = format_addr(instraddr, _64bit_debugger) -# -# # No faulting address means no crash. -# if not faultaddr: -# return -# -# if instraddr: -# crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr) -# -# # Get the cdb line that contains the crashing instruction -# instructionline = get_instr(reporttext, instraddr) -# crashid['exceptions'][exceptionnum]['instructionline'] = instructionline -# if instructionline: -# faultaddr = fix_efa_offset(instructionline, faultaddr, _64bit_debugger) -# -# # Fix faulting pattern endian -# faultaddr = faultaddr.replace('0x', '') -# crashid['exceptions'][exceptionnum]['efa'] = faultaddr -# if _64bit_debugger: -# # 64-bit target app -# faultaddr = faultaddr.zfill(16) -# efaptr = struct.unpack(' 10: + self._64bit_debugger = True + logger.debug() + + def _score_testcase(self): + logger.debug('Scoring testcase: %s', self.crash_hash) + details = self.details + scores = [100] + if details['reallyexploitable'] == True: + # The crash summary is a very interesting one + for exception in details['exceptions']: + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not self.ignorejit: + # EIP is not in a loaded module + scores.append(20) + if details['exceptions'][exception]['shortdesc'] in self.re_set: + efa = '0x' + details['exceptions'][exception]['efa'] + if details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(30) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(20) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(20) + else: + # Faulting address has high entropy. Most exploitable. + scores.append(10) + else: + # The faulting address pattern is not in the fuzzed file + scores.append(40) else: - crashid['exceptions'][exceptionnum]['EIF'] = False + # The crash summary isn't necessarily interesting + for exception in details['exceptions']: + efa = '0x' + details['exceptions'][exception]['efa'] + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not self.ignorejit: + scores.append(20) + elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): + # likely heap corruption. Exploitable, but difficult + scores.append(45) + elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: + # non-continued potential stack buffer overflow + scores.append(40) + elif details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(70) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(60) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(60) + else: + # Faulting address has high entropy. + scores.append(50) + self.score = min(scores) + + +class LinuxResultDriller(ResultDriller): + really_exploitable = [ + 'SegFaultOnPc', + 'BranchAv', + 'StackCodeExection', + 'BadInstruction', + 'ReturnAv', + ] + + def _platform_find_testcases(self, crash_hash, files, root): + # Only use directories that are hashes + # if "0x" in crash_hash: + # Create dictionary for hashes in results dictionary + crasherfile = '' + # Check each of the files in the hash directory + for current_file in files: + # Go through all of the .gdb files and parse them + if current_file.endswith('.gdb'): +# if regex['gdb_report'].match(current_file): + #print 'checking %s' % current_file + gdbfile = os.path.join(root, current_file) + logger.debug('found gdb file: %s', gdbfile) + crasherfile = gdbfile.replace('.gdb', '') + #crasherfile = os.path.join(root, crasherfile) + tcb = LinuxTestCaseBundle(gdbfile, crasherfile, crash_hash, self.re_set) + self.testcase_bundles.append(tcb) def main(): - options = parse_args() - with LinuxResultDriller(ignore_jit=options.ignorejit, - base_dir=options.resultsdir) as rd: + args = parse_args() + + root_logger_to_console(args) + + with LinuxResultDriller(ignore_jit=args.ignorejit, + base_dir=args.resultsdir, + force_reload=args.force) as rd: rd.drill_results() From e17db3dd8a3a833fc25fa8f8593e14ed8fe3612f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 15:42:26 -0400 Subject: [PATCH 0408/1169] relocate file read methods to certfuzz.fuzztools.filetools --- src/certfuzz/file_handlers/basicfile.py | 5 ++-- src/certfuzz/fuzztools/filetools.py | 25 +++++++++++++++++++ .../test/tools/common/test_drillresults.py | 2 +- src/certfuzz/tools/common/drillresults.py | 21 +++------------- src/certfuzz/tools/linux/drillresults.py | 4 +-- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/certfuzz/file_handlers/basicfile.py b/src/certfuzz/file_handlers/basicfile.py index 3b122dc..de3a4b5 100644 --- a/src/certfuzz/file_handlers/basicfile.py +++ b/src/certfuzz/file_handlers/basicfile.py @@ -6,7 +6,7 @@ import hashlib import os -from certfuzz.fuzztools.filetools import check_zip_content +from certfuzz.fuzztools.filetools import check_zip_content, read_bin_file class BasicFile(object): @@ -38,8 +38,7 @@ def read(self): ''' Returns the contents of the file. ''' - with open(self.path, 'rb') as fp: - return fp.read() + return read_bin_file(self.path) def exists(self): return os.path.exists(self.path) diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 1c80997..1c537e9 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -333,6 +333,31 @@ def make_writable(filename): os.chmod(filename, mode | stat.S_IWRITE) +def _read_file(path, perm): + ''' + Generic file read + :param path: + :param perm: + ''' + with open(path, perm) as f: + return f.read() + + +def read_text_file(textfile): + ''' + Read text file + ''' + return _read_file(textfile, 'r') + + +def read_bin_file(binfile): + ''' + Read binary file + ''' + return _read_file(binfile, 'rb') + + + # Adapted from Python Cookbook 2nd Ed. p.88 def all_files(root, patterns='*', single_level=False, yield_folders=False): # Expand patterns from semicolon-separated string to list diff --git a/src/certfuzz/test/tools/common/test_drillresults.py b/src/certfuzz/test/tools/common/test_drillresults.py index 3fe52fe..3ee1b58 100644 --- a/src/certfuzz/test/tools/common/test_drillresults.py +++ b/src/certfuzz/test/tools/common/test_drillresults.py @@ -57,7 +57,7 @@ def test_read_file(self): fd, f = tempfile.mkstemp(text=True) os.write(fd, 'fizzle') os.close(fd) - result = drillresults.read_file(f) + result = drillresults.read_text_file(f) self.assertEqual('fizzle', result) os.remove(f) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 1fe8d45..c09a0a9 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -9,6 +9,7 @@ import logging import argparse from certfuzz.tools.common.errors import DrillResultsError +from certfuzz.fuzztools.filetools import read_text_file, read_bin_file logger = logging.getLogger(__name__) @@ -75,23 +76,6 @@ def parse_args(): return parser.parse_args() -def read_file(textfile): - ''' - Read text file - ''' - with open(textfile, 'r') as f: - return f.read() - - -def read_bin_file(textfile): - ''' - Read binary file - ''' - f = open(textfile, 'rb') - text = f.read() - return text - - def carve(string, token1, token2): startindex = string.find(token1) if startindex == -1: @@ -135,7 +119,7 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set): self.crash_hash = crash_hash self.re_set = re_set - self.reporttext = read_file(self.dbg_outfile) + self.reporttext = read_text_file(self.dbg_outfile) # Read in the fuzzed file self.crasherdata = read_bin_file(self.testcase_file) self.current_dir = os.path.dirname(self.dbg_outfile) @@ -168,6 +152,7 @@ def _check_report(self): def _score_testcase(self): pass + class ResultDriller(object): __metaclass__ = abc.ABCMeta diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index c922421..bf60c22 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -11,8 +11,8 @@ import logging sys.path.insert(0, '/Users/adh/git/bff/src') -from certfuzz.tools.common.drillresults import read_file, carve, carve2, \ - reg_set, ResultDriller, parse_args, TestCaseBundle, set_log_level, \ +from certfuzz.tools.common.drillresults import carve, carve2, \ + reg_set, ResultDriller, parse_args, TestCaseBundle, \ root_logger_to_console logger = logging.getLogger(__name__) From 5b05e4500bf9eaac225a6c21f90d3b11f64e9ba2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 15:53:57 -0400 Subject: [PATCH 0409/1169] pass ignore_jit in from ResultDriller to TestCaseBundle --- src/certfuzz/tools/common/drillresults.py | 22 ++-------------------- src/certfuzz/tools/linux/drillresults.py | 3 ++- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index c09a0a9..5d6d4e7 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -113,11 +113,12 @@ def is_number(s): class TestCaseBundle(object): __metaclass__ = abc.ABCMeta - def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set): + def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, ignore_jit=False): self.dbg_outfile = dbg_outfile self.testcase_file = testcase_file self.crash_hash = crash_hash self.re_set = re_set + self.ignore_jit = ignore_jit self.reporttext = read_text_file(self.dbg_outfile) # Read in the fuzzed file @@ -205,11 +206,6 @@ def process_testcases(self): dir_basename = os.path.basename(root) self._platform_find_testcases(dir_basename, files, root) - -# @abc.abstractmethod -# def _check_report(self, tcb): -# pass - def _check_dirs(self): check_dirs = [self.base_dir, 'results', 'crashers'] for d in check_dirs: @@ -232,20 +228,6 @@ def load_cached(self): # No cached results pass -# def score_testcases(self): -# # Assign a ranking to each crash report. The lower the rank, the higher -# # the exploitability -# # For each of the crash ids in the results dictionary, apply ranking -# for tcb in self.testcase_bundles: -# testcase_key = tcb.crash_hash -# testcase_details = tcb.details -# logger.debug('Scoring testcase: %s', testcase_key) -# try: -# self.crash_scores[testcase_key] = self._score_crasher(testcase_details) -# except KeyError: -# logger.warning("Error scoring crash %s", testcase_key) -# continue - @property def crash_scores(self): return dict([(tcb.crash_hash, tcb.score) for tcb in self.testcase_bundles]) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index bf60c22..ee7748f 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -391,7 +391,8 @@ def _platform_find_testcases(self, crash_hash, files, root): logger.debug('found gdb file: %s', gdbfile) crasherfile = gdbfile.replace('.gdb', '') #crasherfile = os.path.join(root, crasherfile) - tcb = LinuxTestCaseBundle(gdbfile, crasherfile, crash_hash, self.re_set) + tcb = LinuxTestCaseBundle(gdbfile, crasherfile, crash_hash, + self.re_set, self.ignore_jit) self.testcase_bundles.append(tcb) From 048a28d5fa8df51aecfd08ab789b6b85269e8aca Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 16:06:05 -0400 Subject: [PATCH 0410/1169] be consistent with ignore_jit instead of ignorejit --- src/certfuzz/tools/common/drillresults.py | 3 +-- src/certfuzz/tools/linux/drillresults.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 5d6d4e7..0b383fa 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -35,13 +35,12 @@ def _build_arg_parser(): group.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='Set logging to INFO level') - parser.add_argument('-d', '--dir', help='directory to look for results in. Default is "results"', dest='resultsdir', default='../results', type=str) - parser.add_argument('-j', '--ignorejit', dest='ignorejit', + parser.add_argument('-j', '--ignore-jit', dest='ignore_jit', action='store_true', help='Ignore PC in unmapped module (JIT)', default=False) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index ee7748f..271144a 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -315,7 +315,7 @@ def _score_testcase(self): # The crash summary is a very interesting one for exception in details['exceptions']: module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignorejit: + if module == 'unloaded' and not self.ignore_jit: # EIP is not in a loaded module scores.append(20) if details['exceptions'][exception]['shortdesc'] in self.re_set: @@ -342,7 +342,7 @@ def _score_testcase(self): for exception in details['exceptions']: efa = '0x' + details['exceptions'][exception]['efa'] module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignorejit: + if module == 'unloaded' and not self.ignore_jit: scores.append(20) elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): # likely heap corruption. Exploitable, but difficult @@ -401,7 +401,7 @@ def main(): root_logger_to_console(args) - with LinuxResultDriller(ignore_jit=args.ignorejit, + with LinuxResultDriller(ignore_jit=args.ignore_jit, base_dir=args.resultsdir, force_reload=args.force) as rd: rd.drill_results() From 29e106df57a152a84bfb61554f5123a26500c471 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 16:06:29 -0400 Subject: [PATCH 0411/1169] rename gdbfile -> dbg_file --- src/certfuzz/tools/linux/drillresults.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index 271144a..298f410 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -387,11 +387,11 @@ def _platform_find_testcases(self, crash_hash, files, root): if current_file.endswith('.gdb'): # if regex['gdb_report'].match(current_file): #print 'checking %s' % current_file - gdbfile = os.path.join(root, current_file) - logger.debug('found gdb file: %s', gdbfile) - crasherfile = gdbfile.replace('.gdb', '') + dbg_file = os.path.join(root, current_file) + logger.debug('found gdb file: %s', dbg_file) + crasherfile = dbg_file.replace('.gdb', '') #crasherfile = os.path.join(root, crasherfile) - tcb = LinuxTestCaseBundle(gdbfile, crasherfile, crash_hash, + tcb = LinuxTestCaseBundle(dbg_file, crasherfile, crash_hash, self.re_set, self.ignore_jit) self.testcase_bundles.append(tcb) From 46d59c36be756595aa2b5018a061ff384a604b3d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 16:17:31 -0400 Subject: [PATCH 0412/1169] skip test case dirs that don't have the files we need --- src/certfuzz/tools/common/drillresults.py | 16 ++++++++++++++-- src/certfuzz/tools/common/errors.py | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 0b383fa..798e4bc 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -8,7 +8,7 @@ import abc import logging import argparse -from certfuzz.tools.common.errors import DrillResultsError +from certfuzz.tools.common.errors import DrillResultsError, TestCaseBundleError from certfuzz.fuzztools.filetools import read_text_file, read_bin_file logger = logging.getLogger(__name__) @@ -115,6 +115,9 @@ class TestCaseBundle(object): def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, ignore_jit=False): self.dbg_outfile = dbg_outfile self.testcase_file = testcase_file + + self._verify_files_exist() + self.crash_hash = crash_hash self.re_set = re_set self.ignore_jit = ignore_jit @@ -134,6 +137,11 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, ignore_jit=Fa self._check_report() self._score_testcase() + def _verify_files_exist(self): + for f in [self.dbg_outfile, self.testcase_file]: + if not os.path.exists(f): + raise TestCaseBundleError('File not found: {}'.format(f)) + def __enter__(self): return self @@ -203,7 +211,11 @@ def process_testcases(self): for root, dirs, files in os.walk(self.tld): logger.debug('Looking for testcases in %s', root) dir_basename = os.path.basename(root) - self._platform_find_testcases(dir_basename, files, root) + try: + self._platform_find_testcases(dir_basename, files, root) + except TestCaseBundleError as e: + logger.warning('Skipping %s: %s', dir_basename, e) + continue def _check_dirs(self): check_dirs = [self.base_dir, 'results', 'crashers'] diff --git a/src/certfuzz/tools/common/errors.py b/src/certfuzz/tools/common/errors.py index f5b7286..750c823 100644 --- a/src/certfuzz/tools/common/errors.py +++ b/src/certfuzz/tools/common/errors.py @@ -8,3 +8,7 @@ class DrillResultsError(CERTFuzzToolError): pass + + +class TestCaseBundleError(DrillResultsError): + pass From 8c7843013341048d8894bb628b6458efbf8043ce Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 1 Jul 2014 16:21:07 -0400 Subject: [PATCH 0413/1169] add debug log, remove dead code --- src/certfuzz/tools/common/drillresults.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 798e4bc..ec5778c 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -141,6 +141,8 @@ def _verify_files_exist(self): for f in [self.dbg_outfile, self.testcase_file]: if not os.path.exists(f): raise TestCaseBundleError('File not found: {}'.format(f)) + else: + logger.debug('Found file: %s', f) def __enter__(self): return self @@ -176,8 +178,6 @@ def __init__(self, self.pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') self.cached_testcases = None self.testcase_bundles = [] -# self.results = {} -# self.crash_scores = {} self._64bit_debugger = False self.re_set = set(self.really_exploitable) From 29d1c9dc3ed8dbb7301aa647f9fa06b3da7446f2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 08:22:40 -0400 Subject: [PATCH 0414/1169] rename method --- src/certfuzz/test/tools/common/test_drillresults.py | 2 +- src/certfuzz/tools/common/drillresults.py | 4 ++-- src/certfuzz/tools/linux/drillresults.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/test/tools/common/test_drillresults.py b/src/certfuzz/test/tools/common/test_drillresults.py index 3ee1b58..0c15d00 100644 --- a/src/certfuzz/test/tools/common/test_drillresults.py +++ b/src/certfuzz/test/tools/common/test_drillresults.py @@ -15,7 +15,7 @@ class MockRd(drillresults.ResultDriller): # really_exploitable expects a list really_exploitable = list(alphabet) - def _check_report(self): + def _parse_testcase(self): pass def _platform_find_testcases(self): diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index ec5778c..1c1d252 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -134,7 +134,7 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, ignore_jit=Fa # See if we're dealing with 64-bit debugger or target app self._check_64bit() - self._check_report() + self._parse_testcase() self._score_testcase() def _verify_files_exist(self): @@ -155,7 +155,7 @@ def _check_64bit(self): pass @abc.abstractmethod - def _check_report(self): + def _parse_testcase(self): pass @abc.abstractmethod diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index 298f410..ab6f8b0 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -197,7 +197,7 @@ def fix_efa_offset(instructionline, faultaddr, _64bit_debugger): class LinuxTestCaseBundle(TestCaseBundle): - def _check_report(self): + def _parse_testcase(self): ''' Parse the gdb file ''' From 83de2fc48aab80b4c9feab7e4ab196b97ece59e6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 08:23:30 -0400 Subject: [PATCH 0415/1169] move scoring to common class --- src/certfuzz/tools/common/drillresults.py | 60 ++++++++++++++++++++++- src/certfuzz/tools/linux/drillresults.py | 60 ----------------------- 2 files changed, 58 insertions(+), 62 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 1c1d252..d0cc9b5 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -158,9 +158,65 @@ def _check_64bit(self): def _parse_testcase(self): pass - @abc.abstractmethod def _score_testcase(self): - pass + logger.debug('Scoring testcase: %s', self.crash_hash) + details = self.details + scores = [100] + if details['reallyexploitable'] == True: + # The crash summary is a very interesting one + for exception in details['exceptions']: + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not self.ignore_jit: + # EIP is not in a loaded module + scores.append(20) + if details['exceptions'][exception]['shortdesc'] in self.re_set: + efa = '0x' + details['exceptions'][exception]['efa'] + if details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(30) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(20) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(20) + else: + # Faulting address has high entropy. Most exploitable. + scores.append(10) + else: + # The faulting address pattern is not in the fuzzed file + scores.append(40) + else: + # The crash summary isn't necessarily interesting + for exception in details['exceptions']: + efa = '0x' + details['exceptions'][exception]['efa'] + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not self.ignore_jit: + scores.append(20) + elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): + # likely heap corruption. Exploitable, but difficult + scores.append(45) + elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: + # non-continued potential stack buffer overflow + scores.append(40) + elif details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(70) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(60) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(60) + else: + # Faulting address has high entropy. + scores.append(50) + self.score = min(scores) + class ResultDriller(object): diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index ab6f8b0..a8f4c78 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -307,66 +307,6 @@ def _check_64bit(self): self._64bit_debugger = True logger.debug() - def _score_testcase(self): - logger.debug('Scoring testcase: %s', self.crash_hash) - details = self.details - scores = [100] - if details['reallyexploitable'] == True: - # The crash summary is a very interesting one - for exception in details['exceptions']: - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignore_jit: - # EIP is not in a loaded module - scores.append(20) - if details['exceptions'][exception]['shortdesc'] in self.re_set: - efa = '0x' + details['exceptions'][exception]['efa'] - if details['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - scores.append(30) - elif '0x0000' in efa: - # Faulting address is somewhat near null - scores.append(20) - elif '0xffff' in efa: - # Faulting address is likely a negative number - scores.append(20) - else: - # Faulting address has high entropy. Most exploitable. - scores.append(10) - else: - # The faulting address pattern is not in the fuzzed file - scores.append(40) - else: - # The crash summary isn't necessarily interesting - for exception in details['exceptions']: - efa = '0x' + details['exceptions'][exception]['efa'] - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignore_jit: - scores.append(20) - elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): - # likely heap corruption. Exploitable, but difficult - scores.append(45) - elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: - # non-continued potential stack buffer overflow - scores.append(40) - elif details['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - scores.append(70) - elif '0x0000' in efa: - # Faulting address is somewhat near null - scores.append(60) - elif '0xffff' in efa: - # Faulting address is likely a negative number - scores.append(60) - else: - # Faulting address has high entropy. - scores.append(50) - self.score = min(scores) - - class LinuxResultDriller(ResultDriller): really_exploitable = [ 'SegFaultOnPc', From f54d14f188e0d34c6a7c77fe246be8bb0ec01d4e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 08:23:49 -0400 Subject: [PATCH 0416/1169] relocate _check_64bit method --- src/certfuzz/tools/linux/drillresults.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index a8f4c78..188dbef 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -197,6 +197,18 @@ def fix_efa_offset(instructionline, faultaddr, _64bit_debugger): class LinuxTestCaseBundle(TestCaseBundle): + def _check_64bit(self): + ''' + Check if the debugger and target app are 64-bit + ''' + for line in self.reporttext.splitlines(): + m = re.match(regex['bt_addr'], line) + if m: + start_addr = m.group(1) + if len(start_addr) > 10: + self._64bit_debugger = True + logger.debug() + def _parse_testcase(self): ''' Parse the gdb file @@ -295,17 +307,6 @@ def _parse_testcase(self): else: details['exceptions'][exceptionnum]['EIF'] = False - def _check_64bit(self): - ''' - Check if the debugger and target app are 64-bit - ''' - for line in self.reporttext.splitlines(): - m = re.match(regex['bt_addr'], line) - if m: - start_addr = m.group(1) - if len(start_addr) > 10: - self._64bit_debugger = True - logger.debug() class LinuxResultDriller(ResultDriller): really_exploitable = [ From 9ce7d0cf1402fca55e0d22c668549e8efe0c09e8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 08:31:04 -0400 Subject: [PATCH 0417/1169] alphabetize regexes --- src/certfuzz/tools/linux/drillresults.py | 15 +++++++-------- src/certfuzz/tools/windows/drillresults.py | 7 +++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index 188dbef..b5d2b48 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -18,20 +18,19 @@ logger = logging.getLogger(__name__) regex = { - 'gdb_report': re.compile(r'.+.gdb$'), + '64bit_debugger': re.compile(r'^Microsoft.*AMD64$'), + 'bt_addr': re.compile(r'(0x[0-9a-fA-F]+)\s+.+$'), 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), + 'dbg_prompt': re.compile(r'^[0-9]:[0-9][0-9][0-9]> (.*)'), 'frame0': re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+'), + 'gdb_report': re.compile(r'.+.gdb$'), + 'mapped_address': re.compile(r'^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), + 'mapped_address64': re.compile(r'^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), + 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)'), 'regs1': re.compile(r'^eax=.+'), 'regs2': re.compile(r'^eip=.+'), - 'bt_addr': re.compile(r'(0x[0-9a-fA-F]+)\s+.+$'), - '64bit_debugger': re.compile(r'^Microsoft.*AMD64$'), 'syswow64': re.compile(r'ModLoad:.*syswow64.*', re.IGNORECASE), - 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)'), 'vdso': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+\[vdso\]'), - 'mapped_address': re.compile(r'^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), - 'mapped_address64': re.compile(r'^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), - 'syswow64': re.compile(r'ModLoad:.*syswow64.*', re.IGNORECASE), - 'dbg_prompt': re.compile(r'^[0-9]:[0-9][0-9][0-9]> (.*)'), } diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 1d72682..b6b23c0 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -13,14 +13,13 @@ is_number, reg_set, reg64_set, ResultDriller, parse_args regex = { + '64bit_debugger': re.compile('^Microsoft.*AMD64$'), 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]'), + 'mapped_address': re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), + 'mapped_address64': re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), 'msec_report': re.compile('.+.msec$'), 'regs1': re.compile('^eax=.+'), 'regs2': re.compile('^eip=.+'), - '64bit_debugger': re.compile('^Microsoft.*AMD64$'), - 'syswow64': re.compile('ModLoad:.*syswow64.*', re.IGNORECASE), - 'mapped_address': re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), - 'mapped_address64': re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), 'syswow64': re.compile('ModLoad:.*syswow64.*', re.IGNORECASE), } From 009ccf9a3772ecb7b42a8993ccc04ee92d6c579c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 09:19:57 -0400 Subject: [PATCH 0418/1169] reorganize imports --- src/certfuzz/tools/common/drillresults.py | 17 ++++++++++++----- src/certfuzz/tools/linux/drillresults.py | 17 +++++++++-------- src/certfuzz/tools/windows/drillresults.py | 18 ++++++++++++++---- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index d0cc9b5..7100efe 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -3,13 +3,20 @@ @organization: cert.org ''' -import os -import cPickle as pickle +import StringIO import abc -import logging import argparse -from certfuzz.tools.common.errors import DrillResultsError, TestCaseBundleError -from certfuzz.fuzztools.filetools import read_text_file, read_bin_file +import logging +import os +import zipfile + +from certfuzz.fuzztools.filetools import read_bin_file as _read_bin_file +from certfuzz.fuzztools.filetools import read_text_file + +import cPickle as pickle +from certfuzz.tools.common.errors import DrillResultsError +from certfuzz.tools.common.errors import TestCaseBundleError + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index b5d2b48..f2c730c 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -2,18 +2,19 @@ This script looks for interesting crashes and rate them by potential exploitability ''' -import os -import struct import binascii +import logging +import os import re +import struct -import sys -import logging -sys.path.insert(0, '/Users/adh/git/bff/src') +from certfuzz.tools.common.drillresults import ResultDriller +from certfuzz.tools.common.drillresults import TestCaseBundle +from certfuzz.tools.common.drillresults import carve +from certfuzz.tools.common.drillresults import carve2 +from certfuzz.tools.common.drillresults import main as _main +from certfuzz.tools.common.drillresults import reg_set -from certfuzz.tools.common.drillresults import carve, carve2, \ - reg_set, ResultDriller, parse_args, TestCaseBundle, \ - root_logger_to_console logger = logging.getLogger(__name__) diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index b6b23c0..797ec0c 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -2,12 +2,22 @@ This script looks for interesting crashes and rate them by potential exploitability ''' -import os -import struct import binascii +import logging +import os import re -import StringIO -import zipfile +import struct + +from certfuzz.tools.common.drillresults import ResultDriller +from certfuzz.tools.common.drillresults import TestCaseBundle +from certfuzz.tools.common.drillresults import carve +from certfuzz.tools.common.drillresults import carve2 +from certfuzz.tools.common.drillresults import is_number +from certfuzz.tools.common.drillresults import main as _main +from certfuzz.tools.common.drillresults import read_bin_file +from certfuzz.tools.common.drillresults import reg64_set +from certfuzz.tools.common.drillresults import reg_set + from certfuzz.tools.common.drillresults import read_file, carve, carve2, \ is_number, reg_set, reg64_set, ResultDriller, parse_args From e97cdcfb7dcf11159b7c80f5d151f527d71902ec Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 09:21:12 -0400 Subject: [PATCH 0419/1169] add report_all argument, make default print scores <=70 --- src/certfuzz/tools/common/drillresults.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 7100efe..554465d 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -54,6 +54,9 @@ def _build_arg_parser(): parser.add_argument('-f', '--force', dest='force', action='store_true', help='Force recalculation of results') + parser.add_argument('-a', '--all', dest='report_all', + help='Report all scores (default is to only print if <=70)', + default=False) return parser @@ -225,18 +228,24 @@ def _score_testcase(self): self.score = min(scores) - class ResultDriller(object): __metaclass__ = abc.ABCMeta def __init__(self, ignore_jit=False, base_dir='../results', - force_reload=False): + force_reload=False, + report_all=False): self.ignore_jit = ignore_jit self.base_dir = base_dir self.tld = None self.force = force_reload + self.report_all = report_all + + if report_all: + self.max_score = None + else: + self.max_score = 70 self.pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') self.cached_testcases = None @@ -334,6 +343,11 @@ def print_reports(self): results = dict([(tcb.crash_hash, tcb.details) for tcb in self.testcase_bundles]) print "--- Interesting crashes ---\n" for crash_key, score in self.sorted_crashes: + if self.max_score is not None: + if score > self.max_score: + # skip test cases with scores above our max + continue + details = results[crash_key] try: self.print_crash_report(crash_key, score, details) From 38f81d41e285ac24fccad34ed5235ee306a11713 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 09:23:14 -0400 Subject: [PATCH 0420/1169] move file readers to common module --- src/certfuzz/tools/common/drillresults.py | 47 ++++++++++++++++++++++ src/certfuzz/tools/windows/drillresults.py | 29 ------------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 554465d..20d040e 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -118,6 +118,53 @@ def is_number(s): except ValueError: return False +def _read_zip(raw_file_byte_string): + ''' + If the bytes in raw_file_byte_string look like a zip file, + attempt to decompress it and return the concatenated contents of the + decompressed zip + :param raw_file_byte_string: + :return string of bytes + ''' + zbytes = str() + + # For zip files, return the uncompressed bytes + file_like_content = StringIO.StringIO(raw_file_byte_string) + if zipfile.is_zipfile(file_like_content): + # Make sure that it's not an embedded zip + # (e.g. a DOC file from Office 2007) + file_like_content.seek(0) + zipmagic = file_like_content.read(2) + if zipmagic == 'PK': + try: + # The file begins with the PK header + z = zipfile.ZipFile(file_like_content, 'r') + for filename in z.namelist(): + try: + zbytes += z.read(filename) + except: + pass + except: + # If the zip container is fuzzed we may get here + pass + file_like_content.close() + return zbytes + + +def read_bin_file(inputfile): + ''' + Read binary file + ''' + filebytes = _read_bin_file(inputfile) + + #append decommpressed zip bytes + zipbytes = _read_zip(filebytes) + + # _read_zip returns an empty string on failure, so we can safely + # append its result here + return filebytes + zipbytes + + class TestCaseBundle(object): __metaclass__ = abc.ABCMeta diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 797ec0c..789230a 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -72,35 +72,6 @@ def fix_efa_bug(reporttext, instraddr, faultaddr): return faultaddr -def read_bin_file(inputfile): - ''' - Read binary file - ''' - f = open(inputfile, 'rb') - filebytes = f.read() - # For zip files, return the uncompressed bytes - file_like_content = StringIO.StringIO(filebytes) - if zipfile.is_zipfile(file_like_content): - # Make sure that it's not an embedded zip - # (e.g. a DOC file from Office 2007) - file_like_content.seek(0) - zipmagic = file_like_content.read(2) - if zipmagic == 'PK': - try: - # The file begins with the PK header - z = zipfile.ZipFile(file_like_content, 'r') - for filename in z.namelist(): - try: - filebytes += z.read(filename) - except: - pass - except: - # If the zip container is fuzzed we may get here - pass - file_like_content.close() - f.close - return filebytes - def get_ex_num(reporttext, wow64_app): ''' From 2090cb591a80da4a32641ed45f3f0516448fb0c9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 09:27:39 -0400 Subject: [PATCH 0421/1169] continue refactoring of drillresults windows move really_exploitable into TestCaseBundle from ResultDriller consolidate main methods --- src/certfuzz/tools/common/drillresults.py | 35 +- src/certfuzz/tools/linux/drillresults.py | 29 +- src/certfuzz/tools/windows/drillresults.py | 386 ++++++++++----------- 3 files changed, 221 insertions(+), 229 deletions(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index 20d040e..f7a0e8f 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -169,16 +169,17 @@ def read_bin_file(inputfile): class TestCaseBundle(object): __metaclass__ = abc.ABCMeta - def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, ignore_jit=False): + def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self.dbg_outfile = dbg_outfile self.testcase_file = testcase_file self._verify_files_exist() self.crash_hash = crash_hash - self.re_set = re_set self.ignore_jit = ignore_jit + self.re_set = set(self.really_exploitable) + self.reporttext = read_text_file(self.dbg_outfile) # Read in the fuzzed file self.crasherdata = read_bin_file(self.testcase_file) @@ -194,6 +195,10 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, ignore_jit=Fa self._parse_testcase() self._score_testcase() + @abc.abstractproperty + def really_exploitable(self): + return [] + def _verify_files_exist(self): for f in [self.dbg_outfile, self.testcase_file]: if not os.path.exists(f): @@ -298,13 +303,6 @@ def __init__(self, self.cached_testcases = None self.testcase_bundles = [] - self._64bit_debugger = False - self.re_set = set(self.really_exploitable) - - @abc.abstractproperty - def really_exploitable(self): - return [] - def __enter__(self): return self @@ -377,7 +375,7 @@ def print_crash_report(self, crash_key, score, details): print details['exceptions'][exception]['instructionline'] module = details['exceptions'][exception]['pcmodule'] if module == 'unloaded': - if not self.ignorejit: + if not self.ignore_jit: print 'Instruction pointer is not in a loaded module!' else: print 'Code executing in: %s' % module @@ -415,3 +413,20 @@ def drill_results(self): self.process_testcases() self.print_reports() self.cache_results() + + +def main(driller_class=ResultDriller): + ''' + Main method for drill results script. Platform-specific customizations are + passed in via the driller_class argument (which must be implemented elsewhere) + :param driller_class: + ''' + args = parse_args() + root_logger_to_console(args) + with driller_class(ignore_jit=args.ignorejit, + base_dir=args.resultsdir, + force_reload=args.force, + report_all=args.report_all) as rd: + rd.drill_results() + + diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index f2c730c..aebce73 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -197,6 +197,15 @@ def fix_efa_offset(instructionline, faultaddr, _64bit_debugger): class LinuxTestCaseBundle(TestCaseBundle): + really_exploitable = [ + 'SegFaultOnPc', + 'BranchAv', + 'StackCodeExection', + 'BadInstruction', + 'ReturnAv', + ] + + def _check_64bit(self): ''' Check if the debugger and target app are 64-bit @@ -309,14 +318,6 @@ def _parse_testcase(self): class LinuxResultDriller(ResultDriller): - really_exploitable = [ - 'SegFaultOnPc', - 'BranchAv', - 'StackCodeExection', - 'BadInstruction', - 'ReturnAv', - ] - def _platform_find_testcases(self, crash_hash, files, root): # Only use directories that are hashes # if "0x" in crash_hash: @@ -333,20 +334,12 @@ def _platform_find_testcases(self, crash_hash, files, root): crasherfile = dbg_file.replace('.gdb', '') #crasherfile = os.path.join(root, crasherfile) tcb = LinuxTestCaseBundle(dbg_file, crasherfile, crash_hash, - self.re_set, self.ignore_jit) + self.ignore_jit) self.testcase_bundles.append(tcb) def main(): - args = parse_args() - - root_logger_to_console(args) - - with LinuxResultDriller(ignore_jit=args.ignore_jit, - base_dir=args.resultsdir, - force_reload=args.force) as rd: - rd.drill_results() - + _main(driller_class=LinuxResultDriller) if __name__ == '__main__': main() diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 789230a..25d0851 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -19,8 +19,8 @@ from certfuzz.tools.common.drillresults import reg_set -from certfuzz.tools.common.drillresults import read_file, carve, carve2, \ - is_number, reg_set, reg64_set, ResultDriller, parse_args +logger = logging.getLogger(__name__) + regex = { '64bit_debugger': re.compile('^Microsoft.*AMD64$'), @@ -34,135 +34,7 @@ } -# These !exploitable short descriptions indicate a very interesting crash -really_exploitable = [ - 'ReadAVonIP', - 'TaintedDataControlsCodeFlow', - 'ReadAVonControlFlow', - 'DEPViolation', - 'IllegalInstruction', - 'PrivilegedInstruction', - ] - -re_set = set(really_exploitable) - - -def fix_efa_bug(reporttext, instraddr, faultaddr): - ''' - !exploitable often reports an incorrect EFA for 64-bit targets. - If we're dealing with a 64-bit target, we can second-guess the reported EFA - ''' - instructionline = get_instr(reporttext, instraddr) - if not instructionline or "=" not in instructionline: - # Nothing to fix - return faultaddr - if 'ds:' in instructionline: - # There's a target address in the msec file - if '??' in instructionline: - # The AV is on dereferencing where to call - ds = carve(instructionline, "ds:", "=") - else: - # The AV is on accessing the code location - ds = instructionline.split("=")[-1] - else: - # AV must be on current instruction - ds = instructionline.split(' ')[0] - if ds: - faultaddr = ds.replace('`', '') - return faultaddr - - - -def get_ex_num(reporttext, wow64_app): - ''' - Get the exception number by counting the number of continues - ''' - if wow64_app: - pattern = re.compile('^[0-9]:[0-9][0-9][0-9]:x86> (.*)') - else: - pattern = re.compile('^[0-9]:[0-9][0-9][0-9]> (.*)') - - exception = 0 - - for line in reporttext.splitlines(): - n = re.match(pattern, line) - if n: - cdbcmd = n.group(1) - cmds = cdbcmd.split(';') - exception += cmds.count('g') - - return exception - - -def get_regs(reporttext): - ''' - Populate the register dictionary with register values at crash - ''' - for line in reporttext.splitlines(): - if regex['regs1'].match(line) or regex['regs2'].match(line): - regs1 = line.split() - for reg in regs1: - if "=" in reg: - splitreg = reg.split("=") - regdict[splitreg[0]] = splitreg[1] - - -def get_instr(reporttext, instraddr): - ''' - Find the disassembly line for the current (crashing) instruction - ''' - regex = re.compile('^%s\s.+.+\s+' % instraddr) - for line in reporttext.splitlines(): - n = regex.match(line) - if n: - return line - - - - -def fix_efa_offset(instructionline, faultaddr, _64bit_debugger, wow64_app): - ''' - Adjust faulting address for instructions that use offsets - Currently only works for instructions like CALL [reg + offset] - ''' - if _64bit_debugger and not wow64_app: - reg_set = reg64_set - - if '0x' not in faultaddr: - faultaddr = '0x' + faultaddr - instructionpieces = instructionline.split() - if '??' not in instructionpieces[-1]: - # The av is on the address of the code called, not the address - # of the call - return faultaddr - for index, piece in enumerate(instructionpieces): - if piece == 'call': - # CALL instruction - if len(instructionpieces) <= index + 3: - # CALL to just a register. No offset - return faultaddr - address = instructionpieces[index + 3] - if '+' in address: - splitaddress = address.split('+') - reg = splitaddress[0] - reg = reg.replace('[', '') - if reg not in reg_set: - return faultaddr - offset = splitaddress[1] - offset = offset.replace('h', '') - offset = offset.replace(']', '') - if is_number(offset): - if '0x' not in offset: - offset = '0x' + offset - if int(offset, 16) > int(faultaddr, 16): - # TODO: fix up negative numbers - return faultaddr - # Subtract offset to get actual interesting pattern - faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = format_addr(faultaddr.replace('L', '')) - return faultaddr - -def WindowsResultDriller(ResultDriller): +class WindowsTestCaseBundle(TestCaseBundle): # These !exploitable short descriptions indicate a very interesting crash really_exploitable = [ 'ReadAVonIP', @@ -173,41 +45,17 @@ def WindowsResultDriller(ResultDriller): 'PrivilegedInstruction', ] - def __init__(self, ignore_jit=False, base_dir='../results', force_reload=False): - ResultDriller.__init__(self, ignore_jit, base_dir, force_reload) + def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, + ignore_jit): + TestCaseBundle(self, dbg_outfile, testcase_file, crash_hash, re_set, + ignore_jit) self.wow64_app = False - def _platform_find_dbg_output(self, crash_hash, files, root): - if "0x" in crash_hash: - # Create dictionary for hashes in results dictionary - hash_dict = {} - hash_dict['hash'] = crash_hash - self.results[crash_hash] = hash_dict - crasherfile = '' - # Check each of the files in the hash directory - for current_file in files: - # If it's exception #0, strip out the exploitability part of - # the file name. This gives us the crasher file name - if regex['first_msec'].match(current_file): - crasherfile, reportfileext = os.path.splitext(current_file) - crasherfile = crasherfile.replace('-EXP', '') - crasherfile = crasherfile.replace('-PEX', '') - crasherfile = crasherfile.replace('-PNE', '') - crasherfile = crasherfile.replace('-UNK', '') - for current_file in files: - # Go through all of the .msec files and parse them - if regex['msec_report'].match(current_file): - msecfile = os.path.join(root, current_file) - if crasherfile and root not in crasherfile: - crasherfile = os.path.join(root, crasherfile) - dbg_tuple = (msecfile, crasherfile, crash_hash) - self.dbg_out.append(dbg_tuple) - - def check_64bit(self, reporttext): + def _check_64bit(self): ''' Check if the debugger and target app are 64-bit ''' - for line in reporttext.splitlines(): + for line in self.reporttext.splitlines(): n = re.match(regex['64bit_debugger'], line) if n: self._64bit_debugger = True @@ -217,13 +65,17 @@ def check_64bit(self, reporttext): if n: self.wow64_app = True - def _check_report(self, reportfile, crasherfile, crash_hash, cached_results): + def _parse_testcase(self, tcb): ''' Parse the msec file ''' - if cached_results: - if cached_results.get(crash_hash): - self.results[crash_hash] = cached_results[crash_hash] + reportfile = tcb.dbg_outfile + crasherfile = tcb.testcase_file + crash_hash = tcb.crash_hash + + if self.cached_testcases: + if self.cached_testcases.get(crash_hash): + self.results[crash_hash] = self.cached_testcases[crash_hash] return crashid = self.results[crash_hash] @@ -231,13 +83,12 @@ def _check_report(self, reportfile, crasherfile, crash_hash, cached_results): if crasherfile == '': # Old FOE version that didn't do multiple exceptions or rename msec # file with exploitability - crasherfile, reportfileext = os.path.splitext(reportfile) + crasherfile, _junk = os.path.splitext(reportfile) - reporttext = read_file(reportfile) - get_regs(reporttext) + self.get_regs() current_dir = os.path.dirname(reportfile) - exceptionnum = get_ex_num(reporttext, self.wow64_app) - classification = carve(reporttext, "Exploitability Classification: ", "\n") + exceptionnum = self.get_ex_num() + classification = carve(self.reporttext, "Exploitability Classification: ", "\n") try: if classification: # Create a new exception dictionary to add to the crash @@ -257,18 +108,18 @@ def _check_report(self, reportfile, crasherfile, crash_hash, cached_results): if classification: crashid['exceptions'][exceptionnum]['classification'] = classification - shortdesc = carve(reporttext, "Short Description: ", "\n") + shortdesc = carve(self.reporttext, "Short Description: ", "\n") if shortdesc: # Set !exploitable Short Description for the exception crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc # Flag the entire crash ID as really exploitable if this is a good # exception - crashid['reallyexploitable'] = shortdesc in re_set + crashid['reallyexploitable'] = shortdesc in self.re_set # Check if the expected crasher file (fuzzed file) exists if not os.path.isfile(crasherfile): # It's not there, so try to extract the filename from the cdb # commandline - commandline = carve(reporttext, "CommandLine: ", "\n") + commandline = carve(self.reporttext, "CommandLine: ", "\n") args = commandline.split() for arg in args: if "sf_" in arg: @@ -289,9 +140,8 @@ def _check_report(self, reportfile, crasherfile, crash_hash, cached_results): # Set the "fuzzedfile" property for the crash ID crashid['fuzzedfile'] = crasherfile # See if we're dealing with 64-bit debugger or target app - self.check_64bit(reporttext) - faultaddr = carve2(reporttext) - instraddr = carve(reporttext, "Instruction Address:", "\n") + faultaddr = carve2(self.reporttext) + instraddr = carve(self.reporttext, "Instruction Address:", "\n") faultaddr = self.format_addr(faultaddr) instraddr = self.format_addr(instraddr) @@ -303,21 +153,21 @@ def _check_report(self, reportfile, crasherfile, crash_hash, cached_results): # Put backtick into instruction address for pattern matching instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) if shortdesc != 'DEPViolation': - faultaddr = fix_efa_bug(reporttext, instraddr, faultaddr) + faultaddr = self.fix_efa_bug(instraddr, faultaddr) # pc_module = pc_in_mapped_address(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr, _64bit_debugger) + crashid['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(self.reporttext, instraddr, self._64bit_debugger) # Get the cdb line that contains the crashing instruction - instructionline = get_instr(reporttext, instraddr) + instructionline = self.get_instr(self.reporttext, instraddr) crashid['exceptions'][exceptionnum]['instructionline'] = instructionline if instructionline: - faultaddr = fix_efa_offset(instructionline, faultaddr, _64bit_debugger, wow64_app) + faultaddr = self.fix_efa_offset(instructionline, faultaddr) # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') crashid['exceptions'][exceptionnum]['efa'] = faultaddr - if _64bit_debugger and not wow64_app: + if self._64bit_debugger and not self.wow64_app: # 64-bit target app faultaddr = faultaddr.zfill(16) efaptr = struct.unpack(' int(faultaddr, 16): + # TODO: fix up negative numbers + return faultaddr + # Subtract offset to get actual interesting pattern + faultaddr = hex(eval(faultaddr) - eval(offset)) + faultaddr = self.format_addr(faultaddr.replace('L', '')) + return faultaddr - instraddr = instraddr.replace('`', '') - instraddr = int(instraddr, 16) - for line in reporttext.splitlines(): - n = re.match(regex[ma_regex], line) + def get_ex_num(self): + ''' + Get the exception number by counting the number of continues + ''' + if self.wow64_app: + pattern = re.compile('^[0-9]:[0-9][0-9][0-9]:x86> (.*)') + else: + pattern = re.compile('^[0-9]:[0-9][0-9][0-9]> (.*)') + + exception = 0 + + for line in self.reporttext.splitlines(): + n = re.match(pattern, line) if n: - # Strip out backticks present on 64-bit systems - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = n.group(3) - return mapped_module + cdbcmd = n.group(1) + cmds = cdbcmd.split(';') + exception += cmds.count('g') + + return exception + + def get_instr(self, instraddr): + ''' + Find the disassembly line for the current (crashing) instruction + ''' + regex = re.compile('^%s\s.+.+\s+' % instraddr) + for line in self.reporttext.splitlines(): + n = regex.match(line) + if n: + return line + + def fix_efa_bug(self, instraddr, faultaddr): + ''' + !exploitable often reports an incorrect EFA for 64-bit targets. + If we're dealing with a 64-bit target, we can second-guess the reported EFA + ''' + instructionline = self.get_instr(instraddr) + if not instructionline or "=" not in instructionline: + # Nothing to fix + return faultaddr + if 'ds:' in instructionline: + # There's a target address in the msec file + if '??' in instructionline: + # The AV is on dereferencing where to call + ds = carve(instructionline, "ds:", "=") + else: + # The AV is on accessing the code location + ds = instructionline.split("=")[-1] + else: + # AV must be on current instruction + ds = instructionline.split(' ')[0] + if ds: + faultaddr = ds.replace('`', '') + return faultaddr + + def get_regs(self): + ''' + Populate the register dictionary with register values at crash + ''' + for line in self.reporttext.splitlines(): + if regex['regs1'].match(line) or regex['regs2'].match(line): + regs1 = line.split() + for reg in regs1: + if "=" in reg: + splitreg = reg.split("=") + regdict[splitreg[0]] = splitreg[1] + + +class WindowsResultDriller(ResultDriller): + def _platform_find_testcases(self, crash_hash, files, root): + if "0x" in crash_hash: + # Create dictionary for hashes in results dictionary + hash_dict = {} + hash_dict['hash'] = crash_hash + self.results[crash_hash] = hash_dict + crasherfile = '' + # Check each of the files in the hash directory + for current_file in files: + # If it's exception #0, strip out the exploitability part of + # the file name. This gives us the crasher file name + if regex['first_msec'].match(current_file): + crasherfile, reportfileext = os.path.splitext(current_file) + crasherfile = crasherfile.replace('-EXP', '') + crasherfile = crasherfile.replace('-PEX', '') + crasherfile = crasherfile.replace('-PNE', '') + crasherfile = crasherfile.replace('-UNK', '') + for current_file in files: + # Go through all of the .msec files and parse them + if regex['msec_report'].match(current_file): + msecfile = os.path.join(root, current_file) + if crasherfile and root not in crasherfile: + crasherfile = os.path.join(root, crasherfile) + tcb = TestCaseBundle(msecfile, crasherfile, crash_hash, + self.ignore_jit) + self.testcase_bundles.append(tcb) def main(): - options = parse_args() - with WindowsResultDriller(ignore_jit=options.ignorejit, - base_dir=options.resultsdir) as rd: - rd.drill_results() + _main(driller_class=WindowsResultDriller) if __name__ == '__main__': main() From 726bee33eed75da74d51a96ae251e6bb3cff02e5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 09:50:16 -0400 Subject: [PATCH 0422/1169] add regdict as property of TestCaseBundle --- src/certfuzz/tools/common/drillresults.py | 2 ++ src/certfuzz/tools/windows/drillresults.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/tools/common/drillresults.py index f7a0e8f..0c04316 100644 --- a/src/certfuzz/tools/common/drillresults.py +++ b/src/certfuzz/tools/common/drillresults.py @@ -180,6 +180,8 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self.re_set = set(self.really_exploitable) + self.regdict = {} + self.reporttext = read_text_file(self.dbg_outfile) # Read in the fuzzed file self.crasherdata = read_bin_file(self.testcase_file) diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/tools/windows/drillresults.py index 25d0851..3543ab9 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/tools/windows/drillresults.py @@ -342,7 +342,7 @@ def get_regs(self): for reg in regs1: if "=" in reg: splitreg = reg.split("=") - regdict[splitreg[0]] = splitreg[1] + self.regdict[splitreg[0]] = splitreg[1] class WindowsResultDriller(ResultDriller): From 9bc3eaeef234fb942e720f44f02e670959bb958e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 09:50:47 -0400 Subject: [PATCH 0423/1169] refactor functions into methods --- src/certfuzz/tools/linux/drillresults.py | 331 +++++++++++------------ 1 file changed, 163 insertions(+), 168 deletions(-) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index aebce73..6cdd261 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -35,167 +35,6 @@ } -def pc_in_mapped_address(reporttext, instraddr): - ''' - Check if the instruction pointer is in a loaded module - ''' - if not instraddr: - # The gdb file doesn't have anything in it that'll tell us - # where the PC is. - return '' -# print 'checking if %s is mapped...' % instraddr - mapped_module = 'unloaded' - - instraddr = int(instraddr, 16) -# print 'instraddr: %d' % instraddr - for line in reporttext.splitlines(): - #print 'checking: %s for %s' % (line,regex['mapped_frame']) - n = re.search(regex['mapped_frame'], line) - if n: -# print '*** found mapped address regex!' - # Strip out backticks present on 64-bit systems - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = n.group(4) - #print 'mapped_module: %s' % mapped_module - else: - # [vdso] still counts as a mapped module - n = re.search(regex['vdso'], line) - if n: - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = '[vdso]' - - return mapped_module - - -def fix_efa_bug(reporttext, instraddr, faultaddr): - ''' - !exploitable often reports an incorrect EFA for 64-bit targets. - If we're dealing with a 64-bit target, we can second-guess the reported EFA - ''' - instructionline = get_instr(reporttext, instraddr) - if not instructionline: - return faultaddr - ds = carve(instructionline, "ds:", "=") - if ds: - faultaddr = ds.replace('`', '') - return faultaddr - - -def get_ex_num(reporttext): - ''' - Get the exception number by counting the number of continues - ''' - exception = 0 - for line in reporttext.splitlines(): - n = re.match(regex['dbg_prompt'], line) - if n: - cdbcmd = n.group(1) - cmds = cdbcmd.split(';') - for cmd in cmds: - if cmd == 'g': - exception = exception + 1 - return exception - - -def get_instr_addr(reporttext): - ''' - Find the address for the current (crashing) instruction - ''' - instraddr = None - for line in reporttext.splitlines(): - #print 'checking: %s' % line - n = re.match(regex['current_instr'], line) - if n: - instraddr = n.group(1) - #print 'Found instruction address: %s' % instraddr - if not instraddr: - for line in reporttext.splitlines(): - #No disassembly. Resort to frame 0 address - n = re.match(regex['frame0'], line) - if n: - instraddr = n.group(1) - #print 'Found instruction address: %s' % instraddr - return instraddr - - -def get_instr(reporttext, instraddr): - ''' - Find the disassembly line for the current (crashing) instruction - ''' - rgx = regex['current_instr'] - for line in reporttext.splitlines(): - n = rgx.match(line) - if n: - return n.group(3) - return '' - - -def format_addr(faultaddr, _64bit_debugger): - ''' - Format a 64- or 32-bit memory address to a fixed width - ''' - - if not faultaddr: - return - else: - faultaddr = faultaddr.strip() - faultaddr = faultaddr.replace('0x', '') - - if _64bit_debugger: - # Due to a bug in !exploitable, the Exception Faulting Address is - # often wrong with 64-bit targets - if len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(16) - else: - if len(faultaddr) > 10: # 0x12345678 = 10 chars - faultaddr = faultaddr[-8:] - elif len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(8) - - return faultaddr - - -def fix_efa_offset(instructionline, faultaddr, _64bit_debugger): - ''' - Adjust faulting address for instructions that use offsets - Currently only works for instructions like CALL [reg + offset] - ''' - if '0x' not in faultaddr: - faultaddr = '0x' + faultaddr - instructionpieces = instructionline.split() - for index, piece in enumerate(instructionpieces): - if piece == 'call': - # CALL instruction - if len(instructionpieces) <= index + 3: - # CALL to just a register. No offset - return faultaddr - address = instructionpieces[index + 3] - if '+' in address: - splitaddress = address.split('+') - reg = splitaddress[0] - reg = reg.replace('[', '') - if reg not in reg_set: - return faultaddr - offset = splitaddress[1] - offset = offset.replace('h', '') - offset = offset.replace(']', '') - if '0x' not in offset: - offset = '0x' + offset - if int(offset, 16) > int(faultaddr, 16): - # TODO: fix up negative numbers - return faultaddr - # Subtract offset to get actual interesting pattern - faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = format_addr(faultaddr.replace('L', ''), _64bit_debugger) - return faultaddr - - class LinuxTestCaseBundle(TestCaseBundle): really_exploitable = [ 'SegFaultOnPc', @@ -205,7 +44,6 @@ class LinuxTestCaseBundle(TestCaseBundle): 'ReturnAv', ] - def _check_64bit(self): ''' Check if the debugger and target app are 64-bit @@ -275,22 +113,22 @@ def _parse_testcase(self): # Set the "fuzzedfile" property for the crash ID details['fuzzedfile'] = crasherfile faultaddr = carve2(reporttext) - instraddr = get_instr_addr(reporttext) - faultaddr = format_addr(faultaddr, _64bit_debugger) - instraddr = format_addr(instraddr, _64bit_debugger) + instraddr = self.get_instr_addr() + faultaddr = self.format_addr(faultaddr, _64bit_debugger) + instraddr = self.format_addr(instraddr, _64bit_debugger) # No faulting address means no crash. if not faultaddr: return if instraddr: - details['exceptions'][exceptionnum]['pcmodule'] = pc_in_mapped_address(reporttext, instraddr) + details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(instraddr) # Get the cdb line that contains the crashing instruction - instructionline = get_instr(reporttext, instraddr) + instructionline = self.get_instr(instraddr) details['exceptions'][exceptionnum]['instructionline'] = instructionline if instructionline: - faultaddr = fix_efa_offset(instructionline, faultaddr, _64bit_debugger) + faultaddr = self.fix_efa_offset(instructionline, faultaddr) # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') @@ -316,6 +154,163 @@ def _parse_testcase(self): else: details['exceptions'][exceptionnum]['EIF'] = False + def format_addr(self, faultaddr): + ''' + Format a 64- or 32-bit memory address to a fixed width + ''' + + if not faultaddr: + return + else: + faultaddr = faultaddr.strip() + faultaddr = faultaddr.replace('0x', '') + + if self._64bit_debugger: + # Due to a bug in !exploitable, the Exception Faulting Address is + # often wrong with 64-bit targets + if len(faultaddr) < 10: + # pad faultaddr + faultaddr = faultaddr.zfill(16) + else: + if len(faultaddr) > 10: # 0x12345678 = 10 chars + faultaddr = faultaddr[-8:] + elif len(faultaddr) < 10: + # pad faultaddr + faultaddr = faultaddr.zfill(8) + + return faultaddr + + def fix_efa_offset(self, instructionline, faultaddr): + ''' + Adjust faulting address for instructions that use offsets + Currently only works for instructions like CALL [reg + offset] + ''' + if '0x' not in faultaddr: + faultaddr = '0x' + faultaddr + instructionpieces = instructionline.split() + for index, piece in enumerate(instructionpieces): + if piece == 'call': + # CALL instruction + if len(instructionpieces) <= index + 3: + # CALL to just a register. No offset + return faultaddr + address = instructionpieces[index + 3] + if '+' in address: + splitaddress = address.split('+') + reg = splitaddress[0] + reg = reg.replace('[', '') + if reg not in reg_set: + return faultaddr + offset = splitaddress[1] + offset = offset.replace('h', '') + offset = offset.replace(']', '') + if '0x' not in offset: + offset = '0x' + offset + if int(offset, 16) > int(faultaddr, 16): + # TODO: fix up negative numbers + return faultaddr + # Subtract offset to get actual interesting pattern + faultaddr = hex(eval(faultaddr) - eval(offset)) + faultaddr = self.format_addr(faultaddr.replace('L', '')) + return faultaddr + + def get_instr(self, instraddr): + ''' + Find the disassembly line for the current (crashing) instruction + ''' + rgx = regex['current_instr'] + for line in self.reporttext.splitlines(): + n = rgx.match(line) + if n: + return n.group(3) + return '' + + def fix_efa_bug(self, instraddr, faultaddr): + ''' + !exploitable often reports an incorrect EFA for 64-bit targets. + If we're dealing with a 64-bit target, we can second-guess the reported EFA + ''' + instructionline = self.get_instr(instraddr) + if not instructionline: + return faultaddr + ds = carve(instructionline, "ds:", "=") + if ds: + faultaddr = ds.replace('`', '') + return faultaddr + + def pc_in_mapped_address(self, instraddr): + ''' + Check if the instruction pointer is in a loaded module + ''' + if not instraddr: + # The gdb file doesn't have anything in it that'll tell us + # where the PC is. + return '' + # print 'checking if %s is mapped...' % instraddr + mapped_module = 'unloaded' + + instraddr = int(instraddr, 16) + # print 'instraddr: %d' % instraddr + for line in self.reporttext.splitlines(): + #print 'checking: %s for %s' % (line,regex['mapped_frame']) + n = re.search(regex['mapped_frame'], line) + if n: + # print '*** found mapped address regex!' + # Strip out backticks present on 64-bit systems + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + if begin_address < instraddr < end_address: + mapped_module = n.group(4) + #print 'mapped_module: %s' % mapped_module + else: + # [vdso] still counts as a mapped module + n = re.search(regex['vdso'], line) + if n: + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + if begin_address < instraddr < end_address: + mapped_module = '[vdso]' + + return mapped_module + + def get_ex_num(self): + ''' + Get the exception number by counting the number of continues + ''' + exception = 0 + for line in self.reporttext.splitlines(): + n = re.match(regex['dbg_prompt'], line) + if n: + cdbcmd = n.group(1) + cmds = cdbcmd.split(';') + for cmd in cmds: + if cmd == 'g': + exception = exception + 1 + return exception + + def get_instr_addr(self): + ''' + Find the address for the current (crashing) instruction + ''' + instraddr = None + for line in self.reporttext.splitlines(): + #print 'checking: %s' % line + n = re.match(regex['current_instr'], line) + if n: + instraddr = n.group(1) + #print 'Found instruction address: %s' % instraddr + if not instraddr: + for line in self.reporttext.splitlines(): + #No disassembly. Resort to frame 0 address + n = re.match(regex['frame0'], line) + if n: + instraddr = n.group(1) + #print 'Found instruction address: %s' % instraddr + return instraddr + + + + class LinuxResultDriller(ResultDriller): def _platform_find_testcases(self, crash_hash, files, root): From 083c3a4ca0fd6e79ebeb24c9bafb130fa12199cf Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 09:55:40 -0400 Subject: [PATCH 0424/1169] reorder methods so drillresults linux matches drillresults windows --- src/certfuzz/tools/linux/drillresults.py | 103 +++++++++++------------ 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/tools/linux/drillresults.py index 6cdd261..68eec0d 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/tools/linux/drillresults.py @@ -154,6 +154,41 @@ def _parse_testcase(self): else: details['exceptions'][exceptionnum]['EIF'] = False + def pc_in_mapped_address(self, instraddr): + ''' + Check if the instruction pointer is in a loaded module + ''' + if not instraddr: + # The gdb file doesn't have anything in it that'll tell us + # where the PC is. + return '' + # print 'checking if %s is mapped...' % instraddr + mapped_module = 'unloaded' + + instraddr = int(instraddr, 16) + # print 'instraddr: %d' % instraddr + for line in self.reporttext.splitlines(): + #print 'checking: %s for %s' % (line,regex['mapped_frame']) + n = re.search(regex['mapped_frame'], line) + if n: + # print '*** found mapped address regex!' + # Strip out backticks present on 64-bit systems + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + if begin_address < instraddr < end_address: + mapped_module = n.group(4) + #print 'mapped_module: %s' % mapped_module + else: + # [vdso] still counts as a mapped module + n = re.search(regex['vdso'], line) + if n: + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + if begin_address < instraddr < end_address: + mapped_module = '[vdso]' + + return mapped_module + def format_addr(self, faultaddr): ''' Format a 64- or 32-bit memory address to a fixed width @@ -214,6 +249,21 @@ def fix_efa_offset(self, instructionline, faultaddr): faultaddr = self.format_addr(faultaddr.replace('L', '')) return faultaddr + def get_ex_num(self): + ''' + Get the exception number by counting the number of continues + ''' + exception = 0 + for line in self.reporttext.splitlines(): + n = re.match(regex['dbg_prompt'], line) + if n: + cdbcmd = n.group(1) + cmds = cdbcmd.split(';') + for cmd in cmds: + if cmd == 'g': + exception = exception + 1 + return exception + def get_instr(self, instraddr): ''' Find the disassembly line for the current (crashing) instruction @@ -238,56 +288,6 @@ def fix_efa_bug(self, instraddr, faultaddr): faultaddr = ds.replace('`', '') return faultaddr - def pc_in_mapped_address(self, instraddr): - ''' - Check if the instruction pointer is in a loaded module - ''' - if not instraddr: - # The gdb file doesn't have anything in it that'll tell us - # where the PC is. - return '' - # print 'checking if %s is mapped...' % instraddr - mapped_module = 'unloaded' - - instraddr = int(instraddr, 16) - # print 'instraddr: %d' % instraddr - for line in self.reporttext.splitlines(): - #print 'checking: %s for %s' % (line,regex['mapped_frame']) - n = re.search(regex['mapped_frame'], line) - if n: - # print '*** found mapped address regex!' - # Strip out backticks present on 64-bit systems - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = n.group(4) - #print 'mapped_module: %s' % mapped_module - else: - # [vdso] still counts as a mapped module - n = re.search(regex['vdso'], line) - if n: - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = '[vdso]' - - return mapped_module - - def get_ex_num(self): - ''' - Get the exception number by counting the number of continues - ''' - exception = 0 - for line in self.reporttext.splitlines(): - n = re.match(regex['dbg_prompt'], line) - if n: - cdbcmd = n.group(1) - cmds = cdbcmd.split(';') - for cmd in cmds: - if cmd == 'g': - exception = exception + 1 - return exception - def get_instr_addr(self): ''' Find the address for the current (crashing) instruction @@ -309,9 +309,6 @@ def get_instr_addr(self): return instraddr - - - class LinuxResultDriller(ResultDriller): def _platform_find_testcases(self, crash_hash, files, root): # Only use directories that are hashes From ddfaf06d703e9b41c5ecce5b0127cec214cfc45e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 09:58:35 -0400 Subject: [PATCH 0425/1169] Move drillresults modules to their own package certfuzz.drillresults move unit tests to match current package layout (+4 squashed commits) Squashed commits: [37b51a4] move certfuzz.tools.windows.drillresults -> certfuzz.drillresults.drillresults_windows [c2306f2] move certfuzz.tools.linux.drillresults -> certfuzz.drillresults.drillresults_linux [66dd460] move certfuzz.tools.common.drillresults -> certfuzz.drillresults.common [145b2b6] create drillresults package in prep for move from certfuzz.tools.[common, linux, windows].drillresults --- src/certfuzz/drillresults/__init__.py | 0 .../drillresults.py => drillresults/common.py} | 0 .../drillresults_linux.py} | 12 ++++++------ .../drillresults_windows.py} | 18 +++++++++--------- .../{tools/common => drillresults}/errors.py | 0 src/certfuzz/test/drillresults/__init__.py | 0 .../test_common.py} | 3 ++- .../test_drillresults_linux.py} | 1 + .../test_drillresults_windows.py} | 1 + src/linux/tools/drillresults.py | 4 ++-- src/windows/tools/drillresults.py | 4 ++-- 11 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 src/certfuzz/drillresults/__init__.py rename src/certfuzz/{tools/common/drillresults.py => drillresults/common.py} (100%) rename src/certfuzz/{tools/linux/drillresults.py => drillresults/drillresults_linux.py} (97%) rename src/certfuzz/{tools/windows/drillresults.py => drillresults/drillresults_windows.py} (96%) rename src/certfuzz/{tools/common => drillresults}/errors.py (100%) create mode 100644 src/certfuzz/test/drillresults/__init__.py rename src/certfuzz/test/{tools/common/test_drillresults.py => drillresults/test_common.py} (97%) rename src/certfuzz/test/{tools/linux/test_drillresults.py => drillresults/test_drillresults_linux.py} (85%) rename src/certfuzz/test/{tools/windows/test_drillresults.py => drillresults/test_drillresults_windows.py} (85%) diff --git a/src/certfuzz/drillresults/__init__.py b/src/certfuzz/drillresults/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/tools/common/drillresults.py b/src/certfuzz/drillresults/common.py similarity index 100% rename from src/certfuzz/tools/common/drillresults.py rename to src/certfuzz/drillresults/common.py diff --git a/src/certfuzz/tools/linux/drillresults.py b/src/certfuzz/drillresults/drillresults_linux.py similarity index 97% rename from src/certfuzz/tools/linux/drillresults.py rename to src/certfuzz/drillresults/drillresults_linux.py index 68eec0d..3f2e92f 100755 --- a/src/certfuzz/tools/linux/drillresults.py +++ b/src/certfuzz/drillresults/drillresults_linux.py @@ -8,12 +8,12 @@ import re import struct -from certfuzz.tools.common.drillresults import ResultDriller -from certfuzz.tools.common.drillresults import TestCaseBundle -from certfuzz.tools.common.drillresults import carve -from certfuzz.tools.common.drillresults import carve2 -from certfuzz.tools.common.drillresults import main as _main -from certfuzz.tools.common.drillresults import reg_set +from certfuzz.drillresults.common import ResultDriller +from certfuzz.drillresults.common import TestCaseBundle +from certfuzz.drillresults.common import carve +from certfuzz.drillresults.common import carve2 +from certfuzz.drillresults.common import main as _main +from certfuzz.drillresults.common import reg_set logger = logging.getLogger(__name__) diff --git a/src/certfuzz/tools/windows/drillresults.py b/src/certfuzz/drillresults/drillresults_windows.py similarity index 96% rename from src/certfuzz/tools/windows/drillresults.py rename to src/certfuzz/drillresults/drillresults_windows.py index 3543ab9..cf72d40 100644 --- a/src/certfuzz/tools/windows/drillresults.py +++ b/src/certfuzz/drillresults/drillresults_windows.py @@ -8,15 +8,15 @@ import re import struct -from certfuzz.tools.common.drillresults import ResultDriller -from certfuzz.tools.common.drillresults import TestCaseBundle -from certfuzz.tools.common.drillresults import carve -from certfuzz.tools.common.drillresults import carve2 -from certfuzz.tools.common.drillresults import is_number -from certfuzz.tools.common.drillresults import main as _main -from certfuzz.tools.common.drillresults import read_bin_file -from certfuzz.tools.common.drillresults import reg64_set -from certfuzz.tools.common.drillresults import reg_set +from certfuzz.drillresults.common import ResultDriller +from certfuzz.drillresults.common import TestCaseBundle +from certfuzz.drillresults.common import carve +from certfuzz.drillresults.common import carve2 +from certfuzz.drillresults.common import is_number +from certfuzz.drillresults.common import main as _main +from certfuzz.drillresults.common import read_bin_file +from certfuzz.drillresults.common import reg64_set +from certfuzz.drillresults.common import reg_set logger = logging.getLogger(__name__) diff --git a/src/certfuzz/tools/common/errors.py b/src/certfuzz/drillresults/errors.py similarity index 100% rename from src/certfuzz/tools/common/errors.py rename to src/certfuzz/drillresults/errors.py diff --git a/src/certfuzz/test/drillresults/__init__.py b/src/certfuzz/test/drillresults/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/test/tools/common/test_drillresults.py b/src/certfuzz/test/drillresults/test_common.py similarity index 97% rename from src/certfuzz/test/tools/common/test_drillresults.py rename to src/certfuzz/test/drillresults/test_common.py index 0c15d00..f0edf82 100644 --- a/src/certfuzz/test/tools/common/test_drillresults.py +++ b/src/certfuzz/test/drillresults/test_common.py @@ -4,13 +4,14 @@ @organization: cert.org ''' import unittest -from certfuzz.tools.common import drillresults +from certfuzz.drillresults import common as drillresults import tempfile import shutil import os alphabet = 'abcdefghijklmnopqrstuvwxyz' + class MockRd(drillresults.ResultDriller): # really_exploitable expects a list really_exploitable = list(alphabet) diff --git a/src/certfuzz/test/tools/linux/test_drillresults.py b/src/certfuzz/test/drillresults/test_drillresults_linux.py similarity index 85% rename from src/certfuzz/test/tools/linux/test_drillresults.py rename to src/certfuzz/test/drillresults/test_drillresults_linux.py index 1efab39..b54263e 100644 --- a/src/certfuzz/test/tools/linux/test_drillresults.py +++ b/src/certfuzz/test/drillresults/test_drillresults_linux.py @@ -4,6 +4,7 @@ @organization: cert.org ''' import unittest +from certfuzz.drillresults import drillresults_linux class Test(unittest.TestCase): diff --git a/src/certfuzz/test/tools/windows/test_drillresults.py b/src/certfuzz/test/drillresults/test_drillresults_windows.py similarity index 85% rename from src/certfuzz/test/tools/windows/test_drillresults.py rename to src/certfuzz/test/drillresults/test_drillresults_windows.py index 1efab39..29d8582 100644 --- a/src/certfuzz/test/tools/windows/test_drillresults.py +++ b/src/certfuzz/test/drillresults/test_drillresults_windows.py @@ -4,6 +4,7 @@ @organization: cert.org ''' import unittest +from certfuzz.drillresults import drillresults_windows class Test(unittest.TestCase): diff --git a/src/linux/tools/drillresults.py b/src/linux/tools/drillresults.py index 414d1aa..608e369 100755 --- a/src/linux/tools/drillresults.py +++ b/src/linux/tools/drillresults.py @@ -5,13 +5,13 @@ import os import sys try: - from certfuzz.tools.linux.drillresults import main + from certfuzz.drillresults.drillresults_linux import main except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) - from certfuzz.tools.linux.drillresults import main + from certfuzz.drillresults.drillresults_linux import main if __name__ == '__main__': main() diff --git a/src/windows/tools/drillresults.py b/src/windows/tools/drillresults.py index 2014444..ba5b266 100644 --- a/src/windows/tools/drillresults.py +++ b/src/windows/tools/drillresults.py @@ -4,13 +4,13 @@ import os import sys try: - from certfuzz.tools.windows.drillresults import main + from certfuzz.drillresults.drillresults_windows import main except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) - from certfuzz.tools.windows.drillresults import main + from certfuzz.drillresults.drillresults_windows import main if __name__ == '__main__': main() From 486b5e7d10090fd7ad1e19862802c329beba87f3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 10:27:47 -0400 Subject: [PATCH 0426/1169] split TestCaseBundle into a separate module --- src/certfuzz/drillresults/common.py | 118 ---------------- .../drillresults/drillresults_linux.py | 2 +- .../drillresults/drillresults_windows.py | 2 +- .../drillresults/testcasebundle_base.py | 132 ++++++++++++++++++ .../drillresults/test_testcasebundle_base.py | 22 +++ 5 files changed, 156 insertions(+), 120 deletions(-) create mode 100644 src/certfuzz/drillresults/testcasebundle_base.py create mode 100644 src/certfuzz/test/drillresults/test_testcasebundle_base.py diff --git a/src/certfuzz/drillresults/common.py b/src/certfuzz/drillresults/common.py index 0c04316..1b3242f 100644 --- a/src/certfuzz/drillresults/common.py +++ b/src/certfuzz/drillresults/common.py @@ -11,7 +11,6 @@ import zipfile from certfuzz.fuzztools.filetools import read_bin_file as _read_bin_file -from certfuzz.fuzztools.filetools import read_text_file import cPickle as pickle from certfuzz.tools.common.errors import DrillResultsError @@ -165,123 +164,6 @@ def read_bin_file(inputfile): return filebytes + zipbytes - -class TestCaseBundle(object): - __metaclass__ = abc.ABCMeta - - def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): - self.dbg_outfile = dbg_outfile - self.testcase_file = testcase_file - - self._verify_files_exist() - - self.crash_hash = crash_hash - self.ignore_jit = ignore_jit - - self.re_set = set(self.really_exploitable) - - self.regdict = {} - - self.reporttext = read_text_file(self.dbg_outfile) - # Read in the fuzzed file - self.crasherdata = read_bin_file(self.testcase_file) - self.current_dir = os.path.dirname(self.dbg_outfile) - - self.details = {'reallyexploitable': False, - 'exceptions': {}} - self.score = 100 - self._64bit_debugger = False - - # See if we're dealing with 64-bit debugger or target app - self._check_64bit() - self._parse_testcase() - self._score_testcase() - - @abc.abstractproperty - def really_exploitable(self): - return [] - - def _verify_files_exist(self): - for f in [self.dbg_outfile, self.testcase_file]: - if not os.path.exists(f): - raise TestCaseBundleError('File not found: {}'.format(f)) - else: - logger.debug('Found file: %s', f) - - def __enter__(self): - return self - - def __exit__(self, etype, value, traceback): - pass - - @abc.abstractmethod - def _check_64bit(self): - pass - - @abc.abstractmethod - def _parse_testcase(self): - pass - - def _score_testcase(self): - logger.debug('Scoring testcase: %s', self.crash_hash) - details = self.details - scores = [100] - if details['reallyexploitable'] == True: - # The crash summary is a very interesting one - for exception in details['exceptions']: - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignore_jit: - # EIP is not in a loaded module - scores.append(20) - if details['exceptions'][exception]['shortdesc'] in self.re_set: - efa = '0x' + details['exceptions'][exception]['efa'] - if details['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - scores.append(30) - elif '0x0000' in efa: - # Faulting address is somewhat near null - scores.append(20) - elif '0xffff' in efa: - # Faulting address is likely a negative number - scores.append(20) - else: - # Faulting address has high entropy. Most exploitable. - scores.append(10) - else: - # The faulting address pattern is not in the fuzzed file - scores.append(40) - else: - # The crash summary isn't necessarily interesting - for exception in details['exceptions']: - efa = '0x' + details['exceptions'][exception]['efa'] - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignore_jit: - scores.append(20) - elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): - # likely heap corruption. Exploitable, but difficult - scores.append(45) - elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: - # non-continued potential stack buffer overflow - scores.append(40) - elif details['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - scores.append(70) - elif '0x0000' in efa: - # Faulting address is somewhat near null - scores.append(60) - elif '0xffff' in efa: - # Faulting address is likely a negative number - scores.append(60) - else: - # Faulting address has high entropy. - scores.append(50) - self.score = min(scores) - - class ResultDriller(object): __metaclass__ = abc.ABCMeta diff --git a/src/certfuzz/drillresults/drillresults_linux.py b/src/certfuzz/drillresults/drillresults_linux.py index 3f2e92f..cc73062 100755 --- a/src/certfuzz/drillresults/drillresults_linux.py +++ b/src/certfuzz/drillresults/drillresults_linux.py @@ -9,11 +9,11 @@ import struct from certfuzz.drillresults.common import ResultDriller -from certfuzz.drillresults.common import TestCaseBundle from certfuzz.drillresults.common import carve from certfuzz.drillresults.common import carve2 from certfuzz.drillresults.common import main as _main from certfuzz.drillresults.common import reg_set +from certfuzz.drillresults.testcasebundle_base import TestCaseBundle logger = logging.getLogger(__name__) diff --git a/src/certfuzz/drillresults/drillresults_windows.py b/src/certfuzz/drillresults/drillresults_windows.py index cf72d40..0d99098 100644 --- a/src/certfuzz/drillresults/drillresults_windows.py +++ b/src/certfuzz/drillresults/drillresults_windows.py @@ -9,7 +9,6 @@ import struct from certfuzz.drillresults.common import ResultDriller -from certfuzz.drillresults.common import TestCaseBundle from certfuzz.drillresults.common import carve from certfuzz.drillresults.common import carve2 from certfuzz.drillresults.common import is_number @@ -17,6 +16,7 @@ from certfuzz.drillresults.common import read_bin_file from certfuzz.drillresults.common import reg64_set from certfuzz.drillresults.common import reg_set +from certfuzz.drillresults.testcasebundle_base import TestCaseBundle logger = logging.getLogger(__name__) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py new file mode 100644 index 0000000..91d2f8b --- /dev/null +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -0,0 +1,132 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import abc +import logging +import os + +from certfuzz.fuzztools.filetools import read_text_file + +from certfuzz.drillresults.common import read_bin_file +from certfuzz.drillresults.errors import TestCaseBundleError + + +logger = logging.getLogger(__name__) + + +class TestCaseBundle(object): + __metaclass__ = abc.ABCMeta + + def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): + self.dbg_outfile = dbg_outfile + self.testcase_file = testcase_file + + self._verify_files_exist() + + self.crash_hash = crash_hash + self.ignore_jit = ignore_jit + + self.re_set = set(self.really_exploitable) + + self.regdict = {} + + self.reporttext = read_text_file(self.dbg_outfile) + # Read in the fuzzed file + self.crasherdata = read_bin_file(self.testcase_file) + self.current_dir = os.path.dirname(self.dbg_outfile) + + self.details = {'reallyexploitable': False, + 'exceptions': {}} + self.score = 100 + self._64bit_debugger = False + + # See if we're dealing with 64-bit debugger or target app + self._check_64bit() + self._parse_testcase() + self._score_testcase() + + @abc.abstractproperty + def really_exploitable(self): + return [] + + def _verify_files_exist(self): + for f in [self.dbg_outfile, self.testcase_file]: + if not os.path.exists(f): + raise TestCaseBundleError('File not found: {}'.format(f)) + else: + logger.debug('Found file: %s', f) + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + pass + + @abc.abstractmethod + def _check_64bit(self): + pass + + @abc.abstractmethod + def _parse_testcase(self): + pass + + def _score_testcase(self): + logger.debug('Scoring testcase: %s', self.crash_hash) + details = self.details + scores = [100] + if details['reallyexploitable'] == True: + # The crash summary is a very interesting one + for exception in details['exceptions']: + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not self.ignore_jit: + # EIP is not in a loaded module + scores.append(20) + if details['exceptions'][exception]['shortdesc'] in self.re_set: + efa = '0x' + details['exceptions'][exception]['efa'] + if details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(30) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(20) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(20) + else: + # Faulting address has high entropy. Most exploitable. + scores.append(10) + else: + # The faulting address pattern is not in the fuzzed file + scores.append(40) + else: + # The crash summary isn't necessarily interesting + for exception in details['exceptions']: + efa = '0x' + details['exceptions'][exception]['efa'] + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded' and not self.ignore_jit: + scores.append(20) + elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): + # likely heap corruption. Exploitable, but difficult + scores.append(45) + elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: + # non-continued potential stack buffer overflow + scores.append(40) + elif details['exceptions'][exception]['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(70) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(60) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(60) + else: + # Faulting address has high entropy. + scores.append(50) + self.score = min(scores) diff --git a/src/certfuzz/test/drillresults/test_testcasebundle_base.py b/src/certfuzz/test/drillresults/test_testcasebundle_base.py new file mode 100644 index 0000000..f3ab4e9 --- /dev/null +++ b/src/certfuzz/test/drillresults/test_testcasebundle_base.py @@ -0,0 +1,22 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import unittest +from certfuzz.drillresults import testcasebundle_base + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From ef35f5f00ddfafc0bb4e8f9e04da6fc7ab829fa3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 10:36:23 -0400 Subject: [PATCH 0427/1169] split WindowsTestCaseBundle into its own module --- .../drillresults/drillresults_windows.py | 330 +---------------- .../drillresults/testcasebundle_windows.py | 336 ++++++++++++++++++ .../test_testcasebundle_windows.py | 23 ++ 3 files changed, 361 insertions(+), 328 deletions(-) create mode 100644 src/certfuzz/drillresults/testcasebundle_windows.py create mode 100644 src/certfuzz/test/drillresults/test_testcasebundle_windows.py diff --git a/src/certfuzz/drillresults/drillresults_windows.py b/src/certfuzz/drillresults/drillresults_windows.py index 0d99098..7a1a858 100644 --- a/src/certfuzz/drillresults/drillresults_windows.py +++ b/src/certfuzz/drillresults/drillresults_windows.py @@ -2,349 +2,23 @@ This script looks for interesting crashes and rate them by potential exploitability ''' -import binascii import logging import os import re -import struct from certfuzz.drillresults.common import ResultDriller -from certfuzz.drillresults.common import carve -from certfuzz.drillresults.common import carve2 -from certfuzz.drillresults.common import is_number from certfuzz.drillresults.common import main as _main -from certfuzz.drillresults.common import read_bin_file -from certfuzz.drillresults.common import reg64_set -from certfuzz.drillresults.common import reg_set -from certfuzz.drillresults.testcasebundle_base import TestCaseBundle +from certfuzz.drillresults.testcasebundle_windows import WindowsTestCaseBundle as TestCaseBundle logger = logging.getLogger(__name__) - regex = { - '64bit_debugger': re.compile('^Microsoft.*AMD64$'), 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]'), - 'mapped_address': re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), - 'mapped_address64': re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), 'msec_report': re.compile('.+.msec$'), - 'regs1': re.compile('^eax=.+'), - 'regs2': re.compile('^eip=.+'), - 'syswow64': re.compile('ModLoad:.*syswow64.*', re.IGNORECASE), } -class WindowsTestCaseBundle(TestCaseBundle): - # These !exploitable short descriptions indicate a very interesting crash - really_exploitable = [ - 'ReadAVonIP', - 'TaintedDataControlsCodeFlow', - 'ReadAVonControlFlow', - 'DEPViolation', - 'IllegalInstruction', - 'PrivilegedInstruction', - ] - - def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, - ignore_jit): - TestCaseBundle(self, dbg_outfile, testcase_file, crash_hash, re_set, - ignore_jit) - self.wow64_app = False - - def _check_64bit(self): - ''' - Check if the debugger and target app are 64-bit - ''' - for line in self.reporttext.splitlines(): - n = re.match(regex['64bit_debugger'], line) - if n: - self._64bit_debugger = True - - if self._64bit_debugger: - n = re.match(regex['syswow64'], line) - if n: - self.wow64_app = True - - def _parse_testcase(self, tcb): - ''' - Parse the msec file - ''' - reportfile = tcb.dbg_outfile - crasherfile = tcb.testcase_file - crash_hash = tcb.crash_hash - - if self.cached_testcases: - if self.cached_testcases.get(crash_hash): - self.results[crash_hash] = self.cached_testcases[crash_hash] - return - - crashid = self.results[crash_hash] - - if crasherfile == '': - # Old FOE version that didn't do multiple exceptions or rename msec - # file with exploitability - crasherfile, _junk = os.path.splitext(reportfile) - - self.get_regs() - current_dir = os.path.dirname(reportfile) - exceptionnum = self.get_ex_num() - classification = carve(self.reporttext, "Exploitability Classification: ", "\n") - try: - if classification: - # Create a new exception dictionary to add to the crash - exception = {} - crashid['exceptions'][exceptionnum] = exception - except KeyError: - # Crash ID (crash_hash) not yet seen - # Default it to not being "really exploitable" - crashid['reallyexploitable'] = False - # Create a dictionary of exceptions for the crash id - exceptions = {} - crashid['exceptions'] = exceptions - # Create a dictionary for the exception - crashid['exceptions'][exceptionnum] = exception - - # Set !exploitable classification for the exception - if classification: - crashid['exceptions'][exceptionnum]['classification'] = classification - - shortdesc = carve(self.reporttext, "Short Description: ", "\n") - if shortdesc: - # Set !exploitable Short Description for the exception - crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc - # Flag the entire crash ID as really exploitable if this is a good - # exception - crashid['reallyexploitable'] = shortdesc in self.re_set - # Check if the expected crasher file (fuzzed file) exists - if not os.path.isfile(crasherfile): - # It's not there, so try to extract the filename from the cdb - # commandline - commandline = carve(self.reporttext, "CommandLine: ", "\n") - args = commandline.split() - for arg in args: - if "sf_" in arg: - crasherfile = os.path.basename(arg) - if "-" in crasherfile: - # FOE 2.0 verify mode puts a '-' part on the - # filename when invoking cdb, however the resulting file - # is really just 'sf_.' - fileparts = crasherfile.split('-') - m = re.search('\..+', fileparts[1]) - # Recreate the original file name, minus the iteration - crasherfile = os.path.join(current_dir, fileparts[0] + m.group(0)) - else: - crasherfile = os.path.join(current_dir, crasherfile) - if not os.path.isfile(crasherfile): - # Can't find the crasher file - return - # Set the "fuzzedfile" property for the crash ID - crashid['fuzzedfile'] = crasherfile - # See if we're dealing with 64-bit debugger or target app - faultaddr = carve2(self.reporttext) - instraddr = carve(self.reporttext, "Instruction Address:", "\n") - faultaddr = self.format_addr(faultaddr) - instraddr = self.format_addr(instraddr) - - # No faulting address means no crash. - if not faultaddr or not instraddr: - return - - if self._64bit_debugger and not self.wow64_app and instraddr: - # Put backtick into instruction address for pattern matching - instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) - if shortdesc != 'DEPViolation': - faultaddr = self.fix_efa_bug(instraddr, faultaddr) - - # pc_module = pc_in_mapped_address(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(self.reporttext, instraddr, self._64bit_debugger) - - # Get the cdb line that contains the crashing instruction - instructionline = self.get_instr(self.reporttext, instraddr) - crashid['exceptions'][exceptionnum]['instructionline'] = instructionline - if instructionline: - faultaddr = self.fix_efa_offset(instructionline, faultaddr) - - # Fix faulting pattern endian - faultaddr = faultaddr.replace('0x', '') - crashid['exceptions'][exceptionnum]['efa'] = faultaddr - if self._64bit_debugger and not self.wow64_app: - # 64-bit target app - faultaddr = faultaddr.zfill(16) - efaptr = struct.unpack(' 10: - # 0x12345678 = 10 chars - return faultaddr[-8:] - - if len(faultaddr) < 10: - # pad faultaddr - return faultaddr.zfill(8) - - def fix_efa_offset(self, instructionline, faultaddr): - ''' - Adjust faulting address for instructions that use offsets - Currently only works for instructions like CALL [reg + offset] - ''' - if self._64bit_debugger and not self.wow64_app: - reg_set = reg64_set - - if '0x' not in faultaddr: - faultaddr = '0x' + faultaddr - instructionpieces = instructionline.split() - if '??' not in instructionpieces[-1]: - # The av is on the address of the code called, not the address - # of the call - return faultaddr - for index, piece in enumerate(instructionpieces): - if piece == 'call': - # CALL instruction - if len(instructionpieces) <= index + 3: - # CALL to just a register. No offset - return faultaddr - address = instructionpieces[index + 3] - if '+' in address: - splitaddress = address.split('+') - reg = splitaddress[0] - reg = reg.replace('[', '') - if reg not in reg_set: - return faultaddr - offset = splitaddress[1] - offset = offset.replace('h', '') - offset = offset.replace(']', '') - if is_number(offset): - if '0x' not in offset: - offset = '0x' + offset - if int(offset, 16) > int(faultaddr, 16): - # TODO: fix up negative numbers - return faultaddr - # Subtract offset to get actual interesting pattern - faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = self.format_addr(faultaddr.replace('L', '')) - return faultaddr - - def get_ex_num(self): - ''' - Get the exception number by counting the number of continues - ''' - if self.wow64_app: - pattern = re.compile('^[0-9]:[0-9][0-9][0-9]:x86> (.*)') - else: - pattern = re.compile('^[0-9]:[0-9][0-9][0-9]> (.*)') - - exception = 0 - - for line in self.reporttext.splitlines(): - n = re.match(pattern, line) - if n: - cdbcmd = n.group(1) - cmds = cdbcmd.split(';') - exception += cmds.count('g') - - return exception - - def get_instr(self, instraddr): - ''' - Find the disassembly line for the current (crashing) instruction - ''' - regex = re.compile('^%s\s.+.+\s+' % instraddr) - for line in self.reporttext.splitlines(): - n = regex.match(line) - if n: - return line - - def fix_efa_bug(self, instraddr, faultaddr): - ''' - !exploitable often reports an incorrect EFA for 64-bit targets. - If we're dealing with a 64-bit target, we can second-guess the reported EFA - ''' - instructionline = self.get_instr(instraddr) - if not instructionline or "=" not in instructionline: - # Nothing to fix - return faultaddr - if 'ds:' in instructionline: - # There's a target address in the msec file - if '??' in instructionline: - # The AV is on dereferencing where to call - ds = carve(instructionline, "ds:", "=") - else: - # The AV is on accessing the code location - ds = instructionline.split("=")[-1] - else: - # AV must be on current instruction - ds = instructionline.split(' ')[0] - if ds: - faultaddr = ds.replace('`', '') - return faultaddr - - def get_regs(self): - ''' - Populate the register dictionary with register values at crash - ''' - for line in self.reporttext.splitlines(): - if regex['regs1'].match(line) or regex['regs2'].match(line): - regs1 = line.split() - for reg in regs1: - if "=" in reg: - splitreg = reg.split("=") - self.regdict[splitreg[0]] = splitreg[1] - - class WindowsResultDriller(ResultDriller): def _platform_find_testcases(self, crash_hash, files, root): if "0x" in crash_hash: @@ -358,7 +32,7 @@ def _platform_find_testcases(self, crash_hash, files, root): # If it's exception #0, strip out the exploitability part of # the file name. This gives us the crasher file name if regex['first_msec'].match(current_file): - crasherfile, reportfileext = os.path.splitext(current_file) + crasherfile, _junk = os.path.splitext(current_file) crasherfile = crasherfile.replace('-EXP', '') crasherfile = crasherfile.replace('-PEX', '') crasherfile = crasherfile.replace('-PNE', '') diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py new file mode 100644 index 0000000..610befc --- /dev/null +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -0,0 +1,336 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import binascii +import os +import re +import struct + +from certfuzz.drillresults.common import carve +from certfuzz.drillresults.common import carve2 +from certfuzz.drillresults.common import is_number +from certfuzz.drillresults.common import read_bin_file +from certfuzz.drillresults.common import reg64_set +from certfuzz.drillresults.testcasebundle_base import TestCaseBundle + +regex = { + '64bit_debugger': re.compile('^Microsoft.*AMD64$'), + 'mapped_address': re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), + 'mapped_address64': re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), + 'regs1': re.compile('^eax=.+'), + 'regs2': re.compile('^eip=.+'), + 'syswow64': re.compile('ModLoad:.*syswow64.*', re.IGNORECASE), + } + + +class WindowsTestCaseBundle(TestCaseBundle): + # These !exploitable short descriptions indicate a very interesting crash + really_exploitable = [ + 'ReadAVonIP', + 'TaintedDataControlsCodeFlow', + 'ReadAVonControlFlow', + 'DEPViolation', + 'IllegalInstruction', + 'PrivilegedInstruction', + ] + + def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, + ignore_jit): + TestCaseBundle(self, dbg_outfile, testcase_file, crash_hash, re_set, + ignore_jit) + self.wow64_app = False + + def _check_64bit(self): + ''' + Check if the debugger and target app are 64-bit + ''' + for line in self.reporttext.splitlines(): + n = re.match(regex['64bit_debugger'], line) + if n: + self._64bit_debugger = True + + if self._64bit_debugger: + n = re.match(regex['syswow64'], line) + if n: + self.wow64_app = True + + def _parse_testcase(self, tcb): + ''' + Parse the msec file + ''' + reportfile = tcb.dbg_outfile + crasherfile = tcb.testcase_file + crash_hash = tcb.crash_hash + + if self.cached_testcases: + if self.cached_testcases.get(crash_hash): + self.results[crash_hash] = self.cached_testcases[crash_hash] + return + + crashid = self.results[crash_hash] + + if crasherfile == '': + # Old FOE version that didn't do multiple exceptions or rename msec + # file with exploitability + crasherfile, _junk = os.path.splitext(reportfile) + + self.get_regs() + current_dir = os.path.dirname(reportfile) + exceptionnum = self.get_ex_num() + classification = carve(self.reporttext, "Exploitability Classification: ", "\n") + try: + if classification: + # Create a new exception dictionary to add to the crash + exception = {} + crashid['exceptions'][exceptionnum] = exception + except KeyError: + # Crash ID (crash_hash) not yet seen + # Default it to not being "really exploitable" + crashid['reallyexploitable'] = False + # Create a dictionary of exceptions for the crash id + exceptions = {} + crashid['exceptions'] = exceptions + # Create a dictionary for the exception + crashid['exceptions'][exceptionnum] = exception + + # Set !exploitable classification for the exception + if classification: + crashid['exceptions'][exceptionnum]['classification'] = classification + + shortdesc = carve(self.reporttext, "Short Description: ", "\n") + if shortdesc: + # Set !exploitable Short Description for the exception + crashid['exceptions'][exceptionnum]['shortdesc'] = shortdesc + # Flag the entire crash ID as really exploitable if this is a good + # exception + crashid['reallyexploitable'] = shortdesc in self.re_set + # Check if the expected crasher file (fuzzed file) exists + if not os.path.isfile(crasherfile): + # It's not there, so try to extract the filename from the cdb + # commandline + commandline = carve(self.reporttext, "CommandLine: ", "\n") + args = commandline.split() + for arg in args: + if "sf_" in arg: + crasherfile = os.path.basename(arg) + if "-" in crasherfile: + # FOE 2.0 verify mode puts a '-' part on the + # filename when invoking cdb, however the resulting file + # is really just 'sf_.' + fileparts = crasherfile.split('-') + m = re.search('\..+', fileparts[1]) + # Recreate the original file name, minus the iteration + crasherfile = os.path.join(current_dir, fileparts[0] + m.group(0)) + else: + crasherfile = os.path.join(current_dir, crasherfile) + if not os.path.isfile(crasherfile): + # Can't find the crasher file + return + # Set the "fuzzedfile" property for the crash ID + crashid['fuzzedfile'] = crasherfile + # See if we're dealing with 64-bit debugger or target app + faultaddr = carve2(self.reporttext) + instraddr = carve(self.reporttext, "Instruction Address:", "\n") + faultaddr = self.format_addr(faultaddr) + instraddr = self.format_addr(instraddr) + + # No faulting address means no crash. + if not faultaddr or not instraddr: + return + + if self._64bit_debugger and not self.wow64_app and instraddr: + # Put backtick into instruction address for pattern matching + instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) + if shortdesc != 'DEPViolation': + faultaddr = self.fix_efa_bug(instraddr, faultaddr) + + # pc_module = pc_in_mapped_address(reporttext, instraddr) + crashid['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(self.reporttext, instraddr, self._64bit_debugger) + + # Get the cdb line that contains the crashing instruction + instructionline = self.get_instr(self.reporttext, instraddr) + crashid['exceptions'][exceptionnum]['instructionline'] = instructionline + if instructionline: + faultaddr = self.fix_efa_offset(instructionline, faultaddr) + + # Fix faulting pattern endian + faultaddr = faultaddr.replace('0x', '') + crashid['exceptions'][exceptionnum]['efa'] = faultaddr + if self._64bit_debugger and not self.wow64_app: + # 64-bit target app + faultaddr = faultaddr.zfill(16) + efaptr = struct.unpack(' 10: + # 0x12345678 = 10 chars + return faultaddr[-8:] + + if len(faultaddr) < 10: + # pad faultaddr + return faultaddr.zfill(8) + + def fix_efa_offset(self, instructionline, faultaddr): + ''' + Adjust faulting address for instructions that use offsets + Currently only works for instructions like CALL [reg + offset] + ''' + if self._64bit_debugger and not self.wow64_app: + reg_set = reg64_set + + if '0x' not in faultaddr: + faultaddr = '0x' + faultaddr + instructionpieces = instructionline.split() + if '??' not in instructionpieces[-1]: + # The av is on the address of the code called, not the address + # of the call + return faultaddr + for index, piece in enumerate(instructionpieces): + if piece == 'call': + # CALL instruction + if len(instructionpieces) <= index + 3: + # CALL to just a register. No offset + return faultaddr + address = instructionpieces[index + 3] + if '+' in address: + splitaddress = address.split('+') + reg = splitaddress[0] + reg = reg.replace('[', '') + if reg not in reg_set: + return faultaddr + offset = splitaddress[1] + offset = offset.replace('h', '') + offset = offset.replace(']', '') + if is_number(offset): + if '0x' not in offset: + offset = '0x' + offset + if int(offset, 16) > int(faultaddr, 16): + # TODO: fix up negative numbers + return faultaddr + # Subtract offset to get actual interesting pattern + faultaddr = hex(eval(faultaddr) - eval(offset)) + faultaddr = self.format_addr(faultaddr.replace('L', '')) + return faultaddr + + def get_ex_num(self): + ''' + Get the exception number by counting the number of continues + ''' + if self.wow64_app: + pattern = re.compile('^[0-9]:[0-9][0-9][0-9]:x86> (.*)') + else: + pattern = re.compile('^[0-9]:[0-9][0-9][0-9]> (.*)') + + exception = 0 + + for line in self.reporttext.splitlines(): + n = re.match(pattern, line) + if n: + cdbcmd = n.group(1) + cmds = cdbcmd.split(';') + exception += cmds.count('g') + + return exception + + def get_instr(self, instraddr): + ''' + Find the disassembly line for the current (crashing) instruction + ''' + regex = re.compile('^%s\s.+.+\s+' % instraddr) + for line in self.reporttext.splitlines(): + n = regex.match(line) + if n: + return line + + def fix_efa_bug(self, instraddr, faultaddr): + ''' + !exploitable often reports an incorrect EFA for 64-bit targets. + If we're dealing with a 64-bit target, we can second-guess the reported EFA + ''' + instructionline = self.get_instr(instraddr) + if not instructionline or "=" not in instructionline: + # Nothing to fix + return faultaddr + if 'ds:' in instructionline: + # There's a target address in the msec file + if '??' in instructionline: + # The AV is on dereferencing where to call + ds = carve(instructionline, "ds:", "=") + else: + # The AV is on accessing the code location + ds = instructionline.split("=")[-1] + else: + # AV must be on current instruction + ds = instructionline.split(' ')[0] + if ds: + faultaddr = ds.replace('`', '') + return faultaddr + + def get_regs(self): + ''' + Populate the register dictionary with register values at crash + ''' + for line in self.reporttext.splitlines(): + if regex['regs1'].match(line) or regex['regs2'].match(line): + regs1 = line.split() + for reg in regs1: + if "=" in reg: + splitreg = reg.split("=") + self.regdict[splitreg[0]] = splitreg[1] diff --git a/src/certfuzz/test/drillresults/test_testcasebundle_windows.py b/src/certfuzz/test/drillresults/test_testcasebundle_windows.py new file mode 100644 index 0000000..a2b06ad --- /dev/null +++ b/src/certfuzz/test/drillresults/test_testcasebundle_windows.py @@ -0,0 +1,23 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import unittest +from certfuzz.drillresults import testcasebundle_windows + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 152b685e4ea523ad11341efc2006345bef0e6f0a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 10:40:58 -0400 Subject: [PATCH 0428/1169] split LinuxTestCaseBundle into its own module --- .../drillresults/drillresults_linux.py | 299 +---------------- .../drillresults/testcasebundle_linux.py | 308 ++++++++++++++++++ .../drillresults/test_testcasebundle_linux.py | 23 ++ 3 files changed, 332 insertions(+), 298 deletions(-) create mode 100644 src/certfuzz/drillresults/testcasebundle_linux.py create mode 100644 src/certfuzz/test/drillresults/test_testcasebundle_linux.py diff --git a/src/certfuzz/drillresults/drillresults_linux.py b/src/certfuzz/drillresults/drillresults_linux.py index cc73062..4ed865c 100755 --- a/src/certfuzz/drillresults/drillresults_linux.py +++ b/src/certfuzz/drillresults/drillresults_linux.py @@ -2,312 +2,15 @@ This script looks for interesting crashes and rate them by potential exploitability ''' -import binascii import logging import os -import re -import struct from certfuzz.drillresults.common import ResultDriller -from certfuzz.drillresults.common import carve -from certfuzz.drillresults.common import carve2 from certfuzz.drillresults.common import main as _main -from certfuzz.drillresults.common import reg_set -from certfuzz.drillresults.testcasebundle_base import TestCaseBundle - +from certfuzz.drillresults.testcasebundle_linux import LinuxTestCaseBundle logger = logging.getLogger(__name__) -regex = { - '64bit_debugger': re.compile(r'^Microsoft.*AMD64$'), - 'bt_addr': re.compile(r'(0x[0-9a-fA-F]+)\s+.+$'), - 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), - 'dbg_prompt': re.compile(r'^[0-9]:[0-9][0-9][0-9]> (.*)'), - 'frame0': re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+'), - 'gdb_report': re.compile(r'.+.gdb$'), - 'mapped_address': re.compile(r'^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), - 'mapped_address64': re.compile(r'^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), - 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)'), - 'regs1': re.compile(r'^eax=.+'), - 'regs2': re.compile(r'^eip=.+'), - 'syswow64': re.compile(r'ModLoad:.*syswow64.*', re.IGNORECASE), - 'vdso': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+\[vdso\]'), - } - - -class LinuxTestCaseBundle(TestCaseBundle): - really_exploitable = [ - 'SegFaultOnPc', - 'BranchAv', - 'StackCodeExection', - 'BadInstruction', - 'ReturnAv', - ] - - def _check_64bit(self): - ''' - Check if the debugger and target app are 64-bit - ''' - for line in self.reporttext.splitlines(): - m = re.match(regex['bt_addr'], line) - if m: - start_addr = m.group(1) - if len(start_addr) > 10: - self._64bit_debugger = True - logger.debug() - - def _parse_testcase(self): - ''' - Parse the gdb file - ''' - crasherfile = self.testcase_file - reporttext = self.reporttext - _64bit_debugger = self._64bit_debugger - crasherdata = self.crasherdata - - # global _64bit_debugger - - # TODO move this back to ResultDriller class -# if self.cached_results: -# if self.cached_results.get(crash_hash): -# self.results[crash_hash] = self.cached_results[crash_hash] -# return - - details = self.details - - exceptionnum = 0 - classification = carve(reporttext, "Classification: ", "\n") - #print 'classification: %s' % classification - try: - if classification: - # Create a new exception dictionary to add to the crash - exception = {} - details['exceptions'][exceptionnum] = exception - except KeyError: - # Crash ID (crash_hash) not yet seen - # Default it to not being "really exploitable" - details['reallyexploitable'] = False - # Create a dictionary of exceptions for the crash id - exceptions = {} - details['exceptions'] = exceptions - # Create a dictionary for the exception - details['exceptions'][exceptionnum] = exception - - # Set !exploitable classification for the exception - if classification: - details['exceptions'][exceptionnum]['classification'] = classification - - shortdesc = carve(reporttext, "Short description: ", " (") - #print 'shortdesc: %s' % shortdesc - if shortdesc: - # Set !exploitable Short Description for the exception - details['exceptions'][exceptionnum]['shortdesc'] = shortdesc - # Flag the entire crash ID as really exploitable if this is a good - # exception - details['reallyexploitable'] = shortdesc in self.re_set - - if not os.path.isfile(crasherfile): - # Can't find the crasher file - #print "WTF! Cannot find %s" % crasherfile - return - # Set the "fuzzedfile" property for the crash ID - details['fuzzedfile'] = crasherfile - faultaddr = carve2(reporttext) - instraddr = self.get_instr_addr() - faultaddr = self.format_addr(faultaddr, _64bit_debugger) - instraddr = self.format_addr(instraddr, _64bit_debugger) - - # No faulting address means no crash. - if not faultaddr: - return - - if instraddr: - details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(instraddr) - - # Get the cdb line that contains the crashing instruction - instructionline = self.get_instr(instraddr) - details['exceptions'][exceptionnum]['instructionline'] = instructionline - if instructionline: - faultaddr = self.fix_efa_offset(instructionline, faultaddr) - - # Fix faulting pattern endian - faultaddr = faultaddr.replace('0x', '') - details['exceptions'][exceptionnum]['efa'] = faultaddr - if _64bit_debugger: - # 64-bit target app - faultaddr = faultaddr.zfill(16) - efaptr = struct.unpack(' 10: # 0x12345678 = 10 chars - faultaddr = faultaddr[-8:] - elif len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(8) - - return faultaddr - - def fix_efa_offset(self, instructionline, faultaddr): - ''' - Adjust faulting address for instructions that use offsets - Currently only works for instructions like CALL [reg + offset] - ''' - if '0x' not in faultaddr: - faultaddr = '0x' + faultaddr - instructionpieces = instructionline.split() - for index, piece in enumerate(instructionpieces): - if piece == 'call': - # CALL instruction - if len(instructionpieces) <= index + 3: - # CALL to just a register. No offset - return faultaddr - address = instructionpieces[index + 3] - if '+' in address: - splitaddress = address.split('+') - reg = splitaddress[0] - reg = reg.replace('[', '') - if reg not in reg_set: - return faultaddr - offset = splitaddress[1] - offset = offset.replace('h', '') - offset = offset.replace(']', '') - if '0x' not in offset: - offset = '0x' + offset - if int(offset, 16) > int(faultaddr, 16): - # TODO: fix up negative numbers - return faultaddr - # Subtract offset to get actual interesting pattern - faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = self.format_addr(faultaddr.replace('L', '')) - return faultaddr - - def get_ex_num(self): - ''' - Get the exception number by counting the number of continues - ''' - exception = 0 - for line in self.reporttext.splitlines(): - n = re.match(regex['dbg_prompt'], line) - if n: - cdbcmd = n.group(1) - cmds = cdbcmd.split(';') - for cmd in cmds: - if cmd == 'g': - exception = exception + 1 - return exception - - def get_instr(self, instraddr): - ''' - Find the disassembly line for the current (crashing) instruction - ''' - rgx = regex['current_instr'] - for line in self.reporttext.splitlines(): - n = rgx.match(line) - if n: - return n.group(3) - return '' - - def fix_efa_bug(self, instraddr, faultaddr): - ''' - !exploitable often reports an incorrect EFA for 64-bit targets. - If we're dealing with a 64-bit target, we can second-guess the reported EFA - ''' - instructionline = self.get_instr(instraddr) - if not instructionline: - return faultaddr - ds = carve(instructionline, "ds:", "=") - if ds: - faultaddr = ds.replace('`', '') - return faultaddr - - def get_instr_addr(self): - ''' - Find the address for the current (crashing) instruction - ''' - instraddr = None - for line in self.reporttext.splitlines(): - #print 'checking: %s' % line - n = re.match(regex['current_instr'], line) - if n: - instraddr = n.group(1) - #print 'Found instruction address: %s' % instraddr - if not instraddr: - for line in self.reporttext.splitlines(): - #No disassembly. Resort to frame 0 address - n = re.match(regex['frame0'], line) - if n: - instraddr = n.group(1) - #print 'Found instruction address: %s' % instraddr - return instraddr - class LinuxResultDriller(ResultDriller): def _platform_find_testcases(self, crash_hash, files, root): diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py new file mode 100644 index 0000000..381e928 --- /dev/null +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -0,0 +1,308 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import binascii +import struct + +from certfuzz.drillresults.common import carve +from certfuzz.drillresults.common import carve2 +from certfuzz.drillresults.common import reg_set +from certfuzz.drillresults.testcasebundle_base import TestCaseBundle +import logging +import re +import os + + +logger = logging.getLogger(__name__) + +regex = { + '64bit_debugger': re.compile(r'^Microsoft.*AMD64$'), + 'bt_addr': re.compile(r'(0x[0-9a-fA-F]+)\s+.+$'), + 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), + 'dbg_prompt': re.compile(r'^[0-9]:[0-9][0-9][0-9]> (.*)'), + 'frame0': re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+'), + 'gdb_report': re.compile(r'.+.gdb$'), + 'mapped_address': re.compile(r'^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), + 'mapped_address64': re.compile(r'^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), + 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)'), + 'regs1': re.compile(r'^eax=.+'), + 'regs2': re.compile(r'^eip=.+'), + 'syswow64': re.compile(r'ModLoad:.*syswow64.*', re.IGNORECASE), + 'vdso': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+\[vdso\]'), + } + + +class LinuxTestCaseBundle(TestCaseBundle): + really_exploitable = [ + 'SegFaultOnPc', + 'BranchAv', + 'StackCodeExection', + 'BadInstruction', + 'ReturnAv', + ] + + def _check_64bit(self): + ''' + Check if the debugger and target app are 64-bit + ''' + for line in self.reporttext.splitlines(): + m = re.match(regex['bt_addr'], line) + if m: + start_addr = m.group(1) + if len(start_addr) > 10: + self._64bit_debugger = True + logger.debug() + + def _parse_testcase(self): + ''' + Parse the gdb file + ''' + crasherfile = self.testcase_file + reporttext = self.reporttext + _64bit_debugger = self._64bit_debugger + crasherdata = self.crasherdata + + # global _64bit_debugger + + # TODO move this back to ResultDriller class +# if self.cached_results: +# if self.cached_results.get(crash_hash): +# self.results[crash_hash] = self.cached_results[crash_hash] +# return + + details = self.details + + exceptionnum = 0 + classification = carve(reporttext, "Classification: ", "\n") + #print 'classification: %s' % classification + try: + if classification: + # Create a new exception dictionary to add to the crash + exception = {} + details['exceptions'][exceptionnum] = exception + except KeyError: + # Crash ID (crash_hash) not yet seen + # Default it to not being "really exploitable" + details['reallyexploitable'] = False + # Create a dictionary of exceptions for the crash id + exceptions = {} + details['exceptions'] = exceptions + # Create a dictionary for the exception + details['exceptions'][exceptionnum] = exception + + # Set !exploitable classification for the exception + if classification: + details['exceptions'][exceptionnum]['classification'] = classification + + shortdesc = carve(reporttext, "Short description: ", " (") + #print 'shortdesc: %s' % shortdesc + if shortdesc: + # Set !exploitable Short Description for the exception + details['exceptions'][exceptionnum]['shortdesc'] = shortdesc + # Flag the entire crash ID as really exploitable if this is a good + # exception + details['reallyexploitable'] = shortdesc in self.re_set + + if not os.path.isfile(crasherfile): + # Can't find the crasher file + #print "WTF! Cannot find %s" % crasherfile + return + # Set the "fuzzedfile" property for the crash ID + details['fuzzedfile'] = crasherfile + faultaddr = carve2(reporttext) + instraddr = self.get_instr_addr() + faultaddr = self.format_addr(faultaddr, _64bit_debugger) + instraddr = self.format_addr(instraddr, _64bit_debugger) + + # No faulting address means no crash. + if not faultaddr: + return + + if instraddr: + details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(instraddr) + + # Get the cdb line that contains the crashing instruction + instructionline = self.get_instr(instraddr) + details['exceptions'][exceptionnum]['instructionline'] = instructionline + if instructionline: + faultaddr = self.fix_efa_offset(instructionline, faultaddr) + + # Fix faulting pattern endian + faultaddr = faultaddr.replace('0x', '') + details['exceptions'][exceptionnum]['efa'] = faultaddr + if _64bit_debugger: + # 64-bit target app + faultaddr = faultaddr.zfill(16) + efaptr = struct.unpack(' 10: # 0x12345678 = 10 chars + faultaddr = faultaddr[-8:] + elif len(faultaddr) < 10: + # pad faultaddr + faultaddr = faultaddr.zfill(8) + + return faultaddr + + def fix_efa_offset(self, instructionline, faultaddr): + ''' + Adjust faulting address for instructions that use offsets + Currently only works for instructions like CALL [reg + offset] + ''' + if '0x' not in faultaddr: + faultaddr = '0x' + faultaddr + instructionpieces = instructionline.split() + for index, piece in enumerate(instructionpieces): + if piece == 'call': + # CALL instruction + if len(instructionpieces) <= index + 3: + # CALL to just a register. No offset + return faultaddr + address = instructionpieces[index + 3] + if '+' in address: + splitaddress = address.split('+') + reg = splitaddress[0] + reg = reg.replace('[', '') + if reg not in reg_set: + return faultaddr + offset = splitaddress[1] + offset = offset.replace('h', '') + offset = offset.replace(']', '') + if '0x' not in offset: + offset = '0x' + offset + if int(offset, 16) > int(faultaddr, 16): + # TODO: fix up negative numbers + return faultaddr + # Subtract offset to get actual interesting pattern + faultaddr = hex(eval(faultaddr) - eval(offset)) + faultaddr = self.format_addr(faultaddr.replace('L', '')) + return faultaddr + + def get_ex_num(self): + ''' + Get the exception number by counting the number of continues + ''' + exception = 0 + for line in self.reporttext.splitlines(): + n = re.match(regex['dbg_prompt'], line) + if n: + cdbcmd = n.group(1) + cmds = cdbcmd.split(';') + for cmd in cmds: + if cmd == 'g': + exception = exception + 1 + return exception + + def get_instr(self, instraddr): + ''' + Find the disassembly line for the current (crashing) instruction + ''' + rgx = regex['current_instr'] + for line in self.reporttext.splitlines(): + n = rgx.match(line) + if n: + return n.group(3) + return '' + + def fix_efa_bug(self, instraddr, faultaddr): + ''' + !exploitable often reports an incorrect EFA for 64-bit targets. + If we're dealing with a 64-bit target, we can second-guess the reported EFA + ''' + instructionline = self.get_instr(instraddr) + if not instructionline: + return faultaddr + ds = carve(instructionline, "ds:", "=") + if ds: + faultaddr = ds.replace('`', '') + return faultaddr + + def get_instr_addr(self): + ''' + Find the address for the current (crashing) instruction + ''' + instraddr = None + for line in self.reporttext.splitlines(): + #print 'checking: %s' % line + n = re.match(regex['current_instr'], line) + if n: + instraddr = n.group(1) + #print 'Found instruction address: %s' % instraddr + if not instraddr: + for line in self.reporttext.splitlines(): + #No disassembly. Resort to frame 0 address + n = re.match(regex['frame0'], line) + if n: + instraddr = n.group(1) + #print 'Found instruction address: %s' % instraddr + return instraddr diff --git a/src/certfuzz/test/drillresults/test_testcasebundle_linux.py b/src/certfuzz/test/drillresults/test_testcasebundle_linux.py new file mode 100644 index 0000000..092c3fd --- /dev/null +++ b/src/certfuzz/test/drillresults/test_testcasebundle_linux.py @@ -0,0 +1,23 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import unittest +from certfuzz.drillresults import testcasebundle_linux + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 3543ae30beadf648fd7cd6e59f8cc5b93f841be4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 10:51:03 -0400 Subject: [PATCH 0429/1169] split ResultDriller into its own module also fix up unit tests accordingly --- src/certfuzz/drillresults/common.py | 141 +--------------- .../drillresults/drillresults_linux.py | 2 +- .../drillresults/drillresults_windows.py | 2 +- .../drillresults/result_driller_base.py | 151 ++++++++++++++++++ src/certfuzz/test/drillresults/test_common.py | 30 ---- .../drillresults/test_result_driller_base.py | 40 +++++ src/certfuzz/test/fuzztools/test_filetools.py | 23 ++- 7 files changed, 211 insertions(+), 178 deletions(-) create mode 100644 src/certfuzz/drillresults/result_driller_base.py create mode 100644 src/certfuzz/test/drillresults/test_result_driller_base.py diff --git a/src/certfuzz/drillresults/common.py b/src/certfuzz/drillresults/common.py index 1b3242f..b18f9a3 100644 --- a/src/certfuzz/drillresults/common.py +++ b/src/certfuzz/drillresults/common.py @@ -4,17 +4,13 @@ @organization: cert.org ''' import StringIO -import abc import argparse import logging -import os import zipfile from certfuzz.fuzztools.filetools import read_bin_file as _read_bin_file -import cPickle as pickle -from certfuzz.tools.common.errors import DrillResultsError -from certfuzz.tools.common.errors import TestCaseBundleError +from certfuzz.drillresults.result_driller_base import ResultDriller logger = logging.getLogger(__name__) @@ -164,141 +160,6 @@ def read_bin_file(inputfile): return filebytes + zipbytes -class ResultDriller(object): - __metaclass__ = abc.ABCMeta - - def __init__(self, - ignore_jit=False, - base_dir='../results', - force_reload=False, - report_all=False): - self.ignore_jit = ignore_jit - self.base_dir = base_dir - self.tld = None - self.force = force_reload - self.report_all = report_all - - if report_all: - self.max_score = None - else: - self.max_score = 70 - - self.pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') - self.cached_testcases = None - self.testcase_bundles = [] - - def __enter__(self): - return self - - def __exit__(self, etype, value, traceback): - handled = False - - if etype is DrillResultsError: - print "{}: {}".format(etype.__name__, value) - handled = True - - return handled - - @abc.abstractmethod - def _platform_find_testcases(self, crash_hash): - pass - - def process_testcases(self): - ''' - Crawls self.tld looking for crash directories to process. Puts a list - of tuples into self.testcase_bundles. - ''' - # Walk the results directory - for root, dirs, files in os.walk(self.tld): - logger.debug('Looking for testcases in %s', root) - dir_basename = os.path.basename(root) - try: - self._platform_find_testcases(dir_basename, files, root) - except TestCaseBundleError as e: - logger.warning('Skipping %s: %s', dir_basename, e) - continue - - def _check_dirs(self): - check_dirs = [self.base_dir, 'results', 'crashers'] - for d in check_dirs: - if os.path.isdir(d): - self.tld = d - logger.debug('found dir: %s', self.tld) - return - # if you got here, none of them exist - raise DrillResultsError('None of {} appears to be a dir'.format(check_dirs)) - - def load_cached(self): - if self.force: - logger.info('--force option used, ignoring cached results') - return - - try: - with open(self.pickle_file, 'rb') as pkl_file: - self.cached_testcases = pickle.load(pkl_file) - except IOError: - # No cached results - pass - - @property - def crash_scores(self): - return dict([(tcb.crash_hash, tcb.score) for tcb in self.testcase_bundles]) - - def print_crash_report(self, crash_key, score, details): -# details = self.results[crash_key] - print '\n%s - Exploitability rank: %s' % (crash_key, score) - print 'Fuzzed file: %s' % details['fuzzedfile'] - for exception in details['exceptions']: - shortdesc = details['exceptions'][exception]['shortdesc'] - eiftext = '' - efa = '0x' + details['exceptions'][exception]['efa'] - if details['exceptions'][exception]['EIF']: - eiftext = " *** Byte pattern is in fuzzed file! ***" - print 'exception %s: %s accessing %s %s' % (exception, shortdesc, efa, eiftext) - if details['exceptions'][exception]['instructionline']: - print details['exceptions'][exception]['instructionline'] - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded': - if not self.ignore_jit: - print 'Instruction pointer is not in a loaded module!' - else: - print 'Code executing in: %s' % module - - @property - def sorted_crashes(self): - return sorted(self.crash_scores.iteritems(), key=lambda(k, v): (v, k)) - - def print_reports(self): - results = dict([(tcb.crash_hash, tcb.details) for tcb in self.testcase_bundles]) - print "--- Interesting crashes ---\n" - for crash_key, score in self.sorted_crashes: - if self.max_score is not None: - if score > self.max_score: - # skip test cases with scores above our max - continue - - details = results[crash_key] - try: - self.print_crash_report(crash_key, score, details) - except KeyError as e: - logger.warning('Tescase %s is missing information: %s', crash_key, e) - - def cache_results(self): - pkldir = os.path.dirname(self.pickle_file) - if not os.path.exists(pkldir): - os.makedirs(pkldir) - with open(self.pickle_file, 'wb') as pkl_file: - pickle.dump(self.testcase_bundles, pkl_file, -1) - - def drill_results(self): - logger.debug('drill_results') - self._check_dirs() - self.load_cached() - self.process_testcases() - self.print_reports() - self.cache_results() - - def main(driller_class=ResultDriller): ''' Main method for drill results script. Platform-specific customizations are diff --git a/src/certfuzz/drillresults/drillresults_linux.py b/src/certfuzz/drillresults/drillresults_linux.py index 4ed865c..c291740 100755 --- a/src/certfuzz/drillresults/drillresults_linux.py +++ b/src/certfuzz/drillresults/drillresults_linux.py @@ -5,9 +5,9 @@ import logging import os -from certfuzz.drillresults.common import ResultDriller from certfuzz.drillresults.common import main as _main from certfuzz.drillresults.testcasebundle_linux import LinuxTestCaseBundle +from certfuzz.drillresults.result_driller_base import ResultDriller logger = logging.getLogger(__name__) diff --git a/src/certfuzz/drillresults/drillresults_windows.py b/src/certfuzz/drillresults/drillresults_windows.py index 7a1a858..cb4d283 100644 --- a/src/certfuzz/drillresults/drillresults_windows.py +++ b/src/certfuzz/drillresults/drillresults_windows.py @@ -6,9 +6,9 @@ import os import re -from certfuzz.drillresults.common import ResultDriller from certfuzz.drillresults.common import main as _main from certfuzz.drillresults.testcasebundle_windows import WindowsTestCaseBundle as TestCaseBundle +from certfuzz.drillresults.result_driller_base import ResultDriller logger = logging.getLogger(__name__) diff --git a/src/certfuzz/drillresults/result_driller_base.py b/src/certfuzz/drillresults/result_driller_base.py new file mode 100644 index 0000000..7201897 --- /dev/null +++ b/src/certfuzz/drillresults/result_driller_base.py @@ -0,0 +1,151 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import abc +import logging +import os + +import cPickle as pickle +from certfuzz.drillresults.errors import DrillResultsError +from certfuzz.drillresults.errors import TestCaseBundleError + + +logger = logging.getLogger(__name__) + + +class ResultDriller(object): + __metaclass__ = abc.ABCMeta + + def __init__(self, + ignore_jit=False, + base_dir='../results', + force_reload=False, + report_all=False): + self.ignore_jit = ignore_jit + self.base_dir = base_dir + self.tld = None + self.force = force_reload + self.report_all = report_all + + if report_all: + self.max_score = None + else: + self.max_score = 70 + + self.pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') + self.cached_testcases = None + self.testcase_bundles = [] + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + handled = False + + if etype is DrillResultsError: + print "{}: {}".format(etype.__name__, value) + handled = True + + return handled + + @abc.abstractmethod + def _platform_find_testcases(self, crash_hash): + pass + + def process_testcases(self): + ''' + Crawls self.tld looking for crash directories to process. Puts a list + of tuples into self.testcase_bundles. + ''' + # Walk the results directory + for root, dirs, files in os.walk(self.tld): + logger.debug('Looking for testcases in %s', root) + dir_basename = os.path.basename(root) + try: + self._platform_find_testcases(dir_basename, files, root) + except TestCaseBundleError as e: + logger.warning('Skipping %s: %s', dir_basename, e) + continue + + def _check_dirs(self): + check_dirs = [self.base_dir, 'results', 'crashers'] + for d in check_dirs: + if os.path.isdir(d): + self.tld = d + logger.debug('found dir: %s', self.tld) + return + # if you got here, none of them exist + raise DrillResultsError('None of {} appears to be a dir'.format(check_dirs)) + + def load_cached(self): + if self.force: + logger.info('--force option used, ignoring cached results') + return + + try: + with open(self.pickle_file, 'rb') as pkl_file: + self.cached_testcases = pickle.load(pkl_file) + except IOError: + # No cached results + pass + + @property + def crash_scores(self): + return dict([(tcb.crash_hash, tcb.score) for tcb in self.testcase_bundles]) + + def print_crash_report(self, crash_key, score, details): +# details = self.results[crash_key] + print '\n%s - Exploitability rank: %s' % (crash_key, score) + print 'Fuzzed file: %s' % details['fuzzedfile'] + for exception in details['exceptions']: + shortdesc = details['exceptions'][exception]['shortdesc'] + eiftext = '' + efa = '0x' + details['exceptions'][exception]['efa'] + if details['exceptions'][exception]['EIF']: + eiftext = " *** Byte pattern is in fuzzed file! ***" + print 'exception %s: %s accessing %s %s' % (exception, shortdesc, efa, eiftext) + if details['exceptions'][exception]['instructionline']: + print details['exceptions'][exception]['instructionline'] + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded': + if not self.ignore_jit: + print 'Instruction pointer is not in a loaded module!' + else: + print 'Code executing in: %s' % module + + @property + def sorted_crashes(self): + return sorted(self.crash_scores.iteritems(), key=lambda(k, v): (v, k)) + + def print_reports(self): + results = dict([(tcb.crash_hash, tcb.details) for tcb in self.testcase_bundles]) + print "--- Interesting crashes ---\n" + for crash_key, score in self.sorted_crashes: + if self.max_score is not None: + if score > self.max_score: + # skip test cases with scores above our max + continue + + details = results[crash_key] + try: + self.print_crash_report(crash_key, score, details) + except KeyError as e: + logger.warning('Tescase %s is missing information: %s', crash_key, e) + + def cache_results(self): + pkldir = os.path.dirname(self.pickle_file) + if not os.path.exists(pkldir): + os.makedirs(pkldir) + with open(self.pickle_file, 'wb') as pkl_file: + pickle.dump(self.testcase_bundles, pkl_file, -1) + + def drill_results(self): + logger.debug('drill_results') + self._check_dirs() + self.load_cached() + self.process_testcases() + self.print_reports() + self.cache_results() + diff --git a/src/certfuzz/test/drillresults/test_common.py b/src/certfuzz/test/drillresults/test_common.py index f0edf82..fda576e 100644 --- a/src/certfuzz/test/drillresults/test_common.py +++ b/src/certfuzz/test/drillresults/test_common.py @@ -9,21 +9,6 @@ import shutil import os -alphabet = 'abcdefghijklmnopqrstuvwxyz' - - -class MockRd(drillresults.ResultDriller): - # really_exploitable expects a list - really_exploitable = list(alphabet) - - def _parse_testcase(self): - pass - - def _platform_find_testcases(self): - pass - - def check_64bit(self): - pass class Test(unittest.TestCase): @@ -54,14 +39,6 @@ def test_parse_args(self): # not much to test here pass - def test_read_file(self): - fd, f = tempfile.mkstemp(text=True) - os.write(fd, 'fizzle') - os.close(fd) - result = drillresults.read_text_file(f) - self.assertEqual('fizzle', result) - os.remove(f) - def test_carve(self): s = 'redbluegreenyellowred' r = drillresults.carve(s, 'red', 'red') @@ -90,13 +67,6 @@ def test_is_number(self): for s in expect_false: self.assertFalse(drillresults.is_number(s)) - def test_rd_acts_as_metaclass(self): - self.assertRaises(TypeError, drillresults.ResultDriller) - - def test_rd(self): - rd = MockRd() - for letter in alphabet: - self.assertTrue(letter in rd.re_set) if __name__ == "__main__": diff --git a/src/certfuzz/test/drillresults/test_result_driller_base.py b/src/certfuzz/test/drillresults/test_result_driller_base.py new file mode 100644 index 0000000..0b18a09 --- /dev/null +++ b/src/certfuzz/test/drillresults/test_result_driller_base.py @@ -0,0 +1,40 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import unittest +import certfuzz.drillresults.result_driller_base +from certfuzz.drillresults.result_driller_base import ResultDriller + +alphabet = 'abcdefghijklmnopqrstuvwxyz' + + +class MockRd(ResultDriller): + # really_exploitable expects a list + really_exploitable = list(alphabet) + + def _parse_testcase(self): + pass + + def _platform_find_testcases(self): + pass + + def check_64bit(self): + pass + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_rd_acts_as_metaclass(self): + self.assertRaises(TypeError, ResultDriller) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/fuzztools/test_filetools.py b/src/certfuzz/test/fuzztools/test_filetools.py index 65b4107..294ce7e 100644 --- a/src/certfuzz/test/fuzztools/test_filetools.py +++ b/src/certfuzz/test/fuzztools/test_filetools.py @@ -3,18 +3,21 @@ @organization: cert.org ''' +import hashlib import os +import shutil import tempfile +import unittest + +from certfuzz.fuzztools import filetools from certfuzz.fuzztools.filetools import copy_file -from certfuzz.fuzztools.filetools import delete_files from certfuzz.fuzztools.filetools import copy_files +from certfuzz.fuzztools.filetools import delete_files +from certfuzz.fuzztools.filetools import get_file_md5 from certfuzz.fuzztools.filetools import make_directories +from certfuzz.fuzztools.filetools import read_text_file from certfuzz.fuzztools.filetools import write_oneline_to_file -from certfuzz.fuzztools.filetools import get_file_md5 -import hashlib -import shutil -from certfuzz.fuzztools import filetools -import unittest + class Test(unittest.TestCase): def setUp(self): @@ -26,6 +29,14 @@ def tearDown(self): except OSError: pass + def test_read_file(self): + fd, f = tempfile.mkstemp(dir=self.tempdir, text=True) + os.write(fd, 'fizzle') + os.close(fd) + result = read_text_file(f) + self.assertEqual('fizzle', result) + os.remove(f) + def delete_file(self, f): os.remove(f) self.assertFalse(os.path.exists(f)) From dd980fd202cb34ab51c18261ede84008d19c1302 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 10:58:32 -0400 Subject: [PATCH 0430/1169] move LinuxResultDriller into its own module --- .../{drillresults_linux.py => result_driller_linux.py} | 8 -------- src/linux/tools/drillresults.py | 8 +++++--- 2 files changed, 5 insertions(+), 11 deletions(-) rename src/certfuzz/drillresults/{drillresults_linux.py => result_driller_linux.py} (89%) diff --git a/src/certfuzz/drillresults/drillresults_linux.py b/src/certfuzz/drillresults/result_driller_linux.py similarity index 89% rename from src/certfuzz/drillresults/drillresults_linux.py rename to src/certfuzz/drillresults/result_driller_linux.py index c291740..8f707e3 100755 --- a/src/certfuzz/drillresults/drillresults_linux.py +++ b/src/certfuzz/drillresults/result_driller_linux.py @@ -5,7 +5,6 @@ import logging import os -from certfuzz.drillresults.common import main as _main from certfuzz.drillresults.testcasebundle_linux import LinuxTestCaseBundle from certfuzz.drillresults.result_driller_base import ResultDriller @@ -31,10 +30,3 @@ def _platform_find_testcases(self, crash_hash, files, root): tcb = LinuxTestCaseBundle(dbg_file, crasherfile, crash_hash, self.ignore_jit) self.testcase_bundles.append(tcb) - - -def main(): - _main(driller_class=LinuxResultDriller) - -if __name__ == '__main__': - main() diff --git a/src/linux/tools/drillresults.py b/src/linux/tools/drillresults.py index 608e369..218b406 100755 --- a/src/linux/tools/drillresults.py +++ b/src/linux/tools/drillresults.py @@ -5,13 +5,15 @@ import os import sys try: - from certfuzz.drillresults.drillresults_linux import main + from certfuzz.drillresults.common import main + from certfuzz.drillresults.result_driller_linux import LinuxResultDriller except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) - from certfuzz.drillresults.drillresults_linux import main + from certfuzz.drillresults.common import main + from certfuzz.drillresults.result_driller_linux import LinuxResultDriller if __name__ == '__main__': - main() + main(driller_class=LinuxResultDriller) From fcfb2abb7229885303bdef06257a53cca844741c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 11:00:46 -0400 Subject: [PATCH 0431/1169] move WindowsResultDriller into its own module --- ...{drillresults_windows.py => result_driller_windows.py} | 8 -------- src/windows/tools/drillresults.py | 8 +++++--- 2 files changed, 5 insertions(+), 11 deletions(-) rename src/certfuzz/drillresults/{drillresults_windows.py => result_driller_windows.py} (92%) diff --git a/src/certfuzz/drillresults/drillresults_windows.py b/src/certfuzz/drillresults/result_driller_windows.py similarity index 92% rename from src/certfuzz/drillresults/drillresults_windows.py rename to src/certfuzz/drillresults/result_driller_windows.py index cb4d283..ec697a1 100644 --- a/src/certfuzz/drillresults/drillresults_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -6,7 +6,6 @@ import os import re -from certfuzz.drillresults.common import main as _main from certfuzz.drillresults.testcasebundle_windows import WindowsTestCaseBundle as TestCaseBundle from certfuzz.drillresults.result_driller_base import ResultDriller @@ -46,10 +45,3 @@ def _platform_find_testcases(self, crash_hash, files, root): tcb = TestCaseBundle(msecfile, crasherfile, crash_hash, self.ignore_jit) self.testcase_bundles.append(tcb) - - -def main(): - _main(driller_class=WindowsResultDriller) - -if __name__ == '__main__': - main() diff --git a/src/windows/tools/drillresults.py b/src/windows/tools/drillresults.py index ba5b266..71a5a4a 100644 --- a/src/windows/tools/drillresults.py +++ b/src/windows/tools/drillresults.py @@ -4,13 +4,15 @@ import os import sys try: - from certfuzz.drillresults.drillresults_windows import main + from certfuzz.drillresults.common import main + from certfuzz.drillresults.result_driller_windows import WindowsResultDriller except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) - from certfuzz.drillresults.drillresults_windows import main + from certfuzz.drillresults.common import main + from certfuzz.drillresults.result_driller_windows import WindowsResultDriller if __name__ == '__main__': - main() + main(driller_class=WindowsResultDriller) From 3a6e1054e287eaee64f59788bc3480918bf6f2d3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 12:49:12 -0400 Subject: [PATCH 0432/1169] raise a DrillResultsError if main is called without a driller_class --- src/certfuzz/drillresults/common.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/drillresults/common.py b/src/certfuzz/drillresults/common.py index b18f9a3..01bc7c2 100644 --- a/src/certfuzz/drillresults/common.py +++ b/src/certfuzz/drillresults/common.py @@ -10,7 +10,7 @@ from certfuzz.fuzztools.filetools import read_bin_file as _read_bin_file -from certfuzz.drillresults.result_driller_base import ResultDriller +from certfuzz.drillresults.errors import DrillResultsError logger = logging.getLogger(__name__) @@ -113,6 +113,7 @@ def is_number(s): except ValueError: return False + def _read_zip(raw_file_byte_string): ''' If the bytes in raw_file_byte_string look like a zip file, @@ -160,7 +161,7 @@ def read_bin_file(inputfile): return filebytes + zipbytes -def main(driller_class=ResultDriller): +def main(driller_class=None): ''' Main method for drill results script. Platform-specific customizations are passed in via the driller_class argument (which must be implemented elsewhere) @@ -168,10 +169,12 @@ def main(driller_class=ResultDriller): ''' args = parse_args() root_logger_to_console(args) + + if driller_class is None: + raise DrillResultsError('A platform-specific driller_class must be specified.') + with driller_class(ignore_jit=args.ignorejit, base_dir=args.resultsdir, force_reload=args.force, report_all=args.report_all) as rd: rd.drill_results() - - From 2d779b0684863ba7a98dd9cb2eb857077c839547 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 14:28:19 -0400 Subject: [PATCH 0433/1169] continue refactoring out common code --- .../drillresults/testcasebundle_base.py | 3 + .../drillresults/testcasebundle_linux.py | 56 ++++++------- .../drillresults/testcasebundle_windows.py | 82 +++++++++---------- 3 files changed, 69 insertions(+), 72 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 91d2f8b..8c70705 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -41,9 +41,12 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): 'exceptions': {}} self.score = 100 self._64bit_debugger = False + self.classification = None + # See if we're dealing with 64-bit debugger or target app self._check_64bit() + self._get_classification() self._parse_testcase() self._score_testcase() diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 381e928..34d995e 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -43,6 +43,10 @@ class LinuxTestCaseBundle(TestCaseBundle): 'ReturnAv', ] + def _get_classification(self): + self.classification = carve(self.reporttext, "Classification: ", "\n") + logger.debug('Classification: %s', self.classification) + def _check_64bit(self): ''' Check if the debugger and target app are 64-bit @@ -59,11 +63,6 @@ def _parse_testcase(self): ''' Parse the gdb file ''' - crasherfile = self.testcase_file - reporttext = self.reporttext - _64bit_debugger = self._64bit_debugger - crasherdata = self.crasherdata - # global _64bit_debugger # TODO move this back to ResultDriller class @@ -72,68 +71,65 @@ def _parse_testcase(self): # self.results[crash_hash] = self.cached_results[crash_hash] # return - details = self.details - exceptionnum = 0 - classification = carve(reporttext, "Classification: ", "\n") #print 'classification: %s' % classification try: - if classification: + if self.classification: # Create a new exception dictionary to add to the crash exception = {} - details['exceptions'][exceptionnum] = exception + self.details['exceptions'][exceptionnum] = exception except KeyError: # Crash ID (crash_hash) not yet seen # Default it to not being "really exploitable" - details['reallyexploitable'] = False + self.details['reallyexploitable'] = False # Create a dictionary of exceptions for the crash id exceptions = {} - details['exceptions'] = exceptions + self.details['exceptions'] = exceptions # Create a dictionary for the exception - details['exceptions'][exceptionnum] = exception + self.details['exceptions'][exceptionnum] = exception # Set !exploitable classification for the exception - if classification: - details['exceptions'][exceptionnum]['classification'] = classification + if self.classification: + self.details['exceptions'][exceptionnum]['classification'] = self.classification - shortdesc = carve(reporttext, "Short description: ", " (") + shortdesc = carve(self.reporttext, "Short description: ", " (") #print 'shortdesc: %s' % shortdesc if shortdesc: # Set !exploitable Short Description for the exception - details['exceptions'][exceptionnum]['shortdesc'] = shortdesc + self.details['exceptions'][exceptionnum]['shortdesc'] = shortdesc # Flag the entire crash ID as really exploitable if this is a good # exception - details['reallyexploitable'] = shortdesc in self.re_set + self.details['reallyexploitable'] = shortdesc in self.re_set - if not os.path.isfile(crasherfile): + if not os.path.isfile(self.testcase_file): # Can't find the crasher file #print "WTF! Cannot find %s" % crasherfile return # Set the "fuzzedfile" property for the crash ID - details['fuzzedfile'] = crasherfile - faultaddr = carve2(reporttext) + self.details['fuzzedfile'] = self.testcase_file + faultaddr = carve2(self.reporttext) instraddr = self.get_instr_addr() - faultaddr = self.format_addr(faultaddr, _64bit_debugger) - instraddr = self.format_addr(instraddr, _64bit_debugger) + faultaddr = self.format_addr(faultaddr, self._64bit_debugger) + instraddr = self.format_addr(instraddr, self._64bit_debuggerugger) # No faulting address means no crash. if not faultaddr: return if instraddr: - details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(instraddr) + self.details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(instraddr) # Get the cdb line that contains the crashing instruction instructionline = self.get_instr(instraddr) - details['exceptions'][exceptionnum]['instructionline'] = instructionline + self.details['exceptions'][exceptionnum]['instructionline'] = instructionline if instructionline: faultaddr = self.fix_efa_offset(instructionline, faultaddr) # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') - details['exceptions'][exceptionnum]['efa'] = faultaddr - if _64bit_debugger: # 64-bit target app + self.details['exceptions'][exceptionnum]['efa'] = faultaddr + if self._64bit_debugger: faultaddr = faultaddr.zfill(16) efaptr = struct.unpack('' part on the # filename when invoking cdb, however the resulting file # is really just 'sf_.' - fileparts = crasherfile.split('-') + fileparts = self.testcase_file.split('-') m = re.search('\..+', fileparts[1]) # Recreate the original file name, minus the iteration - crasherfile = os.path.join(current_dir, fileparts[0] + m.group(0)) + self.testcase_file = os.path.join(current_dir, fileparts[0] + m.group(0)) else: - crasherfile = os.path.join(current_dir, crasherfile) - if not os.path.isfile(crasherfile): + self.testcase_file = os.path.join(current_dir, self.testcase_file) + if not os.path.isfile(self.testcase_file): # Can't find the crasher file return # Set the "fuzzedfile" property for the crash ID - crashid['fuzzedfile'] = crasherfile + self.details['fuzzedfile'] = self.testcase_file # See if we're dealing with 64-bit debugger or target app faultaddr = carve2(self.reporttext) instraddr = carve(self.reporttext, "Instruction Address:", "\n") @@ -147,17 +148,17 @@ def _parse_testcase(self, tcb): faultaddr = self.fix_efa_bug(instraddr, faultaddr) # pc_module = pc_in_mapped_address(reporttext, instraddr) - crashid['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(self.reporttext, instraddr, self._64bit_debugger) + self.details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(self.reporttext, instraddr, self._64bit_debugger) # Get the cdb line that contains the crashing instruction instructionline = self.get_instr(self.reporttext, instraddr) - crashid['exceptions'][exceptionnum]['instructionline'] = instructionline + self.details['exceptions'][exceptionnum]['instructionline'] = instructionline if instructionline: faultaddr = self.fix_efa_offset(instructionline, faultaddr) # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') - crashid['exceptions'][exceptionnum]['efa'] = faultaddr + self.details['exceptions'][exceptionnum]['efa'] = faultaddr if self._64bit_debugger and not self.wow64_app: # 64-bit target app faultaddr = faultaddr.zfill(16) @@ -173,14 +174,11 @@ def _parse_testcase(self, tcb): efapattern = efapattern.replace('L', '') efapattern = efapattern.zfill(8) - # Read in the fuzzed file - crasherdata = read_bin_file(crasherfile) - # If there's a match, flag this exception has having Efa In File - if binascii.a2b_hex(efapattern) in crasherdata: - crashid['exceptions'][exceptionnum]['EIF'] = True + if binascii.a2b_hex(efapattern) in self.crasherdata: + self.details['exceptions'][exceptionnum]['EIF'] = True else: - crashid['exceptions'][exceptionnum]['EIF'] = False + self.details['exceptions'][exceptionnum]['EIF'] = False def pc_in_mapped_address(self, instraddr): ''' From 89c9ee3265f348414dcd13816601314f7b58d776 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 15:50:29 -0400 Subject: [PATCH 0434/1169] still chasing the ignorejit -> ignore_jit bugs --- src/certfuzz/drillresults/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/drillresults/common.py b/src/certfuzz/drillresults/common.py index 01bc7c2..40662ba 100644 --- a/src/certfuzz/drillresults/common.py +++ b/src/certfuzz/drillresults/common.py @@ -173,7 +173,7 @@ def main(driller_class=None): if driller_class is None: raise DrillResultsError('A platform-specific driller_class must be specified.') - with driller_class(ignore_jit=args.ignorejit, + with driller_class(ignore_jit=args.ignore_jit, base_dir=args.resultsdir, force_reload=args.force, report_all=args.report_all) as rd: From 449f83de9a145b0787637a6e9770b738da6f6900 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 15:51:23 -0400 Subject: [PATCH 0435/1169] add platform-specific errors --- src/certfuzz/drillresults/errors.py | 8 ++++++++ src/certfuzz/drillresults/testcasebundle_linux.py | 1 + src/certfuzz/drillresults/testcasebundle_windows.py | 1 + 3 files changed, 10 insertions(+) diff --git a/src/certfuzz/drillresults/errors.py b/src/certfuzz/drillresults/errors.py index 750c823..f013409 100644 --- a/src/certfuzz/drillresults/errors.py +++ b/src/certfuzz/drillresults/errors.py @@ -12,3 +12,11 @@ class DrillResultsError(CERTFuzzToolError): class TestCaseBundleError(DrillResultsError): pass + + +class LinuxTestCaseBundleError(TestCaseBundleError): + pass + + +class WindowsTestCaseBundleError(TestCaseBundleError): + pass diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 34d995e..871445d 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -9,6 +9,7 @@ from certfuzz.drillresults.common import carve from certfuzz.drillresults.common import carve2 from certfuzz.drillresults.common import reg_set +from certfuzz.drillresults.errors import LinuxTestCaseBundleError from certfuzz.drillresults.testcasebundle_base import TestCaseBundle import logging import re diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 8a9e355..4b5e9fc 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -14,6 +14,7 @@ from certfuzz.drillresults.common import is_number from certfuzz.drillresults.common import read_bin_file from certfuzz.drillresults.common import reg64_set +from certfuzz.drillresults.errors import WindowsTestCaseBundleError from certfuzz.drillresults.testcasebundle_base import TestCaseBundle logger = logging.getLogger(__name__) From 7069280cf403ab44df12f1d86c22a578ab51cf58 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 15:51:55 -0400 Subject: [PATCH 0436/1169] rename msecfile -> dbg_file for consistency w/linux --- src/certfuzz/drillresults/result_driller_windows.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index ec697a1..5c8806f 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -1,7 +1,6 @@ ''' This script looks for interesting crashes and rate them by potential exploitability ''' - import logging import os import re @@ -39,9 +38,9 @@ def _platform_find_testcases(self, crash_hash, files, root): for current_file in files: # Go through all of the .msec files and parse them if regex['msec_report'].match(current_file): - msecfile = os.path.join(root, current_file) + dbg_file = os.path.join(root, current_file) if crasherfile and root not in crasherfile: crasherfile = os.path.join(root, crasherfile) - tcb = TestCaseBundle(msecfile, crasherfile, crash_hash, + tcb = TestCaseBundle(dbg_file, crasherfile, crash_hash, self.ignore_jit) self.testcase_bundles.append(tcb) From cb7123bb8022463aad20c17135f67d8ddc007b9c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 15:52:16 -0400 Subject: [PATCH 0437/1169] rename import for consistency --- src/certfuzz/drillresults/result_driller_linux.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/drillresults/result_driller_linux.py b/src/certfuzz/drillresults/result_driller_linux.py index 8f707e3..f907130 100755 --- a/src/certfuzz/drillresults/result_driller_linux.py +++ b/src/certfuzz/drillresults/result_driller_linux.py @@ -5,7 +5,7 @@ import logging import os -from certfuzz.drillresults.testcasebundle_linux import LinuxTestCaseBundle +from certfuzz.drillresults.testcasebundle_linux import LinuxTestCaseBundle as TestCaseBundle from certfuzz.drillresults.result_driller_base import ResultDriller logger = logging.getLogger(__name__) @@ -27,6 +27,6 @@ def _platform_find_testcases(self, crash_hash, files, root): logger.debug('found gdb file: %s', dbg_file) crasherfile = dbg_file.replace('.gdb', '') #crasherfile = os.path.join(root, crasherfile) - tcb = LinuxTestCaseBundle(dbg_file, crasherfile, crash_hash, + tcb = TestCaseBundle(dbg_file, crasherfile, crash_hash, self.ignore_jit) self.testcase_bundles.append(tcb) From d203ac6289c0d3a052e118703c914f50834c7f15 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 2 Jul 2014 15:54:07 -0400 Subject: [PATCH 0438/1169] continued refactoring --- .../drillresults/testcasebundle_base.py | 40 +++++- .../drillresults/testcasebundle_linux.py | 91 ++++--------- .../drillresults/testcasebundle_windows.py | 124 ++++++++---------- 3 files changed, 122 insertions(+), 133 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 8c70705..b068528 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -23,6 +23,8 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self.dbg_outfile = dbg_outfile self.testcase_file = testcase_file + self._find_testcase_file() + self._verify_files_exist() self.crash_hash = crash_hash @@ -38,15 +40,18 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self.current_dir = os.path.dirname(self.dbg_outfile) self.details = {'reallyexploitable': False, - 'exceptions': {}} + 'exceptions': {}, + 'fuzzedfile': self.testcase_file} + self.score = 100 self._64bit_debugger = False self.classification = None - + self.shortdesc = None # See if we're dealing with 64-bit debugger or target app self._check_64bit() self._get_classification() + self._get_shortdesc() self._parse_testcase() self._score_testcase() @@ -67,6 +72,19 @@ def __enter__(self): def __exit__(self, etype, value, traceback): pass + def _find_testcase_file(self): + if not os.path.isfile(self.testcase_file): + # Can't find the crasher file + raise TestCaseBundleError('Cannot find testcase file %s', self.testcase_file) + + @abc.abstractmethod + def _get_classification(self): + pass + + @abc.abstractmethod + def _get_shortdesc(self): + pass + @abc.abstractmethod def _check_64bit(self): pass @@ -75,6 +93,24 @@ def _check_64bit(self): def _parse_testcase(self): pass + def get_ex_num(self): + ''' + Override this method for platforms where exceptions can be continued + ''' + return 0 + + def _record_exception_info(self, exceptionnum): + if self.classification: + # Create a new exception dictionary to add to the crash + exception = {} + self.details['exceptions'][exceptionnum] = exception + self.details['exceptions'][exceptionnum]['classification'] = self.classification + if self.shortdesc: + # Set !exploitable Short Description for the exception + self.details['exceptions'][exceptionnum]['shortdesc'] = self.shortdesc # Flag the entire crash ID as really exploitable if this is a good + # exception + self.details['reallyexploitable'] = self.shortdesc in self.re_set + def _score_testcase(self): logger.debug('Scoring testcase: %s', self.crash_hash) details = self.details diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 871445d..d34f78e 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -4,6 +4,9 @@ @organization: cert.org ''' import binascii +import logging +import os +import re import struct from certfuzz.drillresults.common import carve @@ -11,9 +14,7 @@ from certfuzz.drillresults.common import reg_set from certfuzz.drillresults.errors import LinuxTestCaseBundleError from certfuzz.drillresults.testcasebundle_base import TestCaseBundle -import logging -import re -import os +from certfuzz.drillresults.errors import TestCaseBundleError logger = logging.getLogger(__name__) @@ -48,6 +49,10 @@ def _get_classification(self): self.classification = carve(self.reporttext, "Classification: ", "\n") logger.debug('Classification: %s', self.classification) + def _get_shortdesc(self): + self.shortdesc = carve(self.reporttext, "Short description: ", " (") + logger.debug('Short Description: %s', self.shortdesc) + def _check_64bit(self): ''' Check if the debugger and target app are 64-bit @@ -64,58 +69,24 @@ def _parse_testcase(self): ''' Parse the gdb file ''' - # global _64bit_debugger - # TODO move this back to ResultDriller class -# if self.cached_results: -# if self.cached_results.get(crash_hash): -# self.results[crash_hash] = self.cached_results[crash_hash] +# if self.cached_testcases: +# if self.cached_testcases.get(self.crash_hash): +# self.results[self.crash_hash] = self.cached_testcases[self.crash_hash] # return - exceptionnum = 0 - #print 'classification: %s' % classification - try: - if self.classification: - # Create a new exception dictionary to add to the crash - exception = {} - self.details['exceptions'][exceptionnum] = exception - except KeyError: - # Crash ID (crash_hash) not yet seen - # Default it to not being "really exploitable" - self.details['reallyexploitable'] = False - # Create a dictionary of exceptions for the crash id - exceptions = {} - self.details['exceptions'] = exceptions - # Create a dictionary for the exception - self.details['exceptions'][exceptionnum] = exception - - # Set !exploitable classification for the exception - if self.classification: - self.details['exceptions'][exceptionnum]['classification'] = self.classification - - shortdesc = carve(self.reporttext, "Short description: ", " (") - #print 'shortdesc: %s' % shortdesc - if shortdesc: - # Set !exploitable Short Description for the exception - self.details['exceptions'][exceptionnum]['shortdesc'] = shortdesc - # Flag the entire crash ID as really exploitable if this is a good - # exception - self.details['reallyexploitable'] = shortdesc in self.re_set - - if not os.path.isfile(self.testcase_file): - # Can't find the crasher file - #print "WTF! Cannot find %s" % crasherfile - return - # Set the "fuzzedfile" property for the crash ID - self.details['fuzzedfile'] = self.testcase_file - faultaddr = carve2(self.reporttext) + exceptionnum = self.get_ex_num() + self._record_exception_info(exceptionnum) + + faultaddr = self.get_fault_addr() instraddr = self.get_instr_addr() - faultaddr = self.format_addr(faultaddr, self._64bit_debugger) - instraddr = self.format_addr(instraddr, self._64bit_debuggerugger) # No faulting address means no crash. if not faultaddr: - return + raise TestCaseBundleError('No faulting address means no crash') + + if not instraddr: + raise TestCaseBundleError('No instraddr address means no crash') if instraddr: self.details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(instraddr) @@ -128,9 +99,10 @@ def _parse_testcase(self): # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') - # 64-bit target app + self.details['exceptions'][exceptionnum]['efa'] = faultaddr if self._64bit_debugger: + # 64-bit target app faultaddr = faultaddr.zfill(16) efaptr = struct.unpack(' Date: Wed, 2 Jul 2014 16:10:24 -0400 Subject: [PATCH 0439/1169] relocate _parse_testcase to common class --- .../drillresults/testcasebundle_base.py | 80 ++++++++++++++++++- .../drillresults/testcasebundle_linux.py | 63 ++------------- .../drillresults/testcasebundle_windows.py | 68 ++-------------- 3 files changed, 88 insertions(+), 123 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index b068528..bd19468 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -11,6 +11,8 @@ from certfuzz.drillresults.common import read_bin_file from certfuzz.drillresults.errors import TestCaseBundleError +import binascii +import struct logger = logging.getLogger(__name__) @@ -59,6 +61,10 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): def really_exploitable(self): return [] + @abc.abstractproperty + def _64bit_target_app(self): + return self._64bit_debugger + def _verify_files_exist(self): for f in [self.dbg_outfile, self.testcase_file]: if not os.path.exists(f): @@ -87,11 +93,79 @@ def _get_shortdesc(self): @abc.abstractmethod def _check_64bit(self): - pass + ''' + Check if the debugger and target app are 64-bit + ''' - @abc.abstractmethod def _parse_testcase(self): - pass + ''' + Parse the debugger output file + ''' + # TODO move this back to ResultDriller class +# if self.cached_testcases: +# if self.cached_testcases.get(self.crash_hash): +# self.results[self.crash_hash] = self.cached_testcases[self.crash_hash] +# return + + exceptionnum = self.get_ex_num() + self._record_exception_info(exceptionnum) + + faultaddr = self.get_fault_addr() + instraddr = self.get_instr_addr() + + # No faulting address means no crash. + if not faultaddr: + raise TestCaseBundleError('No faulting address means no crash') + + if not instraddr: + raise TestCaseBundleError('No instraddr address means no crash') + + instraddr, faultaddr = self._64bit_addr_fixup(faultaddr, instraddr) + + if instraddr: + self.details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(instraddr) + + # Get the cdb line that contains the crashing instruction + instructionline = self.get_instr(instraddr) + self.details['exceptions'][exceptionnum]['instructionline'] = instructionline + if instructionline: + faultaddr = self.fix_efa_offset(instructionline, faultaddr) + + # Fix faulting pattern endian + faultaddr = faultaddr.replace('0x', '') + + self.details['exceptions'][exceptionnum]['efa'] = faultaddr + + if self._64bit_target_app: + # 64-bit target app + faultaddr = faultaddr.zfill(16) + efaptr = struct.unpack(' Date: Thu, 3 Jul 2014 08:00:45 -0400 Subject: [PATCH 0440/1169] add docstrings --- src/certfuzz/drillresults/testcasebundle_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index bd19468..9448f63 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -59,10 +59,16 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): @abc.abstractproperty def really_exploitable(self): + ''' + List of strings indicating debugger descriptions of particular interest + ''' return [] @abc.abstractproperty def _64bit_target_app(self): + ''' + Returns true if the target app is 64-bit. + ''' return self._64bit_debugger def _verify_files_exist(self): From 93a9770de083c3e72b384bda4cc16a416db509bc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Jul 2014 08:01:31 -0400 Subject: [PATCH 0441/1169] remove unused imports --- src/certfuzz/drillresults/testcasebundle_linux.py | 5 ----- src/certfuzz/drillresults/testcasebundle_windows.py | 4 ---- 2 files changed, 9 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 8e7308a..30c86b7 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -3,18 +3,13 @@ @organization: cert.org ''' -import binascii import logging -import os import re -import struct from certfuzz.drillresults.common import carve from certfuzz.drillresults.common import carve2 from certfuzz.drillresults.common import reg_set -from certfuzz.drillresults.errors import LinuxTestCaseBundleError from certfuzz.drillresults.testcasebundle_base import TestCaseBundle -from certfuzz.drillresults.errors import TestCaseBundleError logger = logging.getLogger(__name__) diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 535c6fc..ba6b3f8 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -3,19 +3,15 @@ @organization: cert.org ''' -import binascii import logging import os import re -import struct from certfuzz.drillresults.common import carve from certfuzz.drillresults.common import carve2 from certfuzz.drillresults.common import is_number from certfuzz.drillresults.common import reg64_set -from certfuzz.drillresults.errors import WindowsTestCaseBundleError from certfuzz.drillresults.testcasebundle_base import TestCaseBundle -from certfuzz.drillresults.errors import TestCaseBundleError logger = logging.getLogger(__name__) From 1c75402af70a623a3019ad085b9edf38b02bee0c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Jul 2014 08:02:02 -0400 Subject: [PATCH 0442/1169] Replace regex dict with individual RE_ variables --- .../drillresults/testcasebundle_linux.py | 44 +++++++------------ .../drillresults/testcasebundle_windows.py | 24 +++++----- 2 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 30c86b7..fc1aaf7 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -14,21 +14,11 @@ logger = logging.getLogger(__name__) -regex = { - '64bit_debugger': re.compile(r'^Microsoft.*AMD64$'), - 'bt_addr': re.compile(r'(0x[0-9a-fA-F]+)\s+.+$'), - 'current_instr': re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)'), - 'dbg_prompt': re.compile(r'^[0-9]:[0-9][0-9][0-9]> (.*)'), - 'frame0': re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+'), - 'gdb_report': re.compile(r'.+.gdb$'), - 'mapped_address': re.compile(r'^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), - 'mapped_address64': re.compile(r'^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), - 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)'), - 'regs1': re.compile(r'^eax=.+'), - 'regs2': re.compile(r'^eip=.+'), - 'syswow64': re.compile(r'ModLoad:.*syswow64.*', re.IGNORECASE), - 'vdso': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+\[vdso\]'), - } +RE_BT_ADDR = re.compile(r'(0x[0-9a-fA-F]+)\s+.+$') +RE_MAPPED_FRAME = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)') +RE_VDSO = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+\[vdso\]'), +RE_CURRENT_INSTR = re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)') +RE_FRAME_0 = re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+') class LinuxTestCaseBundle(TestCaseBundle): @@ -50,7 +40,7 @@ def _get_shortdesc(self): def _check_64bit(self): for line in self.reporttext.splitlines(): - m = re.match(regex['bt_addr'], line) + m = re.match(RE_BT_ADDR, line) if m: start_addr = m.group(1) if len(start_addr) > 10: @@ -72,31 +62,31 @@ def pc_in_mapped_address(self, instraddr): # The gdb file doesn't have anything in it that'll tell us # where the PC is. return '' - # print 'checking if %s is mapped...' % instraddr + logger.debug('checking if %s is mapped', instraddr) mapped_module = 'unloaded' instraddr = int(instraddr, 16) - # print 'instraddr: %d' % instraddr + logger.debug('instraddr: %d', instraddr) for line in self.reporttext.splitlines(): - #print 'checking: %s for %s' % (line,regex['mapped_frame']) - n = re.search(regex['mapped_frame'], line) + logger.debug('checking: %s for %s', line, RE_MAPPED_FRAME) + + n = re.search(RE_MAPPED_FRAME, line) if n: - # print '*** found mapped address regex!' + logger.debug('found mapped address regex') # Strip out backticks present on 64-bit systems begin_address = int(n.group(1).replace('`', ''), 16) end_address = int(n.group(2).replace('`', ''), 16) if begin_address < instraddr < end_address: mapped_module = n.group(4) - #print 'mapped_module: %s' % mapped_module else: # [vdso] still counts as a mapped module - n = re.search(regex['vdso'], line) + n = re.search(RE_VDSO, line) if n: begin_address = int(n.group(1).replace('`', ''), 16) end_address = int(n.group(2).replace('`', ''), 16) if begin_address < instraddr < end_address: mapped_module = '[vdso]' - + logger.debug('mapped_module: %s', mapped_module) return mapped_module def format_addr(self, faultaddr): @@ -163,7 +153,7 @@ def get_instr(self, instraddr): ''' Find the disassembly line for the current (crashing) instruction ''' - rgx = regex['current_instr'] + rgx = RE_CURRENT_INSTR for line in self.reporttext.splitlines(): n = rgx.match(line) if n: @@ -190,14 +180,14 @@ def get_instr_addr(self): instraddr = None for line in self.reporttext.splitlines(): #print 'checking: %s' % line - n = re.match(regex['current_instr'], line) + n = re.match(RE_CURRENT_INSTR, line) if n: instraddr = n.group(1) #print 'Found instruction address: %s' % instraddr if not instraddr: for line in self.reporttext.splitlines(): #No disassembly. Resort to frame 0 address - n = re.match(regex['frame0'], line) + n = re.match(RE_FRAME_0, line) if n: instraddr = n.group(1) #print 'Found instruction address: %s' % instraddr diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index ba6b3f8..22e2409 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -16,14 +16,10 @@ logger = logging.getLogger(__name__) -regex = { - '64bit_debugger': re.compile('^Microsoft.*AMD64$'), - 'mapped_address': re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)'), - 'mapped_address64': re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), - 'regs1': re.compile('^eax=.+'), - 'regs2': re.compile('^eip=.+'), - 'syswow64': re.compile('ModLoad:.*syswow64.*', re.IGNORECASE), - } +RE_64BIT_DEBUGGER = re.compile('^Microsoft.*AMD64$') +RE_SYSWOW64 = re.compile('ModLoad:.*syswow64.*', re.IGNORECASE) +RE_MAPPED_ADDRESS = re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)') +RE_MAPPED_ADDRESS64 = re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), class WindowsTestCaseBundle(TestCaseBundle): @@ -58,12 +54,12 @@ def _get_shortdesc(self): def _check_64bit(self): for line in self.reporttext.splitlines(): - n = re.match(regex['64bit_debugger'], line) + n = re.match(RE_64BIT_DEBUGGER, line) if n: self._64bit_debugger = True if self._64bit_debugger: - n = re.match(regex['syswow64'], line) + n = re.match(RE_SYSWOW64, line) if n: self.wow64_app = True @@ -109,15 +105,15 @@ def pc_in_mapped_address(self, instraddr): ''' Check if the instruction pointer is in a loaded module ''' - ma_regex = 'mapped_address' + pattern = RE_MAPPED_ADDRESS mapped_module = 'unloaded' if self._64bit_debugger: - ma_regex = 'mapped_address64' + pattern = RE_MAPPED_ADDRESS64 instraddr = instraddr.replace('`', '') instraddr = int(instraddr, 16) for line in self.reporttext.splitlines(): - n = re.match(regex[ma_regex], line) + n = re.match(pattern, line) if n: # Strip out backticks present on 64-bit systems begin_address = int(n.group(1).replace('`', ''), 16) @@ -259,7 +255,7 @@ def get_fault_addr(self): # Populate the register dictionary with register values at crash # ''' # for line in self.reporttext.splitlines(): -# if regex['regs1'].match(line) or regex['regs2'].match(line): +# if RE_regs1'].match(line) or regex['regs2.match(line): # regs1 = line.split() # for reg in regs1: # if "=" in reg: From feedef8eb1265a9184e1469d11e55cc614c5cf12 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Jul 2014 08:08:02 -0400 Subject: [PATCH 0443/1169] add comment --- src/certfuzz/drillresults/testcasebundle_linux.py | 1 + src/certfuzz/drillresults/testcasebundle_windows.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index fc1aaf7..3c96525 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -14,6 +14,7 @@ logger = logging.getLogger(__name__) +# compile our regular expresssions once RE_BT_ADDR = re.compile(r'(0x[0-9a-fA-F]+)\s+.+$') RE_MAPPED_FRAME = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)') RE_VDSO = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+\[vdso\]'), diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 22e2409..4ee741e 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -16,6 +16,7 @@ logger = logging.getLogger(__name__) +# compile our regular expresssions once RE_64BIT_DEBUGGER = re.compile('^Microsoft.*AMD64$') RE_SYSWOW64 = re.compile('ModLoad:.*syswow64.*', re.IGNORECASE) RE_MAPPED_ADDRESS = re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)') From f3e544f3a139bf7416cb759e936482d73382772d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Jul 2014 08:08:30 -0400 Subject: [PATCH 0444/1169] refactor pc_in_mapped_address method --- .../drillresults/testcasebundle_linux.py | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 3c96525..2c60b09 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -17,7 +17,7 @@ # compile our regular expresssions once RE_BT_ADDR = re.compile(r'(0x[0-9a-fA-F]+)\s+.+$') RE_MAPPED_FRAME = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)') -RE_VDSO = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+\[vdso\]'), +RE_VDSO = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(\[vdso\])') RE_CURRENT_INSTR = re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)') RE_FRAME_0 = re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+') @@ -69,24 +69,16 @@ def pc_in_mapped_address(self, instraddr): instraddr = int(instraddr, 16) logger.debug('instraddr: %d', instraddr) for line in self.reporttext.splitlines(): - logger.debug('checking: %s for %s', line, RE_MAPPED_FRAME) - - n = re.search(RE_MAPPED_FRAME, line) - if n: - logger.debug('found mapped address regex') - # Strip out backticks present on 64-bit systems - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = n.group(4) - else: - # [vdso] still counts as a mapped module - n = re.search(RE_VDSO, line) + for pattern in [RE_MAPPED_FRAME, RE_VDSO]: + logger.debug('checking: %s for %s', line, pattern) + n = re.search(pattern, line) if n: + logger.debug('found mapped address regex') + # Strip out backticks present on 64-bit systems begin_address = int(n.group(1).replace('`', ''), 16) end_address = int(n.group(2).replace('`', ''), 16) if begin_address < instraddr < end_address: - mapped_module = '[vdso]' + mapped_module = n.group(4) logger.debug('mapped_module: %s', mapped_module) return mapped_module From 920ae2ae13e6c7c21f456ef68d1b416cf1494a55 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Jul 2014 09:16:30 -0400 Subject: [PATCH 0445/1169] move format_addr into base class --- .../drillresults/testcasebundle_base.py | 57 +++++++++++++++- .../drillresults/testcasebundle_linux.py | 67 +++++-------------- .../drillresults/testcasebundle_windows.py | 56 ++++++---------- 3 files changed, 91 insertions(+), 89 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 9448f63..53af7c2 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -126,7 +126,7 @@ def _parse_testcase(self): if not instraddr: raise TestCaseBundleError('No instraddr address means no crash') - instraddr, faultaddr = self._64bit_addr_fixup(faultaddr, instraddr) + faultaddr, instraddr = self._64bit_addr_fixup(faultaddr, instraddr) if instraddr: self.details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(instraddr) @@ -163,6 +163,61 @@ def _parse_testcase(self): else: self.details['exceptions'][exceptionnum]['EIF'] = False + @abc.abstractmethod + def _look_for_loaded_module(self, instraddr, line): + ''' + If the line contains loaded module info, see if instraddr is in the module. + If it is, return the module name, otherwise return None + :param instraddr: + :param line: + ''' + + def format_addr(self, faultaddr): + ''' + Format a 64- or 32-bit memory address to a fixed width + ''' + if not faultaddr: + return + + faultaddr = faultaddr.strip().replace('0x', '') + + if self._64bit_target_app: + # Due to a bug in !exploitable, the Exception Faulting Address is + # often wrong with 64-bit targets + if len(faultaddr) < 16: + # pad faultaddr if it's shorter than 64 bits + return faultaddr.zfill(16) + + # if faultaddr is longer than 32 bits, truncate it + if len(faultaddr) > 8: + return faultaddr[-8:] + + # if faultaddr is shorter than 32 bits, pad it + if len(faultaddr) < 8: + # pad faultaddr + return faultaddr.zfill(8) + + return faultaddr + + + def pc_in_mapped_address(self, instraddr): + ''' + Check if the instruction pointer is in a loaded module + ''' + if not instraddr: + # The debugger file doesn't have anything in it that'll tell us + # where the PC is. + return '' + + logger.debug('checking if %s is mapped', instraddr) + for line in self.reporttext.splitlines(): + module_name = self._look_for_loaded_module(instraddr, line) + if module_name is not None: + # short circuit as soon as we find a mapped module + return module_name + # if you got here, instraddr is not in a loaded module + return 'unloaded' + @abc.abstractmethod def _64bit_addr_fixup(self, faultaddr, instraddr): ''' diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 2c60b09..cd2eb5f 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -53,60 +53,25 @@ def _64bit_addr_fixup(self, faultaddr, instraddr): @property def _64bit_target_app(self): - return TestCaseBundle._64bit_target_app(self) - - def pc_in_mapped_address(self, instraddr): - ''' - Check if the instruction pointer is in a loaded module - ''' - if not instraddr: - # The gdb file doesn't have anything in it that'll tell us - # where the PC is. - return '' - logger.debug('checking if %s is mapped', instraddr) - mapped_module = 'unloaded' + return TestCaseBundle._64bit_target_app + def _look_for_loaded_module(self, instraddr, line): + # convert to an int as hex instraddr = int(instraddr, 16) - logger.debug('instraddr: %d', instraddr) - for line in self.reporttext.splitlines(): - for pattern in [RE_MAPPED_FRAME, RE_VDSO]: - logger.debug('checking: %s for %s', line, pattern) - n = re.search(pattern, line) - if n: - logger.debug('found mapped address regex') - # Strip out backticks present on 64-bit systems - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = n.group(4) - logger.debug('mapped_module: %s', mapped_module) - return mapped_module - - def format_addr(self, faultaddr): - ''' - Format a 64- or 32-bit memory address to a fixed width - ''' - - if not faultaddr: - return - else: - faultaddr = faultaddr.strip() - faultaddr = faultaddr.replace('0x', '') - - if self._64bit_debugger: - # Due to a bug in !exploitable, the Exception Faulting Address is - # often wrong with 64-bit targets - if len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(16) - else: - if len(faultaddr) > 10: # 0x12345678 = 10 chars - faultaddr = faultaddr[-8:] - elif len(faultaddr) < 10: - # pad faultaddr - faultaddr = faultaddr.zfill(8) - return faultaddr + for pattern in [RE_MAPPED_FRAME, RE_VDSO]: + n = re.search(pattern, line) + if n: + # Strip out backticks present on 64-bit systems + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + module_name = n.group(4) + logger.debug('%x %x %s %x', begin_address, end_address, module_name, instraddr) + if begin_address < instraddr < end_address: + logger.debug('Matched: %x in %x %x %s', instraddr, + begin_address, end_address, module_name) + # as soon as we find this, we're done + return module_name def fix_efa_offset(self, instructionline, faultaddr): ''' diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 4ee741e..34a57ae 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -10,6 +10,7 @@ from certfuzz.drillresults.common import carve from certfuzz.drillresults.common import carve2 from certfuzz.drillresults.common import is_number +from certfuzz.drillresults.common import reg_set from certfuzz.drillresults.common import reg64_set from certfuzz.drillresults.testcasebundle_base import TestCaseBundle @@ -92,7 +93,7 @@ def _find_testcase_file(self): TestCaseBundle._find_testcase_file(self) def _64bit_addr_fixup(self, faultaddr, instraddr): - if self._64bit_debugger and not self.wow64_app and instraddr: # Put backtick into instruction address for pattern matching + if self._64bit_target_app and instraddr: # Put backtick into instruction address for pattern matching instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) if self.shortdesc != 'DEPViolation': faultaddr = self.fix_efa_bug(instraddr, faultaddr) @@ -102,57 +103,38 @@ def _64bit_addr_fixup(self, faultaddr, instraddr): def _64bit_target_app(self): return self._64bit_debugger and not self.wow64_app - def pc_in_mapped_address(self, instraddr): + def _look_for_loaded_module(self, instraddr, line): ''' - Check if the instruction pointer is in a loaded module + Returns a string containing the module location if found, None otherwise + :param instraddr: + :param line: ''' pattern = RE_MAPPED_ADDRESS - mapped_module = 'unloaded' if self._64bit_debugger: pattern = RE_MAPPED_ADDRESS64 + # convert to an int as hex instraddr = instraddr.replace('`', '') instraddr = int(instraddr, 16) - for line in self.reporttext.splitlines(): - n = re.match(pattern, line) - if n: - # Strip out backticks present on 64-bit systems - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - if begin_address < instraddr < end_address: - mapped_module = n.group(3) - return mapped_module - - def format_addr(self, faultaddr): - ''' - Format a 64- or 32-bit memory address to a fixed width - ''' - if not faultaddr: - return - - faultaddr = faultaddr.strip().replace('0x', '') - - if self._64bit_debugger and not self.wow64_app: - # Due to a bug in !exploitable, the Exception Faulting Address is - # often wrong with 64-bit targets - if len(faultaddr) < 10: - # pad faultaddr - return faultaddr.zfill(16) - - if len(faultaddr) > 10: - # 0x12345678 = 10 chars - return faultaddr[-8:] - if len(faultaddr) < 10: - # pad faultaddr - return faultaddr.zfill(8) + n = re.match(pattern, line) + if n: + # Strip out backticks present on 64-bit systems + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + module_name = n.group(3) + if begin_address < instraddr < end_address: + logger.debug('Matched: %x in %x %x %s', instraddr, + begin_address, end_address, module_name) + # as soon as we find this, we're done + return module_name def fix_efa_offset(self, instructionline, faultaddr): ''' Adjust faulting address for instructions that use offsets Currently only works for instructions like CALL [reg + offset] ''' - if self._64bit_debugger and not self.wow64_app: + if self._64bit_target_app: reg_set = reg64_set if '0x' not in faultaddr: From 7d314dfb111ebc1cb184b49c6c9dc24003e9bcab Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 3 Jul 2014 10:49:44 -0400 Subject: [PATCH 0446/1169] add reg_set or reg64_set to common module --- src/certfuzz/drillresults/testcasebundle_base.py | 8 ++++++++ src/certfuzz/drillresults/testcasebundle_linux.py | 5 +++-- .../drillresults/testcasebundle_windows.py | 14 +++++++------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 53af7c2..572f677 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -13,6 +13,8 @@ from certfuzz.drillresults.errors import TestCaseBundleError import binascii import struct +from certfuzz.drillresults.common import reg64_set +from certfuzz.drillresults.common import reg_set logger = logging.getLogger(__name__) @@ -52,6 +54,12 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): # See if we're dealing with 64-bit debugger or target app self._check_64bit() + + if self._64bit_target_app: + self.reg_set = reg64_set + else: + self.reg_set = reg_set + self._get_classification() self._get_shortdesc() self._parse_testcase() diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index cd2eb5f..360494d 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -8,7 +8,6 @@ from certfuzz.drillresults.common import carve from certfuzz.drillresults.common import carve2 -from certfuzz.drillresults.common import reg_set from certfuzz.drillresults.testcasebundle_base import TestCaseBundle @@ -80,7 +79,9 @@ def fix_efa_offset(self, instructionline, faultaddr): ''' if '0x' not in faultaddr: faultaddr = '0x' + faultaddr + instructionpieces = instructionline.split() + for index, piece in enumerate(instructionpieces): if piece == 'call': # CALL instruction @@ -92,7 +93,7 @@ def fix_efa_offset(self, instructionline, faultaddr): splitaddress = address.split('+') reg = splitaddress[0] reg = reg.replace('[', '') - if reg not in reg_set: + if reg not in self.reg_set: return faultaddr offset = splitaddress[1] offset = offset.replace('h', '') diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 34a57ae..b7f858a 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -10,8 +10,6 @@ from certfuzz.drillresults.common import carve from certfuzz.drillresults.common import carve2 from certfuzz.drillresults.common import is_number -from certfuzz.drillresults.common import reg_set -from certfuzz.drillresults.common import reg64_set from certfuzz.drillresults.testcasebundle_base import TestCaseBundle @@ -93,7 +91,8 @@ def _find_testcase_file(self): TestCaseBundle._find_testcase_file(self) def _64bit_addr_fixup(self, faultaddr, instraddr): - if self._64bit_target_app and instraddr: # Put backtick into instruction address for pattern matching + if self._64bit_target_app and instraddr: + # Put backtick into instruction address for pattern matching instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) if self.shortdesc != 'DEPViolation': faultaddr = self.fix_efa_bug(instraddr, faultaddr) @@ -123,6 +122,7 @@ def _look_for_loaded_module(self, instraddr, line): begin_address = int(n.group(1).replace('`', ''), 16) end_address = int(n.group(2).replace('`', ''), 16) module_name = n.group(3) + logger.debug('%x %x %s %x', begin_address, end_address, module_name, instraddr) if begin_address < instraddr < end_address: logger.debug('Matched: %x in %x %x %s', instraddr, begin_address, end_address, module_name) @@ -134,16 +134,16 @@ def fix_efa_offset(self, instructionline, faultaddr): Adjust faulting address for instructions that use offsets Currently only works for instructions like CALL [reg + offset] ''' - if self._64bit_target_app: - reg_set = reg64_set - if '0x' not in faultaddr: faultaddr = '0x' + faultaddr + instructionpieces = instructionline.split() + if '??' not in instructionpieces[-1]: # The av is on the address of the code called, not the address # of the call return faultaddr + for index, piece in enumerate(instructionpieces): if piece == 'call': # CALL instruction @@ -155,7 +155,7 @@ def fix_efa_offset(self, instructionline, faultaddr): splitaddress = address.split('+') reg = splitaddress[0] reg = reg.replace('[', '') - if reg not in reg_set: + if reg not in self.reg_set: return faultaddr offset = splitaddress[1] offset = offset.replace('h', '') From 189765b56ee5ffd7611c26690a6030126e2777c8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 14 Jul 2014 16:28:14 -0400 Subject: [PATCH 0447/1169] refactor out common fix_efa_offset stuff --- .../drillresults/testcasebundle_base.py | 54 ++++++++++++++++++- .../drillresults/testcasebundle_linux.py | 36 ------------- .../drillresults/testcasebundle_windows.py | 34 +----------- 3 files changed, 54 insertions(+), 70 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 572f677..4f66f6d 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -15,6 +15,7 @@ import struct from certfuzz.drillresults.common import reg64_set from certfuzz.drillresults.common import reg_set +from certfuzz.drillresults.common import is_number logger = logging.getLogger(__name__) @@ -143,7 +144,10 @@ def _parse_testcase(self): instructionline = self.get_instr(instraddr) self.details['exceptions'][exceptionnum]['instructionline'] = instructionline if instructionline: - faultaddr = self.fix_efa_offset(instructionline, faultaddr) + self.instructionpieces = instructionline.split() + faultaddr = self._prefix_0x(faultaddr) + + faultaddr = self.fix_efa_offset(faultaddr) # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') @@ -207,7 +211,6 @@ def format_addr(self, faultaddr): return faultaddr - def pc_in_mapped_address(self, instraddr): ''' Check if the instruction pointer is in a loaded module @@ -312,3 +315,50 @@ def _score_testcase(self): # Faulting address has high entropy. scores.append(50) self.score = min(scores) + + def _prefix_0x(self, addr): + if addr.startswith('0x'): + return addr + else: + return '0x{}'.format(addr) + + def fix_efa_offset(self, faultaddr): + try: + index = self.instructionpieces.index('call') + except ValueError: + return faultaddr + + # CALL instruction + try: + address = self.instructionpieces[index + 3] + except IndexError: + # CALL to just a register. No offset + return faultaddr + + if not '+' in address: + return faultaddr + + splitaddress = address.split('+') + reg = splitaddress[0] + reg = reg.replace('[', '') + + if reg not in self.reg_set: + return faultaddr + + offset = splitaddress[1] + offset = offset.replace('h', '') + offset = offset.replace(']', '') + + if not is_number(offset): + return faultaddr + + offset = self._prefix_0x(offset) + + if int(offset, 16) > int(faultaddr, 16): + # TODO: fix up negative numbers + return faultaddr + + # Subtract offset to get actual interesting pattern + faultaddr = hex(eval(faultaddr) - eval(offset)) + faultaddr = self.format_addr(faultaddr.replace('L', '')) + return faultaddr diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 360494d..dadc183 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -72,42 +72,6 @@ def _look_for_loaded_module(self, instraddr, line): # as soon as we find this, we're done return module_name - def fix_efa_offset(self, instructionline, faultaddr): - ''' - Adjust faulting address for instructions that use offsets - Currently only works for instructions like CALL [reg + offset] - ''' - if '0x' not in faultaddr: - faultaddr = '0x' + faultaddr - - instructionpieces = instructionline.split() - - for index, piece in enumerate(instructionpieces): - if piece == 'call': - # CALL instruction - if len(instructionpieces) <= index + 3: - # CALL to just a register. No offset - return faultaddr - address = instructionpieces[index + 3] - if '+' in address: - splitaddress = address.split('+') - reg = splitaddress[0] - reg = reg.replace('[', '') - if reg not in self.reg_set: - return faultaddr - offset = splitaddress[1] - offset = offset.replace('h', '') - offset = offset.replace(']', '') - if '0x' not in offset: - offset = '0x' + offset - if int(offset, 16) > int(faultaddr, 16): - # TODO: fix up negative numbers - return faultaddr - # Subtract offset to get actual interesting pattern - faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = self.format_addr(faultaddr.replace('L', '')) - return faultaddr - def get_instr(self, instraddr): ''' Find the disassembly line for the current (crashing) instruction diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index b7f858a..d7b145a 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -134,42 +134,12 @@ def fix_efa_offset(self, instructionline, faultaddr): Adjust faulting address for instructions that use offsets Currently only works for instructions like CALL [reg + offset] ''' - if '0x' not in faultaddr: - faultaddr = '0x' + faultaddr - - instructionpieces = instructionline.split() - - if '??' not in instructionpieces[-1]: + if '??' not in self.instructionpieces[-1]: # The av is on the address of the code called, not the address # of the call return faultaddr - for index, piece in enumerate(instructionpieces): - if piece == 'call': - # CALL instruction - if len(instructionpieces) <= index + 3: - # CALL to just a register. No offset - return faultaddr - address = instructionpieces[index + 3] - if '+' in address: - splitaddress = address.split('+') - reg = splitaddress[0] - reg = reg.replace('[', '') - if reg not in self.reg_set: - return faultaddr - offset = splitaddress[1] - offset = offset.replace('h', '') - offset = offset.replace(']', '') - if is_number(offset): - if '0x' not in offset: - offset = '0x' + offset - if int(offset, 16) > int(faultaddr, 16): - # TODO: fix up negative numbers - return faultaddr - # Subtract offset to get actual interesting pattern - faultaddr = hex(eval(faultaddr) - eval(offset)) - faultaddr = self.format_addr(faultaddr.replace('L', '')) - return faultaddr + return TestCaseBundle.fix_efa_offset(self, faultaddr) def get_ex_num(self): ''' From 66a080264460540216769457a8b4f22ad24de123 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 08:44:41 -0400 Subject: [PATCH 0448/1169] move get_fault_addr to base class --- src/certfuzz/drillresults/testcasebundle_base.py | 5 +++++ src/certfuzz/drillresults/testcasebundle_linux.py | 4 ---- .../drillresults/testcasebundle_windows.py | 15 --------------- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 4f66f6d..a04167a 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -362,3 +362,8 @@ def fix_efa_offset(self, faultaddr): faultaddr = hex(eval(faultaddr) - eval(offset)) faultaddr = self.format_addr(faultaddr.replace('L', '')) return faultaddr + + def get_fault_addr(self): + faultaddr = carve2(self.reporttext) + logger.debug('carved fault address: %s', faultaddr) + return self.format_addr(faultaddr) diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index dadc183..5699369 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -115,7 +115,3 @@ def get_instr_addr(self): instraddr = n.group(1) #print 'Found instruction address: %s' % instraddr return self.format_addr(instraddr) - - def get_fault_addr(self): - faultaddr = carve2(self.reporttext) - return self.format_addr(faultaddr) diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index d7b145a..53cfbb1 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -199,18 +199,3 @@ def get_instr_addr(self): instraddr = carve(self.reporttext, "Instruction Address:", "\n") return self.format_addr(instraddr) - def get_fault_addr(self): - faultaddr = carve2(self.reporttext) - return self.format_addr(faultaddr) - -# def get_regs(self): -# ''' -# Populate the register dictionary with register values at crash -# ''' -# for line in self.reporttext.splitlines(): -# if RE_regs1'].match(line) or regex['regs2.match(line): -# regs1 = line.split() -# for reg in regs1: -# if "=" in reg: -# splitreg = reg.split("=") -# self.regdict[splitreg[0]] = splitreg[1] From c9b441cc14a966fce17cb33d3695898e420c146e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 08:45:50 -0400 Subject: [PATCH 0449/1169] refactor common code out of get_instr --- .../drillresults/testcasebundle_base.py | 12 ++++++++++ .../drillresults/testcasebundle_linux.py | 22 ++----------------- .../drillresults/testcasebundle_windows.py | 12 ++++------ 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index a04167a..688b4b0 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -112,6 +112,12 @@ def _check_64bit(self): Check if the debugger and target app are 64-bit ''' + @abc.abstractmethod + def get_instr(self, instraddr): + ''' + Find the disassembly line for the current (crashing) instruction + ''' + def _parse_testcase(self): ''' Parse the debugger output file @@ -245,6 +251,12 @@ def get_ex_num(self): ''' return 0 + def _match_rgx(self, rgx, return_value_func): + for line in self.reporttext.splitlines(): + n = rgx.match(line) + if n: + return return_value_func(n, line) + def _record_exception_info(self, exceptionnum): if self.classification: # Create a new exception dictionary to add to the crash diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 5699369..eeb0770 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -73,28 +73,10 @@ def _look_for_loaded_module(self, instraddr, line): return module_name def get_instr(self, instraddr): - ''' - Find the disassembly line for the current (crashing) instruction - ''' + rvfunc = lambda x, l: x.group(3) rgx = RE_CURRENT_INSTR - for line in self.reporttext.splitlines(): - n = rgx.match(line) - if n: - return n.group(3) - return '' - def fix_efa_bug(self, instraddr, faultaddr): - ''' - !exploitable often reports an incorrect EFA for 64-bit targets. - If we're dealing with a 64-bit target, we can second-guess the reported EFA - ''' - instructionline = self.get_instr(instraddr) - if not instructionline: - return faultaddr - ds = carve(instructionline, "ds:", "=") - if ds: - faultaddr = ds.replace('`', '') - return faultaddr + return self._match_rgx(rgx, rvfunc) def get_instr_addr(self): ''' diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 53cfbb1..45f10ca 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -162,14 +162,10 @@ def get_ex_num(self): return exception def get_instr(self, instraddr): - ''' - Find the disassembly line for the current (crashing) instruction - ''' - regex = re.compile('^%s\s.+.+\s+' % instraddr) - for line in self.reporttext.splitlines(): - n = regex.match(line) - if n: - return line + rvfunc = lambda x, l: l + rgx = re.compile('^%s\s.+.+\s+' % instraddr) + + return self._match_rgx(rgx, rvfunc) def fix_efa_bug(self, instraddr, faultaddr): ''' From 4a07c3ac66c3be3f7eca40b1baad639467c8636e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 08:46:51 -0400 Subject: [PATCH 0450/1169] add debug msgs --- src/certfuzz/drillresults/testcasebundle_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 688b4b0..4cdf7e3 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -194,6 +194,7 @@ def format_addr(self, faultaddr): ''' Format a 64- or 32-bit memory address to a fixed width ''' + logger.debug('formatting address [%s]', faultaddr) if not faultaddr: return @@ -204,15 +205,18 @@ def format_addr(self, faultaddr): # often wrong with 64-bit targets if len(faultaddr) < 16: # pad faultaddr if it's shorter than 64 bits + logger.debug('addr < 64 bits: pad') return faultaddr.zfill(16) # if faultaddr is longer than 32 bits, truncate it if len(faultaddr) > 8: + logger.debug('addr > 32 bits: truncate') return faultaddr[-8:] # if faultaddr is shorter than 32 bits, pad it if len(faultaddr) < 8: # pad faultaddr + logger.debug('addr < 32 bits: pad') return faultaddr.zfill(8) return faultaddr @@ -231,8 +235,10 @@ def pc_in_mapped_address(self, instraddr): module_name = self._look_for_loaded_module(instraddr, line) if module_name is not None: # short circuit as soon as we find a mapped module + logger.debug('module found: %s', module_name) return module_name # if you got here, instraddr is not in a loaded module + logger.debug('addr %s is not in loaded module', instraddr) return 'unloaded' @abc.abstractmethod From b0c5f830c693ef8784764e80627849ea343054d0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 08:47:27 -0400 Subject: [PATCH 0451/1169] clean up imports --- src/certfuzz/drillresults/testcasebundle_base.py | 1 + src/certfuzz/drillresults/testcasebundle_linux.py | 1 - src/certfuzz/drillresults/testcasebundle_windows.py | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 4cdf7e3..9b62656 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -16,6 +16,7 @@ from certfuzz.drillresults.common import reg64_set from certfuzz.drillresults.common import reg_set from certfuzz.drillresults.common import is_number +from certfuzz.drillresults.common import carve2 logger = logging.getLogger(__name__) diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index eeb0770..b9108c3 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -7,7 +7,6 @@ import re from certfuzz.drillresults.common import carve -from certfuzz.drillresults.common import carve2 from certfuzz.drillresults.testcasebundle_base import TestCaseBundle diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 45f10ca..38b7296 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -8,8 +8,6 @@ import re from certfuzz.drillresults.common import carve -from certfuzz.drillresults.common import carve2 -from certfuzz.drillresults.common import is_number from certfuzz.drillresults.testcasebundle_base import TestCaseBundle From 2bf74f99f0f1c97c1eab4ced76e238fd67c6313a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 08:47:48 -0400 Subject: [PATCH 0452/1169] alphabetize regular expressions --- src/certfuzz/drillresults/testcasebundle_linux.py | 4 ++-- src/certfuzz/drillresults/testcasebundle_windows.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index b9108c3..cda3afc 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -14,10 +14,10 @@ # compile our regular expresssions once RE_BT_ADDR = re.compile(r'(0x[0-9a-fA-F]+)\s+.+$') -RE_MAPPED_FRAME = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)') -RE_VDSO = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(\[vdso\])') RE_CURRENT_INSTR = re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)') RE_FRAME_0 = re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+') +RE_MAPPED_FRAME = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)') +RE_VDSO = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(\[vdso\])') class LinuxTestCaseBundle(TestCaseBundle): diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 38b7296..4c07149 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -15,9 +15,9 @@ # compile our regular expresssions once RE_64BIT_DEBUGGER = re.compile('^Microsoft.*AMD64$') -RE_SYSWOW64 = re.compile('ModLoad:.*syswow64.*', re.IGNORECASE) RE_MAPPED_ADDRESS = re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)') RE_MAPPED_ADDRESS64 = re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), +RE_SYSWOW64 = re.compile('ModLoad:.*syswow64.*', re.IGNORECASE) class WindowsTestCaseBundle(TestCaseBundle): From 9445216e1b0843cc3f6417395e02c2cf786eb7bb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 08:48:08 -0400 Subject: [PATCH 0453/1169] refactor scoring into separate methods --- .../drillresults/testcasebundle_base.py | 136 ++++++++++-------- 1 file changed, 79 insertions(+), 57 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 9b62656..45d7d20 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -267,72 +267,94 @@ def _match_rgx(self, rgx, return_value_func): def _record_exception_info(self, exceptionnum): if self.classification: # Create a new exception dictionary to add to the crash - exception = {} - self.details['exceptions'][exceptionnum] = exception - self.details['exceptions'][exceptionnum]['classification'] = self.classification - if self.shortdesc: - # Set !exploitable Short Description for the exception - self.details['exceptions'][exceptionnum]['shortdesc'] = self.shortdesc # Flag the entire crash ID as really exploitable if this is a good - # exception - self.details['reallyexploitable'] = self.shortdesc in self.re_set + self.details['exceptions'][exceptionnum] = {'classification': self.classification} - def _score_testcase(self): - logger.debug('Scoring testcase: %s', self.crash_hash) - details = self.details - scores = [100] - if details['reallyexploitable'] == True: - # The crash summary is a very interesting one - for exception in details['exceptions']: - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignore_jit: - # EIP is not in a loaded module - scores.append(20) - if details['exceptions'][exception]['shortdesc'] in self.re_set: - efa = '0x' + details['exceptions'][exception]['efa'] - if details['exceptions'][exception]['EIF']: - # The faulting address pattern is in the fuzzed file - if '0x000000' in efa: - # Faulting address is near null - scores.append(30) - elif '0x0000' in efa: - # Faulting address is somewhat near null - scores.append(20) - elif '0xffff' in efa: - # Faulting address is likely a negative number - scores.append(20) - else: - # Faulting address has high entropy. Most exploitable. - scores.append(10) - else: - # The faulting address pattern is not in the fuzzed file - scores.append(40) - else: - # The crash summary isn't necessarily interesting - for exception in details['exceptions']: - efa = '0x' + details['exceptions'][exception]['efa'] - module = details['exceptions'][exception]['pcmodule'] - if module == 'unloaded' and not self.ignore_jit: - scores.append(20) - elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): - # likely heap corruption. Exploitable, but difficult - scores.append(45) - elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: - # non-continued potential stack buffer overflow - scores.append(40) - elif details['exceptions'][exception]['EIF']: + if not self.shortdesc: + logger.debug('no short description') + return + + # Set !exploitable Short Description for the exception + self.details['exceptions'][exceptionnum]['shortdesc'] = self.shortdesc + # Flag the entire crash ID as really exploitable if this is a good + # exception + self.details['reallyexploitable'] = self.shortdesc in self.re_set + + def _score_interesting(self): + scores = [] + exceptions = self.details['exceptions'] + + for exception in exceptions.itervalues(): + module = exception['pcmodule'] + if module == 'unloaded' and not self.ignore_jit: + # EIP is not in a loaded module + scores.append(20) + + if exception['shortdesc'] in self.re_set: + efa = '0x' + exception['efa'] + + if exception['EIF']: # The faulting address pattern is in the fuzzed file if '0x000000' in efa: # Faulting address is near null - scores.append(70) + scores.append(30) elif '0x0000' in efa: # Faulting address is somewhat near null - scores.append(60) + scores.append(20) elif '0xffff' in efa: # Faulting address is likely a negative number - scores.append(60) + scores.append(20) else: - # Faulting address has high entropy. - scores.append(50) + # Faulting address has high entropy. Most exploitable. + scores.append(10) + else: + # The faulting address pattern is not in the fuzzed file + scores.append(40) + + return scores + + def _score_less_interesting(self): + scores = [] + exceptions = self.details['exceptions'] + + for exception in exceptions.itervalues(): + efa = '0x' + exception['efa'] + module = exception['pcmodule'] + if module == 'unloaded' and not self.ignore_jit: + scores.append(20) + elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): + # likely heap corruption. Exploitable, but difficult + scores.append(45) + elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: + # non-continued potential stack buffer overflow + scores.append(40) + elif exception['EIF']: + # The faulting address pattern is in the fuzzed file + if '0x000000' in efa: + # Faulting address is near null + scores.append(70) + elif '0x0000' in efa: + # Faulting address is somewhat near null + scores.append(60) + elif '0xffff' in efa: + # Faulting address is likely a negative number + scores.append(60) + else: + # Faulting address has high entropy. + scores.append(50) + + return scores + + def _score_testcase(self): + logger.debug('Scoring testcase: %s', self.crash_hash) + details = self.details + scores = [100] + if details['reallyexploitable'] == True: + # The crash summary is a very interesting one + scores.extend(self._score_interesting()) + else: + # The crash summary isn't necessarily interesting + scores.extend(self._score_less_interesting()) + logger.debug('accumulated scores: %s', scores) self.score = min(scores) def _prefix_0x(self, addr): From fbd79739f136d984bf5ef4da1e094e7488bf0eb7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 10:29:16 -0400 Subject: [PATCH 0454/1169] refactor for testability --- src/certfuzz/drillresults/result_driller_linux.py | 7 ++++--- src/certfuzz/drillresults/result_driller_windows.py | 7 ++++--- src/certfuzz/drillresults/testcasebundle_base.py | 10 ++++++++-- src/certfuzz/drillresults/testcasebundle_windows.py | 1 - 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/drillresults/result_driller_linux.py b/src/certfuzz/drillresults/result_driller_linux.py index f907130..96b49e2 100755 --- a/src/certfuzz/drillresults/result_driller_linux.py +++ b/src/certfuzz/drillresults/result_driller_linux.py @@ -27,6 +27,7 @@ def _platform_find_testcases(self, crash_hash, files, root): logger.debug('found gdb file: %s', dbg_file) crasherfile = dbg_file.replace('.gdb', '') #crasherfile = os.path.join(root, crasherfile) - tcb = TestCaseBundle(dbg_file, crasherfile, crash_hash, - self.ignore_jit) - self.testcase_bundles.append(tcb) + with TestCaseBundle(dbg_file, crasherfile, crash_hash, + self.ignore_jit) as tcb: + tcb.go() + self.testcase_bundles.append(tcb) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index 5c8806f..1f9dd1e 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -41,6 +41,7 @@ def _platform_find_testcases(self, crash_hash, files, root): dbg_file = os.path.join(root, current_file) if crasherfile and root not in crasherfile: crasherfile = os.path.join(root, crasherfile) - tcb = TestCaseBundle(dbg_file, crasherfile, crash_hash, - self.ignore_jit) - self.testcase_bundles.append(tcb) + with TestCaseBundle(dbg_file, crasherfile, crash_hash, + self.ignore_jit) as tcb: + tcb.go() + self.testcase_bundles.append(tcb) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 45d7d20..a724218 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -53,14 +53,14 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self._64bit_debugger = False self.classification = None self.shortdesc = None + self.reg_set = reg_set + def go(self): # See if we're dealing with 64-bit debugger or target app self._check_64bit() if self._64bit_target_app: self.reg_set = reg64_set - else: - self.reg_set = reg_set self._get_classification() self._get_shortdesc() @@ -119,6 +119,12 @@ def get_instr(self, instraddr): Find the disassembly line for the current (crashing) instruction ''' + @abc.abstractmethod + def get_instr_addr(self): + ''' + Find the address for the current (crashing) instruction + ''' + def _parse_testcase(self): ''' Parse the debugger output file diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 4c07149..153c2c9 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -192,4 +192,3 @@ def fix_efa_bug(self, instraddr, faultaddr): def get_instr_addr(self): instraddr = carve(self.reporttext, "Instruction Address:", "\n") return self.format_addr(instraddr) - From b60a96186e29c7da3d9f54cb743258efdab9c63e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 10:29:27 -0400 Subject: [PATCH 0455/1169] add unit tests --- .../drillresults/test_testcasebundle_base.py | 216 +++++++++++++++++- 1 file changed, 212 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/test/drillresults/test_testcasebundle_base.py b/src/certfuzz/test/drillresults/test_testcasebundle_base.py index f3ab4e9..964c0ca 100644 --- a/src/certfuzz/test/drillresults/test_testcasebundle_base.py +++ b/src/certfuzz/test/drillresults/test_testcasebundle_base.py @@ -5,18 +5,226 @@ ''' import unittest from certfuzz.drillresults import testcasebundle_base +import tempfile +import shutil +import os +from certfuzz.drillresults.errors import TestCaseBundleError +import re -class Test(unittest.TestCase): - def setUp(self): +class TCB(testcasebundle_base.TestCaseBundle): + really_exploitable = [] + + def _64bit_addr_fixup(self, faddr, iaddr): + return faddr, iaddr + + def _64bit_target_app(self): pass - def tearDown(self): + def _check_64bit(self): + pass + + def _get_classification(self): pass - def testName(self): + def _get_shortdesc(self): + pass + + def _look_for_loaded_module(self, iaddr, line): + pass + + def get_instr(self): + pass + + def get_instr_addr(self): + return '0xdeadbeef' + + +class Test(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + self.tcb = self._minimal_tcb() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def test_tcb_is_metaclass(self): + self.assertTrue(hasattr(testcasebundle_base.TestCaseBundle, '__metaclass__')) + + # should raise a type error if you try to instantiate it + self.assertRaises(TypeError, testcasebundle_base.TestCaseBundle) + + def _minimal_tcb(self): + dbgf = os.path.join(self.tmpdir, '_debugfile') + tcf = os.path.join(self.tmpdir, '_testcasefile') + crash_sig = 'abracadabra' + with open(dbgf, 'wb') as fp: + fp.write('foo\n') + with open(tcf, 'wb') as fp: + fp.write('bar\n') + return TCB(dbgf, tcf, crash_sig) + + def test_init(self): + dbgf = os.path.join(self.tmpdir, 'debugfile') + tcf = os.path.join(self.tmpdir, 'testcasefile') + crash_sig = 'abracadabra' + + # the files don't exist. we should get an error + self.assertRaises(TestCaseBundleError, TCB, dbgf, tcf, crash_sig) + + with open(dbgf, 'wb') as fp: + fp.write('foo\n') + + # the tc don't exist. we should get an error + self.assertRaises(TestCaseBundleError, TCB, dbgf, tcf, crash_sig) + + with open(tcf, 'wb') as fp: + fp.write('bar\n') + + tcb = TCB(dbgf, tcf, crash_sig) + self.assertEqual(dbgf, tcb.dbg_outfile) + self.assertEqual(tcf, tcb.testcase_file) + self.assertEqual(crash_sig, tcb.crash_hash) + self.assertFalse(tcb.ignore_jit) + + self.assertTrue('foo' in tcb.reporttext) + self.assertTrue('bar' in tcb.crasherdata) + self.assertEqual(self.tmpdir, tcb.current_dir) + + self.assertFalse(tcb.details['reallyexploitable']) + self.assertEqual(tcf, tcb.details['fuzzedfile']) + + self.assertEqual(100, tcb.score) + self.assertFalse(tcb._64bit_debugger) + self.assertEqual(None, tcb.classification) + self.assertEqual(None, tcb.shortdesc) + + def test_runtime_context(self): + self.assertTrue(hasattr(self.tcb, '__enter__')) + self.assertTrue(hasattr(self.tcb, '__exit__')) + + def test_format_addr(self): + # not 64 bit + self.tcb._64bit_target_app = False + for x in xrange(15): + faddr = 'a' * (x + 1) + faddr_hex = '0x' + faddr + result = self.tcb.format_addr(faddr_hex) + self.assertEqual(8, len(result)) + if len(result) >= len(faddr): + self.assertTrue(faddr in result) + else: + trunc_faddr = faddr[-len(result):] + self.assertTrue(trunc_faddr in result) + print faddr, result + + # 64 bit padding + self.tcb._64bit_target_app = True + for x in xrange(15): + faddr = 'a' * (x + 1) + faddr_hex = '0x' + faddr + result = self.tcb.format_addr(faddr_hex) + self.assertEqual(16, len(result)) + self.assertTrue(faddr in result) + print faddr, result + + def test_pc_in_mapped_address(self): + self.assertEqual('', self.tcb.pc_in_mapped_address(None)) + + self.tcb.reporttext = '\n'.join('abcdefghijklmnopqrstuvwxyz') + self.assertEqual('unloaded', self.tcb.pc_in_mapped_address('zzz')) + + # fake out the loaded module check + self.tcb._look_for_loaded_module = lambda x, y: 'foo' + self.assertEqual('foo', self.tcb.pc_in_mapped_address('zzz')) + + def test_get_ex_num(self): + self.assertEqual(0, self.tcb.get_ex_num()) + + def test_match_rgx(self): + self.tcb.reporttext = 'abc\ndef\nghi\n' + rgx = re.compile('.+(h)') + # return just the match + func = lambda x, y: x.group(1) + self.assertEqual('h', self.tcb._match_rgx(rgx, func)) + + # return the line containing the match + func = lambda x, y: y + self.assertEqual('ghi', self.tcb._match_rgx(rgx, func)) + + def test_record_exception(self): pass + def test_score_interesting(self): + # nothing to score gets an empty list + scores = self.tcb._score_interesting() + self.assertEqual(0, len(scores)) + + xc = {'pcmodule': 'unloaded', + 'shortdesc': 'wow'} + + self.tcb.details['exceptions'] = {0: xc} + scores = self.tcb._score_interesting() + self.assertEqual(1, len(scores)) + self.assertTrue(20 in scores) + + self.tcb.re_set.add('wow') + xc['efa'] = 'deadbeef' + xc['EIF'] = False + scores = self.tcb._score_interesting() + self.assertTrue(40 in scores) + + xc['pcmodule'] = 'something' + xc['EIF'] = True + scores = self.tcb._score_interesting() + self.assertTrue(10 in scores) + + xc['efa'] = '0x0000' + scores = self.tcb._score_interesting() + self.assertTrue(20 in scores) + + xc['efa'] = '0x000000' + scores = self.tcb._score_interesting() + self.assertTrue(30 in scores) + + xc['efa'] = '0xffff' + scores = self.tcb._score_interesting() + self.assertTrue(20 in scores) + + def test_score_less_interesting(self): + # nothing to score gets an empty list + scores = self.tcb._score_less_interesting() + self.assertEqual(0, len(scores)) + + xc = {'pcmodule': 'unloaded', + 'efa': 'deadbeef', + 'EIF': False} + + self.tcb.details['exceptions'] = {0: xc} + scores = self.tcb._score_less_interesting() + self.assertEqual(1, len(scores)) + self.assertTrue(20 in scores) + + xc['pcmodule'] = 'something' + xc['EIF'] = True + scores = self.tcb._score_less_interesting() + print scores + self.assertTrue(50 in scores) + + xc['efa'] = '0x0000' + scores = self.tcb._score_less_interesting() + self.assertTrue(60 in scores) + + xc['efa'] = '0x000000' + scores = self.tcb._score_less_interesting() + self.assertTrue(70 in scores) + + xc['efa'] = '0xffff' + scores = self.tcb._score_less_interesting() + self.assertTrue(60 in scores) + + + if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 2f628bc4892c96fcbc8a8f734d7596504cc9dc95 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 10:32:41 -0400 Subject: [PATCH 0456/1169] fix up unit tests --- ...{test_drillresults_linux.py => test_result_driller_linux.py} | 2 +- ...t_drillresults_windows.py => test_result_driller_windows.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/certfuzz/test/drillresults/{test_drillresults_linux.py => test_result_driller_linux.py} (85%) rename src/certfuzz/test/drillresults/{test_drillresults_windows.py => test_result_driller_windows.py} (84%) diff --git a/src/certfuzz/test/drillresults/test_drillresults_linux.py b/src/certfuzz/test/drillresults/test_result_driller_linux.py similarity index 85% rename from src/certfuzz/test/drillresults/test_drillresults_linux.py rename to src/certfuzz/test/drillresults/test_result_driller_linux.py index b54263e..d92a0df 100644 --- a/src/certfuzz/test/drillresults/test_drillresults_linux.py +++ b/src/certfuzz/test/drillresults/test_result_driller_linux.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz.drillresults import drillresults_linux +from certfuzz.drillresults import result_driller_linux class Test(unittest.TestCase): diff --git a/src/certfuzz/test/drillresults/test_drillresults_windows.py b/src/certfuzz/test/drillresults/test_result_driller_windows.py similarity index 84% rename from src/certfuzz/test/drillresults/test_drillresults_windows.py rename to src/certfuzz/test/drillresults/test_result_driller_windows.py index 29d8582..0871b96 100644 --- a/src/certfuzz/test/drillresults/test_drillresults_windows.py +++ b/src/certfuzz/test/drillresults/test_result_driller_windows.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz.drillresults import drillresults_windows +from certfuzz.drillresults import result_driller_windows class Test(unittest.TestCase): From 45886cb8655bfcf92b816f90a3de639c9464fb35 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 11:50:41 -0400 Subject: [PATCH 0457/1169] make_dev now uses 'BFF-linux' and 'BFF-windows' respectively rather than just overwriting 'BFF' in dev_builds dir --- build/devmods/build_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build/devmods/build_base.py b/build/devmods/build_base.py index 6630c2b..90e002b 100644 --- a/build/devmods/build_base.py +++ b/build/devmods/build_base.py @@ -32,7 +32,10 @@ def __init__(self, name=None, platform=None): self.my_path = os.path.abspath(os.path.dirname(__file__)) self.src_path = os.path.abspath(os.path.join(self.my_path, '../../src')) self.dev_builds_path = os.path.abspath(os.path.join(self.src_path, '..', 'dev_builds')) - self.target_path = os.path.abspath(os.path.join(self.dev_builds_path, self.name)) + + target_shortname = '{}-{}'.format(self.name, self.platform) + + self.target_path = os.path.abspath(os.path.join(self.dev_builds_path, target_shortname)) self.platform_path = os.path.join(self.src_path, self.platform) self.license_md_path = os.path.join(self.src_path, '..', 'LICENSE.md') self.license_txt_path = os.path.join(self.target_path, 'COPYING.txt') From 8d182e6beea78c726fb19d1cc64a37aa4d00101f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 25 Jul 2014 10:25:58 -0400 Subject: [PATCH 0458/1169] Test only installing Python on OS X < 10.9 (Mavericks) --- build/distmods/osx/installer/BFF_installer.pmdoc/index.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index a7cd73c..4c919f6 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -17,6 +17,9 @@ + + From c5134a8b302010b639dc2b4b270791ed41e64343 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 28 Jul 2014 11:01:00 -0400 Subject: [PATCH 0459/1169] Revert to providing our own Python version. Removes requirement for gcc for installation. --- build/distmods/osx/installer/BFF_installer.pmdoc/index.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index 4c919f6..a7cd73c 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -17,9 +17,6 @@ - - From d0935004de6503b37cc8603947c8633c052dc2ff Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 21 Oct 2014 14:49:43 -0400 Subject: [PATCH 0460/1169] fix crash on windows where _exectute_child() complains it needs 17 arguments on python 2.7.6 replace killable process with latest from http://dxr.mozilla.org/mozilla-central/source/addon-sdk/source/python-lib/mozrunner/killableprocess.py --- src/certfuzz/runners/killableprocess.py | 252 ++++++++++++++++++------ 1 file changed, 189 insertions(+), 63 deletions(-) diff --git a/src/certfuzz/runners/killableprocess.py b/src/certfuzz/runners/killableprocess.py index 6ed763d..6269023 100644 --- a/src/certfuzz/runners/killableprocess.py +++ b/src/certfuzz/runners/killableprocess.py @@ -3,12 +3,38 @@ # Parts of this module are copied from the subprocess.py file contained # in the Python distribution. # +# Copyright (c) 2003-2004 by Peter Astrand # +# Additions and modifications written by Benjamin Smedberg +# are Copyright (c) 2006 by the Mozilla Foundation +# # +# More Modifications +# Copyright (c) 2006-2007 by Mike Taylor +# Copyright (c) 2007-2008 by Mikeal Rogers # +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: # +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of the +# author not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -r"""killableprocess - Subprocesses which can be reliably killed +"""killableprocess - Subprocesses which can be reliably killed This module is a subclass of the builtin "subprocess" module. It allows processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method. @@ -25,7 +51,9 @@ import sys import os import time +import datetime import types +import exceptions try: from subprocess import CalledProcessError @@ -48,6 +76,11 @@ def __str__(self): else: import signal +# This is normally defined in win32con, but we don't want +# to incur the huge tree of dependencies (pywin32 and friends) +# just to get one constant. So here's our hack +STILL_ACTIVE = 259 + def call(*args, **kwargs): waitargs = {} if "timeout" in kwargs: @@ -71,38 +104,38 @@ def DoNothing(*args): pass class Popen(subprocess.Popen): - if not mswindows: - # Override __init__ to set a preexec_fn - def __init__(self, *args, **kwargs): - if len(args) >= 7: - raise Exception("Arguments preexec_fn and after must be passed by keyword.") - - real_preexec_fn = kwargs.pop("preexec_fn", None) - def setpgid_preexec_fn(): - os.setpgid(0, 0) - if real_preexec_fn: - apply(real_preexec_fn) - - kwargs['preexec_fn'] = setpgid_preexec_fn - - subprocess.Popen.__init__(self, *args, **kwargs) - + kill_called = False if mswindows: - def _execute_child(self, args, executable, preexec_fn, close_fds, - cwd, env, universal_newlines, startupinfo, - creationflags, shell, - p2cread, p2cwrite, - c2pread, c2pwrite, - errread, errwrite): + def _execute_child(self, *args_tuple): + # workaround for bug 958609 + if sys.hexversion < 0x02070600: # prior to 2.7.6 + (args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, startupinfo, + creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) = args_tuple + to_close = set() + else: # 2.7.6 and later + (args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, startupinfo, + creationflags, shell, to_close, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) = args_tuple + if not isinstance(args, types.StringTypes): args = subprocess.list2cmdline(args) + # Always or in the create new process group + creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP + if startupinfo is None: startupinfo = winprocess.STARTUPINFO() if None not in (p2cread, c2pwrite, errwrite): startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES - + startupinfo.hStdInput = int(p2cread) startupinfo.hStdOutput = int(c2pwrite) startupinfo.hStdError = int(errwrite) @@ -112,14 +145,18 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, comspec = os.environ.get("COMSPEC", "cmd.exe") args = comspec + " /c " + args - # We create a new job for this process, so that we can kill - # the process and any sub-processes - self._job = winprocess.CreateJobObject() + # determine if we can create create a job + canCreateJob = winprocess.CanCreateJobObject() + # set process creation flags creationflags |= winprocess.CREATE_SUSPENDED creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT - creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB + if canCreateJob: + # Uncomment this line below to discover very useful things about your environment + #print "++++ killableprocess: releng twistd patch not applied, we can create job objects" + creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB + # create the process hp, ht, pid, tid = winprocess.CreateProcess( executable, args, None, None, # No special security @@ -127,14 +164,22 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, creationflags, winprocess.EnvironmentBlock(env), cwd, startupinfo) - self._child_created = True self._handle = hp self._thread = ht self.pid = pid + self.tid = tid - winprocess.AssignProcessToJobObject(self._job, hp) - winprocess.ResumeThread(ht) + if canCreateJob: + # We create a new job for this process, so that we can kill + # the process and any sub-processes + self._job = winprocess.CreateJobObject() + winprocess.AssignProcessToJobObject(self._job, int(hp)) + else: + self._job = None + + winprocess.ResumeThread(int(ht)) + ht.Close() if p2cread is not None: p2cread.Close() @@ -142,62 +187,143 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, c2pwrite.Close() if errwrite is not None: errwrite.Close() + time.sleep(.1) def kill(self, group=True): """Kill the process. If group=True, all sub-processes will also be killed.""" + self.kill_called = True + if mswindows: - if group: + if group and self._job: winprocess.TerminateJobObject(self._job, 127) else: winprocess.TerminateProcess(self._handle, 127) - self.returncode = 127 + self.returncode = 127 else: if group: - os.killpg(self.pid, signal.SIGKILL) + try: + os.killpg(self.pid, signal.SIGKILL) + except: pass else: os.kill(self.pid, signal.SIGKILL) self.returncode = -9 - def wait(self, timeout=-1, group=True): + def wait(self, timeout=None, group=True): """Wait for the process to terminate. Returns returncode attribute. If timeout seconds are reached and the process has not terminated, it will be forcefully killed. If timeout is -1, wait will not time out.""" + if timeout is not None: + # timeout is now in milliseconds + timeout = timeout * 1000 - if self.returncode is not None: - return self.returncode + starttime = datetime.datetime.now() if mswindows: - if timeout != -1: - timeout = timeout * 1000 + if timeout is None: + timeout = -1 rc = winprocess.WaitForSingleObject(self._handle, timeout) - if rc == winprocess.WAIT_TIMEOUT: - self.kill(group) + + if (rc == winprocess.WAIT_OBJECT_0 or + rc == winprocess.WAIT_ABANDONED or + rc == winprocess.WAIT_FAILED): + # Object has either signaled, or the API call has failed. In + # both cases we want to give the OS the benefit of the doubt + # and supply a little time before we start shooting processes + # with an M-16. + + # Returns 1 if running, 0 if not, -1 if timed out + def check(): + now = datetime.datetime.now() + diff = now - starttime + if (diff.seconds * 1000000 + diff.microseconds) < (timeout * 1000): # (1000*1000) + if self._job: + if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0): + # Job Object is still containing active processes + return 1 + else: + # No job, we use GetExitCodeProcess, which will tell us if the process is still active + self.returncode = winprocess.GetExitCodeProcess(self._handle) + if (self.returncode == STILL_ACTIVE): + # Process still active, continue waiting + return 1 + # Process not active, return 0 + return 0 + else: + # Timed out, return -1 + return -1 + + notdone = check() + while notdone == 1: + time.sleep(.5) + notdone = check() + + if notdone == -1: + # Then check timed out, we have a hung process, attempt + # last ditch kill with explosives + self.kill(group) + else: - self.returncode = winprocess.GetExitCodeProcess(self._handle) + # In this case waitforsingleobject timed out. We have to + # take the process behind the woodshed and shoot it. + self.kill(group) + else: - if timeout == -1: - subprocess.Popen.wait(self) - return self.returncode - - starttime = time.time() - - # Make sure there is a signal handler for SIGCHLD installed - oldsignal = signal.signal(signal.SIGCHLD, DoNothing) - - while time.time() < starttime + timeout - 0.01: - pid, sts = os.waitpid(self.pid, os.WNOHANG) - if pid != 0: - self._handle_exitstatus(sts) - signal.signal(signal.SIGCHLD, oldsignal) + if sys.platform in ('linux2', 'sunos5', 'solaris') \ + or sys.platform.startswith('freebsd'): + def group_wait(timeout): + try: + os.waitpid(self.pid, 0) + except OSError, e: + pass # If wait has already been called on this pid, bad things happen + return self.returncode + elif sys.platform == 'darwin': + def group_wait(timeout): + try: + count = 0 + if timeout is None and self.kill_called: + timeout = 10 # Have to set some kind of timeout or else this could go on forever + if timeout is None: + while 1: + os.killpg(self.pid, signal.SIG_DFL) + while ((count * 2) <= timeout): + os.killpg(self.pid, signal.SIG_DFL) + # count is increased by 500ms for every 0.5s of sleep + time.sleep(.5); count += 500 + except exceptions.OSError: + return self.returncode + + if timeout is None: + if group is True: + return group_wait(timeout) + else: + subprocess.Popen.wait(self) return self.returncode - - # time.sleep is interrupted by signals (good!) - newtimeout = timeout - time.time() + starttime - time.sleep(newtimeout) - self.kill(group) - signal.signal(signal.SIGCHLD, oldsignal) - subprocess.Popen.wait(self) + returncode = False + + now = datetime.datetime.now() + diff = now - starttime + while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000) and (returncode is False): + if group is True: + return group_wait(timeout) + else: + if subprocess.poll() is not None: + returncode = self.returncode + time.sleep(.5) + now = datetime.datetime.now() + diff = now - starttime + return self.returncode return self.returncode + # We get random maxint errors from subprocesses __del__ + __del__ = lambda self: None + +def setpgid_preexec_fn(): + os.setpgid(0, 0) + +def runCommand(cmd, **kwargs): + if sys.platform != "win32": + return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs) + else: + return Popen(cmd, **kwargs) From e97eca34aff548df7b9492a6b62a628b7601651f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 21 Oct 2014 16:19:07 -0400 Subject: [PATCH 0461/1169] Only allow wget to try once to get PIN --- src/linux/batch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index 06b2fc7..3313d4d 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -70,7 +70,7 @@ if [[ "$platform" =~ "Linux" ]]; then echo PIN not detected. Downloading... tarball=~/fuzzing/`basename $PINURL` pindir=`basename $tarball .tar.gz` - wget $PINURL -O $tarball + wget --tries=1 $PINURL -O $tarball if [[ -f $tarball ]]; then tar xzvf $tarball -C ~ mv ~/$pindir ~/pin From 9b58fca38fb56d029aaeeba06e58d819d7709c60 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Oct 2014 08:32:53 -0400 Subject: [PATCH 0462/1169] create ZzufFuzzer class --- src/certfuzz/fuzzers/zzuf.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/certfuzz/fuzzers/zzuf.py diff --git a/src/certfuzz/fuzzers/zzuf.py b/src/certfuzz/fuzzers/zzuf.py new file mode 100644 index 0000000..77a0915 --- /dev/null +++ b/src/certfuzz/fuzzers/zzuf.py @@ -0,0 +1,27 @@ +''' +Created on Oct 22, 2014 + +@author: adh +''' +from certfuzz.fuzzers import Fuzzer +import subprocess + + +class ZzufFuzzer(Fuzzer): + ''' + This "fuzzer" copies input_file_path to output_file_path. Useful for + testing and "refining" a set of crashing test cases through a debugger. + ''' + def _fuzz(self): + # run zzuf and put its output in self.fuzzed + self.range = self.sf.rangefinder.next_item() + zzufargs = ['zzuf', + '--quiet', + '--ratio={}:{}'.format(self.range.min, self.range.max), + '--seed={}'.format(self.rng_seed), + ] + p = subprocess.Popen(args=zzufargs, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + (stdoutdata, _stderrdata) = p.communicate(input=self.input) + self.fuzzed = stdoutdata + +_fuzzer_class = ZzufFuzzer From 74e6fa9d1ed5fe93bc4731c369aab05e7eccc8cc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Oct 2014 08:47:47 -0400 Subject: [PATCH 0463/1169] add unit tests for ZzufFuzzer --- src/certfuzz/test/fuzzers/test_zzuf.py | 65 ++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/certfuzz/test/fuzzers/test_zzuf.py diff --git a/src/certfuzz/test/fuzzers/test_zzuf.py b/src/certfuzz/test/fuzzers/test_zzuf.py new file mode 100644 index 0000000..53ddb55 --- /dev/null +++ b/src/certfuzz/test/fuzzers/test_zzuf.py @@ -0,0 +1,65 @@ +''' +Created on Oct 22, 2014 + +@organization: cert.org +''' +import unittest +from certfuzz.test.mocks import MockSeedfile +import tempfile +import shutil +from certfuzz.fuzzers.zzuf import ZzufFuzzer +import hashlib + + +class Test(unittest.TestCase): + + def setUp(self): + self.sf = seedfile_obj = MockSeedfile() + self.tempdir = tempfile.mkdtemp() + self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', + dir=self.tempdir) + rng_seed = 0 + iteration = 0 + self.options = {} +# self.options = {'min_ratio': 0.1, 'max_ratio': 0.2} + self.args = (seedfile_obj, outdir_base, rng_seed, iteration, self.options) + + def tearDown(self): + shutil.rmtree(self.tempdir) + + def _fail_if_not_fuzzed(self, fuzzed): + for c in fuzzed: + if c != 'A': + break + else: + self.fail('Input not fuzzed') + + def test_is_minimizable(self): + f = ZzufFuzzer(*self.args) + self.assertTrue(f.is_minimizable) + + def test_fuzz(self): + self.assertTrue(self.sf.len > 0) + fuzzed_output_seen = set() + for i in xrange(200): + with ZzufFuzzer(*self.args) as f: + f.iteration = i + f._fuzz() + # same length, different output + self.assertEqual(self.sf.len, len(f.fuzzed)) + self._fail_if_not_fuzzed(f.fuzzed) + + # check for no repeats + md5 = hashlib.md5(f.fuzzed).hexdigest() + if md5 in fuzzed_output_seen: + self.fail('Fuzzer repeated output: %s' % md5) + else: + fuzzed_output_seen.add(md5) + + # confirm ratio +# self.assertAlmostEqual(f.ratio, f.fuzzed_bit_ratio(), 2) + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 43b6b7a3a1d51cc849a956728cc7b4c39da5a7bc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Oct 2014 09:10:25 -0400 Subject: [PATCH 0464/1169] figure out where zzuf is dynamically throw an error if it is not found in $PATH also update unit tests --- src/certfuzz/fuzzers/zzuf.py | 22 +++++++++++++++------- src/certfuzz/test/fuzzers/test_zzuf.py | 12 +++++++++--- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/fuzzers/zzuf.py b/src/certfuzz/fuzzers/zzuf.py index 77a0915..4524613 100644 --- a/src/certfuzz/fuzzers/zzuf.py +++ b/src/certfuzz/fuzzers/zzuf.py @@ -3,22 +3,30 @@ @author: adh ''' -from certfuzz.fuzzers import Fuzzer import subprocess +from certfuzz.fuzzers.fuzzer_base import MinimizableFuzzer +from certfuzz.fuzzers.errors import FuzzerError +from distutils.spawn import find_executable +import os -class ZzufFuzzer(Fuzzer): - ''' - This "fuzzer" copies input_file_path to output_file_path. Useful for - testing and "refining" a set of crashing test cases through a debugger. +class ZzufFuzzer(MinimizableFuzzer): ''' + This fuzzer uses Sam Hocevar's zzuf to mangle self.input and puts the results into + self.fuzzed''' def _fuzz(self): # run zzuf and put its output in self.fuzzed + + zzufloc = find_executable('zzuf') + if zzufloc is None: + raise FuzzerError('Unable to locate zzuf in %s' % os.environ['PATH']) + self.range = self.sf.rangefinder.next_item() - zzufargs = ['zzuf', + + zzufargs = [zzufloc, '--quiet', '--ratio={}:{}'.format(self.range.min, self.range.max), - '--seed={}'.format(self.rng_seed), + '--seed={}'.format(self.iteration), ] p = subprocess.Popen(args=zzufargs, stdin=subprocess.PIPE, stdout=subprocess.PIPE) (stdoutdata, _stderrdata) = p.communicate(input=self.input) diff --git a/src/certfuzz/test/fuzzers/test_zzuf.py b/src/certfuzz/test/fuzzers/test_zzuf.py index 53ddb55..b04c755 100644 --- a/src/certfuzz/test/fuzzers/test_zzuf.py +++ b/src/certfuzz/test/fuzzers/test_zzuf.py @@ -9,11 +9,20 @@ import shutil from certfuzz.fuzzers.zzuf import ZzufFuzzer import hashlib +import os class Test(unittest.TestCase): def setUp(self): + # zzuf might not be in the default paths, so check a few other + # locations too + alternate_bin_locs = set(['/opt/local/bin']) + alt_bin_locs_exist = (l for l in alternate_bin_locs if os.path.exists(l)) + add_locs = (l for l in alt_bin_locs_exist if not l in os.environ['PATH']) + for loc in add_locs: + os.environ['PATH'] += os.pathsep + loc + self.sf = seedfile_obj = MockSeedfile() self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', @@ -56,9 +65,6 @@ def test_fuzz(self): else: fuzzed_output_seen.add(md5) - # confirm ratio -# self.assertAlmostEqual(f.ratio, f.fuzzed_bit_ratio(), 2) - if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From 731e94c5bded7cd9141039c619535ba4f6c560aa Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 27 Oct 2014 09:15:26 -0400 Subject: [PATCH 0465/1169] use a more specific error --- src/certfuzz/fuzzers/errors.py | 4 ++++ src/certfuzz/fuzzers/zzuf.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/fuzzers/errors.py b/src/certfuzz/fuzzers/errors.py index 0dd42b3..3174baa 100644 --- a/src/certfuzz/fuzzers/errors.py +++ b/src/certfuzz/fuzzers/errors.py @@ -18,3 +18,7 @@ class FuzzerExhaustedError(FuzzerError): class FuzzerInputMatchesOutputError(FuzzerError): pass + + +class FuzzerNotFoundError(FuzzerError): + pass diff --git a/src/certfuzz/fuzzers/zzuf.py b/src/certfuzz/fuzzers/zzuf.py index 4524613..6239881 100644 --- a/src/certfuzz/fuzzers/zzuf.py +++ b/src/certfuzz/fuzzers/zzuf.py @@ -5,7 +5,7 @@ ''' import subprocess from certfuzz.fuzzers.fuzzer_base import MinimizableFuzzer -from certfuzz.fuzzers.errors import FuzzerError +from certfuzz.fuzzers.errors import FuzzerNotFoundError from distutils.spawn import find_executable import os @@ -19,7 +19,7 @@ def _fuzz(self): zzufloc = find_executable('zzuf') if zzufloc is None: - raise FuzzerError('Unable to locate zzuf in %s' % os.environ['PATH']) + raise FuzzerNotFoundError('Unable to locate zzuf in %s' % os.environ['PATH']) self.range = self.sf.rangefinder.next_item() From e7934fae3764a4b76ae5892b77011f2c84b2e3a8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 27 Oct 2014 09:15:51 -0400 Subject: [PATCH 0466/1169] skip test if zzuf not found --- src/certfuzz/test/fuzzers/test_zzuf.py | 31 ++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/certfuzz/test/fuzzers/test_zzuf.py b/src/certfuzz/test/fuzzers/test_zzuf.py index b04c755..2f0b941 100644 --- a/src/certfuzz/test/fuzzers/test_zzuf.py +++ b/src/certfuzz/test/fuzzers/test_zzuf.py @@ -10,6 +10,7 @@ from certfuzz.fuzzers.zzuf import ZzufFuzzer import hashlib import os +from certfuzz.fuzzers.errors import FuzzerNotFoundError class Test(unittest.TestCase): @@ -50,21 +51,23 @@ def test_is_minimizable(self): def test_fuzz(self): self.assertTrue(self.sf.len > 0) fuzzed_output_seen = set() - for i in xrange(200): - with ZzufFuzzer(*self.args) as f: - f.iteration = i - f._fuzz() - # same length, different output - self.assertEqual(self.sf.len, len(f.fuzzed)) - self._fail_if_not_fuzzed(f.fuzzed) - - # check for no repeats - md5 = hashlib.md5(f.fuzzed).hexdigest() - if md5 in fuzzed_output_seen: - self.fail('Fuzzer repeated output: %s' % md5) - else: - fuzzed_output_seen.add(md5) + try: + for i in xrange(200): + with ZzufFuzzer(*self.args) as f: + f.iteration = i + f._fuzz() + # same length, different output + self.assertEqual(self.sf.len, len(f.fuzzed)) + self._fail_if_not_fuzzed(f.fuzzed) + # check for no repeats + md5 = hashlib.md5(f.fuzzed).hexdigest() + if md5 in fuzzed_output_seen: + self.fail('Fuzzer repeated output: %s' % md5) + else: + fuzzed_output_seen.add(md5) + except FuzzerNotFoundError as e: + self.skipTest("zzuf not found in path") if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From df71148d1d6249530e654e055afb2589afc7d2b5 Mon Sep 17 00:00:00 2001 From: "Edward J. Schwartz" Date: Thu, 9 Oct 2014 14:34:02 -0700 Subject: [PATCH 0467/1169] Raise an exception when the config file cannot be opened --- src/certfuzz/campaign/config/bff_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index c242b6b..e1e4b0c 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -40,7 +40,7 @@ def read_config_options(cfg_file): @rtype: ConfigHelper ''' config = ConfigParser.ConfigParser() - config.read(cfg_file) + config.readfp(open(cfg_file)) return ConfigHelper(config) From b23b0493f154a718de2800ef7b983bc516bad192 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 27 Oct 2014 13:07:07 -0400 Subject: [PATCH 0468/1169] use with... syntax for open. --- src/certfuzz/campaign/config/bff_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index e1e4b0c..ae133a4 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -40,7 +40,8 @@ def read_config_options(cfg_file): @rtype: ConfigHelper ''' config = ConfigParser.ConfigParser() - config.readfp(open(cfg_file)) + with open(cfg_file) as fp: + config.readfp(fp) return ConfigHelper(config) From 90f7a0eda125ac6994d454187b4a7566c1f4db88 Mon Sep 17 00:00:00 2001 From: "Edward J. Schwartz" Date: Thu, 9 Oct 2014 12:58:30 -0700 Subject: [PATCH 0469/1169] Initial keep_duplicates implementation --- src/certfuzz/campaign/config/bff_config.py | 4 ++++ src/certfuzz/iteration/iteration_linux.py | 8 ++++++-- src/linux/conf.d/bff.cfg | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py index ae133a4..2ac2953 100644 --- a/src/certfuzz/campaign/config/bff_config.py +++ b/src/certfuzz/campaign/config/bff_config.py @@ -108,6 +108,10 @@ def __init__(self, cfg): self.recycle_crashers = self.cfg.getboolean('verifier', 'recycle_crashers') else: self.recycle_crashers = False + if self.cfg.has_option('verifier', 'keep_duplicates'): + self.keep_duplicates = self.cfg.getboolean('verifier', 'keep_duplicates') + else: + self.keep_duplicates = False # [directories] self.remote_dir = os.path.expanduser(self.cfg.get('directories', 'remote_dir')) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index a067ee5..fa1f08d 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -200,10 +200,11 @@ def _verify(self, testcase): tc.is_unique = is_new_to_campaign and not crash_dir_found + self.dbg_out_file_orig = testcase.dbg.file + logger.debug('Original debugger file: %s', self.dbg_out_file_orig) + if tc.is_unique: logger.info('%s first seen at %d', tc.signature, tc.seednum) - self.dbg_out_file_orig = testcase.dbg.file - logger.debug('Original debugger file: %s', self.dbg_out_file_orig) self._minimize(tc) @@ -212,6 +213,9 @@ def _verify(self, testcase): self.verified.append(tc) else: logger.debug('%s was found, not unique', tc.signature) + if self.cfg.keep_duplicates: + logger.debug('Analyzing %s anyway because keep_duplicates is set', tc.signature) + self.verified.append(tc) def _minimize(self, testcase): if self.cfg.minimizecrashers: diff --git a/src/linux/conf.d/bff.cfg b/src/linux/conf.d/bff.cfg index 709eee2..3dd4d6b 100644 --- a/src/linux/conf.d/bff.cfg +++ b/src/linux/conf.d/bff.cfg @@ -135,6 +135,10 @@ minimize_to_string = False # rather than finding new underlying vulnerabilities. recycle_crashers = False +# Save crashes even when they are unique? If set to True, all +# crashing inputs are saved, even when their backtraces are not +# unique. +keep_duplicates = False ################################################################ # APPLICATION TIMEOUTS From caa5ee37fa6f7dfecd6ea527f031fb553e082dfc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Dec 2014 13:24:15 -0500 Subject: [PATCH 0470/1169] Blind attempt at adding Yosemite-compatible CrashWrangler to OSX installer --- .../1152854_crashwrangler.xml | 4 ++-- .../BFF_installer.pmdoc/29exc-contents.xml | 1 + .../installer/BFF_installer.pmdoc/29exc.xml | 18 ++++++++++++++++++ .../installer/BFF_installer.pmdoc/index.xml | 2 ++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 build/distmods/osx/installer/BFF_installer.pmdoc/29exc-contents.xml create mode 100644 build/distmods/osx/installer/BFF_installer.pmdoc/29exc.xml diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml index fb03708..febe577 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml @@ -1,11 +1,11 @@ - + com.cert.cc.certBff.52854_crashwrangler.pkg 1.0 -52854_crashwrangler.zip +56876_crashwrangler.zip /Applications/BFF.app/Contents/Resources/packages diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/29exc-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/29exc-contents.xml new file mode 100644 index 0000000..69b34ea --- /dev/null +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/29exc-contents.xml @@ -0,0 +1 @@ + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/29exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/29exc.xml new file mode 100644 index 0000000..c277912 --- /dev/null +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/29exc.xml @@ -0,0 +1,18 @@ + + +com.cert.cc.certBff.exc_handler_yosemite.pkg +1.0 + + + +crashwrangler/binaries/exc_handler_yosemite +/usr/local/bin + + + + +parent +installFrom.isRelativeType +installTo + + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index a7cd73c..de8bfa3 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -49,6 +49,7 @@ + @@ -109,6 +110,7 @@ 26callgrind.xml 27pyyaml.xml 28exc.xml +29exc.xml properties.customizeOption properties.title From d06322b25c259f212096872d5ec52f28116a2b49 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Dec 2014 16:05:24 -0500 Subject: [PATCH 0471/1169] Don't install hcluster on Yosemite or later (it doesn't compile). Switch to using just ulimit for huge file prevention. Apple fixed bug 11809010 --- .../osx/installer/BFF_installer.pmdoc/index.xml | 1 + src/linux/batch.sh | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index de8bfa3..50287e4 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -35,6 +35,7 @@ + diff --git a/src/linux/batch.sh b/src/linux/batch.sh index 3313d4d..155350f 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -56,13 +56,9 @@ else mypython="python" fi -if [[ "$platform" =~ "Darwin" ]]; then - # This doesn't actually do anything - # Maybe Apple will eventually fix it? - launchctl limit filesize 1048576 -else - ulimit -f 1048576 -fi + +# Prevent creation of huge files +ulimit -f 1048576 if [[ "$platform" =~ "Linux" ]]; then if [[ ! -f ~/pin/pin ]]; then From ac74d3663c4e8910c381ce9e914291be979caff7 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Dec 2014 16:26:43 -0500 Subject: [PATCH 0472/1169] Forgot xml tag --- build/distmods/osx/installer/BFF_installer.pmdoc/index.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index 50287e4..ebc16fa 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -35,7 +35,9 @@ + From ddf2065cafcfe2cc2a16801cf9519176d24fd21f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Dec 2014 16:42:22 -0500 Subject: [PATCH 0473/1169] swap requirement logic. Only install hcluster if OS version < 10.10.0 --- build/distmods/osx/installer/BFF_installer.pmdoc/index.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index ebc16fa..19095c6 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -36,7 +36,7 @@ - From 3404884dfd1285c632ea80d14869dcc685093e8c Mon Sep 17 00:00:00 2001 From: "Edward J. Schwartz" Date: Fri, 12 Dec 2014 12:18:57 -0500 Subject: [PATCH 0474/1169] Fix error in reset script --- src/linux/reset_bff.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linux/reset_bff.sh b/src/linux/reset_bff.sh index 3c565ce..4a93d59 100755 --- a/src/linux/reset_bff.sh +++ b/src/linux/reset_bff.sh @@ -5,7 +5,7 @@ # local copy of the config file so that you can start # fresh. Probably best to reboot afterwards -platform=`uname -a` +platform=`uname` if [ $# -lt 1 ] then @@ -35,7 +35,7 @@ rm -rfv ~/fuzzing rm -rfv ~/bff.cfg if [ "$1" = "--remove-results" ]; then - if [ "$platform" =~ "Linux" ]; then + if [ "$platform" = "Linux" ]; then # wipe the remote results too sudo rm -rfv ~/results/* else From 850b8dd172bc37b6dff97e9e8ac0bee9e9c6523e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Oct 2014 09:29:24 -0400 Subject: [PATCH 0475/1169] create empty zzufrun Runner and test modules --- src/certfuzz/runners/zzufrun.py | 5 +++++ src/certfuzz/test/runners/test_zzufrun.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/certfuzz/runners/zzufrun.py create mode 100644 src/certfuzz/test/runners/test_zzufrun.py diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py new file mode 100644 index 0000000..602aed2 --- /dev/null +++ b/src/certfuzz/runners/zzufrun.py @@ -0,0 +1,5 @@ +''' +Created on Oct 22, 2014 + +@author: adh +''' diff --git a/src/certfuzz/test/runners/test_zzufrun.py b/src/certfuzz/test/runners/test_zzufrun.py new file mode 100644 index 0000000..8089b32 --- /dev/null +++ b/src/certfuzz/test/runners/test_zzufrun.py @@ -0,0 +1,22 @@ +''' +Created on Oct 22, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 17e2ef5a967a70d2e22a67a028714cbf4a394f75 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 24 Oct 2014 14:35:08 -0400 Subject: [PATCH 0476/1169] unify __init__ args & consolidate default attributes into parent class --- src/certfuzz/runners/runner_base.py | 3 ++- src/certfuzz/runners/winrun.py | 7 +------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/runners/runner_base.py b/src/certfuzz/runners/runner_base.py index 5aebbd4..22a7db3 100644 --- a/src/certfuzz/runners/runner_base.py +++ b/src/certfuzz/runners/runner_base.py @@ -11,7 +11,7 @@ class Runner(object): ''' classdocs ''' - def __init__(self, options, workingdir_base): + def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): ''' Constructor ''' @@ -22,6 +22,7 @@ def __init__(self, options, workingdir_base): self.hideoutput = options.get('hideoutput', False) self.runtimeout = options.get('runtimeout', 5) self.saw_crash = False + self.fuzzed_file = fuzzed_file self.workingdir = workingdir_base diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 8c8bb35..6e8255a 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -60,7 +60,7 @@ def kill(p): class WinRunner(RunnerBase): def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): - RunnerBase.__init__(self, options, workingdir_base) + RunnerBase.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) logger.debug('Initialize Runner') self.runtimeout = None @@ -72,11 +72,6 @@ def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): except KeyError: raise RunnerError('At least one exception code must be specified in runner config.') -# self._parse_options() - self.saw_crash = False - - self.workingdir = workingdir_base - (self.cmd, self.cmdlist) = get_command_args_list(cmd_template, fuzzed_file) logger.debug('Command: %s', self.cmd) From 21a753939e0556da03a2f45ac9556516b3de3826 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 24 Oct 2014 15:58:40 -0400 Subject: [PATCH 0477/1169] first shot at zzuf runner. doesn't work yet --- src/certfuzz/fuzzers/zzuf.py | 15 ++++-- src/certfuzz/runners/zzufrun.py | 56 +++++++++++++++++++++++ src/certfuzz/test/runners/test_zzufrun.py | 49 ++++++++++++++++++-- 3 files changed, 111 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/fuzzers/zzuf.py b/src/certfuzz/fuzzers/zzuf.py index 6239881..d9395e8 100644 --- a/src/certfuzz/fuzzers/zzuf.py +++ b/src/certfuzz/fuzzers/zzuf.py @@ -14,16 +14,21 @@ class ZzufFuzzer(MinimizableFuzzer): ''' This fuzzer uses Sam Hocevar's zzuf to mangle self.input and puts the results into self.fuzzed''' - def _fuzz(self): - # run zzuf and put its output in self.fuzzed + _zzuf_loc = None + + def __enter__(self): + self = MinimizableFuzzer.__enter__(self) - zzufloc = find_executable('zzuf') - if zzufloc is None: + self._zzuf_loc = find_executable('zzuf') + if self._zzuf_loc is None: raise FuzzerNotFoundError('Unable to locate zzuf in %s' % os.environ['PATH']) + return self + + def _fuzz(self): self.range = self.sf.rangefinder.next_item() - zzufargs = [zzufloc, + zzufargs = [self._zzuf_loc, '--quiet', '--ratio={}:{}'.format(self.range.min, self.range.max), '--seed={}'.format(self.iteration), diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 602aed2..70154af 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -3,3 +3,59 @@ @author: adh ''' +from certfuzz.runners.runner_base import Runner +from distutils.spawn import find_executable +from certfuzz.runners.errors import RunnerError +import os +import subprocess +import logging +from collections import deque + +logger = logging.getLogger(__name__) + + +class ZzufRunner(Runner): + def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): + Runner.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) + self._zzuf_loc = None + self._zzuf_args = None + self._zzuf_log_basename = 'zzuf_log.txt' + self._zzuf_log = os.path.join(self.workingdir, self._zzuf_log_basename) + self._quiet = options.get('quiet', True) + + def _get_zzuf_args(self): + self._zzuf_args = deque(['--signal', + '--ratio=0.0', + '--seed=0', + '--max-crashes=1', + '--max-usertime=5.00', + ]) + + if self._quiet: + self._zzuf_args.appendleft('--quiet') + + def _find_zzuf(self): + self._zzuf_loc = find_executable('zzuf') + if self._zzuf_loc is None: + raise RunnerError('Unable to locate zzuf, $PATH={}'.format(os.environ['PATH'])) + + def __enter__(self): + self = Runner.__enter__(self) + + self._find_zzuf() + self._get_zzuf_args() + self._zzuf_args.appendleft(self._zzuf_loc) + logger.debug('_zzuf_args=%s', self._zzuf_args) + + return self + + def _run(self): + if not len(self._zzuf_args): + raise RunnerError('_zzuf_args is empty') + + with open(self.fuzzed_file, 'rb') as ff, open(self._zzuf_log, 'ab') as zo: + p = subprocess.Popen(self._zzuf_args, cwd=self.workingdir, stdin=ff, stderr=zo) + rc = p.wait() + + if rc != 0: + raise RunnerError('zzuf returncode: {}'.format(rc)) diff --git a/src/certfuzz/test/runners/test_zzufrun.py b/src/certfuzz/test/runners/test_zzufrun.py index 8089b32..d2bcd90 100644 --- a/src/certfuzz/test/runners/test_zzufrun.py +++ b/src/certfuzz/test/runners/test_zzufrun.py @@ -4,17 +4,58 @@ @organization: cert.org ''' import unittest +from certfuzz.runners.zzufrun import ZzufRunner +import shutil +import tempfile +import os class Test(unittest.TestCase): - def setUp(self): - pass + # zzuf might not be in the default paths, so check a few other + # locations too + alternate_bin_locs = set(['/opt/local/bin']) + alt_bin_locs_exist = (l for l in alternate_bin_locs if os.path.exists(l)) + add_locs = (l for l in alt_bin_locs_exist if not l in os.environ['PATH']) + for loc in add_locs: + os.environ['PATH'] += os.pathsep + loc + + self._filecontent = 'A' * 1000 + + self.tmpdir = tempfile.mkdtemp() + fd, fname = tempfile.mkstemp(suffix='fuzzed', dir=self.tmpdir) + os.write(fd, self._filecontent) + os.close(fd) + + self.ff = fname def tearDown(self): - pass + shutil.rmtree(self.tmpdir) + + def test_quiet_flag(self): + zr = ZzufRunner(options={'quiet': True}, cmd_template=None, + fuzzed_file=None, workingdir_base=self.tmpdir) + self.assertTrue(zr._quiet) + with zr: + self.assertTrue('--quiet' in zr._zzuf_args) + + zr = ZzufRunner(options={'quiet': False}, cmd_template=None, + fuzzed_file=None, workingdir_base=self.tmpdir) + self.assertFalse(zr._quiet) + with zr: + self.assertFalse('--quiet' in zr._zzuf_args) + + + def test_run(self): + options = {} + cmd_template = '' + + for i in xrange(100): + with ZzufRunner(options, cmd_template, self.ff, self.tmpdir) as r: + print r.__dict__ + r._run() - def testName(self): + self.assertTrue(False) pass if __name__ == "__main__": From be45438dce30c1c4259150741443b124be3d0e5a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 30 Jun 2014 08:57:08 -0400 Subject: [PATCH 0478/1169] add metaclass stuff to IterationBase3 --- src/certfuzz/iteration/iteration_base3.py | 41 ++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 0c898b4..23e98b4 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -10,12 +10,15 @@ from certfuzz.analyzers.errors import AnalyzerEmptyOutputError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +import abc logger = logging.getLogger(__name__) class IterationBase3(object): + __metaclass__ = abc.ABCMeta + def __init__(self, workdirbase): logger.debug('init') self.workdirbase = workdirbase @@ -45,27 +48,35 @@ def __exit__(self, etype, value, traceback): return handled + @abc.abstractmethod def _pre_fuzz(self): pass + @abc.abstractmethod def _fuzz(self): pass + @abc.abstractmethod def _post_fuzz(self): pass + @abc.abstractmethod def _pre_run(self): pass + @abc.abstractmethod def _run(self): pass + @abc.abstractmethod def _post_run(self): pass + @abc.abstractmethod def _pre_analyze(self, testcase): pass + @abc.abstractmethod def _analyze(self, testcase): ''' Loops through all known analyzer_classes for a given testcase @@ -83,52 +94,78 @@ def _analyze(self, testcase): self.analyzed.append(testcase) + @abc.abstractmethod def _post_analyze(self, testcase): pass + @abc.abstractmethod def _pre_verify(self, testcase): pass + @abc.abstractmethod def _verify(self, testcase): pass + @abc.abstractmethod def _post_verify(self, testcase): pass + @abc.abstractmethod def _pre_report(self, testcase): pass + @abc.abstractmethod def _report(self, testcase): pass + @abc.abstractmethod def _post_report(self, testcase): pass def fuzz(self): + ''' + Prepares a test case + ''' logger.debug('fuzz') self._pre_fuzz() self._fuzz() self._post_fuzz() def run(self): + ''' + Runs a test case. Populates self.candidates if it finds anything + interesting. + ''' logger.debug('run') self._pre_run() self._run() self._post_run() def verify(self, testcase): + ''' + Verifies a test case. + :param testcase: + ''' logger.debug('verify') self._pre_verify(testcase) self._verify(testcase) self._post_verify(testcase) def analyze(self, testcase): + ''' + Analyzes a test case + :param testcase: + ''' logger.debug('analyze') self._pre_analyze(testcase) self._analyze(testcase) self._post_analyze(testcase) def report(self, testcase): + ''' + Prepares the test case report + :param testcase: + ''' logger.debug('report') self._pre_report(testcase) self._report(testcase) @@ -143,6 +180,9 @@ def go(self): if not self.candidates: return + + # FIXME: This is wrong. Don't modify the list you're looping over. + # TODO: Come up with a better way to do this. # every test case is a candidate until verified # use a while loop so we have the option of adding # candidates during the loop @@ -160,4 +200,3 @@ def go(self): testcase = self.analyzed.pop(0) self.report(testcase) - From ad4846c20f4f04704c5a725ef948f9164eb68de2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 15 Jul 2014 16:25:09 -0400 Subject: [PATCH 0479/1169] replace lists with queues. --- src/certfuzz/iteration/iteration_base3.py | 31 +++++++++-------------- src/certfuzz/iteration/iteration_linux.py | 11 ++++---- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 23e98b4..593282d 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -11,6 +11,7 @@ from certfuzz.file_handlers.watchdog_file import touch_watchdog_file import abc +import Queue logger = logging.getLogger(__name__) @@ -25,9 +26,9 @@ def __init__(self, workdirbase): self.working_dir = None self.analyzer_classes = [] - self.candidates = [] - self.verified = [] - self.analyzed = [] + self.candidates = Queue.Queue() + self.verified = Queue.Queue() + self.analyzed = Queue.Queue() self.debug = True @@ -92,7 +93,7 @@ def _analyze(self, testcase): except AnalyzerEmptyOutputError: logger.warning('Unexpected empty output from analyzer_class. Continuing') - self.analyzed.append(testcase) + self.analyzed.put(testcase) @abc.abstractmethod def _post_analyze(self, testcase): @@ -176,27 +177,19 @@ def go(self): self.fuzz() self.run() - # short circuit if nothing found - if not self.candidates: - return - - - # FIXME: This is wrong. Don't modify the list you're looping over. - # TODO: Come up with a better way to do this. # every test case is a candidate until verified # use a while loop so we have the option of adding # candidates during the loop - while len(self.candidates) > 0: - testcase = self.candidates.pop(0) + while not self.candidates.empty(): + testcase = self.candidates.get() self.verify(testcase) - # analyze each verified crash - while len(self.verified) > 0: - testcase = self.verified.pop(0) + while not self.verified.empty(): + testcase = self.verified.get() self.analyze(testcase) - # construct output bundle for each analyzed test case - while len(self.analyzed) > 0: - testcase = self.analyzed.pop(0) + while not self.analyzed.empty(): + testcase = self.analyzed.get() self.report(testcase) + diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index fa1f08d..a0769fb 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -175,7 +175,7 @@ def _post_run(self): testcase.logger.debug("zzuflog: %s", zzuf_log.line) # testcase.logger.info('Command: %s', testcase.cmdline) - self.candidates.append(testcase) + self.candidates.put(testcase) def _verify(self, testcase): ''' @@ -205,12 +205,13 @@ def _verify(self, testcase): if tc.is_unique: logger.info('%s first seen at %d', tc.signature, tc.seednum) - + self.dbg_out_file_orig = tc.dbg.file + logger.debug('Original debugger file: %s', self.dbg_out_file_orig) self._minimize(tc) # we're ready to proceed with this testcase # so add it to the verified list - self.verified.append(tc) + self.verified.put(tc) else: logger.debug('%s was found, not unique', tc.signature) if self.cfg.keep_duplicates: @@ -248,7 +249,8 @@ def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): maxtime=self.cfg.minimizertimeout ) as m: m.go() - self.candidates.extend(m.other_crashes.values()) + for new_tc in m.other_crashes.values(): + self.candidates.put(new_tc) except MinimizerError as e: logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) m = None @@ -308,7 +310,6 @@ def _post_analyze(self, testcase): # & range. Should we still do that? self.record_success() - def _pre_report(self, testcase): uniqlogger = get_uniq_logger(self.cfg.uniq_log) uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) From 3b660e143ea8a2a2aded78633c5f1f0cc8e83ae3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 09:30:49 -0400 Subject: [PATCH 0480/1169] we were using testcase as a var name twice, don't do that --- src/certfuzz/iteration/iteration_linux.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index a0769fb..ca38cfb 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -151,11 +151,10 @@ def _post_run(self): with ZzufTestCase(seedfile=self.seedfile, seed=self.s1, range=zzuf_range, - working_dir=self.working_dir) as testcase: - testcase.generate() + working_dir=self.working_dir) as ztc: + ztc.generate() - # Do internal verification using GDB / Valgrind / Stderr - fuzzedfile = BasicFile(testcase.outfile) + fuzzedfile = BasicFile(ztc.outfile) testcase = BffCrash(cfg=self.cfg, seedfile=self.seedfile, From a68e52106057bae499f9e9449e9adae1550f72ef Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 09:35:00 -0400 Subject: [PATCH 0481/1169] change the verify/analyze/report steps into a pipeline of coroutines Using a coroutine pipeline lets us eliminate a couple queues, and starts to allow for more flexibility in how we assemble analyses downstream. --- src/certfuzz/helpers/coroutine.py | 14 ++++ src/certfuzz/iteration/iteration_base3.py | 95 +++++++++++++---------- src/certfuzz/iteration/iteration_linux.py | 14 ++-- 3 files changed, 75 insertions(+), 48 deletions(-) create mode 100644 src/certfuzz/helpers/coroutine.py diff --git a/src/certfuzz/helpers/coroutine.py b/src/certfuzz/helpers/coroutine.py new file mode 100644 index 0000000..e952556 --- /dev/null +++ b/src/certfuzz/helpers/coroutine.py @@ -0,0 +1,14 @@ +''' +Created on Jul 16, 2014 + +@author: adh +''' + + +#from http://www.dabeaz.com/coroutines/coroutine.py +def coroutine(func): + def start(*args, **kwargs): + cr = func(*args, **kwargs) + cr.next() + return cr + return start diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 593282d..d76032b 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -12,6 +12,7 @@ from certfuzz.file_handlers.watchdog_file import touch_watchdog_file import abc import Queue +from certfuzz.helpers.coroutine import coroutine logger = logging.getLogger(__name__) @@ -26,9 +27,9 @@ def __init__(self, workdirbase): self.working_dir = None self.analyzer_classes = [] - self.candidates = Queue.Queue() - self.verified = Queue.Queue() - self.analyzed = Queue.Queue() + self.tc_candidate_q = Queue.Queue() + + self.analysis_pipeline = self.verify(self.analyze(self.report())) self.debug = True @@ -93,8 +94,6 @@ def _analyze(self, testcase): except AnalyzerEmptyOutputError: logger.warning('Unexpected empty output from analyzer_class. Continuing') - self.analyzed.put(testcase) - @abc.abstractmethod def _post_analyze(self, testcase): pass @@ -134,7 +133,7 @@ def fuzz(self): def run(self): ''' - Runs a test case. Populates self.candidates if it finds anything + Runs a test case. Populates self.tc_candidate_q if it finds anything interesting. ''' logger.debug('run') @@ -142,35 +141,63 @@ def run(self): self._run() self._post_run() - def verify(self, testcase): + @coroutine + def verify(self, target=None): ''' - Verifies a test case. + Verifies that a test case is unique before sending the test case. Acts + as a filter on the analysis pipeline. :param testcase: ''' - logger.debug('verify') - self._pre_verify(testcase) - self._verify(testcase) - self._post_verify(testcase) - - def analyze(self, testcase): + logger.debug('Verifier standing by for testcases') + while True: + testcase = (yield) + + logger.debug('verify testcase') + self._pre_verify(testcase) + self._verify(testcase) + self._post_verify(testcase) + + if target is not None: + if testcase.is_unique: + # we're ready to proceed with this testcase + # so send it downstream + target.send(testcase) + + @coroutine + def analyze(self, target=None): ''' - Analyzes a test case + Analyzes a test case before passing it down the pipeline :param testcase: ''' - logger.debug('analyze') - self._pre_analyze(testcase) - self._analyze(testcase) - self._post_analyze(testcase) + logger.debug('Analyzer standing by for testcases') + while True: + testcase = (yield) - def report(self, testcase): + logger.debug('analyze testcase') + self._pre_analyze(testcase) + self._analyze(testcase) + self._post_analyze(testcase) + + if target is not None: + target.send(testcase) + + @coroutine + def report(self, target=None): ''' - Prepares the test case report + Prepares the test case report. :param testcase: ''' - logger.debug('report') - self._pre_report(testcase) - self._report(testcase) - self._post_report(testcase) + logger.debug('Reporter standing by for testcases') + while True: + testcase = (yield) + + logger.debug('report testcase') + self._pre_report(testcase) + self._report(testcase) + self._post_report(testcase) + + if target is not None: + target.send(testcase) def go(self): logger.debug('go') @@ -179,17 +206,7 @@ def go(self): # every test case is a candidate until verified # use a while loop so we have the option of adding - # candidates during the loop - while not self.candidates.empty(): - testcase = self.candidates.get() - self.verify(testcase) - - while not self.verified.empty(): - testcase = self.verified.get() - self.analyze(testcase) - - while not self.analyzed.empty(): - testcase = self.analyzed.get() - self.report(testcase) - - + # tc_candidate_q during the loop + while not self.tc_candidate_q.empty(): + testcase = self.tc_candidate_q.get() + self.analysis_pipeline.send(testcase) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index ca38cfb..488896b 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -16,8 +16,7 @@ from certfuzz.crash.bff_crash import BffCrash from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport -from certfuzz.file_handlers import seedfile_set -from certfuzz.fuzztools import bff_helper as z, filetools +from certfuzz.fuzztools import filetools from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.fuzztools.zzuf import Zzuf, ZzufTestCase from certfuzz.fuzztools.zzuflog import ZzufLog @@ -174,7 +173,7 @@ def _post_run(self): testcase.logger.debug("zzuflog: %s", zzuf_log.line) # testcase.logger.info('Command: %s', testcase.cmdline) - self.candidates.put(testcase) + self.tc_candidate_q.put(testcase) def _verify(self, testcase): ''' @@ -184,8 +183,8 @@ def _verify(self, testcase): STATE_TIMER.enter_state('verify_testcase') IterationBase3._verify(self, testcase) - # if you find more testcases, append them to self.candidates - # verified crashes append to self.verified + # if you find more testcases, append them to self.tc_candidate_q + # tc_verified_q crashes append to self.tc_verified_q logger.debug('verifying crash') with testcase as tc: @@ -208,9 +207,6 @@ def _verify(self, testcase): logger.debug('Original debugger file: %s', self.dbg_out_file_orig) self._minimize(tc) - # we're ready to proceed with this testcase - # so add it to the verified list - self.verified.put(tc) else: logger.debug('%s was found, not unique', tc.signature) if self.cfg.keep_duplicates: @@ -249,7 +245,7 @@ def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): ) as m: m.go() for new_tc in m.other_crashes.values(): - self.candidates.put(new_tc) + self.tc_candidate_q.put(new_tc) except MinimizerError as e: logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) m = None From 9df8be4fbb3381d0890d995a0092ed0101a3e4ed Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 10:58:27 -0400 Subject: [PATCH 0482/1169] reorder methods. also allow multiple downstream targets for each coroutine --- src/certfuzz/iteration/iteration_base3.py | 55 ++++---- src/certfuzz/iteration/iteration_linux.py | 153 +++++++++++----------- 2 files changed, 104 insertions(+), 104 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index d76032b..320f959 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -29,13 +29,15 @@ def __init__(self, workdirbase): self.tc_candidate_q = Queue.Queue() - self.analysis_pipeline = self.verify(self.analyze(self.report())) + # this gets set up in __enter__ + self.analysis_pipeline = None self.debug = True def __enter__(self): self.working_dir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) logger.debug('workdir=%s', self.working_dir) + self._setup_analysis_pipeline() return self def __exit__(self, etype, value, traceback): @@ -50,7 +52,11 @@ def __exit__(self, etype, value, traceback): return handled - @abc.abstractmethod + def _setup_analysis_pipeline(self): + # build up the pipeline: + # verify | analyze | report + self.analysis_pipeline = self.verify(self.analyze(self.report())) + def _pre_fuzz(self): pass @@ -58,11 +64,9 @@ def _pre_fuzz(self): def _fuzz(self): pass - @abc.abstractmethod def _post_fuzz(self): pass - @abc.abstractmethod def _pre_run(self): pass @@ -70,11 +74,19 @@ def _pre_run(self): def _run(self): pass - @abc.abstractmethod def _post_run(self): pass + def _pre_verify(self, testcase): + pass + @abc.abstractmethod + def _verify(self, testcase): + pass + + def _post_verify(self, testcase): + pass + def _pre_analyze(self, testcase): pass @@ -94,23 +106,9 @@ def _analyze(self, testcase): except AnalyzerEmptyOutputError: logger.warning('Unexpected empty output from analyzer_class. Continuing') - @abc.abstractmethod def _post_analyze(self, testcase): pass - @abc.abstractmethod - def _pre_verify(self, testcase): - pass - - @abc.abstractmethod - def _verify(self, testcase): - pass - - @abc.abstractmethod - def _post_verify(self, testcase): - pass - - @abc.abstractmethod def _pre_report(self, testcase): pass @@ -118,7 +116,6 @@ def _pre_report(self, testcase): def _report(self, testcase): pass - @abc.abstractmethod def _post_report(self, testcase): pass @@ -142,11 +139,11 @@ def run(self): self._post_run() @coroutine - def verify(self, target=None): + def verify(self, *targets): ''' Verifies that a test case is unique before sending the test case. Acts as a filter on the analysis pipeline. - :param testcase: + :param targets: one or more downstream coroutines to send the testcase to ''' logger.debug('Verifier standing by for testcases') while True: @@ -157,17 +154,17 @@ def verify(self, target=None): self._verify(testcase) self._post_verify(testcase) - if target is not None: + for target in targets: if testcase.is_unique: # we're ready to proceed with this testcase # so send it downstream target.send(testcase) @coroutine - def analyze(self, target=None): + def analyze(self, *targets): ''' Analyzes a test case before passing it down the pipeline - :param testcase: + :param targets: one or more downstream coroutines to send the testcase to ''' logger.debug('Analyzer standing by for testcases') while True: @@ -178,14 +175,14 @@ def analyze(self, target=None): self._analyze(testcase) self._post_analyze(testcase) - if target is not None: + for target in targets: target.send(testcase) @coroutine - def report(self, target=None): + def report(self, *targets): ''' Prepares the test case report. - :param testcase: + :param targets: one or more downstream coroutines to send the testcase to ''' logger.debug('Reporter standing by for testcases') while True: @@ -196,7 +193,7 @@ def report(self, target=None): self._report(testcase) self._post_report(testcase) - if target is not None: + for target in targets: target.send(testcase) def go(self): diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 488896b..0b548f0 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -76,30 +76,10 @@ def __exit__(self, etype, value, traceback): self.cfg.clean_tmpdir() return handled - def record_success(self): - self.sf_set.record_success(key=self.seedfile.md5) - self.rf.record_success(key=self.r.id) - - def record_failure(self): - self.record_tries() - - def record_tries(self): - self.sf_set.record_tries(key=self.seedfile.md5, tries=1) - self.rf.record_tries(key=self.r.id, tries=1) - - def _setup_analyzers(self): - self.analyzer_classes.append(stderr.StdErr) - self.analyzer_classes.append(cw_gmalloc.CrashWranglerGmalloc) - - if self.cfg.use_valgrind: - self.analyzer_classes.append(valgrind.Valgrind) - self.analyzer_classes.append(callgrind.Callgrind) - - if self.cfg.use_pin_calltrace: - self.analyzer_classes.append(pin_calltrace.Pin_calltrace) + def _fuzz(self): + pass def _pre_run(self): - IterationBase3._pre_run(self) # do the fuzz cmdline = self.cfg.get_command(self.sf.path) @@ -213,6 +193,82 @@ def _verify(self, testcase): logger.debug('Analyzing %s anyway because keep_duplicates is set', tc.signature) self.verified.append(tc) + def _pre_analyze(self, testcase): + STATE_TIMER.enter_state('analyze_testcase') + + # get one last debugger output for the newly minimized file + if testcase.pc_in_function: + # change the debugger template + testcase.set_debugger_template('complete') + else: + # use a debugger template that specifies fixed offsets from $pc for disassembly + testcase.set_debugger_template('complete_nofunction') + logger.info('Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) + testcase.get_debug_output(testcase.fuzzedfile.path) + + if self.dbg_out_file_orig != testcase.dbg.file: + # we have a new debugger output + # remove the old one + filetools.delete_files(self.dbg_out_file_orig) + if os.path.exists(self.dbg_out_file_orig): + logger.warning('Failed to remove old debugger file %s', self.dbg_out_file_orig) + else: + logger.debug('Removed old debug file %s', self.dbg_out_file_orig) + + def _analyze(self, testcase): + IterationBase3._analyze(self, testcase) + + def _post_analyze(self, testcase): + logger.info('Annotating callgrind output') + try: + annotate_callgrind(testcase) + annotate_callgrind_tree(testcase) + except CallgrindAnnotateEmptyOutputFileError: + logger.warning('Unexpected empty output from annotate_callgrind. Continuing') + except CallgrindAnnotateMissingInputFileError: + logger.warning('Missing callgrind output. Continuing') + + def _pre_report(self, testcase): + uniqlogger = get_uniq_logger(self.cfg.uniq_log) + uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) + logger.info('%s first seen at %d', testcase.signature, testcase.seednum) + + # whether it was unique or not, record some details for posterity + # record the details of this crash so we can regenerate it later if needed + testcase.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', testcase.seedfile.basename, testcase.seednum, testcase.range, testcase.fuzzedfile.path) + testcase.logger.info('PC=%s', testcase.pc) + + def _report(self, testcase): + testcase.copy_files() + + def _post_report(self, testcase): + # always clean up after yourself + testcase.clean_tmpdir() + # clean up + testcase.delete_files() + + def record_success(self): + self.sf_set.record_success(key=self.seedfile.md5) + self.rf.record_success(key=self.r.id) + + def record_failure(self): + self.record_tries() + + def record_tries(self): + self.sf_set.record_tries(key=self.seedfile.md5, tries=1) + self.rf.record_tries(key=self.r.id, tries=1) + + def _setup_analyzers(self): + self.analyzer_classes.append(stderr.StdErr) + self.analyzer_classes.append(cw_gmalloc.CrashWranglerGmalloc) + + if self.cfg.use_valgrind: + self.analyzer_classes.append(valgrind.Valgrind) + self.analyzer_classes.append(callgrind.Callgrind) + + if self.cfg.use_pin_calltrace: + self.analyzer_classes.append(pin_calltrace.Pin_calltrace) + def _minimize(self, testcase): if self.cfg.minimizecrashers: self._mininimize_to_seedfile(testcase) @@ -250,41 +306,6 @@ def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) m = None - def _pre_analyze(self, testcase): - IterationBase3._pre_analyze(self, testcase) - - STATE_TIMER.enter_state('analyze_testcase') - - # get one last debugger output for the newly minimized file - if testcase.pc_in_function: - # change the debugger template - testcase.set_debugger_template('complete') - else: - # use a debugger template that specifies fixed offsets from $pc for disassembly - testcase.set_debugger_template('complete_nofunction') - logger.info('Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) - testcase.get_debug_output(testcase.fuzzedfile.path) - - if self.dbg_out_file_orig != testcase.dbg.file: - # we have a new debugger output - # remove the old one - filetools.delete_files(self.dbg_out_file_orig) - if os.path.exists(self.dbg_out_file_orig): - logger.warning('Failed to remove old debugger file %s', self.dbg_out_file_orig) - else: - logger.debug('Removed old debug file %s', self.dbg_out_file_orig) - - def _post_analyze(self, testcase): - IterationBase3._post_analyze(self, testcase) - - logger.info('Annotating callgrind output') - try: - annotate_callgrind(testcase) - annotate_callgrind_tree(testcase) - except CallgrindAnnotateEmptyOutputFileError: - logger.warning('Unexpected empty output from annotate_callgrind. Continuing') - except CallgrindAnnotateMissingInputFileError: - logger.warning('Missing callgrind output. Continuing') # TODO # if self.cfg.recycle_crashers: @@ -305,21 +326,3 @@ def _post_analyze(self, testcase): # & range. Should we still do that? self.record_success() - def _pre_report(self, testcase): - uniqlogger = get_uniq_logger(self.cfg.uniq_log) - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) - logger.info('%s first seen at %d', testcase.signature, testcase.seednum) - - # whether it was unique or not, record some details for posterity - # record the details of this crash so we can regenerate it later if needed - testcase.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', testcase.seedfile.basename, testcase.seednum, testcase.range, testcase.fuzzedfile.path) - testcase.logger.info('PC=%s', testcase.pc) - - def _report(self, testcase): - testcase.copy_files() - - def _post_report(self, testcase): - # always clean up after yourself - testcase.clean_tmpdir() - # clean up - testcase.delete_files() From a947d4899f4f0b8a783175800f631f43490cfc8c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 11:46:51 -0400 Subject: [PATCH 0483/1169] move minimizer up from inside verify method to be part of the analysis pipeline --- src/certfuzz/iteration/iteration_base3.py | 32 +++++++++++++++++++++-- src/certfuzz/iteration/iteration_linux.py | 13 +++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 320f959..646fdac 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -54,8 +54,12 @@ def __exit__(self, etype, value, traceback): def _setup_analysis_pipeline(self): # build up the pipeline: - # verify | analyze | report - self.analysis_pipeline = self.verify(self.analyze(self.report())) + # verify | minimize | analyze | report + r = self.report() + a = self.analyze(r) + m = self.minimize(a) + + self.analysis_pipeline = self.verify(m) def _pre_fuzz(self): pass @@ -87,6 +91,16 @@ def _verify(self, testcase): def _post_verify(self, testcase): pass + def _pre_minimize(self, testcase): + pass + + @abc.abstractmethod + def _minimize(self, testacse): + pass + + def _post_minimize(self, testcase): + pass + def _pre_analyze(self, testcase): pass @@ -160,6 +174,20 @@ def verify(self, *targets): # so send it downstream target.send(testcase) + @coroutine + def minimize(self, *targets): + logger.debug('Minimizer standing by for testcases') + while True: + testcase = (yield) + + logger.debug('minimize testcase') + self._pre_minimize(testcase) + self._minimize(testcase) + self._post_minimize(testcase) + + for target in targets: + target.send(testcase) + @coroutine def analyze(self, *targets): ''' diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 0b548f0..5987274 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -185,14 +185,18 @@ def _verify(self, testcase): logger.info('%s first seen at %d', tc.signature, tc.seednum) self.dbg_out_file_orig = tc.dbg.file logger.debug('Original debugger file: %s', self.dbg_out_file_orig) - self._minimize(tc) - else: logger.debug('%s was found, not unique', tc.signature) if self.cfg.keep_duplicates: logger.debug('Analyzing %s anyway because keep_duplicates is set', tc.signature) self.verified.append(tc) + def _minimize(self, testcase): + if self.cfg.minimizecrashers: + self._mininimize_to_seedfile(testcase) + if self.cfg.minimize_to_string: + self._minimize_to_string(testcase) + def _pre_analyze(self, testcase): STATE_TIMER.enter_state('analyze_testcase') @@ -269,11 +273,6 @@ def _setup_analyzers(self): if self.cfg.use_pin_calltrace: self.analyzer_classes.append(pin_calltrace.Pin_calltrace) - def _minimize(self, testcase): - if self.cfg.minimizecrashers: - self._mininimize_to_seedfile(testcase) - if self.cfg.minimize_to_string: - self._minimize_to_string(testcase) def _mininimize_to_seedfile(self, testcase): self._minimize_generic(testcase, sftarget=True, confidence=0.999) From 72a0835988b2818f7da8d1f6e0b7e38b63b27d05 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 12:36:59 -0400 Subject: [PATCH 0484/1169] refactor test case processing into a distinct method --- src/certfuzz/iteration/iteration_base3.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 646fdac..f73569b 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -224,14 +224,16 @@ def report(self, *targets): for target in targets: target.send(testcase) - def go(self): - logger.debug('go') - self.fuzz() - self.run() - + def process_testcases(self): # every test case is a candidate until verified # use a while loop so we have the option of adding # tc_candidate_q during the loop while not self.tc_candidate_q.empty(): testcase = self.tc_candidate_q.get() self.analysis_pipeline.send(testcase) + + def go(self): + logger.debug('go') + self.fuzz() + self.run() + self.process_testcases() From fb8fe49894d99ba095e51b7a4e60c94d227621be Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 12:38:24 -0400 Subject: [PATCH 0485/1169] make windows inherit from IterationBase3 --- src/certfuzz/iteration/iteration_base.py | 60 --------------------- src/certfuzz/iteration/iteration_linux.py | 2 +- src/certfuzz/iteration/iteration_windows.py | 31 ++++++++--- 3 files changed, 26 insertions(+), 67 deletions(-) delete mode 100644 src/certfuzz/iteration/iteration_base.py diff --git a/src/certfuzz/iteration/iteration_base.py b/src/certfuzz/iteration/iteration_base.py deleted file mode 100644 index b3b5c21..0000000 --- a/src/certfuzz/iteration/iteration_base.py +++ /dev/null @@ -1,60 +0,0 @@ -''' -Created on Aug 1, 2013 - -@organization: cert.org -''' -import logging - -logger = logging.getLogger(__name__) - - -class IterationBase2(object): - def __init__(self): - pass - - def __enter__(self): - pass - - def __exit__(self, etype, value, traceback): - pass - - def keep_crash(self, crash): - pass - - def _create_minimizer_cfg(self): - pass - - def minimize(self, crash): - pass - - def _copy_seedfile(self): - pass - - def copy_files(self, crash): - pass - - def record_success(self): - pass - - def record_failure(self): - pass - - def _process_crash(self, crash): - pass - - def _log_crash(self, crash): - pass - - def _build_crash(self, fuzzer, cmdlist, dbg_opts, fuzzed_file): - pass - - def _fuzz_and_run(self): - pass - - def go(self): - logger.info('Iteration: %d File: %s', self.current_seed, self.sf.path) - self._fuzz_and_run() - - # process all the crashes - for c in self.crashes: - self._process_crash(c) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 5987274..d8bf023 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -220,6 +220,7 @@ def _pre_analyze(self, testcase): logger.debug('Removed old debug file %s', self.dbg_out_file_orig) def _analyze(self, testcase): + # we'll just use the implementation in our parent class IterationBase3._analyze(self, testcase) def _post_analyze(self, testcase): @@ -273,7 +274,6 @@ def _setup_analyzers(self): if self.cfg.use_pin_calltrace: self.analyzer_classes.append(pin_calltrace.Pin_calltrace) - def _mininimize_to_seedfile(self, testcase): self._minimize_generic(testcase, sftarget=True, confidence=0.999) # calculate the hamming distances for this crash diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 6699acf..b07ea20 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -24,7 +24,8 @@ from certfuzz.runners.errors import RunnerRegistryError from certfuzz.iteration.errors import IterationError -from certfuzz.iteration.iteration_base import IterationBase2 +from certfuzz.iteration.iteration_base3 import IterationBase3 +#from certfuzz.iteration.iteration_base import IterationBase2 logger = logging.getLogger(__name__) @@ -33,10 +34,11 @@ MAX_IOERRORS = 5 -class Iteration(IterationBase2): +class Iteration(IterationBase3): def __init__(self, sf, rng_seed, current_seed, config, fuzzer, runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, - cmd_template, uniq_func, working_dir_base, outdir, debug): + cmd_template, uniq_func, workdirbase, outdir, debug): + IterationBase3.__init__(self, workdirbase) self.sf = sf self.r = None self.rng_seed = rng_seed @@ -54,7 +56,6 @@ def __init__(self, sf, rng_seed, current_seed, config, fuzzer, self.uniq_func = uniq_func self.fuzzed = False self.outdir = outdir - self.working_dir_base = working_dir_base self.crash = None self.success = False self.iteration_tmpdir_pfx = 'iteration_' @@ -71,7 +72,7 @@ def __enter__(self): ''' set up an iteration context ''' - self.working_dir = tempfile.mkdtemp(prefix=self.iteration_tmpdir_pfx, dir=self.working_dir_base) + self.working_dir = tempfile.mkdtemp(prefix=self.iteration_tmpdir_pfx, dir=self.workdirbase) self.crashes = [] return self @@ -133,7 +134,7 @@ def __exit__(self, etype, value, traceback): # this iteration's temp dir paths = [self.working_dir] # sweep up any iteration temp dirs left behind previously - pattern = os.path.join(self.working_dir_base, self.iteration_tmpdir_pfx + '*') + pattern = os.path.join(self.workdirbase, self.iteration_tmpdir_pfx + '*') paths.extend(glob.glob(pattern)) delete_files_or_dirs(paths) # wipe them out, all of them @@ -141,6 +142,24 @@ def __exit__(self, etype, value, traceback): return handled + def _fuzz(self): + pass + + def _run(self): + pass + + def _verify(self, testcase): + pass + + def _minimize(self, testcase): + pass + + def _analyze(self, testcase): + pass + + def _report(self, testcase): + pass + def keep_crash(self, crash): '''Given a crash, decide whether it is a keeper. Returns a tuple containing a boolean indicating whether to keep the crash, and From 1dbbc700e279624d4004702eada81b1a4a562e84 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 13:14:12 -0400 Subject: [PATCH 0486/1169] refactor windows iteration functions into IterationBase3's methods --- src/certfuzz/campaign/campaign_base.py | 2 +- src/certfuzz/campaign/campaign_windows.py | 4 +- src/certfuzz/iteration/iteration_base3.py | 2 +- src/certfuzz/iteration/iteration_linux.py | 7 +- src/certfuzz/iteration/iteration_windows.py | 204 +++++++++----------- 5 files changed, 99 insertions(+), 120 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 623786c..74389fe 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -234,7 +234,7 @@ def _check_prog(self): def _set_fuzzer(self): self.fuzzer_module = import_module_by_name(self.fuzzer_module_name) - self.fuzzer = self.fuzzer_module._fuzzer_class + self.fuzzer_cls = self.fuzzer_module._fuzzer_class def _set_runner(self): if self.runner_module_name: diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index b802735..dc98f2f 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -79,7 +79,7 @@ def __getstate__(self): # for attributes that are modules, # we can safely delete them as they will be # reconstituted when we __enter__ a context - for key in ['fuzzer_module', 'fuzzer', + for key in ['fuzzer_module', 'fuzzer_cls', 'runner_module', 'runner', 'debugger_module', 'dbg_class' ]: @@ -212,7 +212,7 @@ def _do_iteration(self, sf, rng_seed, seednum): # use a with...as to ensure we always hit # the __enter__ and __exit__ methods of the # newly created Iteration() - with Iteration(sf, rng_seed, seednum, self.config, self.fuzzer, + with Iteration(sf, rng_seed, seednum, self.config, self.fuzzer_cls, self.runner, self.debugger_module, self.dbg_class, self.keep_heisenbugs, self.keep_duplicates, self.cmd_template, self._crash_is_unique, diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index f73569b..e10ccbd 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -169,7 +169,7 @@ def verify(self, *targets): self._post_verify(testcase) for target in targets: - if testcase.is_unique: + if testcase.should_proceed_with_analysis: # we're ready to proceed with this testcase # so send it downstream target.send(testcase) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index d8bf023..eb8ac47 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -176,12 +176,9 @@ def _verify(self, testcase): # crash_dir_found = filetools.find_or_create_dir(tc.result_dir) - tc.is_unique = is_new_to_campaign and not crash_dir_found + tc.should_proceed_with_analysis = is_new_to_campaign and not crash_dir_found - self.dbg_out_file_orig = testcase.dbg.file - logger.debug('Original debugger file: %s', self.dbg_out_file_orig) - - if tc.is_unique: + if tc.should_proceed_with_analysis: logger.info('%s first seen at %d', tc.signature, tc.seednum) self.dbg_out_file_orig = tc.dbg.file logger.debug('Original debugger file: %s', self.dbg_out_file_orig) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index b07ea20..f6d7cbc 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -35,7 +35,7 @@ class Iteration(IterationBase3): - def __init__(self, sf, rng_seed, current_seed, config, fuzzer, + def __init__(self, sf, rng_seed, current_seed, config, fuzzer_cls, runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, cmd_template, uniq_func, workdirbase, outdir, debug): IterationBase3.__init__(self, workdirbase) @@ -44,7 +44,7 @@ def __init__(self, sf, rng_seed, current_seed, config, fuzzer, self.rng_seed = rng_seed self.current_seed = current_seed self.config = config - self.fuzzer = fuzzer + self.fuzzer_cls = fuzzer_cls self.runner = runner self.debugger_module = debugger self.debugger_class = dbg_class @@ -68,14 +68,6 @@ def __init__(self, sf, rng_seed, current_seed, config, fuzzer, # runner is not null self.retries = 4 - def __enter__(self): - ''' - set up an iteration context - ''' - self.working_dir = tempfile.mkdtemp(prefix=self.iteration_tmpdir_pfx, dir=self.workdirbase) - self.crashes = [] - return self - def __exit__(self, etype, value, traceback): global IOERROR_COUNT @@ -143,42 +135,114 @@ def __exit__(self, etype, value, traceback): return handled def _fuzz(self): - pass + # generated test case (fuzzed input) + logger.info('...fuzzing') + fuzz_opts = self.config['fuzzer'] + fuzz_args = self.sf, self.working_dir, self.rng_seed, self.current_seed, fuzz_opts + with self.fuzzer_cls(*fuzz_args) as fuzzer: + fuzzer.fuzz() + self.fuzzed = True + self.r = fuzzer.range + if self.r: + logger.info('Selected r: %s', self.r) + # decide if we can minimize this case later + # do this here (and not sooner) because the fuzzer_cls could + # decide at runtime whether it is or is not minimizable + self.minimizable = fuzzer.is_minimizable and self.config['runoptions']['minimize'] + + # hang on to this fuzzer instance, we use it in _run + self.fuzzer = fuzzer def _run(self): + # analysis is required in two cases: + # 1) runner is not defined (self.runner == None) + # 2) runner is defined, and detects crash (runner.saw_crash == True) + # this takes care of case 1 by default + analysis_needed = True + if self.runner: + logger.info('...running %s', self.runner.__name__) + with self.runner(self.config['runner'], + self.cmd_template, + self.fuzzer.output_file_path, + self.working_dir) as runner: + runner.run() + # this takes care of case 2 + analysis_needed = runner.saw_crash + # is further analysis needed? + if analysis_needed: + logger.info('...analyzing') + cmdlist = get_command_args_list(self.cmd_template, self.fuzzer.output_file_path)[1] + dbg_opts = self.config['debugger'] + fuzzed_file = BasicFile(self.fuzzer.output_file_path) + self._build_crash(self.fuzzer, cmdlist, dbg_opts, fuzzed_file) + else: + logger.debug('...no crash') pass def _verify(self, testcase): - pass + keep_it, reason = self.keep_testcase(testcase) + + if not keep_it: + logger.info('Candidate testcase rejected: %s', reason) + testcase.should_proceed_with_analysis = False + return + + logger.debug('Keeping testcase (reason=%s)', reason) + testcase.should_proceed_with_analysis = True + logger.info("Crash confirmed: %s Exploitability: %s Faulting Address: %s", testcase.crash_hash, testcase.exp, testcase.faddr) + if self.minimizable: + testcase.should_proceed_with_analysis = True + self.success = True def _minimize(self, testcase): - pass + logger.info('Minimizing testcase %s', testcase.signature) + logger.debug('config = %s', self.config) + + config = self._create_minimizer_cfg() + + debuggers.verify_supported_platform() + + kwargs = {'cfg': config, + 'crash': testcase, + 'seedfile_as_target': True, + 'bitwise': False, + 'confidence': 0.999, + 'tempdir': self.working_dir, + 'maxtime': self.config['runoptions']['minimizer_timeout'] + } + + with Minimizer(**kwargs) as minimizer: + minimizer.go() + if len(minimizer.other_crashes): + # minimzer found other crashes, so we should add them + # to our list for subsequent processing + self.crashes.extend(minimizer.other_crashes.values()) def _analyze(self, testcase): pass def _report(self, testcase): - pass + self.copy_files(testcase) - def keep_crash(self, crash): - '''Given a crash, decide whether it is a keeper. Returns a tuple - containing a boolean indicating whether to keep the crash, and + def keep_testcase(self, testcase): + '''Given a testcase, decide whether it is a keeper. Returns a tuple + containing a boolean indicating whether to keep the testcase, and a string containing the reason for the boolean result. - @param crash: a crash object + @param testcase: a testcase object @return (bool,str) ''' - if crash.is_crash: + if testcase.is_crash: if self.keep_duplicates: return (True, 'keep duplicates') - elif self.uniq_func(crash.signature): + elif self.uniq_func(testcase.signature): # Check if crasher directory exists already - target_dir = crash._get_output_dir(self.outdir) + target_dir = testcase._get_output_dir(self.outdir) if os.path.exists(target_dir): - return (False, 'skip duplicate %s' % crash.signature) + return (False, 'skip duplicate %s' % testcase.signature) else: return (True, 'unique') else: - return (False, 'skip duplicate %s' % crash.signature) + return (False, 'skip duplicate %s' % testcase.signature) elif not self.runner: return (False, 'not a crash') elif self.keep_heisenbugs: @@ -199,30 +263,6 @@ class DummyCfg(object): config.watchdogfile = os.devnull return config - def minimize(self, crash): - logger.info('Minimizing crash %s', crash.signature) - logger.debug('config = %s', self.config) - - config = self._create_minimizer_cfg() - - debuggers.verify_supported_platform() - - kwargs = {'cfg': config, - 'crash': crash, - 'seedfile_as_target': True, - 'bitwise': False, - 'confidence': 0.999, - 'tempdir': self.working_dir, - 'maxtime': self.config['runoptions']['minimizer_timeout'] - } - - with Minimizer(**kwargs) as minimizer: - minimizer.go() - if len(minimizer.other_crashes): - # minimzer found other crashes, so we should add them - # to our list for subsequent processing - self.crashes.extend(minimizer.other_crashes.values()) - def _copy_seedfile(self): target = os.path.join(self.working_dir, self.sf.basename) logger.debug('Copy files to %s: %s', self.working_dir, target) @@ -265,28 +305,7 @@ def record_failure(self): # FIXME # self.sf.record_failure() - def _process_crash(self, crash): - ''' - processes a single crash - @param crash: the crash to process - ''' - keep_it, reason = self.keep_crash(crash) - - if not keep_it: - logger.info('Candidate crash rejected: %s', reason) - return - - logger.debug('Keeping crash (reason=%s)', reason) - logger.info("Crash confirmed: %s Exploitability: %s Faulting Address: %s", crash.crash_hash, crash.exp, crash.faddr) - if self.minimizable: - try: - self.minimize(crash) - except MinimizerError as e: - logger.warning('Caught minimizer error: %s', e) - self.copy_files(crash) - self.success = True - - def _log_crash(self, crash): + def _log_testcase(self, crash): # pretty-print the crash for debugging logger.debug('Crash:') from pprint import pformat @@ -295,48 +314,11 @@ def _log_crash(self, crash): logger.debug('... %s', line.rstrip()) def _build_crash(self, fuzzer, cmdlist, dbg_opts, fuzzed_file): - logger.debug('Building crash object') + logger.debug('Building testcase object') with WindowsCrash(self.cmd_template, self.sf, fuzzed_file, cmdlist, fuzzer, self.debugger_class, dbg_opts, self.working_dir, self.config['runoptions']['keep_unique_faddr'], self.config['target']['program'], heisenbug_retries=self.retries, - copy_fuzzedfile=fuzzer.fuzzed_changes_input) as crash: - self._log_crash(crash) - self.crashes.append(crash) - - def _fuzz_and_run(self): - # generated test case (fuzzed input) - logger.info('...fuzzing') - fuzz_opts = self.config['fuzzer'] - fuzz_args = self.sf, self.working_dir, self.rng_seed, self.current_seed, fuzz_opts - with self.fuzzer(*fuzz_args) as fuzzer: - fuzzer.fuzz() - self.fuzzed = True - self.r = fuzzer.range - if self.r: - logger.info('Selected r: %s', self.r) - fuzzed_file_full_path = fuzzer.output_file_path - # decide if we can minimize this case later - # do this here (and not sooner) because the fuzzer could - # decide at runtime whether it is or is not minimizable - self.minimizable = fuzzer.is_minimizable and self.config['runoptions']['minimize'] - # analysis is required in two cases: - # 1) runner is not defined (self.runner == None) - # 2) runner is defined, and detects crash (runner.saw_crash == True) - # this takes care of case 1 by default - analysis_needed = True - if self.runner: - logger.info('...running %s', self.runner.__name__) - run_args = self.config['runner'], self.cmd_template, fuzzed_file_full_path, self.working_dir - with self.runner(*run_args) as runner: - runner.run() - # this takes care of case 2 - analysis_needed = runner.saw_crash - # is further analysis needed? - if analysis_needed: - logger.info('...analyzing') - cmdlist = get_command_args_list(self.cmd_template, fuzzer.output_file_path)[1] - dbg_opts = self.config['debugger'] - fuzzed_file = BasicFile(fuzzer.output_file_path) - self._build_crash(fuzzer, cmdlist, dbg_opts, fuzzed_file) - else: - logger.debug('...no crash') + copy_fuzzedfile=fuzzer.fuzzed_changes_input) as testcase: + self._log_testcase(testcase) + # put it on the queue for the analysis pipeline + self.tc_candidate_q.put(testcase) From 06588daf80cc454d68ecf850855dd93b69610dc1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 14:14:21 -0400 Subject: [PATCH 0487/1169] refactor common stuff into base class --- src/certfuzz/campaign/campaign_windows.py | 3 +- src/certfuzz/crash/crash_base.py | 1 + src/certfuzz/iteration/iteration_base3.py | 46 ++++++++- src/certfuzz/iteration/iteration_linux.py | 72 ++++---------- src/certfuzz/iteration/iteration_windows.py | 100 ++++++++------------ 5 files changed, 106 insertions(+), 116 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index dc98f2f..7e05964 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -216,7 +216,8 @@ def _do_iteration(self, sf, rng_seed, seednum): self.runner, self.debugger_module, self.dbg_class, self.keep_heisenbugs, self.keep_duplicates, self.cmd_template, self._crash_is_unique, - self.working_dir, self.outdir, self.debug) as iteration: + self.working_dir, self.outdir, self.debug, self.seedfile_set, + sf.rangefinder) as iteration: try: iteration.go() except FuzzerExhaustedError: diff --git a/src/certfuzz/crash/crash_base.py b/src/certfuzz/crash/crash_base.py index d601045..b79dfc3 100644 --- a/src/certfuzz/crash/crash_base.py +++ b/src/certfuzz/crash/crash_base.py @@ -36,6 +36,7 @@ def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): self.is_crash = False self.debugger_file = None self.is_unique = False + self.should_proceed_with_analysis = False self.is_corrupt_stack = False self.copy_fuzzedfile = True self.pc = None diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index e10ccbd..2fab493 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -21,9 +21,22 @@ class IterationBase3(object): __metaclass__ = abc.ABCMeta - def __init__(self, workdirbase): + def __init__(self, + seedfile=None, + seednum=None, + workdirbase=None, + outdir=None, + sf_set=None, + rf=None): + logger.debug('init') + self.seedfile = seedfile + self.seednum = seednum self.workdirbase = workdirbase + self.outdir = outdir + self.sf_set = sf_set + self.rf = rf + self.working_dir = None self.analyzer_classes = [] @@ -32,6 +45,9 @@ def __init__(self, workdirbase): # this gets set up in __enter__ self.analysis_pipeline = None + # flag that will decide whether we score as a success or failure + self.success = False + self.debug = True def __enter__(self): @@ -43,6 +59,12 @@ def __enter__(self): def __exit__(self, etype, value, traceback): handled = False + if self.success: + # score it so we can learn + self.record_success() + else: + self.record_failure() + if etype and self.debug: # leave it behind if we're in debug mode # and there's a problem @@ -95,8 +117,14 @@ def _pre_minimize(self, testcase): pass @abc.abstractmethod - def _minimize(self, testacse): - pass + def _minimize(self, testcase): + ''' + try to reduce the Hamming Distance between the testcase file and the + known good seedfile. testcase.fuzzedfile will be replaced with the + minimized result + + :param testcase: the testcase to work on + ''' def _post_minimize(self, testcase): pass @@ -152,6 +180,18 @@ def run(self): self._run() self._post_run() + def record_success(self): + self.sf_set.record_success(key=self.seedfile.md5) + self.rf.record_success(key=self.r.id) + + def record_failure(self): + self.record_tries() + + def record_tries(self): + self.sf_set.record_tries(key=self.seedfile.md5, tries=1) + self.rf.record_tries(key=self.r.id, tries=1) + + @coroutine def verify(self, *targets): ''' diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index eb8ac47..b5499bb 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -41,15 +41,11 @@ def get_uniq_logger(logfile): class Iteration(IterationBase3): def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None, sf_set=None, rf=None, outdir=None): - IterationBase3.__init__(self, workdirbase) + IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, + sf_set, rf) self.cfg = cfg - self.seednum = seednum - self.seedfile = seedfile self.r = r self.quiet_flag = quiet - self.sf_set = sf_set - self.rf = rf - self.outdir = outdir self.testcase_base_dir = os.path.join(self.outdir, 'crashers') @@ -60,11 +56,6 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No self._setup_analyzers() - # convenience aliases - self.s1 = self.seednum - self.s2 = self.s1 - self.sf = self.seedfile - def __enter__(self): IterationBase3.__enter__(self) check_ppid() @@ -81,13 +72,13 @@ def _fuzz(self): def _pre_run(self): # do the fuzz - cmdline = self.cfg.get_command(self.sf.path) + cmdline = self.cfg.get_command(self.seedfile.path) STATE_TIMER.enter_state('fuzzing') - self.zzuf = Zzuf(self.cfg.local_dir, self.s1, - self.s1, + self.zzuf = Zzuf(self.cfg.local_dir, self.seednum, + self.seednum, cmdline, - self.sf.path, + self.seedfile.path, self.cfg.zzuf_log_file, self.cfg.copymode, self.r.min, @@ -114,7 +105,7 @@ def _post_run(self): # we must have seen a crash # get the results - zzuf_log = ZzufLog(self.cfg.zzuf_log_file, self.cfg.zzuf_log_out(self.sf.output_dir)) + zzuf_log = ZzufLog(self.cfg.zzuf_log_file, self.cfg.zzuf_log_out(self.seedfile.output_dir)) # Don't generate cases for killed process or out-of-memory # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will @@ -128,7 +119,7 @@ def _post_run(self): # a true crash zzuf_range = zzuf_log.range - with ZzufTestCase(seedfile=self.seedfile, seed=self.s1, + with ZzufTestCase(seedfile=self.seedfile, seed=self.seednum, range=zzuf_range, working_dir=self.working_dir) as ztc: ztc.generate() @@ -144,7 +135,7 @@ def _post_run(self): backtrace_lines=self.cfg.backtracelevels, crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, - seednum=self.s1, + seednum=self.seednum, range=self.r) # record the zzuf log line for this crash @@ -182,6 +173,7 @@ def _verify(self, testcase): logger.info('%s first seen at %d', tc.signature, tc.seednum) self.dbg_out_file_orig = tc.dbg.file logger.debug('Original debugger file: %s', self.dbg_out_file_orig) + self.success = True else: logger.debug('%s was found, not unique', tc.signature) if self.cfg.keep_duplicates: @@ -194,6 +186,17 @@ def _minimize(self, testcase): if self.cfg.minimize_to_string: self._minimize_to_string(testcase) + def _post_minimize(self, testcase): + pass + # TODO +# if self.cfg.recycle_crashers: +# logger.debug('Recycling crash as seedfile') +# iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] +# crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext +# crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) +# filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) +# seedfile_set.add_file(crasherseed_path) + def _pre_analyze(self, testcase): STATE_TIMER.enter_state('analyze_testcase') @@ -249,17 +252,6 @@ def _post_report(self, testcase): # clean up testcase.delete_files() - def record_success(self): - self.sf_set.record_success(key=self.seedfile.md5) - self.rf.record_success(key=self.r.id) - - def record_failure(self): - self.record_tries() - - def record_tries(self): - self.sf_set.record_tries(key=self.seedfile.md5, tries=1) - self.rf.record_tries(key=self.r.id, tries=1) - def _setup_analyzers(self): self.analyzer_classes.append(stderr.StdErr) self.analyzer_classes.append(cw_gmalloc.CrashWranglerGmalloc) @@ -284,8 +276,6 @@ def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): touch_watchdog_file() STATE_TIMER.enter_state('minimize_testcase') - # try to reduce the Hamming Distance between the crasher file and the known good seedfile - # crash.fuzzedfile will be replaced with the minimized result try: with Minimizer(cfg=self.cfg, crash=testcase, @@ -302,23 +292,3 @@ def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) m = None - -# TODO -# if self.cfg.recycle_crashers: -# logger.debug('Recycling crash as seedfile') -# iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] -# crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext -# crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) -# filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) -# seedfile_set.add_file(crasherseed_path) - -# # score this crash for the seedfile -# testcase.seedfile.record_success(testcase.signature, tries=0) -# if testcase.range: -# # ...and for the range -# testcase.range.record_success(testcase.signature, tries=0) - # TODO: make sure this is scoring the right thing. - # in older code (see above) we kept track of specific crashes seen per seedfile - # & range. Should we still do that? - self.record_success() - diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index f6d7cbc..7a78016 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -35,14 +35,14 @@ class Iteration(IterationBase3): - def __init__(self, sf, rng_seed, current_seed, config, fuzzer_cls, + def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, - cmd_template, uniq_func, workdirbase, outdir, debug): - IterationBase3.__init__(self, workdirbase) - self.sf = sf + cmd_template, uniq_func, workdirbase, outdir, debug, + sf_set, rf): + IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, + sf_set, rf) self.r = None self.rng_seed = rng_seed - self.current_seed = current_seed self.config = config self.fuzzer_cls = fuzzer_cls self.runner = runner @@ -55,9 +55,6 @@ def __init__(self, sf, rng_seed, current_seed, config, fuzzer_cls, self.cmd_template = string.Template(cmd_template) self.uniq_func = uniq_func self.fuzzed = False - self.outdir = outdir - self.crash = None - self.success = False self.iteration_tmpdir_pfx = 'iteration_' self.minimizable = False @@ -69,6 +66,7 @@ def __init__(self, sf, rng_seed, current_seed, config, fuzzer_cls, self.retries = 4 def __exit__(self, etype, value, traceback): + handled = IterationBase3.__exit__(self, etype, value, traceback) global IOERROR_COUNT @@ -77,19 +75,18 @@ def __exit__(self, etype, value, traceback): IOERROR_COUNT = 0 # check for exceptions we want to handle - handled = False if etype is FuzzerExhaustedError: # let Fuzzer Exhaustion filter up to the campaign level handled = False elif etype is FuzzerInputMatchesOutputError: # Non-fuzzing happens sometimes, just log and move on - logger.debug('Skipping seed %d, fuzzed == input', self.current_seed) + logger.debug('Skipping seed %d, fuzzed == input', self.seednum) handled = True elif etype is FuzzerError: - logger.warning('Failed to fuzz, Skipping seed %d.', self.current_seed) + logger.warning('Failed to fuzz, Skipping seed %d.', self.seednum) handled = True elif etype is DebuggerFileError: - logger.warning('Failed to debug, Skipping seed %d', self.current_seed) + logger.warning('Failed to debug, Skipping seed %d', self.seednum) handled = True elif etype is RunnerRegistryError: logger.warning('Runner cannot set registry entries. Consider null runner in config?') @@ -109,37 +106,35 @@ def __exit__(self, etype, value, traceback): if etype and not handled: logger.warning('Iteration terminating abnormally due to %s: %s', etype.__name__, value) else: - logger.info('Done with iteration %d', self.current_seed) - - # count this iteration - if self.success: - self.record_success() - else: - self.record_failure() + logger.info('Done with iteration %d', self.seednum) if self.debug and etype and not handled: # don't clean up if we're in debug mode and have an unhandled exception logger.debug('Skipping cleanup since we are in debug mode.') else: - # wrap up this iteration - logger.debug('Cleanup iteration %s', self.current_seed) - # this iteration's temp dir - paths = [self.working_dir] - # sweep up any iteration temp dirs left behind previously - pattern = os.path.join(self.workdirbase, self.iteration_tmpdir_pfx + '*') - paths.extend(glob.glob(pattern)) - delete_files_or_dirs(paths) - # wipe them out, all of them - TmpReaper().clean_tmp() + self._tidy() return handled + def _tidy(self): + # wrap up this iteration + paths = [] + # sweep up any iteration temp dirs left behind previously + pattern = os.path.join(self.workdirbase, self.iteration_tmpdir_pfx + '*') + paths.extend(glob.glob(pattern)) + delete_files_or_dirs(paths) + # wipe them out, all of them + TmpReaper().clean_tmp() + def _fuzz(self): # generated test case (fuzzed input) logger.info('...fuzzing') fuzz_opts = self.config['fuzzer'] - fuzz_args = self.sf, self.working_dir, self.rng_seed, self.current_seed, fuzz_opts - with self.fuzzer_cls(*fuzz_args) as fuzzer: + with self.fuzzer_cls(self.seedfile, + self.working_dir, + self.rng_seed, + self.seednum, + fuzz_opts) as fuzzer: fuzzer.fuzz() self.fuzzed = True self.r = fuzzer.range @@ -213,10 +208,11 @@ def _minimize(self, testcase): with Minimizer(**kwargs) as minimizer: minimizer.go() - if len(minimizer.other_crashes): - # minimzer found other crashes, so we should add them - # to our list for subsequent processing - self.crashes.extend(minimizer.other_crashes.values()) + + # minimzer found other crashes, so we should add them + # to our list for subsequent processing + for tc in minimizer.other_crashes.values(): + self.tc_candidate_q.put(tc) def _analyze(self, testcase): pass @@ -264,9 +260,9 @@ class DummyCfg(object): return config def _copy_seedfile(self): - target = os.path.join(self.working_dir, self.sf.basename) + target = os.path.join(self.working_dir, self.seedfile.basename) logger.debug('Copy files to %s: %s', self.working_dir, target) - shutil.copy(self.sf.path, target) + shutil.copy(self.seedfile.path, target) def copy_files(self, crash): if not self.outdir: @@ -285,26 +281,6 @@ def copy_files(self, crash): shutil.copytree(crash.tempdir, target_dir) assert os.path.isdir(target_dir) - def record_success(self): - crash = self.crashes[0] - if self.r: - # ranges should only get scored on the first crash - # found in this iteration. Others found via minimization - # don't count for this r - # FIXME - pass - # self.r.record_success(crash.signature) - # FIXME - # self.sf.record_success(crash.signature) - - def record_failure(self): - if self.r: - # FIXME - pass - # self.r.record_failure() - # FIXME - # self.sf.record_failure() - def _log_testcase(self, crash): # pretty-print the crash for debugging logger.debug('Crash:') @@ -315,10 +291,12 @@ def _log_testcase(self, crash): def _build_crash(self, fuzzer, cmdlist, dbg_opts, fuzzed_file): logger.debug('Building testcase object') - with WindowsCrash(self.cmd_template, self.sf, fuzzed_file, cmdlist, fuzzer, self.debugger_class, - dbg_opts, self.working_dir, self.config['runoptions']['keep_unique_faddr'], - self.config['target']['program'], heisenbug_retries=self.retries, - copy_fuzzedfile=fuzzer.fuzzed_changes_input) as testcase: + with WindowsCrash(self.cmd_template, self.seedfile, fuzzed_file, cmdlist, + fuzzer, self.debugger_class, dbg_opts, + self.working_dir, self.config['runoptions']['keep_unique_faddr'], + self.config['target']['program'], + heisenbug_retries=self.retries, + copy_fuzzedfile=fuzzer.fuzzed_changes_input) as testcase: self._log_testcase(testcase) # put it on the queue for the analysis pipeline self.tc_candidate_q.put(testcase) From fad5efdfcac5ba10897420bb784c4b83ca3210ab Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 16 Jul 2014 16:25:47 -0400 Subject: [PATCH 0488/1169] break out test case analysis into a separate package / class architecture --- src/certfuzz/iteration/iteration_base3.py | 183 ++------------- src/certfuzz/iteration/iteration_linux.py | 219 +++--------------- src/certfuzz/iteration/iteration_windows.py | 155 ++----------- src/certfuzz/testcase_pipeline/__init__.py | 0 .../testcase_pipeline_base.py | 194 ++++++++++++++++ .../testcase_pipeline_linux.py | 177 ++++++++++++++ .../testcase_pipeline_windows.py | 129 +++++++++++ 7 files changed, 568 insertions(+), 489 deletions(-) create mode 100644 src/certfuzz/testcase_pipeline/__init__.py create mode 100644 src/certfuzz/testcase_pipeline/testcase_pipeline_base.py create mode 100644 src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py create mode 100644 src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 2fab493..d05b806 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -6,14 +6,7 @@ import logging import shutil import tempfile - -from certfuzz.analyzers.errors import AnalyzerEmptyOutputError - -from certfuzz.file_handlers.watchdog_file import touch_watchdog_file import abc -import Queue -from certfuzz.helpers.coroutine import coroutine - logger = logging.getLogger(__name__) @@ -27,7 +20,8 @@ def __init__(self, workdirbase=None, outdir=None, sf_set=None, - rf=None): + rf=None, + uniq_func=None): logger.debug('init') self.seedfile = seedfile @@ -37,23 +31,32 @@ def __init__(self, self.sf_set = sf_set self.rf = rf - self.working_dir = None - self.analyzer_classes = [] + self.pipeline_options = {} - self.tc_candidate_q = Queue.Queue() + if uniq_func is None: + self.uniq_func = lambda _tc_id: True + else: + self.uniq_func = uniq_func + + self.working_dir = None - # this gets set up in __enter__ - self.analysis_pipeline = None + self.testcases = [] # flag that will decide whether we score as a success or failure self.success = False self.debug = True + @abc.abstractproperty + def tcpipeline_cls(self): + ''' + Defines the class to use as a TestCasePipeline + ''' + def __enter__(self): self.working_dir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) logger.debug('workdir=%s', self.working_dir) - self._setup_analysis_pipeline() +# self._setup_analysis_pipeline() return self def __exit__(self, etype, value, traceback): @@ -74,15 +77,6 @@ def __exit__(self, etype, value, traceback): return handled - def _setup_analysis_pipeline(self): - # build up the pipeline: - # verify | minimize | analyze | report - r = self.report() - a = self.analyze(r) - m = self.minimize(a) - - self.analysis_pipeline = self.verify(m) - def _pre_fuzz(self): pass @@ -103,64 +97,6 @@ def _run(self): def _post_run(self): pass - def _pre_verify(self, testcase): - pass - - @abc.abstractmethod - def _verify(self, testcase): - pass - - def _post_verify(self, testcase): - pass - - def _pre_minimize(self, testcase): - pass - - @abc.abstractmethod - def _minimize(self, testcase): - ''' - try to reduce the Hamming Distance between the testcase file and the - known good seedfile. testcase.fuzzedfile will be replaced with the - minimized result - - :param testcase: the testcase to work on - ''' - - def _post_minimize(self, testcase): - pass - - def _pre_analyze(self, testcase): - pass - - @abc.abstractmethod - def _analyze(self, testcase): - ''' - Loops through all known analyzer_classes for a given testcase - :param testcase: - ''' - for analyzer_class in self.analyzer_classes: - touch_watchdog_file() - - analyzer_instance = analyzer_class(self.cfg, testcase) - if analyzer_instance: - try: - analyzer_instance.go() - except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from analyzer_class. Continuing') - - def _post_analyze(self, testcase): - pass - - def _pre_report(self, testcase): - pass - - @abc.abstractmethod - def _report(self, testcase): - pass - - def _post_report(self, testcase): - pass - def fuzz(self): ''' Prepares a test case @@ -191,86 +127,13 @@ def record_tries(self): self.sf_set.record_tries(key=self.seedfile.md5, tries=1) self.rf.record_tries(key=self.r.id, tries=1) - - @coroutine - def verify(self, *targets): - ''' - Verifies that a test case is unique before sending the test case. Acts - as a filter on the analysis pipeline. - :param targets: one or more downstream coroutines to send the testcase to - ''' - logger.debug('Verifier standing by for testcases') - while True: - testcase = (yield) - - logger.debug('verify testcase') - self._pre_verify(testcase) - self._verify(testcase) - self._post_verify(testcase) - - for target in targets: - if testcase.should_proceed_with_analysis: - # we're ready to proceed with this testcase - # so send it downstream - target.send(testcase) - - @coroutine - def minimize(self, *targets): - logger.debug('Minimizer standing by for testcases') - while True: - testcase = (yield) - - logger.debug('minimize testcase') - self._pre_minimize(testcase) - self._minimize(testcase) - self._post_minimize(testcase) - - for target in targets: - target.send(testcase) - - @coroutine - def analyze(self, *targets): - ''' - Analyzes a test case before passing it down the pipeline - :param targets: one or more downstream coroutines to send the testcase to - ''' - logger.debug('Analyzer standing by for testcases') - while True: - testcase = (yield) - - logger.debug('analyze testcase') - self._pre_analyze(testcase) - self._analyze(testcase) - self._post_analyze(testcase) - - for target in targets: - target.send(testcase) - - @coroutine - def report(self, *targets): - ''' - Prepares the test case report. - :param targets: one or more downstream coroutines to send the testcase to - ''' - logger.debug('Reporter standing by for testcases') - while True: - testcase = (yield) - - logger.debug('report testcase') - self._pre_report(testcase) - self._report(testcase) - self._post_report(testcase) - - for target in targets: - target.send(testcase) - def process_testcases(self): - # every test case is a candidate until verified - # use a while loop so we have the option of adding - # tc_candidate_q during the loop - while not self.tc_candidate_q.empty(): - testcase = self.tc_candidate_q.get() - self.analysis_pipeline.send(testcase) + # hand it off to our pipeline class + with self.tcpipeline_cls(testcases=self.testcases, + uniq_func=self.uniq_func, + cfg=self.cfg, + options=self.pipeline_options) as pipeline: + pipeline.go() def go(self): logger.debug('go') diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index b5499bb..19057fa 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -7,54 +7,45 @@ import os from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.analyzers import cw_gmalloc, pin_calltrace, stderr, valgrind -from certfuzz.analyzers.callgrind import callgrind -from certfuzz.analyzers.callgrind.annotate import annotate_callgrind, \ - annotate_callgrind_tree -from certfuzz.analyzers.callgrind.errors import \ - CallgrindAnnotateEmptyOutputFileError, CallgrindAnnotateMissingInputFileError from certfuzz.crash.bff_crash import BffCrash from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport -from certfuzz.fuzztools import filetools from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.fuzztools.zzuf import Zzuf, ZzufTestCase from certfuzz.fuzztools.zzuflog import ZzufLog -from certfuzz.minimizer import MinimizerError, UnixMinimizer as Minimizer - -from certfuzz.file_handlers.watchdog_file import touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.iteration_base3 import IterationBase3 +from certfuzz.testcase_pipeline.testcase_pipeline_linux import LinuxTestCasePipeline logger = logging.getLogger(__name__) -def get_uniq_logger(logfile): - l = logging.getLogger('uniq_crash') - if len(l.handlers) == 0: - hdlr = logging.FileHandler(logfile) - l.addHandler(hdlr) - return l - - class Iteration(IterationBase3): + tcpipeline_cls = LinuxTestCasePipeline + def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None, sf_set=None, rf=None, outdir=None): IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, - sf_set, rf) + sf_set, rf, uniq_func) self.cfg = cfg self.r = r self.quiet_flag = quiet self.testcase_base_dir = os.path.join(self.outdir, 'crashers') - if uniq_func is None: - self.uniq_func = lambda _tc_id: True - else: - self.uniq_func = uniq_func + self._zzuf_range = None + self._zzuf_line = None - self._setup_analyzers() + self.pipeline_options = { + 'use_valgrind': self.cfg.use_valgrind, + 'use_pin_calltrace': self.cfg.use_pin_calltrace, + 'minimize_crashers': self.cfg.minimizecrashers, + 'minimize_to_string': self.cfg.minimize_to_string, + 'uniq_log': self.cfg.uniq_log, + 'local_dir': self.cfg.local_dir, + 'minimizertimeout': self.cfg.minimizertimeout, + } def __enter__(self): IterationBase3.__enter__(self) @@ -87,12 +78,9 @@ def _pre_run(self): self.quiet_flag) def _run(self): - IterationBase3._run(self) self.zzuf.go() def _post_run(self): - IterationBase3._post_run(self) - STATE_TIMER.enter_state('checking_results') # we must have made it through this chunk without a crash # so go to next chunk @@ -110,23 +98,25 @@ def _post_run(self): # Don't generate cases for killed process or out-of-memory # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will # report the exit code in its output log. The exit code is 128 + the signal number. - crash_status = zzuf_log.crash_logged(self.cfg.copymode) + analysis_needed = zzuf_log.crash_logged(self.cfg.copymode) - if not crash_status: + if not analysis_needed: return - logger.info('Generating testcase for %s', zzuf_log.line) - # a true crash - zzuf_range = zzuf_log.range + # store a few things for use downstream + self._zzuf_range = zzuf_log.range + self._zzuf_line = zzuf_log.line + self._construct_testcase() + def _construct_testcase(self): with ZzufTestCase(seedfile=self.seedfile, seed=self.seednum, - range=zzuf_range, + range=self._zzuf_range, working_dir=self.working_dir) as ztc: ztc.generate() - fuzzedfile = BasicFile(ztc.outfile) - testcase = BffCrash(cfg=self.cfg, + logger.info('Building testcase object') + with BffCrash(cfg=self.cfg, seedfile=self.seedfile, fuzzedfile=fuzzedfile, program=self.cfg.program, @@ -136,159 +126,6 @@ def _post_run(self): crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, seednum=self.seednum, - range=self.r) - - # record the zzuf log line for this crash - testcase.get_logger() - - testcase.logger.debug("zzuflog: %s", zzuf_log.line) -# testcase.logger.info('Command: %s', testcase.cmdline) - - self.tc_candidate_q.put(testcase) - - def _verify(self, testcase): - ''' - Confirms that a test case is interesting enough to pursue further analysis - :param testcase: - ''' - STATE_TIMER.enter_state('verify_testcase') - IterationBase3._verify(self, testcase) - - # if you find more testcases, append them to self.tc_candidate_q - # tc_verified_q crashes append to self.tc_verified_q - - logger.debug('verifying crash') - with testcase as tc: - if tc.is_crash: - - is_new_to_campaign = self.uniq_func(tc.signature) - - # fall back to checking if the crash directory exists - # - crash_dir_found = filetools.find_or_create_dir(tc.result_dir) - - tc.should_proceed_with_analysis = is_new_to_campaign and not crash_dir_found - - if tc.should_proceed_with_analysis: - logger.info('%s first seen at %d', tc.signature, tc.seednum) - self.dbg_out_file_orig = tc.dbg.file - logger.debug('Original debugger file: %s', self.dbg_out_file_orig) - self.success = True - else: - logger.debug('%s was found, not unique', tc.signature) - if self.cfg.keep_duplicates: - logger.debug('Analyzing %s anyway because keep_duplicates is set', tc.signature) - self.verified.append(tc) - - def _minimize(self, testcase): - if self.cfg.minimizecrashers: - self._mininimize_to_seedfile(testcase) - if self.cfg.minimize_to_string: - self._minimize_to_string(testcase) - - def _post_minimize(self, testcase): - pass - # TODO -# if self.cfg.recycle_crashers: -# logger.debug('Recycling crash as seedfile') -# iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] -# crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext -# crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) -# filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) -# seedfile_set.add_file(crasherseed_path) - - def _pre_analyze(self, testcase): - STATE_TIMER.enter_state('analyze_testcase') - - # get one last debugger output for the newly minimized file - if testcase.pc_in_function: - # change the debugger template - testcase.set_debugger_template('complete') - else: - # use a debugger template that specifies fixed offsets from $pc for disassembly - testcase.set_debugger_template('complete_nofunction') - logger.info('Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) - testcase.get_debug_output(testcase.fuzzedfile.path) - - if self.dbg_out_file_orig != testcase.dbg.file: - # we have a new debugger output - # remove the old one - filetools.delete_files(self.dbg_out_file_orig) - if os.path.exists(self.dbg_out_file_orig): - logger.warning('Failed to remove old debugger file %s', self.dbg_out_file_orig) - else: - logger.debug('Removed old debug file %s', self.dbg_out_file_orig) - - def _analyze(self, testcase): - # we'll just use the implementation in our parent class - IterationBase3._analyze(self, testcase) - - def _post_analyze(self, testcase): - logger.info('Annotating callgrind output') - try: - annotate_callgrind(testcase) - annotate_callgrind_tree(testcase) - except CallgrindAnnotateEmptyOutputFileError: - logger.warning('Unexpected empty output from annotate_callgrind. Continuing') - except CallgrindAnnotateMissingInputFileError: - logger.warning('Missing callgrind output. Continuing') - - def _pre_report(self, testcase): - uniqlogger = get_uniq_logger(self.cfg.uniq_log) - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) - logger.info('%s first seen at %d', testcase.signature, testcase.seednum) - - # whether it was unique or not, record some details for posterity - # record the details of this crash so we can regenerate it later if needed - testcase.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', testcase.seedfile.basename, testcase.seednum, testcase.range, testcase.fuzzedfile.path) - testcase.logger.info('PC=%s', testcase.pc) - - def _report(self, testcase): - testcase.copy_files() - - def _post_report(self, testcase): - # always clean up after yourself - testcase.clean_tmpdir() - # clean up - testcase.delete_files() - - def _setup_analyzers(self): - self.analyzer_classes.append(stderr.StdErr) - self.analyzer_classes.append(cw_gmalloc.CrashWranglerGmalloc) - - if self.cfg.use_valgrind: - self.analyzer_classes.append(valgrind.Valgrind) - self.analyzer_classes.append(callgrind.Callgrind) - - if self.cfg.use_pin_calltrace: - self.analyzer_classes.append(pin_calltrace.Pin_calltrace) - - def _mininimize_to_seedfile(self, testcase): - self._minimize_generic(testcase, sftarget=True, confidence=0.999) - # calculate the hamming distances for this crash - # between the original seedfile and the minimized fuzzed file - testcase.calculate_hamming_distances() - - def _minimize_to_string(self, testcase): - self._minimize_generic(testcase, sftarget=False, confidence=0.9) - - def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): - touch_watchdog_file() - - STATE_TIMER.enter_state('minimize_testcase') - try: - with Minimizer(cfg=self.cfg, - crash=testcase, - bitwise=False, - seedfile_as_target=sftarget, - confidence=confidence, - tempdir=self.cfg.local_dir, - maxtime=self.cfg.minimizertimeout - ) as m: - m.go() - for new_tc in m.other_crashes.values(): - self.tc_candidate_q.put(new_tc) - except MinimizerError as e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) - m = None - + range=self.r) as testcase: + # put it on the list for the analysis pipeline + self.testcases.append(testcase) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 7a78016..c459848 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -6,11 +6,8 @@ import glob import logging import os -import shutil import string -import tempfile -from certfuzz import debuggers from certfuzz.campaign.config.config_windows import get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers.output_parsers import DebuggerFileError @@ -18,16 +15,12 @@ from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.fuzzers.errors import FuzzerError, FuzzerExhaustedError, \ FuzzerInputMatchesOutputError -from certfuzz.fuzztools import filetools from certfuzz.fuzztools.filetools import delete_files_or_dirs -from certfuzz.minimizer import MinimizerError, WindowsMinimizer as Minimizer +from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.runners.errors import RunnerRegistryError +from certfuzz.testcase_pipeline.testcase_pipeline_windows import WindowsTestCasePipeline -from certfuzz.iteration.errors import IterationError -from certfuzz.iteration.iteration_base3 import IterationBase3 #from certfuzz.iteration.iteration_base import IterationBase2 - - logger = logging.getLogger(__name__) IOERROR_COUNT = 0 @@ -35,12 +28,14 @@ class Iteration(IterationBase3): + tcpipeline_cls = WindowsTestCasePipeline + def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, cmd_template, uniq_func, workdirbase, outdir, debug, sf_set, rf): IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, - sf_set, rf) + sf_set, rf, uniq_func) self.r = None self.rng_seed = rng_seed self.config = config @@ -53,7 +48,6 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, self.keep_duplicates = keep_duplicates self.keep_heisenbugs = keep_heisenbugs self.cmd_template = string.Template(cmd_template) - self.uniq_func = uniq_func self.fuzzed = False self.iteration_tmpdir_pfx = 'iteration_' self.minimizable = False @@ -163,140 +157,25 @@ def _run(self): runner.run() # this takes care of case 2 analysis_needed = runner.saw_crash - # is further analysis needed? - if analysis_needed: - logger.info('...analyzing') - cmdlist = get_command_args_list(self.cmd_template, self.fuzzer.output_file_path)[1] - dbg_opts = self.config['debugger'] - fuzzed_file = BasicFile(self.fuzzer.output_file_path) - self._build_crash(self.fuzzer, cmdlist, dbg_opts, fuzzed_file) - else: - logger.debug('...no crash') - pass - def _verify(self, testcase): - keep_it, reason = self.keep_testcase(testcase) - - if not keep_it: - logger.info('Candidate testcase rejected: %s', reason) - testcase.should_proceed_with_analysis = False + # is further analysis needed? + if not analysis_needed: return - logger.debug('Keeping testcase (reason=%s)', reason) - testcase.should_proceed_with_analysis = True - logger.info("Crash confirmed: %s Exploitability: %s Faulting Address: %s", testcase.crash_hash, testcase.exp, testcase.faddr) - if self.minimizable: - testcase.should_proceed_with_analysis = True - self.success = True - - def _minimize(self, testcase): - logger.info('Minimizing testcase %s', testcase.signature) - logger.debug('config = %s', self.config) - - config = self._create_minimizer_cfg() - - debuggers.verify_supported_platform() - - kwargs = {'cfg': config, - 'crash': testcase, - 'seedfile_as_target': True, - 'bitwise': False, - 'confidence': 0.999, - 'tempdir': self.working_dir, - 'maxtime': self.config['runoptions']['minimizer_timeout'] - } - - with Minimizer(**kwargs) as minimizer: - minimizer.go() + self._construct_testcase() - # minimzer found other crashes, so we should add them - # to our list for subsequent processing - for tc in minimizer.other_crashes.values(): - self.tc_candidate_q.put(tc) + def _construct_testcase(self): + cmdlist = get_command_args_list(self.cmd_template, self.fuzzer.output_file_path)[1] + dbg_opts = self.config['debugger'] + fuzzed_file = BasicFile(self.fuzzer.output_file_path) - def _analyze(self, testcase): - pass - - def _report(self, testcase): - self.copy_files(testcase) - - def keep_testcase(self, testcase): - '''Given a testcase, decide whether it is a keeper. Returns a tuple - containing a boolean indicating whether to keep the testcase, and - a string containing the reason for the boolean result. - @param testcase: a testcase object - @return (bool,str) - ''' - if testcase.is_crash: - if self.keep_duplicates: - return (True, 'keep duplicates') - elif self.uniq_func(testcase.signature): - # Check if crasher directory exists already - target_dir = testcase._get_output_dir(self.outdir) - if os.path.exists(target_dir): - return (False, 'skip duplicate %s' % testcase.signature) - else: - return (True, 'unique') - else: - return (False, 'skip duplicate %s' % testcase.signature) - elif not self.runner: - return (False, 'not a crash') - elif self.keep_heisenbugs: - return (True, 'heisenbug') - else: - return (False, 'skip heisenbugs') - - def _create_minimizer_cfg(self): - class DummyCfg(object): - pass - config = DummyCfg() - config.backtracelevels = 5 # doesn't matter what this is, we don't use it - config.debugger_timeout = self.config['debugger']['runtimeout'] - config.get_command_args_list = lambda x: get_command_args_list(self.cmd_template, x)[1] - config.program = self.config['target']['program'] - config.killprocname = None - config.exclude_unmapped_frames = False - config.watchdogfile = os.devnull - return config - - def _copy_seedfile(self): - target = os.path.join(self.working_dir, self.seedfile.basename) - logger.debug('Copy files to %s: %s', self.working_dir, target) - shutil.copy(self.seedfile.path, target) - - def copy_files(self, crash): - if not self.outdir: - raise IterationError('Need a target dir to copy to') - - logger.debug('target_base=%s', self.outdir) - - target_dir = crash._get_output_dir(self.outdir) - - if os.path.exists(target_dir): - logger.debug('Repeat crash, will not copy to %s', target_dir) - else: - # make sure target_base exists already - filetools.find_or_create_dir(self.outdir) - logger.debug('Copying to %s', target_dir) - shutil.copytree(crash.tempdir, target_dir) - assert os.path.isdir(target_dir) - - def _log_testcase(self, crash): - # pretty-print the crash for debugging - logger.debug('Crash:') - from pprint import pformat - formatted = pformat(crash.__dict__) - for line in formatted.splitlines(): - logger.debug('... %s', line.rstrip()) - - def _build_crash(self, fuzzer, cmdlist, dbg_opts, fuzzed_file): logger.debug('Building testcase object') with WindowsCrash(self.cmd_template, self.seedfile, fuzzed_file, cmdlist, - fuzzer, self.debugger_class, dbg_opts, + self.fuzzer, self.debugger_class, dbg_opts, self.working_dir, self.config['runoptions']['keep_unique_faddr'], self.config['target']['program'], heisenbug_retries=self.retries, - copy_fuzzedfile=fuzzer.fuzzed_changes_input) as testcase: - self._log_testcase(testcase) - # put it on the queue for the analysis pipeline - self.tc_candidate_q.put(testcase) + copy_fuzzedfile=self.fuzzer.fuzzed_changes_input) as testcase: + + # put it on the list for the analysis pipeline + self.testcases.append(testcase) diff --git a/src/certfuzz/testcase_pipeline/__init__.py b/src/certfuzz/testcase_pipeline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/testcase_pipeline/testcase_pipeline_base.py b/src/certfuzz/testcase_pipeline/testcase_pipeline_base.py new file mode 100644 index 0000000..0f4bc22 --- /dev/null +++ b/src/certfuzz/testcase_pipeline/testcase_pipeline_base.py @@ -0,0 +1,194 @@ +''' +Created on Jul 16, 2014 + +@organization: cert.org +''' +import Queue +from certfuzz.helpers.coroutine import coroutine +import logging +import abc +from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +from certfuzz.analyzers.errors import AnalyzerEmptyOutputError + +logger = logging.getLogger(__name__) + + +class TestCasePipelineBase(object): + ''' + Implements a pipeline for filtering and processing a testcase + ''' + __metaclass__ = abc.ABCMeta + + def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None): + ''' + Constructor + ''' + self.cfg = cfg + self.options = options + self.uniq_func = uniq_func + self.tc_candidate_q = Queue.Queue() + + self.analyzer_classes = [] + self._setup_analyzers() + + # this gets set up in __enter__ + self.analysis_pipeline = None + + if testcases is not None: + for testcase in testcases: + self.tc_candidate_q.put(testcase) + + def __enter__(self): + self._setup_analysis_pipeline() + return self + + def __exit__(self, etype, value, traceback): + pass + + @abc.abstractmethod + def _setup_analyzers(self): + pass + + def _setup_analysis_pipeline(self): + # build up the pipeline: + # verify | minimize | analyze | report + r = self.report() + a = self.analyze(r) + m = self.minimize(a) + + self.analysis_pipeline = self.verify(m) + + @coroutine + def verify(self, *targets): + ''' + Verifies that a test case is unique before sending the test case. Acts + as a filter on the analysis pipeline. + :param targets: one or more downstream coroutines to send the testcase to + ''' + logger.debug('Verifier standing by for testcases') + while True: + testcase = (yield) + + logger.debug('verify testcase') + self._pre_verify(testcase) + self._verify(testcase) + self._post_verify(testcase) + + for target in targets: + if testcase.should_proceed_with_analysis: + # we're ready to proceed with this testcase + # so send it downstream + target.send(testcase) + + @coroutine + def minimize(self, *targets): + logger.debug('Minimizer standing by for testcases') + while True: + testcase = (yield) + + logger.debug('minimize testcase') + self._pre_minimize(testcase) + self._minimize(testcase) + self._post_minimize(testcase) + + for target in targets: + target.send(testcase) + + @coroutine + def analyze(self, *targets): + ''' + Analyzes a test case before passing it down the pipeline + :param targets: one or more downstream coroutines to send the testcase to + ''' + logger.debug('Analyzer standing by for testcases') + while True: + testcase = (yield) + + logger.debug('analyze testcase') + self._pre_analyze(testcase) + self._analyze(testcase) + self._post_analyze(testcase) + + for target in targets: + target.send(testcase) + + @coroutine + def report(self, *targets): + ''' + Prepares the test case report. + :param targets: one or more downstream coroutines to send the testcase to + ''' + logger.debug('Reporter standing by for testcases') + while True: + testcase = (yield) + + logger.debug('report testcase') + self._pre_report(testcase) + self._report(testcase) + self._post_report(testcase) + + for target in targets: + target.send(testcase) + + def _pre_verify(self, testcase): + pass + + @abc.abstractmethod + def _verify(self, testcase): + pass + + def _post_verify(self, testcase): + pass + + def _pre_minimize(self, testcase): + pass + + @abc.abstractmethod + def _minimize(self, testcase): + ''' + try to reduce the Hamming Distance between the testcase file and the + known good seedfile. testcase.fuzzedfile will be replaced with the + minimized result + + :param testcase: the testcase to work on + ''' + + def _post_minimize(self, testcase): + pass + + def _pre_analyze(self, testcase): + pass + + @abc.abstractmethod + def _analyze(self, testcase): + ''' + Loops through all known analyzer_classes for a given testcase + :param testcase: + ''' + for analyzer_class in self.analyzer_classes: + touch_watchdog_file() + + analyzer_instance = analyzer_class(self.cfg, testcase) + if analyzer_instance: + try: + analyzer_instance.go() + except AnalyzerEmptyOutputError: + logger.warning('Unexpected empty output from analyzer_class. Continuing') + + def _post_analyze(self, testcase): + pass + + def _pre_report(self, testcase): + pass + + @abc.abstractmethod + def _report(self, testcase): + pass + + def _post_report(self, testcase): + pass + + def go(self): + while not self.tc_candidate_q.empty(): + testcase = self.tc_candidate_q.get() + self.analysis_pipeline.send(testcase) diff --git a/src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py b/src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py new file mode 100644 index 0000000..d24ae06 --- /dev/null +++ b/src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py @@ -0,0 +1,177 @@ +''' +Created on Jul 16, 2014 + +@organization: cert.org +''' +from certfuzz.testcase_pipeline.testcase_pipeline_base import TestCasePipelineBase +from certfuzz.fuzztools.state_timer import STATE_TIMER +import logging +from certfuzz.fuzztools import filetools +import os +from certfuzz.analyzers.callgrind.annotate import annotate_callgrind +from certfuzz.analyzers.callgrind.annotate import annotate_callgrind_tree +from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError +from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError +from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +from certfuzz.minimizer import MinimizerError, UnixMinimizer as Minimizer +from certfuzz.analyzers import stderr +from certfuzz.analyzers import cw_gmalloc +from certfuzz.analyzers import valgrind +from certfuzz.analyzers.callgrind import callgrind +from certfuzz.analyzers import pin_calltrace + +logger = logging.getLogger(__name__) + + +def get_uniq_logger(logfile): + l = logging.getLogger('uniq_crash') + if len(l.handlers) == 0: + hdlr = logging.FileHandler(logfile) + l.addHandler(hdlr) + return l + + +class LinuxTestCasePipeline(TestCasePipelineBase): + + def _setup_analyzers(self): + self.analyzer_classes.append(stderr.StdErr) + self.analyzer_classes.append(cw_gmalloc.CrashWranglerGmalloc) + + if self.options.get('use_valgrind'): + self.analyzer_classes.append(valgrind.Valgrind) + self.analyzer_classes.append(callgrind.Callgrind) + + if self.options.get('use_pin_calltrace'): + self.analyzer_classes.append(pin_calltrace.Pin_calltrace) + + def _verify(self, testcase): + ''' + Confirms that a test case is interesting enough to pursue further analysis + :param testcase: + ''' + STATE_TIMER.enter_state('verify_testcase') + TestCasePipelineBase._verify(self, testcase) + + # if you find more testcases, append them to self.tc_candidate_q + # tc_verified_q crashes append to self.tc_verified_q + + logger.debug('verifying crash') + with testcase as tc: + if tc.is_crash: + + is_new_to_campaign = self.uniq_func(tc.signature) + + # fall back to checking if the crash directory exists + # + crash_dir_found = filetools.find_or_create_dir(tc.result_dir) + + tc.should_proceed_with_analysis = is_new_to_campaign and not crash_dir_found + + if tc.should_proceed_with_analysis: + logger.info('%s first seen at %d', tc.signature, tc.seednum) + self.dbg_out_file_orig = tc.dbg.file + logger.debug('Original debugger file: %s', self.dbg_out_file_orig) + self.success = True + else: + logger.debug('%s was found, not unique', tc.signature) + + def _minimize(self, testcase): + if self.options.get('minimize_crashers'): + self._mininimize_to_seedfile(testcase) + if self.options.get('minimize_to_string'): + self._minimize_to_string(testcase) + + def _post_minimize(self, testcase): + pass + # TODO +# if self.cfg.recycle_crashers: +# logger.debug('Recycling crash as seedfile') +# iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] +# crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext +# crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) +# filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) +# seedfile_set.add_file(crasherseed_path) + + def _pre_analyze(self, testcase): + STATE_TIMER.enter_state('analyze_testcase') + + # get one last debugger output for the newly minimized file + if testcase.pc_in_function: + # change the debugger template + testcase.set_debugger_template('complete') + else: + # use a debugger template that specifies fixed offsets from $pc for disassembly + testcase.set_debugger_template('complete_nofunction') + logger.info('Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) + testcase.get_debug_output(testcase.fuzzedfile.path) + + if self.dbg_out_file_orig != testcase.dbg.file: + # we have a new debugger output + # remove the old one + filetools.delete_files(self.dbg_out_file_orig) + if os.path.exists(self.dbg_out_file_orig): + logger.warning('Failed to remove old debugger file %s', self.dbg_out_file_orig) + else: + logger.debug('Removed old debug file %s', self.dbg_out_file_orig) + + def _analyze(self, testcase): + # we'll just use the implementation in our parent class + TestCasePipelineBase._analyze(self, testcase) + + def _post_analyze(self, testcase): + logger.info('Annotating callgrind output') + try: + annotate_callgrind(testcase) + annotate_callgrind_tree(testcase) + except CallgrindAnnotateEmptyOutputFileError: + logger.warning('Unexpected empty output from annotate_callgrind. Continuing') + except CallgrindAnnotateMissingInputFileError: + logger.warning('Missing callgrind output. Continuing') + + def _pre_report(self, testcase): + uniqlogger = get_uniq_logger(self.options.get('uniq_log')) + uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) + logger.info('%s first seen at %d', testcase.signature, testcase.seednum) + + # whether it was unique or not, record some details for posterity + # record the details of this crash so we can regenerate it later if needed + testcase.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', testcase.seedfile.basename, testcase.seednum, testcase.range, testcase.fuzzedfile.path) + testcase.logger.info('PC=%s', testcase.pc) + + def _report(self, testcase): + testcase.copy_files() + + def _post_report(self, testcase): + # always clean up after yourself + testcase.clean_tmpdir() + # clean up + testcase.delete_files() + + def _mininimize_to_seedfile(self, testcase): + self._minimize_generic(testcase, sftarget=True, confidence=0.999) + # calculate the hamming distances for this crash + # between the original seedfile and the minimized fuzzed file + testcase.calculate_hamming_distances() + + def _minimize_to_string(self, testcase): + self._minimize_generic(testcase, sftarget=False, confidence=0.9) + + def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): + touch_watchdog_file() + + STATE_TIMER.enter_state('minimize_testcase') + try: + with Minimizer(cfg=self.cfg, + crash=testcase, + bitwise=False, + seedfile_as_target=sftarget, + confidence=confidence, + tempdir=self.options.get('local_dir'), + maxtime=self.options.get('minimizertimeout'), + ) as m: + m.go() + for new_tc in m.other_crashes.values(): + self.tc_candidate_q.put(new_tc) + except MinimizerError as e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) + m = None diff --git a/src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py b/src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py new file mode 100644 index 0000000..a41b61a --- /dev/null +++ b/src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py @@ -0,0 +1,129 @@ +''' +Created on Jul 16, 2014 + +@organization: cert.org +''' +from certfuzz.testcase_pipeline.testcase_pipeline_base import TestCasePipelineBase + +import logging +from certfuzz import debuggers +from certfuzz.minimizer import WindowsMinimizer as Minimizer +import os +from certfuzz.campaign.config.config_windows import get_command_args_list +from certfuzz.iteration.errors import IterationError +from certfuzz.fuzztools import filetools +import shutil + +logger = logging.getLogger(__name__) + + +class WindowTestCasePipeline(TestCasePipelineBase): + def _pre_verify(self, testcase): + # pretty-print the testcase for debugging + logger.debug('Testcase:') + from pprint import pformat + formatted = pformat(testcase.__dict__) + for line in formatted.splitlines(): + logger.debug('... %s', line.rstrip()) + + def _verify(self, testcase): + keep_it, reason = self.keep_testcase(testcase) + + if not keep_it: + logger.info('Candidate testcase rejected: %s', reason) + testcase.should_proceed_with_analysis = False + return + + logger.debug('Keeping testcase (reason=%s)', reason) + testcase.should_proceed_with_analysis = True + logger.info("Crash confirmed: %s Exploitability: %s Faulting Address: %s", testcase.crash_hash, testcase.exp, testcase.faddr) + if self.minimizable: + testcase.should_proceed_with_analysis = True + self.success = True + + def _minimize(self, testcase): + logger.info('Minimizing testcase %s', testcase.signature) + logger.debug('config = %s', self.config) + + config = self._create_minimizer_cfg() + + debuggers.verify_supported_platform() + + kwargs = {'cfg': config, + 'crash': testcase, + 'seedfile_as_target': True, + 'bitwise': False, + 'confidence': 0.999, + 'tempdir': self.working_dir, + 'maxtime': self.config['runoptions']['minimizer_timeout'] + } + + with Minimizer(**kwargs) as minimizer: + minimizer.go() + + # minimzer found other crashes, so we should add them + # to our list for subsequent processing + for tc in minimizer.other_crashes.values(): + self.tc_candidate_q.put(tc) + + def _analyze(self, testcase): + pass + + def _report(self, testcase): + self.copy_files(testcase) + + def keep_testcase(self, testcase): + '''Given a testcase, decide whether it is a keeper. Returns a tuple + containing a boolean indicating whether to keep the testcase, and + a string containing the reason for the boolean result. + @param testcase: a testcase object + @return (bool,str) + ''' + if testcase.is_crash: + if self.keep_duplicates: + return (True, 'keep duplicates') + elif self.uniq_func(testcase.signature): + # Check if crasher directory exists already + target_dir = testcase._get_output_dir(self.outdir) + if os.path.exists(target_dir): + return (False, 'skip duplicate %s' % testcase.signature) + else: + return (True, 'unique') + else: + return (False, 'skip duplicate %s' % testcase.signature) + elif not self.runner: + return (False, 'not a crash') + elif self.keep_heisenbugs: + return (True, 'heisenbug') + else: + return (False, 'skip heisenbugs') + + def _create_minimizer_cfg(self): + class DummyCfg(object): + pass + config = DummyCfg() + config.backtracelevels = 5 # doesn't matter what this is, we don't use it + config.debugger_timeout = self.config['debugger']['runtimeout'] + config.get_command_args_list = lambda x: get_command_args_list(self.cmd_template, x)[1] + config.program = self.config['target']['program'] + config.killprocname = None + config.exclude_unmapped_frames = False + config.watchdogfile = os.devnull + return config + + def copy_files(self, crash): + if not self.outdir: + raise IterationError('Need a target dir to copy to') + + logger.debug('target_base=%s', self.outdir) + + target_dir = crash._get_output_dir(self.outdir) + + if os.path.exists(target_dir): + logger.debug('Repeat crash, will not copy to %s', target_dir) + else: + # make sure target_base exists already + filetools.find_or_create_dir(self.outdir) + logger.debug('Copying to %s', target_dir) + shutil.copytree(crash.tempdir, target_dir) + assert os.path.isdir(target_dir) From fc198f919ce9caf967360e7411429c5b3b079bdd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 08:25:29 -0400 Subject: [PATCH 0489/1169] organize imports --- src/certfuzz/iteration/iteration_linux.py | 7 ++++--- src/certfuzz/iteration/iteration_windows.py | 6 ++++-- .../testcase_pipeline_base.py | 8 +++++--- .../testcase_pipeline_linux.py | 18 ++++++++++-------- .../testcase_pipeline_windows.py | 13 +++++++------ 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 19057fa..d3dfca5 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -6,14 +6,15 @@ import logging import os -from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.crash.bff_crash import BffCrash from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.fuzztools.state_timer import STATE_TIMER -from certfuzz.fuzztools.zzuf import Zzuf, ZzufTestCase +from certfuzz.fuzztools.zzuf import Zzuf +from certfuzz.fuzztools.zzuf import ZzufTestCase from certfuzz.fuzztools.zzuflog import ZzufLog -from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.testcase_pipeline.testcase_pipeline_linux import LinuxTestCasePipeline diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index c459848..8231e61 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -13,13 +13,15 @@ from certfuzz.debuggers.output_parsers import DebuggerFileError from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.file_handlers.tmp_reaper import TmpReaper -from certfuzz.fuzzers.errors import FuzzerError, FuzzerExhaustedError, \ - FuzzerInputMatchesOutputError +from certfuzz.fuzzers.errors import FuzzerError +from certfuzz.fuzzers.errors import FuzzerExhaustedError +from certfuzz.fuzzers.errors import FuzzerInputMatchesOutputError from certfuzz.fuzztools.filetools import delete_files_or_dirs from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.runners.errors import RunnerRegistryError from certfuzz.testcase_pipeline.testcase_pipeline_windows import WindowsTestCasePipeline + #from certfuzz.iteration.iteration_base import IterationBase2 logger = logging.getLogger(__name__) diff --git a/src/certfuzz/testcase_pipeline/testcase_pipeline_base.py b/src/certfuzz/testcase_pipeline/testcase_pipeline_base.py index 0f4bc22..3c47f4d 100644 --- a/src/certfuzz/testcase_pipeline/testcase_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/testcase_pipeline_base.py @@ -4,11 +4,13 @@ @organization: cert.org ''' import Queue -from certfuzz.helpers.coroutine import coroutine -import logging import abc -from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +import logging + from certfuzz.analyzers.errors import AnalyzerEmptyOutputError +from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +from certfuzz.helpers.coroutine import coroutine + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py b/src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py index d24ae06..cfff62d 100644 --- a/src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py @@ -3,22 +3,24 @@ @organization: cert.org ''' -from certfuzz.testcase_pipeline.testcase_pipeline_base import TestCasePipelineBase -from certfuzz.fuzztools.state_timer import STATE_TIMER import logging -from certfuzz.fuzztools import filetools import os + +from certfuzz.analyzers import cw_gmalloc +from certfuzz.analyzers import pin_calltrace +from certfuzz.analyzers import stderr +from certfuzz.analyzers import valgrind +from certfuzz.analyzers.callgrind import callgrind from certfuzz.analyzers.callgrind.annotate import annotate_callgrind from certfuzz.analyzers.callgrind.annotate import annotate_callgrind_tree from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +from certfuzz.fuzztools import filetools +from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.minimizer import MinimizerError, UnixMinimizer as Minimizer -from certfuzz.analyzers import stderr -from certfuzz.analyzers import cw_gmalloc -from certfuzz.analyzers import valgrind -from certfuzz.analyzers.callgrind import callgrind -from certfuzz.analyzers import pin_calltrace +from certfuzz.testcase_pipeline.testcase_pipeline_base import TestCasePipelineBase + logger = logging.getLogger(__name__) diff --git a/src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py b/src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py index a41b61a..cd6dae8 100644 --- a/src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py @@ -3,16 +3,17 @@ @organization: cert.org ''' -from certfuzz.testcase_pipeline.testcase_pipeline_base import TestCasePipelineBase - import logging -from certfuzz import debuggers -from certfuzz.minimizer import WindowsMinimizer as Minimizer import os +import shutil + +from certfuzz import debuggers from certfuzz.campaign.config.config_windows import get_command_args_list -from certfuzz.iteration.errors import IterationError from certfuzz.fuzztools import filetools -import shutil +from certfuzz.iteration.errors import IterationError +from certfuzz.minimizer import WindowsMinimizer as Minimizer +from certfuzz.testcase_pipeline.testcase_pipeline_base import TestCasePipelineBase + logger = logging.getLogger(__name__) From cf4418f5a3739157c2c3f31f5f73a3b88c777515 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 08:27:38 -0400 Subject: [PATCH 0490/1169] remove unused imports, these are already handled in campaign_linux --- src/certfuzz/iteration/iteration_linux.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index d3dfca5..209c6d4 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -7,8 +7,6 @@ import os from certfuzz.crash.bff_crash import BffCrash -from certfuzz.debuggers import crashwrangler # @UnusedImport -from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.fuzztools.state_timer import STATE_TIMER From 6150b0122e01c4d0bba17c8689f8066fccd6b6ad Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 08:36:25 -0400 Subject: [PATCH 0491/1169] rename Iteration classes to reflect platform. also rename test case pipeline modules --- src/certfuzz/campaign/campaign_linux.py | 4 ++-- src/certfuzz/campaign/campaign_windows.py | 6 +++--- src/certfuzz/iteration/iteration_linux.py | 4 ++-- src/certfuzz/iteration/iteration_windows.py | 6 +++--- src/certfuzz/test/iteration/test_iteration_windows.py | 4 ++-- .../{testcase_pipeline_base.py => tc_pipeline_base.py} | 0 .../{testcase_pipeline_linux.py => tc_pipeline_linux.py} | 2 +- ...{testcase_pipeline_windows.py => tc_pipeline_windows.py} | 4 ++-- 8 files changed, 15 insertions(+), 15 deletions(-) rename src/certfuzz/testcase_pipeline/{testcase_pipeline_base.py => tc_pipeline_base.py} (100%) rename src/certfuzz/testcase_pipeline/{testcase_pipeline_linux.py => tc_pipeline_linux.py} (98%) rename src/certfuzz/testcase_pipeline/{testcase_pipeline_windows.py => tc_pipeline_windows.py} (97%) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index df140e7..36c174a 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -23,7 +23,7 @@ from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid -from certfuzz.iteration.iteration_linux import Iteration +from certfuzz.iteration.iteration_linux import LinuxIteration logger = logging.getLogger(__name__) @@ -213,7 +213,7 @@ def _do_interval(self): def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() - with Iteration(cfg=self.config, seednum=seednum, seedfile=seedfile, r=range_obj, workdirbase=self.working_dir, quiet=quiet_flag, + with LinuxIteration(cfg=self.config, seednum=seednum, seedfile=seedfile, r=range_obj, workdirbase=self.working_dir, quiet=quiet_flag, uniq_func=self._crash_is_unique, sf_set=self.seedfile_set, rf=seedfile.rangefinder, diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 7e05964..386e247 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -15,7 +15,7 @@ from certfuzz.fuzzers.errors import FuzzerExhaustedError from certfuzz.runners.killableprocess import Popen from certfuzz.campaign.config.config_windows import Config -from certfuzz.iteration.iteration_windows import Iteration +from certfuzz.iteration.iteration_windows import WindowsIteration logger = logging.getLogger(__name__) @@ -211,8 +211,8 @@ def _do_interval(self): def _do_iteration(self, sf, rng_seed, seednum): # use a with...as to ensure we always hit # the __enter__ and __exit__ methods of the - # newly created Iteration() - with Iteration(sf, rng_seed, seednum, self.config, self.fuzzer_cls, + # newly created WindowsIteration() + with WindowsIteration(sf, rng_seed, seednum, self.config, self.fuzzer_cls, self.runner, self.debugger_module, self.dbg_class, self.keep_heisenbugs, self.keep_duplicates, self.cmd_template, self._crash_is_unique, diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 209c6d4..198e0d1 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -14,13 +14,13 @@ from certfuzz.fuzztools.zzuf import ZzufTestCase from certfuzz.fuzztools.zzuflog import ZzufLog from certfuzz.iteration.iteration_base3 import IterationBase3 -from certfuzz.testcase_pipeline.testcase_pipeline_linux import LinuxTestCasePipeline +from certfuzz.testcase_pipeline.tc_pipeline_linux import LinuxTestCasePipeline logger = logging.getLogger(__name__) -class Iteration(IterationBase3): +class LinuxIteration(IterationBase3): tcpipeline_cls = LinuxTestCasePipeline def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None, diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 8231e61..b3f4f0d 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -19,7 +19,7 @@ from certfuzz.fuzztools.filetools import delete_files_or_dirs from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.runners.errors import RunnerRegistryError -from certfuzz.testcase_pipeline.testcase_pipeline_windows import WindowsTestCasePipeline +from certfuzz.testcase_pipeline.tc_pipeline_windows import WindowsTestCasePipeline #from certfuzz.iteration.iteration_base import IterationBase2 @@ -29,7 +29,7 @@ MAX_IOERRORS = 5 -class Iteration(IterationBase3): +class WindowsIteration(IterationBase3): tcpipeline_cls = WindowsTestCasePipeline def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, @@ -100,7 +100,7 @@ def __exit__(self, etype, value, traceback): # log something different if we failed to handle an exception if etype and not handled: - logger.warning('Iteration terminating abnormally due to %s: %s', etype.__name__, value) + logger.warning('WindowsIteration terminating abnormally due to %s: %s', etype.__name__, value) else: logger.info('Done with iteration %d', self.seednum) diff --git a/src/certfuzz/test/iteration/test_iteration_windows.py b/src/certfuzz/test/iteration/test_iteration_windows.py index 1088abc..d7463e7 100644 --- a/src/certfuzz/test/iteration/test_iteration_windows.py +++ b/src/certfuzz/test/iteration/test_iteration_windows.py @@ -5,7 +5,7 @@ ''' import unittest -from certfuzz.iteration.iteration_windows import Iteration +from certfuzz.iteration.iteration_windows import WindowsIteration class Test(unittest.TestCase): @@ -13,7 +13,7 @@ class Test(unittest.TestCase): def setUp(self): args = list('0123456789ABCDE') args[3] = {'runoptions': {'keep_unique_faddr': False}} - self.iteration = Iteration(*args) + self.iteration = WindowsIteration(*args) def tearDown(self): pass diff --git a/src/certfuzz/testcase_pipeline/testcase_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py similarity index 100% rename from src/certfuzz/testcase_pipeline/testcase_pipeline_base.py rename to src/certfuzz/testcase_pipeline/tc_pipeline_base.py diff --git a/src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py similarity index 98% rename from src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py rename to src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index cfff62d..0a8b4d6 100644 --- a/src/certfuzz/testcase_pipeline/testcase_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -19,7 +19,7 @@ from certfuzz.fuzztools import filetools from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.minimizer import MinimizerError, UnixMinimizer as Minimizer -from certfuzz.testcase_pipeline.testcase_pipeline_base import TestCasePipelineBase +from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase logger = logging.getLogger(__name__) diff --git a/src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py similarity index 97% rename from src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py rename to src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index cd6dae8..087ab4c 100644 --- a/src/certfuzz/testcase_pipeline/testcase_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -12,13 +12,13 @@ from certfuzz.fuzztools import filetools from certfuzz.iteration.errors import IterationError from certfuzz.minimizer import WindowsMinimizer as Minimizer -from certfuzz.testcase_pipeline.testcase_pipeline_base import TestCasePipelineBase +from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase logger = logging.getLogger(__name__) -class WindowTestCasePipeline(TestCasePipelineBase): +class WindowsTestCasePipeline(TestCasePipelineBase): def _pre_verify(self, testcase): # pretty-print the testcase for debugging logger.debug('Testcase:') From a0f30e7c0d5bc681931d86eb3764c38b36fa1f8e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 08:43:35 -0400 Subject: [PATCH 0492/1169] refactor config storage into base class --- src/certfuzz/iteration/iteration_base3.py | 4 +++- src/certfuzz/iteration/iteration_linux.py | 3 +-- src/certfuzz/iteration/iteration_windows.py | 15 +++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index d05b806..801c432 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -21,7 +21,8 @@ def __init__(self, outdir=None, sf_set=None, rf=None, - uniq_func=None): + uniq_func=None, + config=None): logger.debug('init') self.seedfile = seedfile @@ -30,6 +31,7 @@ def __init__(self, self.outdir = outdir self.sf_set = sf_set self.rf = rf + self.cfg = config self.pipeline_options = {} diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 198e0d1..9355d5a 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -26,8 +26,7 @@ class LinuxIteration(IterationBase3): def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None, sf_set=None, rf=None, outdir=None): IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, - sf_set, rf, uniq_func) - self.cfg = cfg + sf_set, rf, uniq_func, cfg) self.r = r self.quiet_flag = quiet diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index b3f4f0d..7e6d25c 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -37,10 +37,9 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, cmd_template, uniq_func, workdirbase, outdir, debug, sf_set, rf): IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, - sf_set, rf, uniq_func) + sf_set, rf, uniq_func, config) self.r = None self.rng_seed = rng_seed - self.config = config self.fuzzer_cls = fuzzer_cls self.runner = runner self.debugger_module = debugger @@ -125,7 +124,7 @@ def _tidy(self): def _fuzz(self): # generated test case (fuzzed input) logger.info('...fuzzing') - fuzz_opts = self.config['fuzzer'] + fuzz_opts = self.cfg['fuzzer'] with self.fuzzer_cls(self.seedfile, self.working_dir, self.rng_seed, @@ -139,7 +138,7 @@ def _fuzz(self): # decide if we can minimize this case later # do this here (and not sooner) because the fuzzer_cls could # decide at runtime whether it is or is not minimizable - self.minimizable = fuzzer.is_minimizable and self.config['runoptions']['minimize'] + self.minimizable = fuzzer.is_minimizable and self.cfg['runoptions']['minimize'] # hang on to this fuzzer instance, we use it in _run self.fuzzer = fuzzer @@ -152,7 +151,7 @@ def _run(self): analysis_needed = True if self.runner: logger.info('...running %s', self.runner.__name__) - with self.runner(self.config['runner'], + with self.runner(self.cfg['runner'], self.cmd_template, self.fuzzer.output_file_path, self.working_dir) as runner: @@ -168,14 +167,14 @@ def _run(self): def _construct_testcase(self): cmdlist = get_command_args_list(self.cmd_template, self.fuzzer.output_file_path)[1] - dbg_opts = self.config['debugger'] + dbg_opts = self.cfg['debugger'] fuzzed_file = BasicFile(self.fuzzer.output_file_path) logger.debug('Building testcase object') with WindowsCrash(self.cmd_template, self.seedfile, fuzzed_file, cmdlist, self.fuzzer, self.debugger_class, dbg_opts, - self.working_dir, self.config['runoptions']['keep_unique_faddr'], - self.config['target']['program'], + self.working_dir, self.cfg['runoptions']['keep_unique_faddr'], + self.cfg['target']['program'], heisenbug_retries=self.retries, copy_fuzzedfile=self.fuzzer.fuzzed_changes_input) as testcase: From 3c55ed45c7a74ad211635492836f347ae979bc1f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 08:46:00 -0400 Subject: [PATCH 0493/1169] move r (range) var into base class --- src/certfuzz/iteration/iteration_base3.py | 4 +++- src/certfuzz/iteration/iteration_linux.py | 3 +-- src/certfuzz/iteration/iteration_windows.py | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 801c432..dbd985d 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -22,7 +22,8 @@ def __init__(self, sf_set=None, rf=None, uniq_func=None, - config=None): + config=None, + r=None): logger.debug('init') self.seedfile = seedfile @@ -32,6 +33,7 @@ def __init__(self, self.sf_set = sf_set self.rf = rf self.cfg = config + self.r = r self.pipeline_options = {} diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 9355d5a..6e1dab1 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -26,8 +26,7 @@ class LinuxIteration(IterationBase3): def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None, sf_set=None, rf=None, outdir=None): IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, - sf_set, rf, uniq_func, cfg) - self.r = r + sf_set, rf, uniq_func, cfg, r) self.quiet_flag = quiet self.testcase_base_dir = os.path.join(self.outdir, 'crashers') diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 7e6d25c..1c74208 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -37,8 +37,7 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, cmd_template, uniq_func, workdirbase, outdir, debug, sf_set, rf): IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, - sf_set, rf, uniq_func, config) - self.r = None + sf_set, rf, uniq_func, config, None) self.rng_seed = rng_seed self.fuzzer_cls = fuzzer_cls self.runner = runner From 5d403d20e4d6b50990bceba25ff33d9e1baf1a49 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 08:57:53 -0400 Subject: [PATCH 0494/1169] move tempdir prefix setting to base class --- src/certfuzz/iteration/iteration_base3.py | 4 +++- src/certfuzz/iteration/iteration_windows.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index dbd985d..448d17e 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -13,6 +13,7 @@ class IterationBase3(object): __metaclass__ = abc.ABCMeta + _tmpdir_pfx = 'iteration_' def __init__(self, seedfile=None, @@ -58,7 +59,8 @@ def tcpipeline_cls(self): ''' def __enter__(self): - self.working_dir = tempfile.mkdtemp(prefix='iteration-', dir=self.workdirbase) + self.working_dir = tempfile.mkdtemp(prefix=self._tmpdir_pfx, + dir=self.workdirbase) logger.debug('workdir=%s', self.working_dir) # self._setup_analysis_pipeline() return self diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 1c74208..bc6aec1 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -49,7 +49,6 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, self.keep_heisenbugs = keep_heisenbugs self.cmd_template = string.Template(cmd_template) self.fuzzed = False - self.iteration_tmpdir_pfx = 'iteration_' self.minimizable = False if self.runner is None: @@ -114,7 +113,7 @@ def _tidy(self): # wrap up this iteration paths = [] # sweep up any iteration temp dirs left behind previously - pattern = os.path.join(self.workdirbase, self.iteration_tmpdir_pfx + '*') + pattern = os.path.join(self.workdirbase, self._tmpdir_pfx + '*') paths.extend(glob.glob(pattern)) delete_files_or_dirs(paths) # wipe them out, all of them From 50db9726ff2ece726bbdae810829da0a1d41a57b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 08:58:13 -0400 Subject: [PATCH 0495/1169] pass in options to pipeline --- src/certfuzz/iteration/iteration_windows.py | 10 ++++++++-- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index bc6aec1..0399353 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -44,9 +44,10 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, self.debugger_module = debugger self.debugger_class = dbg_class self.debug = debug + # TODO: do we use keep_uniq_faddr at all? self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] - self.keep_duplicates = keep_duplicates - self.keep_heisenbugs = keep_heisenbugs +# self.keep_duplicates = keep_duplicates +# self.keep_heisenbugs = keep_heisenbugs self.cmd_template = string.Template(cmd_template) self.fuzzed = False self.minimizable = False @@ -58,6 +59,11 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, # runner is not null self.retries = 4 + self.pipeline_options = { + 'keep_duplicates': keep_duplicates, + 'keep_heisenbugs': keep_heisenbugs, + } + def __exit__(self, etype, value, traceback): handled = IterationBase3.__exit__(self, etype, value, traceback) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 087ab4c..0099c4e 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -81,7 +81,7 @@ def keep_testcase(self, testcase): @return (bool,str) ''' if testcase.is_crash: - if self.keep_duplicates: + if self.options['keep_duplicates']: return (True, 'keep duplicates') elif self.uniq_func(testcase.signature): # Check if crasher directory exists already @@ -94,7 +94,7 @@ def keep_testcase(self, testcase): return (False, 'skip duplicate %s' % testcase.signature) elif not self.runner: return (False, 'not a crash') - elif self.keep_heisenbugs: + elif self.options['keep_heisenbugs']: return (True, 'heisenbug') else: return (False, 'skip heisenbugs') From 4c781ab00de5422437edf457e6538a8db2052d22 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 09:07:00 -0400 Subject: [PATCH 0496/1169] add required method --- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 0099c4e..2cf622d 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -19,6 +19,9 @@ class WindowsTestCasePipeline(TestCasePipelineBase): + def _setup_analyzers(self): + TestCasePipelineBase._setup_analyzers(self) + def _pre_verify(self, testcase): # pretty-print the testcase for debugging logger.debug('Testcase:') From f84e829a0d1313363324a5f79351816d25ff2c7b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 09:07:25 -0400 Subject: [PATCH 0497/1169] rename variable crash to test case --- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 2cf622d..bc06571 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -115,13 +115,13 @@ class DummyCfg(object): config.watchdogfile = os.devnull return config - def copy_files(self, crash): + def copy_files(self, testcase): if not self.outdir: raise IterationError('Need a target dir to copy to') logger.debug('target_base=%s', self.outdir) - target_dir = crash._get_output_dir(self.outdir) + target_dir = testcase._get_output_dir(self.outdir) if os.path.exists(target_dir): logger.debug('Repeat crash, will not copy to %s', target_dir) @@ -129,5 +129,5 @@ def copy_files(self, crash): # make sure target_base exists already filetools.find_or_create_dir(self.outdir) logger.debug('Copying to %s', target_dir) - shutil.copytree(crash.tempdir, target_dir) + shutil.copytree(testcase.tempdir, target_dir) assert os.path.isdir(target_dir) From 81befc72d38ed242ef42c39fc680b60bdb5b555d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 10:07:57 -0400 Subject: [PATCH 0498/1169] refactor more common stuff into base class --- src/certfuzz/iteration/iteration_base3.py | 4 +++- src/certfuzz/iteration/iteration_windows.py | 7 +++---- src/certfuzz/testcase_pipeline/tc_pipeline_base.py | 7 ++++++- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 1 + src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 10 +++++----- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 448d17e..a12b5e6 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -138,7 +138,9 @@ def process_testcases(self): with self.tcpipeline_cls(testcases=self.testcases, uniq_func=self.uniq_func, cfg=self.cfg, - options=self.pipeline_options) as pipeline: + options=self.pipeline_options, + outdir=self.outdir, + workdirbase=self.working_dir) as pipeline: pipeline.go() def go(self): diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 0399353..870c22a 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -46,11 +46,8 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, self.debug = debug # TODO: do we use keep_uniq_faddr at all? self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] -# self.keep_duplicates = keep_duplicates -# self.keep_heisenbugs = keep_heisenbugs self.cmd_template = string.Template(cmd_template) self.fuzzed = False - self.minimizable = False if self.runner is None: # null runner case @@ -62,6 +59,7 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, self.pipeline_options = { 'keep_duplicates': keep_duplicates, 'keep_heisenbugs': keep_heisenbugs, + 'minimizable': False, } def __exit__(self, etype, value, traceback): @@ -139,10 +137,11 @@ def _fuzz(self): self.r = fuzzer.range if self.r: logger.info('Selected r: %s', self.r) + # decide if we can minimize this case later # do this here (and not sooner) because the fuzzer_cls could # decide at runtime whether it is or is not minimizable - self.minimizable = fuzzer.is_minimizable and self.cfg['runoptions']['minimize'] + self.pipeline_options['minimizable'] = fuzzer.is_minimizable and self.cfg['runoptions']['minimize'] # hang on to this fuzzer instance, we use it in _run self.fuzzer = fuzzer diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 3c47f4d..e011dc6 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -21,13 +21,18 @@ class TestCasePipelineBase(object): ''' __metaclass__ = abc.ABCMeta - def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None): + def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, + outdir=None, workdirbase=None): ''' Constructor ''' self.cfg = cfg self.options = options self.uniq_func = uniq_func + self.outdir = outdir + + self.working_dir = workdirbase + self.tc_candidate_q = Queue.Queue() self.analyzer_classes = [] diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 0a8b4d6..1b7eea9 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -141,6 +141,7 @@ def _pre_report(self, testcase): testcase.logger.info('PC=%s', testcase.pc) def _report(self, testcase): + # TODO move BffCrash.copy_files into this module testcase.copy_files() def _post_report(self, testcase): diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index bc06571..87f677f 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -41,13 +41,13 @@ def _verify(self, testcase): logger.debug('Keeping testcase (reason=%s)', reason) testcase.should_proceed_with_analysis = True logger.info("Crash confirmed: %s Exploitability: %s Faulting Address: %s", testcase.crash_hash, testcase.exp, testcase.faddr) - if self.minimizable: + if self.options['minimizable']: testcase.should_proceed_with_analysis = True self.success = True def _minimize(self, testcase): logger.info('Minimizing testcase %s', testcase.signature) - logger.debug('config = %s', self.config) + logger.debug('config = %s', self.cfg) config = self._create_minimizer_cfg() @@ -59,7 +59,7 @@ def _minimize(self, testcase): 'bitwise': False, 'confidence': 0.999, 'tempdir': self.working_dir, - 'maxtime': self.config['runoptions']['minimizer_timeout'] + 'maxtime': self.cfg['runoptions']['minimizer_timeout'] } with Minimizer(**kwargs) as minimizer: @@ -107,9 +107,9 @@ class DummyCfg(object): pass config = DummyCfg() config.backtracelevels = 5 # doesn't matter what this is, we don't use it - config.debugger_timeout = self.config['debugger']['runtimeout'] + config.debugger_timeout = self.cfg['debugger']['runtimeout'] config.get_command_args_list = lambda x: get_command_args_list(self.cmd_template, x)[1] - config.program = self.config['target']['program'] + config.program = self.cfg['target']['program'] config.killprocname = None config.exclude_unmapped_frames = False config.watchdogfile = os.devnull From 46a624189c38d419d9c170f1c384c79ad4137e47 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 11:11:52 -0400 Subject: [PATCH 0499/1169] set the test case logger once we're sticking with it --- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 1b7eea9..59718b7 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -77,6 +77,9 @@ def _verify(self, testcase): else: logger.debug('%s was found, not unique', tc.signature) + def _post_verify(self, testcase): + testcase.get_logger() + def _minimize(self, testcase): if self.options.get('minimize_crashers'): self._mininimize_to_seedfile(testcase) From 7915943bdcc116dbb0f44a71ba07616c054ffd75 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 11:12:54 -0400 Subject: [PATCH 0500/1169] improve error handling when the result dir does not exist --- src/certfuzz/crash/bff_crash.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index 9f6688a..c872b39 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -178,7 +178,9 @@ def get_logger(self): ''' self.logger = logging.getLogger(self.signature) if len(self.logger.handlers) == 0: - assert os.path.exists(self.result_dir) + if not os.path.exists(self.result_dir): + logger.error('Result path not found: %s', self.result_dir) + raise CrashError('Result path not found: {}'.format(self.result_dir)) logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) logfile = '%s.log' % self.signature logger.debug('logfile=%s', logfile) From 0e388df7b97c8c76ff3d5dc8029d50e423fedba4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 17 Jul 2014 11:13:41 -0400 Subject: [PATCH 0501/1169] pass cmd template as an option to pipeline --- src/certfuzz/iteration/iteration_windows.py | 4 +++- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 870c22a..f6980a2 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -46,9 +46,10 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, self.debug = debug # TODO: do we use keep_uniq_faddr at all? self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] - self.cmd_template = string.Template(cmd_template) self.fuzzed = False + self.cmd_template = string.Template(cmd_template) + if self.runner is None: # null runner case self.retries = 0 @@ -60,6 +61,7 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, 'keep_duplicates': keep_duplicates, 'keep_heisenbugs': keep_heisenbugs, 'minimizable': False, + 'cmd_template': self.cmd_template, } def __exit__(self, etype, value, traceback): diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 87f677f..535500a 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -108,7 +108,7 @@ class DummyCfg(object): config = DummyCfg() config.backtracelevels = 5 # doesn't matter what this is, we don't use it config.debugger_timeout = self.cfg['debugger']['runtimeout'] - config.get_command_args_list = lambda x: get_command_args_list(self.cmd_template, x)[1] + config.get_command_args_list = lambda x: get_command_args_list(self.options['cmd_template'], x)[1] config.program = self.cfg['target']['program'] config.killprocname = None config.exclude_unmapped_frames = False From 2fca839fcfb8b7299740d573c5c861c99255a6d9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 18 Jul 2014 08:52:25 -0400 Subject: [PATCH 0502/1169] wrap shutil.rmtree in an exponential back off to avoid race condition on windows --- src/certfuzz/fuzztools/filetools.py | 12 +++++++++++- src/certfuzz/iteration/iteration_base3.py | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 1c537e9..2964e3c 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -74,12 +74,22 @@ def make_directories(*paths): mkdir_p(d) +@exponential_backoff +def rm_rf(path): + ''' + Wraps shutil.rmtree with an exponential backoff to avoid contention with + HGFS on some platforms (usually Windows) + :param path: + ''' + shutil.rmtree(path) + + def delete_files(*files): delete_files2(files) @exponential_backoff -def delete_files2(files=[]): +def delete_files2(files): ''' Deletes given a list of paths @return: none diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index a12b5e6..3b629d2 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -4,9 +4,9 @@ @author: adh ''' import logging -import shutil import tempfile import abc +from certfuzz.fuzztools.filetools import rm_rf logger = logging.getLogger(__name__) @@ -79,7 +79,7 @@ def __exit__(self, etype, value, traceback): # and there's a problem logger.debug('Skipping cleanup since we are in debug mode.') else: - shutil.rmtree(self.working_dir) + rm_rf(self.working_dir) return handled From ed7ef0b9a2b962daa6e021e1923fc66bd400fd1d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 18 Jul 2014 09:14:17 -0400 Subject: [PATCH 0503/1169] rename Crash -> Testcase --- src/certfuzz/crash/__init__.py | 2 +- src/certfuzz/crash/bff_crash.py | 12 ++++++------ src/certfuzz/crash/crash_base.py | 4 ++-- src/certfuzz/crash/crash_windows.py | 8 ++++---- src/certfuzz/crash/testcase_base.py | 11 ----------- 5 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/certfuzz/crash/__init__.py b/src/certfuzz/crash/__init__.py index bbc1ec0..6d48b4d 100644 --- a/src/certfuzz/crash/__init__.py +++ b/src/certfuzz/crash/__init__.py @@ -1,5 +1,5 @@ from certfuzz.crash.testcase_base import TestCaseBase -from certfuzz.crash.crash_base import Crash +from certfuzz.crash.crash_base import Testcase from certfuzz.crash.bff_crash import BffCrash from certfuzz.crash.errors import CrashError from certfuzz.crash.errors import TestCaseError diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index c872b39..83fe75e 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -6,7 +6,7 @@ import logging import os -from certfuzz.crash.crash_base import Crash, CrashError +from certfuzz.crash.crash_base import Testcase, CrashError from certfuzz.debuggers import registration from certfuzz.fuzztools import hostinfo, filetools @@ -24,7 +24,7 @@ host_info = hostinfo.HostInfo() -class BffCrash(Crash): +class BffCrash(Testcase): ''' classdocs ''' @@ -36,7 +36,7 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, ''' Constructor ''' - Crash.__init__(self, seedfile, fuzzedfile, debugger_timeout) + Testcase.__init__(self, seedfile, fuzzedfile, debugger_timeout) self.cfg = cfg self.workdir_base = workdir_base self.program = program @@ -70,7 +70,7 @@ def set_debugger_template(self, option='bt_only'): raise CrashError('Debugger template does not exist at %s' % self.debugger_template) def update_crash_details(self): - Crash.update_crash_details(self) + Testcase.update_crash_details(self) self.cmdargs = self.cfg.get_command_args_list(self.fuzzedfile.path) # self.debugger_file = debuggers.get_debug_file(self.fuzzedfile.path) @@ -140,9 +140,9 @@ def get_signature(self): if not self.signature: self.signature = self.dbg.get_crash_signature(self.backtrace_lines) if self.signature: - logger.debug("Crash signature is %s", self.signature) + logger.debug("Testcase signature is %s", self.signature) else: - raise CrashError('Crash has no signature.') + raise CrashError('Testcase has no signature.') if self.dbg.total_stack_corruption: # total_stack_corruption. Use pin calltrace to get a backtrace analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self) diff --git a/src/certfuzz/crash/crash_base.py b/src/certfuzz/crash/crash_base.py index b79dfc3..e7d6ee9 100644 --- a/src/certfuzz/crash/crash_base.py +++ b/src/certfuzz/crash/crash_base.py @@ -16,11 +16,11 @@ logger = logging.getLogger(__name__) -class Crash(TestCaseBase): +class Testcase(TestCaseBase): tmpdir_pfx = 'crash-' def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): - logger.debug('Inititalize Crash') + logger.debug('Inititalize Testcase') TestCaseBase.__init__(self, seedfile, fuzzedfile) self.debugger_timeout = dbg_timeout diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/crash/crash_windows.py index 613e26a..4760fb8 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/crash/crash_windows.py @@ -8,7 +8,7 @@ import os from certfuzz.campaign.config.config_windows import get_command_args_list -from certfuzz.crash.crash_base import Crash +from certfuzz.crash.crash_base import Testcase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import best_effort_move from certfuzz.helpers import random_str @@ -37,7 +37,7 @@ def logerror(func, path, excinfo): } -class WindowsCrash(Crash): +class WindowsCrash(Testcase): tmpdir_pfx = 'bff-crash-' # TODO: do we still need fuzzer as an arg? @@ -47,7 +47,7 @@ def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, dbg_timeout = dbg_opts['runtimeout'] - Crash.__init__(self, seedfile, fuzzedfile, dbg_timeout) + Testcase.__init__(self, seedfile, fuzzedfile, dbg_timeout) self.dbg_class = dbg_class self.dbg_opts = dbg_opts @@ -94,7 +94,7 @@ def update_crash_details(self): as needed. Used in both object runtime context and for refresh after a crash object is copied. ''' - Crash.update_crash_details(self) + Testcase.update_crash_details(self) # Reset properties that need to be regenerated self.exception_depth = 0 self.parsed_outputs = [] diff --git a/src/certfuzz/crash/testcase_base.py b/src/certfuzz/crash/testcase_base.py index c484724..589ea9a 100644 --- a/src/certfuzz/crash/testcase_base.py +++ b/src/certfuzz/crash/testcase_base.py @@ -64,14 +64,3 @@ def calculate_hamming_distances_a(self): self.hd_bits = hamming.bitwise_hd(a_string, fuzzed) self.hd_bytes = hamming.bytewise_hd(a_string, fuzzed) - - def to_TestCaseDoc(self): - from ..db.couchdb.datatypes import TestCaseDoc - - doc = TestCaseDoc() - doc.crash_signature = self.signature - doc.fuzzed_file = self.fuzzedfile.to_FileDoc().id - doc.seed_file = self.seedfile.to_FileDoc().id - doc.bitwise_hd = self.hd_bits - doc.bytewise_hd = self.hd_bytes - return doc From bb550e81f744b7e8b62de9807a063ae65c1e5e83 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 18 Jul 2014 13:18:56 -0400 Subject: [PATCH 0504/1169] pass runner presence into test case pipeline --- src/certfuzz/iteration/iteration_windows.py | 1 + .../testcase_pipeline/tc_pipeline_windows.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index f6980a2..8b73175 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -62,6 +62,7 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, 'keep_heisenbugs': keep_heisenbugs, 'minimizable': False, 'cmd_template': self.cmd_template, + 'used_runner': self.runner is not None, } def __exit__(self, etype, value, traceback): diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 535500a..753f8b2 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -95,7 +95,7 @@ def keep_testcase(self, testcase): return (True, 'unique') else: return (False, 'skip duplicate %s' % testcase.signature) - elif not self.runner: + elif not self.options['used_runner']: return (False, 'not a crash') elif self.options['keep_heisenbugs']: return (True, 'heisenbug') @@ -125,9 +125,10 @@ def copy_files(self, testcase): if os.path.exists(target_dir): logger.debug('Repeat crash, will not copy to %s', target_dir) - else: - # make sure target_base exists already - filetools.find_or_create_dir(self.outdir) - logger.debug('Copying to %s', target_dir) - shutil.copytree(testcase.tempdir, target_dir) - assert os.path.isdir(target_dir) + return + + # make sure target_base exists already + filetools.find_or_create_dir(self.outdir) + logger.debug('Copying to %s', target_dir) + shutil.copytree(testcase.tempdir, target_dir) + assert os.path.isdir(target_dir) From 8cf7db1b79aaeabcda0d49f801e201d895244227 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 18 Jul 2014 13:23:41 -0400 Subject: [PATCH 0505/1169] create error class --- src/certfuzz/testcase_pipeline/errors.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/certfuzz/testcase_pipeline/errors.py diff --git a/src/certfuzz/testcase_pipeline/errors.py b/src/certfuzz/testcase_pipeline/errors.py new file mode 100644 index 0000000..b61a095 --- /dev/null +++ b/src/certfuzz/testcase_pipeline/errors.py @@ -0,0 +1,10 @@ +''' +Created on Jul 18, 2014 + +@author: adh +''' +from certfuzz.errors import CERTFuzzError + + +class TestCasePipelineError(CERTFuzzError): + pass From c3bfe6229205715b4f2be7c6a2e7d415eb27d6ce Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 18 Jul 2014 13:49:09 -0400 Subject: [PATCH 0506/1169] move copy files to tc_pipeline_base class --- src/certfuzz/crash/crash_base.py | 18 -------------- .../testcase_pipeline/tc_pipeline_base.py | 24 +++++++++++++++++++ .../testcase_pipeline/tc_pipeline_linux.py | 9 +++---- .../testcase_pipeline/tc_pipeline_windows.py | 23 +++--------------- 4 files changed, 32 insertions(+), 42 deletions(-) diff --git a/src/certfuzz/crash/crash_base.py b/src/certfuzz/crash/crash_base.py index e7d6ee9..c73e4fb 100644 --- a/src/certfuzz/crash/crash_base.py +++ b/src/certfuzz/crash/crash_base.py @@ -71,11 +71,6 @@ def _rename_fuzzed_file(self): def _set_attr_from_dbg(self, attrname): raise NotImplementedError - def _temp_output_files(self): - t = self.tempdir - file_list = [os.path.join(t, f) for f in os.listdir(t)] - return file_list - def _verify_crash_base_dir(self): raise NotImplementedError @@ -102,19 +97,6 @@ def clean_tmpdir(self): def confirm_crash(self): raise NotImplementedError - def copy_files(self, target=None): - if not target: - target = self.result_dir - if not target or not os.path.isdir(target): - raise CrashError("Target directory does not exist: %s" % target) - - logger.debug('Copying to %s', target) - file_list = self._temp_output_files() - for f in file_list: - logger.debug('\t...file: %s', f) - - filetools.copy_files(target, *file_list) - def copy_files_to_temp(self): if self.fuzzedfile and self.copy_fuzzedfile: filetools.copy_file(self.fuzzedfile.path, self.tempdir) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index e011dc6..4796ea4 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -10,6 +10,10 @@ from certfuzz.analyzers.errors import AnalyzerEmptyOutputError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file from certfuzz.helpers.coroutine import coroutine +from certfuzz.testcase_pipeline.errors import TestCasePipelineError +import os +from certfuzz.fuzztools import filetools +import shutil logger = logging.getLogger(__name__) @@ -199,3 +203,23 @@ def go(self): while not self.tc_candidate_q.empty(): testcase = self.tc_candidate_q.get() self.analysis_pipeline.send(testcase) + + def _copy_files(self, testcase): + if not self.outdir: + raise TestCasePipelineError('No outdir set') + + logger.debug('target_base=%s', self.outdir) + + target_dir = testcase.result_dir + + if os.path.exists(target_dir): + logger.debug('Repeat crash, will not copy to %s', target_dir) + return + + # make sure target_base exists already + filetools.find_or_create_dir(self.outdir) + logger.debug('Copying to %s', target_dir) + shutil.copytree(testcase.tempdir, target_dir) + + if not os.path.exists(target_dir): + raise TestCasePipelineError('Failed to create target dir %s', target_dir) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 59718b7..d09abcb 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -20,6 +20,8 @@ from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.minimizer import MinimizerError, UnixMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase +from certfuzz.testcase_pipeline.errors import TestCasePipelineError +import shutil logger = logging.getLogger(__name__) @@ -82,8 +84,10 @@ def _post_verify(self, testcase): def _minimize(self, testcase): if self.options.get('minimize_crashers'): + touch_watchdog_file() self._mininimize_to_seedfile(testcase) if self.options.get('minimize_to_string'): + touch_watchdog_file() self._minimize_to_string(testcase) def _post_minimize(self, testcase): @@ -144,8 +148,7 @@ def _pre_report(self, testcase): testcase.logger.info('PC=%s', testcase.pc) def _report(self, testcase): - # TODO move BffCrash.copy_files into this module - testcase.copy_files() + self._copy_files(testcase) def _post_report(self, testcase): # always clean up after yourself @@ -163,8 +166,6 @@ def _minimize_to_string(self, testcase): self._minimize_generic(testcase, sftarget=False, confidence=0.9) def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): - touch_watchdog_file() - STATE_TIMER.enter_state('minimize_testcase') try: with Minimizer(cfg=self.cfg, diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 753f8b2..a95d5c4 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -13,6 +13,7 @@ from certfuzz.iteration.errors import IterationError from certfuzz.minimizer import WindowsMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase +from certfuzz.testcase_pipeline.errors import TestCasePipelineError logger = logging.getLogger(__name__) @@ -20,7 +21,7 @@ class WindowsTestCasePipeline(TestCasePipelineBase): def _setup_analyzers(self): - TestCasePipelineBase._setup_analyzers(self) + pass def _pre_verify(self, testcase): # pretty-print the testcase for debugging @@ -74,7 +75,7 @@ def _analyze(self, testcase): pass def _report(self, testcase): - self.copy_files(testcase) + self._copy_files(testcase) def keep_testcase(self, testcase): '''Given a testcase, decide whether it is a keeper. Returns a tuple @@ -114,21 +115,3 @@ class DummyCfg(object): config.exclude_unmapped_frames = False config.watchdogfile = os.devnull return config - - def copy_files(self, testcase): - if not self.outdir: - raise IterationError('Need a target dir to copy to') - - logger.debug('target_base=%s', self.outdir) - - target_dir = testcase._get_output_dir(self.outdir) - - if os.path.exists(target_dir): - logger.debug('Repeat crash, will not copy to %s', target_dir) - return - - # make sure target_base exists already - filetools.find_or_create_dir(self.outdir) - logger.debug('Copying to %s', target_dir) - shutil.copytree(testcase.tempdir, target_dir) - assert os.path.isdir(target_dir) From aa86811cf1f09739cc68e196f63505d65b1c69f1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 27 Oct 2014 14:49:43 -0400 Subject: [PATCH 0507/1169] use more specific error --- src/certfuzz/runners/errors.py | 3 +++ src/certfuzz/runners/zzufrun.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/runners/errors.py b/src/certfuzz/runners/errors.py index baf3890..4075618 100644 --- a/src/certfuzz/runners/errors.py +++ b/src/certfuzz/runners/errors.py @@ -21,3 +21,6 @@ class RunnerPlatformVersionError(RunnerError): class RunnerRegistryError(RunnerError): pass + +class RunnerNotFoundError(RunnerError): + pass diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 70154af..8c635b5 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -10,6 +10,7 @@ import subprocess import logging from collections import deque +from certfuzz.runners.errors import RunnerNotFoundError logger = logging.getLogger(__name__) @@ -37,7 +38,7 @@ def _get_zzuf_args(self): def _find_zzuf(self): self._zzuf_loc = find_executable('zzuf') if self._zzuf_loc is None: - raise RunnerError('Unable to locate zzuf, $PATH={}'.format(os.environ['PATH'])) + raise RunnerNotFoundError('Unable to locate zzuf, $PATH={}'.format(os.environ['PATH'])) def __enter__(self): self = Runner.__enter__(self) From b2441b31bf3b2452217af9839c06be91458fef1d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 07:34:27 -0400 Subject: [PATCH 0508/1169] test _find_zzuf --- src/certfuzz/runners/zzufrun.py | 5 +-- src/certfuzz/test/runners/test_zzufrun.py | 41 ++++++++++++++++++----- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 8c635b5..c043b64 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -23,6 +23,7 @@ def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): self._zzuf_log_basename = 'zzuf_log.txt' self._zzuf_log = os.path.join(self.workingdir, self._zzuf_log_basename) self._quiet = options.get('quiet', True) + self._zzuf_basename = 'zzuf' def _get_zzuf_args(self): self._zzuf_args = deque(['--signal', @@ -36,9 +37,9 @@ def _get_zzuf_args(self): self._zzuf_args.appendleft('--quiet') def _find_zzuf(self): - self._zzuf_loc = find_executable('zzuf') + self._zzuf_loc = find_executable(self._zzuf_basename) if self._zzuf_loc is None: - raise RunnerNotFoundError('Unable to locate zzuf, $PATH={}'.format(os.environ['PATH'])) + raise RunnerNotFoundError('Unable to locate {}, $PATH={}'.format(self._zzuf_basename, os.environ['PATH'])) def __enter__(self): self = Runner.__enter__(self) diff --git a/src/certfuzz/test/runners/test_zzufrun.py b/src/certfuzz/test/runners/test_zzufrun.py index d2bcd90..e9ee20d 100644 --- a/src/certfuzz/test/runners/test_zzufrun.py +++ b/src/certfuzz/test/runners/test_zzufrun.py @@ -8,6 +8,8 @@ import shutil import tempfile import os +import stat +from certfuzz.runners.errors import RunnerNotFoundError class Test(unittest.TestCase): @@ -45,18 +47,39 @@ def test_quiet_flag(self): with zr: self.assertFalse('--quiet' in zr._zzuf_args) + def test_find_zzuf(self): + (fd, fname) = tempfile.mkstemp(prefix='zzufrun_test_', dir=self.tmpdir) + os.close(fd) + os.remove(fname) + self.assertFalse(os.path.exists(fname)) + + for exe in ['/bin/ls', fname]: + zr = ZzufRunner(options={}, cmd_template=None, + fuzzed_file=None, workingdir_base=self.tmpdir) + self.assertEqual(zr._zzuf_basename, 'zzuf') + + _basename = os.path.basename(exe) + zr._zzuf_basename = _basename + + self.assertEqual(None, zr._zzuf_loc) + if os.path.exists(exe): + zr._find_zzuf() + self.assertEqual(exe, zr._zzuf_loc) + else: + self.assertRaises(RunnerNotFoundError, zr._find_zzuf) - def test_run(self): - options = {} - cmd_template = '' - for i in xrange(100): - with ZzufRunner(options, cmd_template, self.ff, self.tmpdir) as r: - print r.__dict__ - r._run() +# +# def test_run(self): +# options = {} +# cmd_template = '' +# +# for i in xrange(100): +# with ZzufRunner(options, cmd_template, self.ff, self.tmpdir) as r: +# print r.__dict__ +# r._run() +# self.assertTrue(False) - self.assertTrue(False) - pass if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From a55cd3dbc24cf3472c2d1a170ec1aee743bbb36c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 08:40:41 -0400 Subject: [PATCH 0509/1169] clean up unit test for zzufrun._run() --- src/certfuzz/test/runners/test_zzufrun.py | 32 +++++++++++++++-------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/test/runners/test_zzufrun.py b/src/certfuzz/test/runners/test_zzufrun.py index e9ee20d..6010191 100644 --- a/src/certfuzz/test/runners/test_zzufrun.py +++ b/src/certfuzz/test/runners/test_zzufrun.py @@ -68,17 +68,27 @@ def test_find_zzuf(self): else: self.assertRaises(RunnerNotFoundError, zr._find_zzuf) - -# -# def test_run(self): -# options = {} -# cmd_template = '' -# -# for i in xrange(100): -# with ZzufRunner(options, cmd_template, self.ff, self.tmpdir) as r: -# print r.__dict__ -# r._run() -# self.assertTrue(False) + def test_run(self): + options = {} + cmd_template = '' + + touch = '/usr/bin/touch' + if not os.path.exists(touch): + # bail out if touch doesn't exist + return + + for _ in xrange(100): + zr = ZzufRunner(options, cmd_template, self.ff, self.tmpdir) + fd, fname = tempfile.mkstemp(prefix='zzufrun_test_', dir=self.tmpdir) + os.close(fd) + os.remove(fname) + with zr: + # we're really just testing the structure of the _run method, + # not whether zzuf works + zr._zzuf_args = [touch, fname] + self.assertFalse(os.path.exists(fname)) + zr._run() + self.assertTrue(os.path.exists(fname)) if __name__ == "__main__": From 6b574627513ff30dea65b1e795d04ceb51ea469f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 09:38:49 -0400 Subject: [PATCH 0510/1169] remove STATE_TIMER --- src/certfuzz/campaign/campaign_linux.py | 3 --- src/certfuzz/iteration/iteration_linux.py | 3 --- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 5 ----- 3 files changed, 11 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 36c174a..5b2641b 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -19,7 +19,6 @@ from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.fuzztools import subprocess_helper as subp from certfuzz.fuzztools.process_killer import ProcessKiller -from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid @@ -197,8 +196,6 @@ def _do_interval(self): r = sf.rangefinder.next_item() qf = not self.first_chunk - logger.info(STATE_TIMER) - interval_limit = self.current_seed + self.seed_interval # start an iteration interval diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 6e1dab1..0c97e68 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -9,7 +9,6 @@ from certfuzz.crash.bff_crash import BffCrash from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.ppid_observer import check_ppid -from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.fuzztools.zzuf import Zzuf from certfuzz.fuzztools.zzuf import ZzufTestCase from certfuzz.fuzztools.zzuflog import ZzufLog @@ -62,7 +61,6 @@ def _pre_run(self): # do the fuzz cmdline = self.cfg.get_command(self.seedfile.path) - STATE_TIMER.enter_state('fuzzing') self.zzuf = Zzuf(self.cfg.local_dir, self.seednum, self.seednum, cmdline, @@ -78,7 +76,6 @@ def _run(self): self.zzuf.go() def _post_run(self): - STATE_TIMER.enter_state('checking_results') # we must have made it through this chunk without a crash # so go to next chunk diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index d09abcb..69c3e8c 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -17,7 +17,6 @@ from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file from certfuzz.fuzztools import filetools -from certfuzz.fuzztools.state_timer import STATE_TIMER from certfuzz.minimizer import MinimizerError, UnixMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.testcase_pipeline.errors import TestCasePipelineError @@ -53,7 +52,6 @@ def _verify(self, testcase): Confirms that a test case is interesting enough to pursue further analysis :param testcase: ''' - STATE_TIMER.enter_state('verify_testcase') TestCasePipelineBase._verify(self, testcase) # if you find more testcases, append them to self.tc_candidate_q @@ -102,8 +100,6 @@ def _post_minimize(self, testcase): # seedfile_set.add_file(crasherseed_path) def _pre_analyze(self, testcase): - STATE_TIMER.enter_state('analyze_testcase') - # get one last debugger output for the newly minimized file if testcase.pc_in_function: # change the debugger template @@ -166,7 +162,6 @@ def _minimize_to_string(self, testcase): self._minimize_generic(testcase, sftarget=False, confidence=0.9) def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): - STATE_TIMER.enter_state('minimize_testcase') try: with Minimizer(cfg=self.cfg, crash=testcase, From af4c1b5dcc0e23408863d53e88e11e45b51c2d67 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 09:39:49 -0400 Subject: [PATCH 0511/1169] start to integrate zzuf fuzzer and runner objects not expected to work yet --- src/certfuzz/iteration/iteration_linux.py | 54 +++++++++++++++-------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 0c97e68..080386d 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -9,11 +9,12 @@ from certfuzz.crash.bff_crash import BffCrash from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.ppid_observer import check_ppid -from certfuzz.fuzztools.zzuf import Zzuf from certfuzz.fuzztools.zzuf import ZzufTestCase from certfuzz.fuzztools.zzuflog import ZzufLog from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.testcase_pipeline.tc_pipeline_linux import LinuxTestCasePipeline +from certfuzz.runners.zzufrun import ZzufRunner +from certfuzz.fuzzers.zzuf import ZzufFuzzer logger = logging.getLogger(__name__) @@ -54,26 +55,43 @@ def __exit__(self, etype, value, traceback): self.cfg.clean_tmpdir() return handled + def _pre_fuzz(self): + fuzz_opts = self.config['fuzzer'] + self.fuzzer = ZzufFuzzer(self.sf, + self.working_dir, + self.rng_seed, + self.current_seed, + fuzz_opts) + def _fuzz(self): - pass - - def _pre_run(self): - # do the fuzz - cmdline = self.cfg.get_command(self.seedfile.path) - - self.zzuf = Zzuf(self.cfg.local_dir, self.seednum, - self.seednum, - cmdline, - self.seedfile.path, - self.cfg.zzuf_log_file, - self.cfg.copymode, - self.r.min, - self.r.max, - self.cfg.progtimeout, - self.quiet_flag) + with self.fuzzer: + self.fuzzer.fuzz() + self.fuzzed = True + + def _post_fuzz(self): + self.r = self.fuzzer.range + if self.r: + logger.info('Selected r: %s', self.r) + + fuzzed_file_full_path = self.fuzzer.output_file_path + # decide if we can minimize this case later + # do this here (and not sooner) because the fuzzer could + # decide at runtime whether it is or is not minimizable + self.minimizable = self.fuzzer.is_minimizable and self.config['runoptions']['minimize'] + # analysis is required in two cases: + # 1) runner is not defined (self.runner == None) + # 2) runner is defined, and detects crash (runner.saw_crash == True) + # this takes care of case 1 by default + analysis_needed = True def _run(self): - self.zzuf.go() + options = {} + cmd_template = '' + fuzzed_file = '' + workingdir_base = self.working_dir + runner = ZzufRunner(options, cmd_template, fuzzed_file, workingdir_base) + with runner: + runner.run() def _post_run(self): # we must have made it through this chunk without a crash From 91f94c89090983e6794d365dca52cc3c69182caa Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 10:30:18 -0400 Subject: [PATCH 0512/1169] refactor runner setup --- src/certfuzz/iteration/iteration_linux.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 080386d..b3edb75 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -84,22 +84,21 @@ def _post_fuzz(self): # this takes care of case 1 by default analysis_needed = True - def _run(self): + def _pre_run(self): options = {} cmd_template = '' fuzzed_file = '' workingdir_base = self.working_dir - runner = ZzufRunner(options, cmd_template, fuzzed_file, workingdir_base) - with runner: - runner.run() + self.runner = ZzufRunner(options, cmd_template, fuzzed_file, workingdir_base) - def _post_run(self): - # we must have made it through this chunk without a crash - # so go to next chunk + def _run(self): + with self.runner: + self.runner.run() + def _post_run(self): self.record_tries() - if not self.zzuf.saw_crash: + if not self.runner.saw_crash: logger.debug('No crash seen') return From 33981dd16034cb9fb43053dbf8495ae6d12611ce Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 10:30:43 -0400 Subject: [PATCH 0513/1169] refactor analysis_needed flag usage --- src/certfuzz/iteration/iteration_linux.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index b3edb75..e780f01 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -33,6 +33,13 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No self._zzuf_range = None self._zzuf_line = None + # analysis is required in two cases: + # 1) runner is not defined (self.runner == None) + # 2) runner is defined, and detects crash (runner.saw_crash == True) + # this takes care of case 1 by default + analysis_needed = True + + self._analysis_needed = True self.pipeline_options = { 'use_valgrind': self.cfg.use_valgrind, @@ -78,11 +85,6 @@ def _post_fuzz(self): # do this here (and not sooner) because the fuzzer could # decide at runtime whether it is or is not minimizable self.minimizable = self.fuzzer.is_minimizable and self.config['runoptions']['minimize'] - # analysis is required in two cases: - # 1) runner is not defined (self.runner == None) - # 2) runner is defined, and detects crash (runner.saw_crash == True) - # this takes care of case 1 by default - analysis_needed = True def _pre_run(self): options = {} @@ -109,9 +111,9 @@ def _post_run(self): # Don't generate cases for killed process or out-of-memory # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will # report the exit code in its output log. The exit code is 128 + the signal number. - analysis_needed = zzuf_log.crash_logged(self.cfg.copymode) + self._analysis_needed = zzuf_log.crash_logged(self.cfg.copymode) - if not analysis_needed: + if not self.analysis_needed: return # store a few things for use downstream From ce03d9c7ff371bee00593a72acbf6e5490e202fd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 12:44:55 -0400 Subject: [PATCH 0514/1169] clean up --- src/certfuzz/iteration/iteration_base3.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 3b629d2..54c0611 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -135,13 +135,14 @@ def record_tries(self): def process_testcases(self): # hand it off to our pipeline class - with self.tcpipeline_cls(testcases=self.testcases, - uniq_func=self.uniq_func, - cfg=self.cfg, - options=self.pipeline_options, - outdir=self.outdir, - workdirbase=self.working_dir) as pipeline: - pipeline.go() + pipeline = self.tcpipeline_cls(testcases=self.testcases, + uniq_func=self.uniq_func, + cfg=self.cfg, + options=self.pipeline_options, + outdir=self.outdir, + workdirbase=self.working_dir) + with pipeline: + pipeline.go() def go(self): logger.debug('go') From 516f29eaebf8ab4f3f859c4ef89694ec12bdce3e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 13:04:48 -0400 Subject: [PATCH 0515/1169] rename self.fuzzed to self.output --- src/certfuzz/fuzzers/bitmut.py | 2 +- src/certfuzz/fuzzers/bytemut.py | 2 +- src/certfuzz/fuzzers/drop.py | 4 +-- src/certfuzz/fuzzers/fuzzer_base.py | 26 +++++++++---------- src/certfuzz/fuzzers/insert.py | 4 +-- src/certfuzz/fuzzers/swap.py | 2 +- src/certfuzz/fuzzers/truncate.py | 6 ++--- src/certfuzz/fuzzers/wave.py | 2 +- src/certfuzz/fuzzers/zzuf.py | 4 +-- src/certfuzz/test/fuzzers/test_bitmut.py | 4 +-- src/certfuzz/test/fuzzers/test_bytemut.py | 6 ++--- src/certfuzz/test/fuzzers/test_crlfmut.py | 6 ++--- src/certfuzz/test/fuzzers/test_crmut.py | 4 +-- src/certfuzz/test/fuzzers/test_drop.py | 6 ++--- src/certfuzz/test/fuzzers/test_fuzzer_base.py | 12 ++++----- src/certfuzz/test/fuzzers/test_insert.py | 6 ++--- src/certfuzz/test/fuzzers/test_swap.py | 10 +++---- src/certfuzz/test/fuzzers/test_truncate.py | 2 +- src/certfuzz/test/fuzzers/test_wave.py | 2 +- src/certfuzz/test/fuzzers/test_zzuf.py | 6 ++--- 20 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/certfuzz/fuzzers/bitmut.py b/src/certfuzz/fuzzers/bitmut.py index 745d5a5..319fd5e 100644 --- a/src/certfuzz/fuzzers/bitmut.py +++ b/src/certfuzz/fuzzers/bitmut.py @@ -74,6 +74,6 @@ def _fuzz(self): for idx, val in enumerate(self.input): self.input[idx] = mask[idx] ^ val - self.fuzzed = self.input + self.output = self.input _fuzzer_class = BitMutFuzzer diff --git a/src/certfuzz/fuzzers/bytemut.py b/src/certfuzz/fuzzers/bytemut.py index 25abc60..41640ec 100644 --- a/src/certfuzz/fuzzers/bytemut.py +++ b/src/certfuzz/fuzzers/bytemut.py @@ -64,7 +64,7 @@ def _fuzz(self): self.range = self.sf.rangefinder.next_item() range_list = self.options.get('range_list') - self.fuzzed = fuzz(fuzz_input=self.input, + self.output = fuzz(fuzz_input=self.input, seed_val=self.rng_seed, jump_idx=self.iteration, ratio_min=self.range.min, diff --git a/src/certfuzz/fuzzers/drop.py b/src/certfuzz/fuzzers/drop.py index b345254..2db91c5 100644 --- a/src/certfuzz/fuzzers/drop.py +++ b/src/certfuzz/fuzzers/drop.py @@ -16,7 +16,7 @@ class DropFuzzer(Fuzzer): ''' def _fuzz(self): ''' - Drop individual bytes of input and put output in self.fuzzed + Drop individual bytes of input and put output in self.output ''' # TODO: add range list support to drop fuzzer @@ -39,6 +39,6 @@ def _fuzz(self): logger.debug('%s - dropped byte 0x%02x', self.sf.basename, byte_pos) - self.fuzzed = self.input + self.output = self.input _fuzzer_class = DropFuzzer diff --git a/src/certfuzz/fuzzers/fuzzer_base.py b/src/certfuzz/fuzzers/fuzzer_base.py index 71815e4..19f04b3 100644 --- a/src/certfuzz/fuzzers/fuzzer_base.py +++ b/src/certfuzz/fuzzers/fuzzer_base.py @@ -74,7 +74,7 @@ def __init__(self, seedfile_obj, outdir_base, rng_seed, iteration, options): self.output_file_path = os.path.join(self.tmpdir, self.basename_fuzzed) self.input = None - self.fuzzed = None + self.output = None self.fuzzed_changes_input = True # Not all fuzzers use rangefinder. Default to None and # set it in child classes for those that do @@ -100,13 +100,13 @@ def write_fuzzed(self, outdir=None): else: outfile = self.output_file_path - if self.fuzzed: - write_file(self.fuzzed, outfile) + if self.output: + write_file(self.output, outfile) self.output_file_path = outfile return os.path.exists(outfile) def fuzz(self): - if not self.fuzzed: + if not self.output: self._prefuzz() self._fuzz() self._postfuzz() @@ -123,7 +123,7 @@ def fuzz(self): # # # throw an exception if for some reason we didn't fuzz the input # # some fuzzers don't materially alter the file every time, e.g., swap -# if self.input == self.fuzzed: +# if self.input == self.output: # raise FuzzerInputMatchesOutputError('Fuzz failed: input matches output') def _prefuzz(self): @@ -143,11 +143,11 @@ def _postfuzz(self): def _fuzz(self): ''' Override this method to implement your fuzzer. The seed file contents - are in self.input. Put the output into self.fuzzed. + are in self.input. Put the output into self.output. ''' # disable fuzzed_changes_input since we're copying in -> out self.fuzzed_changes_input = False - self.fuzzed = self.input + self.output = self.input def _validate(self): ''' @@ -215,10 +215,10 @@ def _postfuzz(self): if self.options.get('fuzz_zip_container') or not self.sf.is_zip: return - '''rebuild the zip file and put it in self.fuzzed + '''rebuild the zip file and put it in self.output Note: We assume that the fuzzer has not changes the lengths of the archived files, otherwise we won't be able to properly - split self.fuzzed + split self.output ''' logger.debug('Creating in-memory zip with mutated contents.') @@ -230,17 +230,17 @@ def _postfuzz(self): source ''' for name, info in self.saved_arcinfo.iteritems(): - # write out fuzzed file + # write out output file if info[2] == 0 or info[2] == 8: # Python zipfile only supports compression types 0 and 8 compressiontype = info[2] else: logger.warning('Compression type %s is not supported. Overriding', info[2]) compressiontype = 8 - tempzip.writestr(name, str(self.fuzzed[info[0]:info[0] + info[1]]), + tempzip.writestr(name, str(self.output[info[0]:info[0] + info[1]]), compress_type=compressiontype) tempzip.close() - # get the byte string version of the archive and put in self.fuzzed - self.fuzzed = inmemzip.getvalue() + # get the byte string version of the archive and put in self.output + self.output = inmemzip.getvalue() inmemzip.close() diff --git a/src/certfuzz/fuzzers/insert.py b/src/certfuzz/fuzzers/insert.py index ecfb392..c3077a4 100644 --- a/src/certfuzz/fuzzers/insert.py +++ b/src/certfuzz/fuzzers/insert.py @@ -18,7 +18,7 @@ class InsertFuzzer(Fuzzer): ''' def _fuzz(self): ''' - Insert individual bytes of input and put output in self.fuzzed + Insert individual bytes of input and put output in self.output ''' # TODO: add range list support to insert fuzzer @@ -44,6 +44,6 @@ def _fuzz(self): logger.debug('%s - inserted byte 0x%02x at 0x%02x', self.sf.basename, byte_to_insert, byte_pos) - self.fuzzed = self.input + self.output = self.input _fuzzer_class = InsertFuzzer diff --git a/src/certfuzz/fuzzers/swap.py b/src/certfuzz/fuzzers/swap.py index eacca7d..3bc498d 100644 --- a/src/certfuzz/fuzzers/swap.py +++ b/src/certfuzz/fuzzers/swap.py @@ -28,6 +28,6 @@ def _fuzz(self): logger.debug('%s - swap bytes %d <-> %d', self.sf.basename, a, b) self.input[b], self.input[a] = self.input[a], self.input[b] - self.fuzzed = self.input + self.output = self.input _fuzzer_class = SwapFuzzer diff --git a/src/certfuzz/fuzzers/truncate.py b/src/certfuzz/fuzzers/truncate.py index 330b938..3de1a81 100644 --- a/src/certfuzz/fuzzers/truncate.py +++ b/src/certfuzz/fuzzers/truncate.py @@ -17,11 +17,11 @@ class TruncateFuzzer(Fuzzer): This fuzzer module iterates through an input file, dropping an additional byte from the file each time. (The file gets shorter.) - @raise FuzzerExhaustedError: when the fuzzed file reaches zero length + @raise FuzzerExhaustedError: when the output file reaches zero length ''' def _fuzz(self): ''' - Drop individual bytes of input and put output in self.fuzzed + Drop individual bytes of input and put output in self.output ''' # self.tries starts at zero @@ -35,6 +35,6 @@ def _fuzz(self): logger.debug('%s - truncated %s bytes', self.sf.basename, byte_pos) - self.fuzzed = truncated + self.output = truncated _fuzzer_class = TruncateFuzzer diff --git a/src/certfuzz/fuzzers/wave.py b/src/certfuzz/fuzzers/wave.py index 2600ce3..ae09bc9 100644 --- a/src/certfuzz/fuzzers/wave.py +++ b/src/certfuzz/fuzzers/wave.py @@ -36,6 +36,6 @@ def _fuzz(self): logger.debug('%s - set byte 0x%02x to 0x%02x', self.sf.basename, q, r) - self.fuzzed = self.input + self.output = self.input _fuzzer_class = WaveFuzzer diff --git a/src/certfuzz/fuzzers/zzuf.py b/src/certfuzz/fuzzers/zzuf.py index d9395e8..644722d 100644 --- a/src/certfuzz/fuzzers/zzuf.py +++ b/src/certfuzz/fuzzers/zzuf.py @@ -13,7 +13,7 @@ class ZzufFuzzer(MinimizableFuzzer): ''' This fuzzer uses Sam Hocevar's zzuf to mangle self.input and puts the results into - self.fuzzed''' + self.output''' _zzuf_loc = None def __enter__(self): @@ -35,6 +35,6 @@ def _fuzz(self): ] p = subprocess.Popen(args=zzufargs, stdin=subprocess.PIPE, stdout=subprocess.PIPE) (stdoutdata, _stderrdata) = p.communicate(input=self.input) - self.fuzzed = stdoutdata + self.output = stdoutdata _fuzzer_class = ZzufFuzzer diff --git a/src/certfuzz/test/fuzzers/test_bitmut.py b/src/certfuzz/test/fuzzers/test_bitmut.py index a1a80a9..84c1f1c 100644 --- a/src/certfuzz/test/fuzzers/test_bitmut.py +++ b/src/certfuzz/test/fuzzers/test_bitmut.py @@ -44,8 +44,8 @@ def test_fuzz(self): f.iteration = i f._fuzz() # same length, different output - self.assertEqual(self.sf.len, len(f.fuzzed)) - self._fail_if_not_fuzzed(f.fuzzed) + self.assertEqual(self.sf.len, len(f.output)) + self._fail_if_not_fuzzed(f.output) # confirm ratio # self.assertAlmostEqual(f.ratio, f.fuzzed_bit_ratio(), 2) diff --git a/src/certfuzz/test/fuzzers/test_bytemut.py b/src/certfuzz/test/fuzzers/test_bytemut.py index 66d5db4..775d531 100644 --- a/src/certfuzz/test/fuzzers/test_bytemut.py +++ b/src/certfuzz/test/fuzzers/test_bytemut.py @@ -119,8 +119,8 @@ def test_bytemutfuzzer_fuzz(self): f.iteration = i f._fuzz() # same length, different output - self.assertEqual(self.sf.len, len(f.fuzzed)) - self._fail_if_not_fuzzed(f.fuzzed) + self.assertEqual(self.sf.len, len(f.output)) + self._fail_if_not_fuzzed(f.output) # confirm ratio # self.assertGreaterEqual(f.fuzzed_byte_ratio(), MockRange().min) # self.assertLessEqual(f.fuzzed_byte_ratio(), MockRange().max) @@ -138,7 +138,7 @@ def test_consistency(self): for _ in range(20): with ByteMutFuzzer(self.sf, self.outdir, x, x, self.options) as f: f._fuzz() - result = str(f.fuzzed) + result = str(f.output) if last_result: self.assertEqual(result, last_result) else: diff --git a/src/certfuzz/test/fuzzers/test_crlfmut.py b/src/certfuzz/test/fuzzers/test_crlfmut.py index f5af037..967f726 100644 --- a/src/certfuzz/test/fuzzers/test_crlfmut.py +++ b/src/certfuzz/test/fuzzers/test_crlfmut.py @@ -127,8 +127,8 @@ def test_nullmutfuzzer_fuzz(self): f.iteration = i f._fuzz() # same length, different output - self.assertEqual(self.sf.len, len(f.fuzzed)) - self._fail_if_not_fuzzed(f.fuzzed) + self.assertEqual(self.sf.len, len(f.output)) + self._fail_if_not_fuzzed(f.output) # confirm ratio # self.assertGreaterEqual(2 * f.fuzzed_byte_ratio() / self.chars_inserted, MockRange().min) # self.assertLessEqual(f.fuzzed_byte_ratio() / self.chars_inserted, MockRange().max) @@ -145,7 +145,7 @@ def test_consistency(self): for _ in range(20): with CRLFMutFuzzer(self.sf, self.outdir, x, x, self.options) as f: f._fuzz() - result = str(f.fuzzed) + result = str(f.output) if last_result: self.assertEqual(result, last_result) else: diff --git a/src/certfuzz/test/fuzzers/test_crmut.py b/src/certfuzz/test/fuzzers/test_crmut.py index 77e384e..704f5c3 100644 --- a/src/certfuzz/test/fuzzers/test_crmut.py +++ b/src/certfuzz/test/fuzzers/test_crmut.py @@ -126,7 +126,7 @@ def test_nullmutfuzzer_fuzz(self): f.iteration = i f._fuzz() # same length, different output - self.assertEqual(self.sf.len, len(f.fuzzed)) + self.assertEqual(self.sf.len, len(f.output)) self._fail_if_not_fuzzed(f.input) # confirm ratio # self.assertGreaterEqual(f.fuzzed_byte_ratio() / self.chars_inserted, MockRange().min) @@ -144,7 +144,7 @@ def test_consistency(self): for _ in range(20): with CRMutFuzzer(self.sf, self.outdir, x, x, self.options) as f: f._fuzz() - result = str(f.fuzzed) + result = str(f.output) if last_result: self.assertEqual(result, last_result) else: diff --git a/src/certfuzz/test/fuzzers/test_drop.py b/src/certfuzz/test/fuzzers/test_drop.py index 02eff41..604a50d 100644 --- a/src/certfuzz/test/fuzzers/test_drop.py +++ b/src/certfuzz/test/fuzzers/test_drop.py @@ -39,11 +39,11 @@ def test_fuzz_in_range(self): self.sf.tries = x with DropFuzzer(*self.args) as f: f._fuzz() - self.assertEqual(len(f.fuzzed), self.sf.len - 1) + self.assertEqual(len(f.output), self.sf.len - 1) if x > 0: - self.assertEqual(chr(f.fuzzed[x - 1]), self.sf.value[x]) + self.assertEqual(chr(f.output[x - 1]), self.sf.value[x]) if x < self.sf.len - 1: - self.assertEqual(chr(f.fuzzed[x]), self.sf.value[x + 1]) + self.assertEqual(chr(f.output[x]), self.sf.value[x + 1]) def test_fuzz_out_of_range(self): self.sf.tries = self.sf.len + 1 diff --git a/src/certfuzz/test/fuzzers/test_fuzzer_base.py b/src/certfuzz/test/fuzzers/test_fuzzer_base.py index 5c61c6f..4565b80 100644 --- a/src/certfuzz/test/fuzzers/test_fuzzer_base.py +++ b/src/certfuzz/test/fuzzers/test_fuzzer_base.py @@ -35,8 +35,8 @@ def test_read_input(self): def test_no_write_if_not_fuzzed(self): with Fuzzer(*self.args) as f: self.assertFalse(os.path.exists(f.output_file_path), f.output_file_path) - # if we haven't fuzzed, don't write - f.fuzzed = None + # if we haven't output, don't write + f.output = None f.write_fuzzed() self.assertFalse(os.path.exists(f.output_file_path)) @@ -45,14 +45,14 @@ def test_write_fuzzed(self): self.assertFalse(os.path.exists(f.output_file_path), f.output_file_path) - # if we have fuzzed, write - f.fuzzed = 'abcd' + # if we have output, write + f.output = 'abcd' f.write_fuzzed() self.assertTrue(os.path.exists(f.output_file_path)) - self.assertEqual(os.path.getsize(f.output_file_path), len(f.fuzzed)) + self.assertEqual(os.path.getsize(f.output_file_path), len(f.output)) with open(f.output_file_path, 'rb') as fd: written = fd.read() - self.assertEqual(written, f.fuzzed) + self.assertEqual(written, f.output) def test_minimizable_attribute(self): yes = MinimizableFuzzer(*self.args) diff --git a/src/certfuzz/test/fuzzers/test_insert.py b/src/certfuzz/test/fuzzers/test_insert.py index bac59e4..f14fe83 100644 --- a/src/certfuzz/test/fuzzers/test_insert.py +++ b/src/certfuzz/test/fuzzers/test_insert.py @@ -39,10 +39,10 @@ def test_fuzz_in_range(self): self.sf.tries = x with InsertFuzzer(*self.args) as f: f._fuzz() - self.assertEqual(len(f.fuzzed), self.sf.len + 1) - self.assertEqual(chr(f.fuzzed[x + 1]), self.sf.value[x]) + self.assertEqual(len(f.output), self.sf.len + 1) + self.assertEqual(chr(f.output[x + 1]), self.sf.value[x]) if x > 0: - self.assertEqual(chr(f.fuzzed[x - 1]), self.sf.value[x - 1]) + self.assertEqual(chr(f.output[x - 1]), self.sf.value[x - 1]) def test_fuzz_out_of_range(self): self.sf.tries = self.sf.len + 1 diff --git a/src/certfuzz/test/fuzzers/test_swap.py b/src/certfuzz/test/fuzzers/test_swap.py index 902085b..01762ab 100644 --- a/src/certfuzz/test/fuzzers/test_swap.py +++ b/src/certfuzz/test/fuzzers/test_swap.py @@ -29,25 +29,25 @@ def tearDown(self): def test_fuzz(self): self.sf.value = "ABCDE" with SwapFuzzer(*self.args) as f: - self.assertEqual(f.fuzzed, None) + self.assertEqual(f.output, None) self.sf.tries = 0 f._fuzz() - self.assertEqual(f.fuzzed, "BACDE") + self.assertEqual(f.output, "BACDE") self.sf.value = "ABCDE" with SwapFuzzer(*self.args) as f: - self.assertEqual(f.fuzzed, None) + self.assertEqual(f.output, None) self.sf.tries = 1 self.sf.value = "ABCDE" f._fuzz() - self.assertEqual(f.fuzzed, "ACBDE") + self.assertEqual(f.output, "ACBDE") self.sf.value = "ABCDE" with SwapFuzzer(*self.args) as f: self.sf.tries = 3 self.sf.value = "ABCDE" f._fuzz() - self.assertEqual(f.fuzzed, "ABCED") + self.assertEqual(f.output, "ABCED") def test_fuzz_out_of_range(self): self.sf.tries = len(self.sf.value) diff --git a/src/certfuzz/test/fuzzers/test_truncate.py b/src/certfuzz/test/fuzzers/test_truncate.py index e1d0442..31080c6 100644 --- a/src/certfuzz/test/fuzzers/test_truncate.py +++ b/src/certfuzz/test/fuzzers/test_truncate.py @@ -40,7 +40,7 @@ def test_fuzz_in_range(self): with TruncateFuzzer(*self.args) as f: f._fuzz() n_expected = self.sf.len - self.sf.tries - 1 - self.assertEqual(len(f.fuzzed), n_expected) + self.assertEqual(len(f.output), n_expected) def test_fuzz_out_of_range(self): self.sf.tries = self.sf.len + 1 diff --git a/src/certfuzz/test/fuzzers/test_wave.py b/src/certfuzz/test/fuzzers/test_wave.py index e93838d..51aa3ba 100644 --- a/src/certfuzz/test/fuzzers/test_wave.py +++ b/src/certfuzz/test/fuzzers/test_wave.py @@ -41,7 +41,7 @@ def test_fuzz_in_range(self): f._fuzz() pos = x / 256 # note integer math val = x % 256 - self.assertEqual(f.fuzzed[pos], val) + self.assertEqual(f.output[pos], val) def test_fuzz_out_of_range(self): self.sf.tries = self.sf.len * 256 + 1 diff --git a/src/certfuzz/test/fuzzers/test_zzuf.py b/src/certfuzz/test/fuzzers/test_zzuf.py index 2f0b941..31278d9 100644 --- a/src/certfuzz/test/fuzzers/test_zzuf.py +++ b/src/certfuzz/test/fuzzers/test_zzuf.py @@ -57,11 +57,11 @@ def test_fuzz(self): f.iteration = i f._fuzz() # same length, different output - self.assertEqual(self.sf.len, len(f.fuzzed)) - self._fail_if_not_fuzzed(f.fuzzed) + self.assertEqual(self.sf.len, len(f.output)) + self._fail_if_not_fuzzed(f.output) # check for no repeats - md5 = hashlib.md5(f.fuzzed).hexdigest() + md5 = hashlib.md5(f.output).hexdigest() if md5 in fuzzed_output_seen: self.fail('Fuzzer repeated output: %s' % md5) else: From e1232a50d5f0ed264779e7960c8bcedb54555f7e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 13:15:24 -0400 Subject: [PATCH 0516/1169] rename fuzzed attribute for clarity --- src/certfuzz/minimizer/minimizer_base.py | 32 +++++++++---------- .../test/minimizer/test_minimizer_base.py | 6 ++-- src/certfuzz/tools/linux/minimize.py | 4 +-- src/certfuzz/tools/windows/minimize.py | 4 +-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index a55dce1..c551d33 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -142,15 +142,15 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, # self.is_zipfile = self.check_zipfile(self.crash.fuzzedfile.path) - self.fuzzed = self._read_fuzzed() + self.fuzzed_content = self._read_fuzzed() self.seed = self._read_seed() # none of this will work if the files are of different size - if len(self.seed) != len(self.fuzzed): - raise MinimizerError('Minimizer requires seed and fuzzed to have the same length. %d != %d' % (len(self.seed), len(self.fuzzed))) + if len(self.seed) != len(self.fuzzed_content): + raise MinimizerError('Minimizer requires seed and fuzzed_content to have the same length. %d != %d' % (len(self.seed), len(self.fuzzed_content))) # initialize the hamming distance - self.start_distance = self.hd_func(self.seed, self.fuzzed) + self.start_distance = self.hd_func(self.seed, self.fuzzed_content) self.min_distance = self.start_distance # some programs crash differently depending on where the @@ -214,7 +214,7 @@ def __exit__(self, etype, value, traceback): def _read_fuzzed(self): ''' - returns the contents of the fuzzed file + returns the contents of the fuzzed_content file ''' return self.crash.fuzzedfile.read() @@ -226,9 +226,9 @@ def _read_seed(self): if self.seedfile_as_target: return self.crash.seedfile.read() elif self.preferx: - return self.minchar * len(self.fuzzed) + return self.minchar * len(self.fuzzed_content) else: - return text.metasploit_pattern_orig(len(self.fuzzed)) + return text.metasploit_pattern_orig(len(self.fuzzed_content)) def _logger_setup(self): dirname = os.path.dirname(self.minimizer_logfile) @@ -342,7 +342,7 @@ def _crash_builder(self): filetools.copy_file(self.tempfile, outfile) newcrash.fuzzedfile = BasicFile(outfile) - self.logger.debug('\tNew fuzzed file: %s %s', newcrash.fuzzedfile.path, newcrash.fuzzedfile.md5) + self.logger.debug('\tNew fuzzed_content file: %s %s', newcrash.fuzzedfile.path, newcrash.fuzzedfile.md5) # clear out the copied crash signature so that it will be regenerated newcrash.signature = None @@ -505,7 +505,7 @@ def _write_file(self): write_file(''.join(self.newfuzzed), self.tempfile) def go(self): - # start by copying the fuzzed file since as of now it's our best fit + # start by copying the fuzzed_content file since as of now it's our best fit filetools.copy_file(self.crash.fuzzedfile.path, self.outputfile) # replace the fuzzedfile object in crash with the minimized copy @@ -602,7 +602,7 @@ def go(self): # we have a better match, write it to a file if not len(self.newfuzzed): - raise MinimizerError('New fuzzed content is empty.') + raise MinimizerError('New fuzzed_content content is empty.') self._write_file() @@ -610,10 +610,10 @@ def go(self): # record the result # 1. copy the tempfile filetools.best_effort_move(self.tempfile, self.outputfile) - # 2. replace the fuzzed file in the crasher with the current one + # 2. replace the fuzzed_content file in the crasher with the current one self.crash.fuzzedfile = BasicFile(self.outputfile) - # 3. replace the current fuzzed with newfuzzed - self.fuzzed = self.newfuzzed + # 3. replace the current fuzzed_content with newfuzzed + self.fuzzed_content = self.newfuzzed self.min_distance = self.newfuzzed_hd got_hit = True @@ -654,8 +654,8 @@ def go(self): self.logger.info('We were looking for [%s] ...', self._crash_hashes_string()) for (md5, count) in self.crash_sigs_found.items(): self.logger.info('\t...and found %s\t%d times', md5, count) - if self.fuzzed: - self.bytemap = hamming.bytemap(self.seed, self.fuzzed) + if self.fuzzed_content: + self.bytemap = hamming.bytemap(self.seed, self.fuzzed_content) self.logger.info('Bytemap: %s', self.bytemap) def get_mask(self): @@ -676,7 +676,7 @@ def swap_bytes(self): # or that we didn't drop any bytes at all # so keep trying until both are true while not (0 < newfuzzed_hd < self.min_distance): - newfuzzed, newfuzzed_hd = self.swap_func(self.seed, self.fuzzed) + newfuzzed, newfuzzed_hd = self.swap_func(self.seed, self.fuzzed_content) # we know our hd is > 0 and < what it was when we started self.newfuzzed = newfuzzed diff --git a/src/certfuzz/test/minimizer/test_minimizer_base.py b/src/certfuzz/test/minimizer/test_minimizer_base.py index 52478e3..8e17455 100644 --- a/src/certfuzz/test/minimizer/test_minimizer_base.py +++ b/src/certfuzz/test/minimizer/test_minimizer_base.py @@ -91,8 +91,8 @@ def test_print_intermediate_log(self): def test_set_discard_chance(self): self.m.seed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - self.m.fuzzed = "abcdefghijklmnopqrstuvwxyz" - self.m.min_distance = hamming.bytewise_hd(self.m.seed, self.m.fuzzed) + self.m.fuzzed_content = "abcdefghijklmnopqrstuvwxyz" + self.m.min_distance = hamming.bytewise_hd(self.m.seed, self.m.fuzzed_content) self.assertEqual(self.m.min_distance, 26) for tsg in xrange(1, 20): @@ -110,7 +110,7 @@ def test_swap_bytes(self): for dc in (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9): self.m.discard_chance = dc self.m.seed = seed - self.m.fuzzed = fuzzed + self.m.fuzzed_content = fuzzed self.m.min_distance = 26 self.m.swap_func = self.m.bytewise_swap2 self.m.swap_bytes() diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index f14d855..67f853f 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -158,7 +158,7 @@ def main(): if options.stringmode: logger.debug('x character substitution') - length = len(minimize.fuzzed) + length = len(minimize.fuzzed_content) if options.prefer_x_target: #We minimized to 'x', so we attempt to get metasploit as a freebie targetstring = list(text.metasploit_pattern_orig(length)) @@ -168,7 +168,7 @@ def main(): targetstring = list('x' * length) filename_modifier = '-x' - fuzzed = list(minimize.fuzzed) + fuzzed = list(minimize.fuzzed_content) for idx in minimize.bytemap: logger.debug('Swapping index %d', idx) targetstring[idx] = fuzzed[idx] diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index b2b863a..95f6be1 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -177,7 +177,7 @@ def main(): if options.stringmode: logger.debug('x character substitution') - length = len(minimize.fuzzed) + length = len(minimize.fuzzed_content) if options.prefer_x_target: # We minimized to 'x', so we attempt to get metasploit as a freebie targetstring = list(text.metasploit_pattern_orig(length)) @@ -187,7 +187,7 @@ def main(): targetstring = list('x' * length) filename_modifier = '-x' - fuzzed = list(minimize.fuzzed) + fuzzed = list(minimize.fuzzed_content) for idx in minimize.bytemap: logger.debug('Swapping index %d', idx) targetstring[idx] = fuzzed[idx] From 522cdcdd9154d85b59af01c01dd58d029ce913a1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 13:24:14 -0400 Subject: [PATCH 0517/1169] eliminate superfluous 'fuzzed' flag --- src/certfuzz/iteration/iteration_linux.py | 1 - src/certfuzz/iteration/iteration_windows.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index e780f01..80a76de 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -73,7 +73,6 @@ def _pre_fuzz(self): def _fuzz(self): with self.fuzzer: self.fuzzer.fuzz() - self.fuzzed = True def _post_fuzz(self): self.r = self.fuzzer.range diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 8b73175..df55533 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -46,7 +46,6 @@ def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, self.debug = debug # TODO: do we use keep_uniq_faddr at all? self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] - self.fuzzed = False self.cmd_template = string.Template(cmd_template) @@ -136,7 +135,6 @@ def _fuzz(self): self.seednum, fuzz_opts) as fuzzer: fuzzer.fuzz() - self.fuzzed = True self.r = fuzzer.range if self.r: logger.info('Selected r: %s', self.r) From d685c29c9f6375a678663c30952e660003098845 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 13:37:51 -0400 Subject: [PATCH 0518/1169] fix indentation on comments --- src/certfuzz/iteration/iteration_linux.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 80a76de..05d3956 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -80,9 +80,9 @@ def _post_fuzz(self): logger.info('Selected r: %s', self.r) fuzzed_file_full_path = self.fuzzer.output_file_path - # decide if we can minimize this case later - # do this here (and not sooner) because the fuzzer could - # decide at runtime whether it is or is not minimizable + # decide if we can minimize this case later + # do this here (and not sooner) because the fuzzer could + # decide at runtime whether it is or is not minimizable self.minimizable = self.fuzzer.is_minimizable and self.config['runoptions']['minimize'] def _pre_run(self): From c54294b588e54835a0eb6636edcc95b3b0d73fce Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 28 Oct 2014 13:39:13 -0400 Subject: [PATCH 0519/1169] refactor common _fuzz() method to base class --- src/certfuzz/iteration/iteration_base3.py | 4 ++-- src/certfuzz/iteration/iteration_linux.py | 4 ---- src/certfuzz/iteration/iteration_windows.py | 21 +++++++++------------ 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 54c0611..9f01810 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -86,9 +86,9 @@ def __exit__(self, etype, value, traceback): def _pre_fuzz(self): pass - @abc.abstractmethod def _fuzz(self): - pass + with self.fuzzer: + self.fuzzer.fuzz() def _post_fuzz(self): pass diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 05d3956..1a38cd2 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -70,10 +70,6 @@ def _pre_fuzz(self): self.current_seed, fuzz_opts) - def _fuzz(self): - with self.fuzzer: - self.fuzzer.fuzz() - def _post_fuzz(self): self.r = self.fuzzer.range if self.r: diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index df55533..f036fe8 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -125,27 +125,24 @@ def _tidy(self): # wipe them out, all of them TmpReaper().clean_tmp() - def _fuzz(self): + def _pre_fuzz(self): # generated test case (fuzzed input) logger.info('...fuzzing') fuzz_opts = self.cfg['fuzzer'] - with self.fuzzer_cls(self.seedfile, + self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.rng_seed, - self.seednum, - fuzz_opts) as fuzzer: - fuzzer.fuzz() - self.r = fuzzer.range - if self.r: - logger.info('Selected r: %s', self.r) + self.seednum, fuzz_opts) + + def _post_fuzz(self): + self.r = self.fuzzer.range + if self.r: + logger.info('Selected r: %s', self.r) # decide if we can minimize this case later # do this here (and not sooner) because the fuzzer_cls could # decide at runtime whether it is or is not minimizable - self.pipeline_options['minimizable'] = fuzzer.is_minimizable and self.cfg['runoptions']['minimize'] - - # hang on to this fuzzer instance, we use it in _run - self.fuzzer = fuzzer + self.pipeline_options['minimizable'] = self.fuzzer.is_minimizable and self.cfg['runoptions']['minimize'] def _run(self): # analysis is required in two cases: From 0831b5dd5c29435cfc8014be1813cbb16eb2f493 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 29 Oct 2014 08:47:54 -0400 Subject: [PATCH 0520/1169] continue integrating zzuf runner setup from iteration class --- src/certfuzz/iteration/iteration_linux.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 1a38cd2..6233855 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -82,9 +82,9 @@ def _post_fuzz(self): self.minimizable = self.fuzzer.is_minimizable and self.config['runoptions']['minimize'] def _pre_run(self): - options = {} + options = self.options cmd_template = '' - fuzzed_file = '' + fuzzed_file = self.fuzzer.output_file_path workingdir_base = self.working_dir self.runner = ZzufRunner(options, cmd_template, fuzzed_file, workingdir_base) From 2155c54757f29ddfa5355b3195b258ea92ad9002 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 29 Oct 2014 09:07:39 -0400 Subject: [PATCH 0521/1169] add empty unit tests --- src/certfuzz/test/helpers/test_coroutine.py | 22 +++++++++++++++++++ .../test/testcase_pipeline/__init__.py | 0 .../test_tc_pipeline_base.py | 22 +++++++++++++++++++ .../test_tc_pipeline_linux.py | 22 +++++++++++++++++++ .../test_tc_pipeline_windows.py | 22 +++++++++++++++++++ 5 files changed, 88 insertions(+) create mode 100644 src/certfuzz/test/helpers/test_coroutine.py create mode 100644 src/certfuzz/test/testcase_pipeline/__init__.py create mode 100644 src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py create mode 100644 src/certfuzz/test/testcase_pipeline/test_tc_pipeline_linux.py create mode 100644 src/certfuzz/test/testcase_pipeline/test_tc_pipeline_windows.py diff --git a/src/certfuzz/test/helpers/test_coroutine.py b/src/certfuzz/test/helpers/test_coroutine.py new file mode 100644 index 0000000..69e73a9 --- /dev/null +++ b/src/certfuzz/test/helpers/test_coroutine.py @@ -0,0 +1,22 @@ +''' +Created on Oct 29, 2014 + +@organization: cert.org +''' +import unittest +import certfuzz.helpers.coroutine + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/testcase_pipeline/__init__.py b/src/certfuzz/test/testcase_pipeline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py new file mode 100644 index 0000000..77aa453 --- /dev/null +++ b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py @@ -0,0 +1,22 @@ +''' +Created on Oct 29, 2014 + +@organization: cert.org +''' +import unittest +import certfuzz.testcase_pipeline.tc_pipeline_base + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_linux.py b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_linux.py new file mode 100644 index 0000000..3ffc6cf --- /dev/null +++ b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_linux.py @@ -0,0 +1,22 @@ +''' +Created on Oct 29, 2014 + +@organization: cert.org +''' +import unittest +import certfuzz.testcase_pipeline.tc_pipeline_linux + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_windows.py b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_windows.py new file mode 100644 index 0000000..f9f41a7 --- /dev/null +++ b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_windows.py @@ -0,0 +1,22 @@ +''' +Created on Oct 29, 2014 + +@organization: cert.org +''' +import unittest +import certfuzz.testcase_pipeline.tc_pipeline_windows + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From cfe015eb07e22ba201c345af7ef21fd6bfb8e134 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 29 Oct 2014 10:22:34 -0400 Subject: [PATCH 0522/1169] add unit test --- src/certfuzz/test/helpers/test_coroutine.py | 25 +++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/test/helpers/test_coroutine.py b/src/certfuzz/test/helpers/test_coroutine.py index 69e73a9..0ec3e21 100644 --- a/src/certfuzz/test/helpers/test_coroutine.py +++ b/src/certfuzz/test/helpers/test_coroutine.py @@ -6,6 +6,7 @@ import unittest import certfuzz.helpers.coroutine + class Test(unittest.TestCase): def setUp(self): @@ -14,8 +15,28 @@ def setUp(self): def tearDown(self): pass - def testName(self): - pass + def test_coroutine(self): + results = [] + + # set up a simple function that multiplies + # the values sent to it and appends it to + # a list + @certfuzz.helpers.coroutine.coroutine + def func(results): + while True: + x = (yield) + results.append(2 * x) + + # set up the coroutine + c = func(results) + + # confirm that results has the right size and values + for i in xrange(100): + self.assertEqual(i, len(results)) + c.send(i) + self.assertEqual(i + 1, len(results)) + self.assertEqual(i * 2, results[-1]) + if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From 68c45ff9af4154452ee00596645f6c4851ff06e4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 29 Oct 2014 10:42:35 -0400 Subject: [PATCH 0523/1169] add unit test of abstract base class implementation --- .../test_tc_pipeline_base.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py index 77aa453..5286262 100644 --- a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py +++ b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py @@ -6,6 +6,24 @@ import unittest import certfuzz.testcase_pipeline.tc_pipeline_base + +class TCPL_Impl(certfuzz.testcase_pipeline.tc_pipeline_base.TestCasePipelineBase): + def _setup_analyzers(self): + pass + + def _minimize(self): + pass + + def _verify(self): + pass + + def _analyze(self): + pass + + def _report(self): + pass + + class Test(unittest.TestCase): def setUp(self): @@ -14,8 +32,16 @@ def setUp(self): def tearDown(self): pass - def testName(self): - pass + def test_abstract_class(self): + cls = certfuzz.testcase_pipeline.tc_pipeline_base.TestCasePipelineBase + + # should fail since we haven't implemented the abc methods + self.assertRaises(TypeError, cls) + + try: + TCPL_Impl() + except TypeError as e: + self.fail('Dummy implementation class failed to instantiate: {}'.format(e)) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From 4c590c7addda09f38adbb2bc24bc5b72d0bccbbf Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 29 Oct 2014 11:24:07 -0400 Subject: [PATCH 0524/1169] add unit test for pipeline functions --- .../test_tc_pipeline_base.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py index 5286262..3dafbde 100644 --- a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py +++ b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py @@ -43,6 +43,35 @@ def test_abstract_class(self): except TypeError as e: self.fail('Dummy implementation class failed to instantiate: {}'.format(e)) + def test_pipeline_coroutines(self): + tcpl = TCPL_Impl() + results = [] + + def func(tc): + results.append(tc) + + tcpl._verify = func + tcpl._minimize = func + tcpl._analyze = func + tcpl._report = func + funcs2check = [tcpl.verify, + tcpl.minimize, + tcpl.analyze, + tcpl.report, + ] + + # if this test works, results will get [0,1,2,...] + # because each of the above functions will call + # its corresponding _function which will append + # i to results + for i, plfunc in enumerate(funcs2check): + #setup the pipeline coroutine + testfunc = plfunc() + self.assertEqual(i, len(results)) + testfunc.send(i) + self.assertEqual(i + 1, len(results)) + self.assertEqual(i, results[-1]) + if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 1c3e5ef76352f3a4f3113eb263e9886bdce819d3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 29 Oct 2014 13:30:06 -0400 Subject: [PATCH 0525/1169] add test case for _analyze() --- .../test_tc_pipeline_base.py | 71 +++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py index 3dafbde..f969662 100644 --- a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py +++ b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py @@ -5,32 +5,36 @@ ''' import unittest import certfuzz.testcase_pipeline.tc_pipeline_base +import tempfile +import shutil +import os +import certfuzz.testcase_pipeline.tc_pipeline_base class TCPL_Impl(certfuzz.testcase_pipeline.tc_pipeline_base.TestCasePipelineBase): def _setup_analyzers(self): pass - def _minimize(self): + def _minimize(self, testcase): pass - def _verify(self): + def _verify(self, testcase): pass - def _analyze(self): + def _analyze(self, testcase): pass - def _report(self): + def _report(self, testcase): pass class Test(unittest.TestCase): def setUp(self): - pass + self.tmpdir = tempfile.mkdtemp() def tearDown(self): - pass + shutil.rmtree(self.tmpdir) def test_abstract_class(self): cls = certfuzz.testcase_pipeline.tc_pipeline_base.TestCasePipelineBase @@ -72,6 +76,61 @@ def func(tc): self.assertEqual(i + 1, len(results)) self.assertEqual(i, results[-1]) + def test_copy_files(self): + tcpl = TCPL_Impl() + + class MockTestCase(object): + result_dir = tempfile.mkdtemp(dir=self.tmpdir) + + tc = MockTestCase() + fd, fname = tempfile.mkstemp(dir=tc.result_dir) + os.write(fd, fname) + os.close(fd) + + self.assertEqual([os.path.basename(fname)], + os.listdir(tc.result_dir)) + tcpl.outdir = tempfile.mkdtemp(dir=self.tmpdir) + + self.assertEqual([], + os.listdir(tcpl.outdir)) + tcpl._copy_files(tc) + self.assertEqual([os.path.basename(fname)], + os.listdir(tc.result_dir)) + + def test_analyze(self): + class TCPL_Impl2(TCPL_Impl): + def _analyze(self, testcase): + certfuzz.testcase_pipeline.tc_pipeline_base.TestCasePipelineBase._analyze(self, testcase) + + tcpl = TCPL_Impl2() + + class MockTestCase(object): + pass + + analyzer_count = [] + + class MockAnalyzer(object): + def __init__(self, *args, **kwargs): + pass + + def go(self): + analyzer_count.append(1) + + touch_watchdog_call_count = [] + + def inc_cc(): + touch_watchdog_call_count.append(1) + + # monkey patch touch_watchdog_file + certfuzz.testcase_pipeline.tc_pipeline_base.touch_watchdog_file = inc_cc + + tc = MockTestCase() + tcpl.analyzer_classes = [MockAnalyzer for _ in xrange(5)] + tcpl._analyze(tc) + self.assertEqual(5, sum(analyzer_count)) + self.assertEqual(5, sum(touch_watchdog_call_count)) + + if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 8e1d1e601256577a6c306094630faa70f8d55ec4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 29 Oct 2014 15:43:39 -0400 Subject: [PATCH 0526/1169] refactor config file reading into its own method --- src/certfuzz/campaign/campaign_base.py | 7 +++++++ src/certfuzz/campaign/campaign_linux.py | 9 ++++++--- src/certfuzz/campaign/campaign_windows.py | 15 ++++++++++----- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 74389fe..6814364 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -67,6 +67,7 @@ def __init__(self, config_file, result_dir=None, debug=False): logger.debug('initialize %s', self.__class__.__name__) # Read the cfg file self.config_file = config_file + self.config = None self.cached_state_file = None self.debug = debug self._version = __version__ @@ -96,6 +97,12 @@ def __init__(self, config_file, result_dir=None, debug=False): if result_dir: self.outdir_base = os.path.abspath(result_dir) + self._read_config_file(self.config_file) + + @abc.abstractmethod + def _read_config_file(self): + logger.info('Reading config from %s', self.config_file) + def _common_init(self): ''' Initializes some additional properties common to all platforms diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 5b2641b..30aaec6 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -56,12 +56,11 @@ class LinuxCampaign(CampaignBase): ''' Extends CampaignBase to add linux-specific features. ''' + def __init__(self, config_file=None, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) - # read configs - logger.info('Reading config from %s', self.config_file) - self.config = cfg_helper.read_config_options(self.config_file) + self._read_config_file() # pull stuff out of configs self.campaign_id = self.config.campaign_id @@ -84,6 +83,10 @@ def __init__(self, config_file=None, result_dir=None, debug=False): # give up if prog is a script self._check_for_script() + def _read_config_file(self): + CampaignBase._read_config_file(self) + self.config = cfg_helper.read_config_options(self.config_file) + def _pre_enter(self): self._start_process_killer() self._set_unbuffered_stdout() diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 386e247..44a329e 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -30,11 +30,7 @@ def __init__(self, config_file, result_dir=None, debug=False): self.gui_app = False - #read configs - logger.info('Reading config from %s', self.config_file) - cfgobj = Config(self.config_file) - self.config = cfgobj.config - self.configdate = cfgobj.configdate + self._read_config_file() # pull stuff out of configs self.campaign_id = self.config['campaign']['id'] @@ -69,6 +65,15 @@ def __init__(self, config_file, result_dir=None, debug=False): # must occur after work_dir_base, outdir_base, and campaign_id are set self._common_init() + def _read_config_file(self): + CampaignBase._read_config_file(self) + + #read configs + cfgobj = Config(self.config_file) + self.config = cfgobj.config + self.configdate = cfgobj.configdate + + def __getstate__(self): state = self.__dict__.copy() From b196912565eb4b9c49315a2dbb6d7cea2094887a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 30 Oct 2014 13:50:51 -0400 Subject: [PATCH 0527/1169] _read_config_file is now called from the base class --- src/certfuzz/campaign/campaign_linux.py | 2 -- src/certfuzz/campaign/campaign_windows.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 30aaec6..6e81aa9 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -60,8 +60,6 @@ class LinuxCampaign(CampaignBase): def __init__(self, config_file=None, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) - self._read_config_file() - # pull stuff out of configs self.campaign_id = self.config.campaign_id self.current_seed = self.config.start_seed diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 44a329e..a74ecbd 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -30,8 +30,6 @@ def __init__(self, config_file, result_dir=None, debug=False): self.gui_app = False - self._read_config_file() - # pull stuff out of configs self.campaign_id = self.config['campaign']['id'] From b7b2ef02affea8a89f8872b65e4fe6f6d4e8c78d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 30 Oct 2014 14:00:32 -0400 Subject: [PATCH 0528/1169] rename config base class --- src/certfuzz/campaign/campaign_linux.py | 4 ++-- src/certfuzz/campaign/config/__init__.py | 2 -- src/certfuzz/campaign/config/config_base.py | 4 ++-- src/certfuzz/campaign/config/config_linux.py | 18 ++++++++++++++++++ src/certfuzz/campaign/config/config_windows.py | 2 +- .../test/campaign/config/test_config_base.py | 10 +++++----- src/certfuzz/tools/windows/clean_windows.py | 6 +++--- 7 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/certfuzz/campaign/config/config_linux.py diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 6e81aa9..245c6e8 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -11,7 +11,6 @@ import time from certfuzz.campaign.campaign_base import CampaignBase -from certfuzz.campaign.config import bff_config as cfg_helper from certfuzz.campaign.errors import CampaignScriptError from certfuzz.debuggers import crashwrangler #@UnusedImport from certfuzz.debuggers import gdb #@UnusedImport @@ -23,6 +22,7 @@ from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.iteration_linux import LinuxIteration +from certfuzz.campaign.config.bff_config import read_config_options logger = logging.getLogger(__name__) @@ -83,7 +83,7 @@ def __init__(self, config_file=None, result_dir=None, debug=False): def _read_config_file(self): CampaignBase._read_config_file(self) - self.config = cfg_helper.read_config_options(self.config_file) + self.config = read_config_options(self.config_file) def _pre_enter(self): self._start_process_killer() diff --git a/src/certfuzz/campaign/config/__init__.py b/src/certfuzz/campaign/config/__init__.py index e2f7882..e69de29 100644 --- a/src/certfuzz/campaign/config/__init__.py +++ b/src/certfuzz/campaign/config/__init__.py @@ -1,2 +0,0 @@ -from .config_base import parse_yaml -from .config_base import Config diff --git a/src/certfuzz/campaign/config/config_base.py b/src/certfuzz/campaign/config/config_base.py index b352bc9..92120a0 100644 --- a/src/certfuzz/campaign/config/config_base.py +++ b/src/certfuzz/campaign/config/config_base.py @@ -17,7 +17,7 @@ def parse_yaml(yaml_file): return yaml.load(open(yaml_file, 'r')) -class Config(object): +class ConfigBase(object): ''' If you are inheriting this class, add validation methods to self.validations to have them run automatically at initialization. @@ -48,7 +48,7 @@ def load(self): def _verify_load(self): if self.config is None: - raise ConfigError('Config load failed for {}'.format(self.file)) + raise ConfigError('ConfigBase load failed for {}'.format(self.file)) def validate(self): for validation in self.validations: diff --git a/src/certfuzz/campaign/config/config_linux.py b/src/certfuzz/campaign/config/config_linux.py new file mode 100644 index 0000000..dbb887b --- /dev/null +++ b/src/certfuzz/campaign/config/config_linux.py @@ -0,0 +1,18 @@ +''' +Created on Oct 30, 2014 + +@organization: cert.org +''' +from certfuzz.campaign.config.config_base import ConfigBase + + +class LinuxConfig(ConfigBase): + ''' + classdocs + ''' + + + def __init__(self, params): + ''' + Constructor + ''' diff --git a/src/certfuzz/campaign/config/config_windows.py b/src/certfuzz/campaign/config/config_windows.py index 6f9884e..a8ebe8f 100644 --- a/src/certfuzz/campaign/config/config_windows.py +++ b/src/certfuzz/campaign/config/config_windows.py @@ -7,7 +7,7 @@ import shlex from string import Template -from certfuzz.campaign.config.config_base import Config as ConfigBase +from certfuzz.campaign.config.config_base import ConfigBase from certfuzz.campaign.config.errors import ConfigError from certfuzz.helpers import quoted diff --git a/src/certfuzz/test/campaign/config/test_config_base.py b/src/certfuzz/test/campaign/config/test_config_base.py index f166e28..5c00201 100644 --- a/src/certfuzz/test/campaign/config/test_config_base.py +++ b/src/certfuzz/test/campaign/config/test_config_base.py @@ -47,13 +47,13 @@ def test_parse_yaml(self): def test_config_init(self): thing, f = self._write_yaml() - c = config.Config(f) + c = config.ConfigBase(f) self.assertEqual(f, c.file) self.assertEqual(thing, c.config) def test_validate(self): dummy, f = self._write_yaml() - c = config.Config(f) + c = config.ConfigBase(f) # add some validations c.validations.append(_counter) c.validations.append(_counter) @@ -67,13 +67,13 @@ def test_validate(self): def test_init_fails_if_load_fails(self): dummy, f = self._write_yaml() os.remove(f) - self.assertRaises(ConfigError, config.Config, f) + self.assertRaises(ConfigError, config.ConfigBase, f) def test_verify_load(self): # write another yaml file _thing, f = self._write_yaml() # sub the new file name - c = config.Config(f) + c = config.ConfigBase(f) c.config = None self.assertRaises(ConfigError, c._verify_load) @@ -81,7 +81,7 @@ def test_load(self): # write another yaml file thing, f = self._write_yaml() # sub the new file name - c = config.Config(f) + c = config.ConfigBase(f) # we should get the thing back again self.assertEqual(thing, c.config) diff --git a/src/certfuzz/tools/windows/clean_windows.py b/src/certfuzz/tools/windows/clean_windows.py index c9e5bdf..d831aae 100644 --- a/src/certfuzz/tools/windows/clean_windows.py +++ b/src/certfuzz/tools/windows/clean_windows.py @@ -25,7 +25,7 @@ def main(): import optparse try: from certfuzz.fuzztools.filetools import delete_contents_of - from certfuzz.campaign.config import Config + from certfuzz.campaign.config.config_base import ConfigBase except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH import sys @@ -33,7 +33,7 @@ def main(): parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) from certfuzz.fuzztools.filetools import delete_contents_of - from certfuzz.campaign.config import Config + from certfuzz.campaign.config.config_base import ConfigBase if not os.path.exists(defaults['config']): defaults['config'] = '../configs/bff.yaml' @@ -46,7 +46,7 @@ def main(): parser.add_option('', '--debug', dest='debug', action='store_true', default=defaults['debug']) options, _args = parser.parse_args() - cfgobj = Config(options.configfile) + cfgobj = ConfigBase(options.configfile) c = cfgobj.config if options.debug: From b3a8ef29d3a3d20bc330102b3cf8d1368da6bd8b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 30 Oct 2014 14:06:45 -0400 Subject: [PATCH 0529/1169] rename windows config class --- src/certfuzz/campaign/campaign_windows.py | 4 ++-- src/certfuzz/campaign/config/config_windows.py | 2 +- src/certfuzz/test/campaign/config/test_config_windows.py | 6 +++--- src/certfuzz/tools/windows/minimize.py | 8 ++++---- src/certfuzz/tools/windows/repro.py | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index a74ecbd..2adf4b5 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -14,7 +14,7 @@ from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.fuzzers.errors import FuzzerExhaustedError from certfuzz.runners.killableprocess import Popen -from certfuzz.campaign.config.config_windows import Config +from certfuzz.campaign.config.config_windows import WindowsConfig from certfuzz.iteration.iteration_windows import WindowsIteration @@ -67,7 +67,7 @@ def _read_config_file(self): CampaignBase._read_config_file(self) #read configs - cfgobj = Config(self.config_file) + cfgobj = WindowsConfig(self.config_file) self.config = cfgobj.config self.configdate = cfgobj.configdate diff --git a/src/certfuzz/campaign/config/config_windows.py b/src/certfuzz/campaign/config/config_windows.py index a8ebe8f..7bac383 100644 --- a/src/certfuzz/campaign/config/config_windows.py +++ b/src/certfuzz/campaign/config/config_windows.py @@ -30,7 +30,7 @@ def get_command_args_list(cmd_template, infile, posix=True): return cmd, cmdlist -class Config(ConfigBase): +class WindowsConfig(ConfigBase): def _add_validations(self): self.validations.append(self._validate_debugger_timeout_exceeds_runner) self.validations.append(self._validate_new_options) diff --git a/src/certfuzz/test/campaign/config/test_config_windows.py b/src/certfuzz/test/campaign/config/test_config_windows.py index faa001e..d74fed2 100644 --- a/src/certfuzz/test/campaign/config/test_config_windows.py +++ b/src/certfuzz/test/campaign/config/test_config_windows.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz.campaign.config.config_windows import Config +from certfuzz.campaign.config.config_windows import WindowsConfig import os import yaml import tempfile @@ -28,7 +28,7 @@ def _get_minimal_config(self): with open(f, 'w') as fd: yaml.dump(self.cfg_in, fd) - return Config(f) + return WindowsConfig(f) def setUp(self): self.tempdir = tempfile.mkdtemp() @@ -43,7 +43,7 @@ def test_empty_cfg_raises_exception(self): with open(f, 'w') as fd: yaml.dump(self.cfg_in, fd) - self.assertRaises(KeyError, Config, f) + self.assertRaises(KeyError, WindowsConfig, f) def test_minimal_config(self): try: diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 95f6be1..16eaf40 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -14,7 +14,7 @@ from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer import WindowsMinimizer as Minimizer - from certfuzz.campaign.config.config_windows import Config, get_command_args_list + from certfuzz.campaign.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport except ImportError: @@ -27,7 +27,7 @@ from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer import WindowsMinimizer as Minimizer - from certfuzz.campaign.config.config_windows import Config, get_command_args_list + from certfuzz.campaign.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport @@ -102,7 +102,7 @@ def main(): cfg_file = options.config else: cfg_file = "../conf.d/bff.cfg" - logger.debug('Config file: %s', cfg_file) + logger.debug('WindowsConfig file: %s', cfg_file) if options.stringmode and options.target: parser.error('Options --stringmode and --target are mutually exclusive.') @@ -143,7 +143,7 @@ def main(): else: parser.error('fuzzedfile must be specified') - config = Config(cfg_file).config + config = WindowsConfig(cfg_file).config cfg = _create_minimizer_cfg(config) if options.target: diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index 32d01da..4248c64 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -12,7 +12,7 @@ from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.campaign.config.config_windows import Config, get_command_args_list +from certfuzz.campaign.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.debuggers import msec # @UnusedImport @@ -74,7 +74,7 @@ def main(): logger.setLevel(logging.INFO) cfg_file = options.config - logger.debug('Config file: %s', cfg_file) + logger.debug('WindowsConfig file: %s', cfg_file) if len(args) and os.path.exists(args[0]): fullpath_fuzzed_file = os.path.abspath(args[0]) @@ -83,7 +83,7 @@ def main(): else: parser.error('fuzzedfile must be specified') - config = Config(cfg_file).config + config = WindowsConfig(cfg_file).config iterationpath = '' template = string.Template(config['target']['cmdline_template']) From 86eae60db6b6cf355d9aeca801c1dd0e5b12a74a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 30 Oct 2014 16:07:04 -0400 Subject: [PATCH 0530/1169] use runtime context manager for reading config --- src/certfuzz/campaign/campaign_windows.py | 6 +++--- src/certfuzz/campaign/config/config_base.py | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 2adf4b5..7b66b04 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -67,9 +67,9 @@ def _read_config_file(self): CampaignBase._read_config_file(self) #read configs - cfgobj = WindowsConfig(self.config_file) - self.config = cfgobj.config - self.configdate = cfgobj.configdate + with WindowsConfig(self.config_file) as cfgobj: + self.config = cfgobj.config + self.configdate = cfgobj.configdate def __getstate__(self): diff --git a/src/certfuzz/campaign/config/config_base.py b/src/certfuzz/campaign/config/config_base.py index 92120a0..c4c52f1 100644 --- a/src/certfuzz/campaign/config/config_base.py +++ b/src/certfuzz/campaign/config/config_base.py @@ -26,14 +26,18 @@ def __init__(self, config_file): self.file = config_file self.config = None self.configdate = None + self.validations = [] + def __enter__(self): self.load() self._verify_load() self._set_derived_options() - - self.validations = [] self._add_validations() self.validate() + return self + + def __exit__(self, etype, value, traceback): + pass def load(self): logger.debug('loading config from %s', self.file) From b8dcbe1db379d3f2978683c5e3cb28ed12bf6792 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 7 Nov 2014 14:18:02 -0500 Subject: [PATCH 0531/1169] be consistent with how we invoke config reading --- src/certfuzz/campaign/campaign_linux.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 245c6e8..42e0370 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -22,7 +22,7 @@ from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.iteration_linux import LinuxIteration -from certfuzz.campaign.config.bff_config import read_config_options +from certfuzz.campaign.config.config_linux import LinuxConfig logger = logging.getLogger(__name__) @@ -83,7 +83,11 @@ def __init__(self, config_file=None, result_dir=None, debug=False): def _read_config_file(self): CampaignBase._read_config_file(self) - self.config = read_config_options(self.config_file) + + with LinuxConfig(self.config_file) as cfgobj: + self.config = cfgobj.config + self.configdate = cfgobj.configdate + def _pre_enter(self): self._start_process_killer() From 25a1fea9fa0ca0f7b4f14abb820504023e16b788 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 7 Nov 2014 14:19:08 -0500 Subject: [PATCH 0532/1169] if __enter__ returns self.go then you just call it --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/campaign/campaign_windows.py | 2 +- src/certfuzz/iteration/iteration_base3.py | 2 +- src/certfuzz/iteration/iteration_linux.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 42e0370..c0245ce 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -220,4 +220,4 @@ def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): sf_set=self.seedfile_set, rf=seedfile.rangefinder, outdir=self.outdir) as iteration: - iteration.go() + iteration() diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 7b66b04..2a015b3 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -222,7 +222,7 @@ def _do_iteration(self, sf, rng_seed, seednum): self.working_dir, self.outdir, self.debug, self.seedfile_set, sf.rangefinder) as iteration: try: - iteration.go() + iteration() except FuzzerExhaustedError: # Some fuzzers run out of things to do. They should # raise a FuzzerExhaustedError when that happens. diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 9f01810..e801859 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -63,7 +63,7 @@ def __enter__(self): dir=self.workdirbase) logger.debug('workdir=%s', self.working_dir) # self._setup_analysis_pipeline() - return self + return self.go def __exit__(self, etype, value, traceback): handled = False diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 6233855..55ae0b3 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -54,7 +54,7 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No def __enter__(self): IterationBase3.__enter__(self) check_ppid() - return self + return self.go def __exit__(self, etype, value, traceback): handled = IterationBase3.__exit__(self, etype, value, traceback) From 3641ee3aae8c8a15528d2fa404231d9a87aaa891 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 7 Nov 2014 14:19:44 -0500 Subject: [PATCH 0533/1169] change pipeline runtime context calling --- src/certfuzz/iteration/iteration_base3.py | 2 +- src/certfuzz/testcase_pipeline/tc_pipeline_base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index e801859..9715350 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -142,7 +142,7 @@ def process_testcases(self): outdir=self.outdir, workdirbase=self.working_dir) with pipeline: - pipeline.go() + pipeline() def go(self): logger.debug('go') diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 4796ea4..c961d31 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -51,7 +51,7 @@ def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, def __enter__(self): self._setup_analysis_pipeline() - return self + return self.go def __exit__(self, etype, value, traceback): pass From 84e87a0b387fe62819a1435d31e2bc484db51947 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 7 Nov 2014 14:20:46 -0500 Subject: [PATCH 0534/1169] consolidate with syntax --- src/certfuzz/iteration/iteration_base3.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 9715350..d6a9ad2 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -135,13 +135,12 @@ def record_tries(self): def process_testcases(self): # hand it off to our pipeline class - pipeline = self.tcpipeline_cls(testcases=self.testcases, + with self.tcpipeline_cls(testcases=self.testcases, uniq_func=self.uniq_func, cfg=self.cfg, options=self.pipeline_options, outdir=self.outdir, - workdirbase=self.working_dir) - with pipeline: + workdirbase=self.working_dir) as pipeline: pipeline() def go(self): From 0257621aaaedd122fd49a2b5860fd66780e4824b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 7 Nov 2014 14:32:33 -0500 Subject: [PATCH 0535/1169] eradicate super() --- src/certfuzz/analyzers/callgrind/callgrind.py | 2 +- src/certfuzz/analyzers/cw_gmalloc.py | 2 +- src/certfuzz/analyzers/pin_calltrace.py | 2 +- src/certfuzz/analyzers/stderr.py | 2 +- src/certfuzz/analyzers/valgrind.py | 2 +- src/certfuzz/debuggers/crashwrangler.py | 2 +- src/certfuzz/debuggers/msec.py | 2 +- src/certfuzz/debuggers/output_parsers/abrtfile.py | 2 +- src/certfuzz/debuggers/output_parsers/konqifile.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/analyzers/callgrind/callgrind.py b/src/certfuzz/analyzers/callgrind/callgrind.py index 0e60848..858fb1f 100644 --- a/src/certfuzz/analyzers/callgrind/callgrind.py +++ b/src/certfuzz/analyzers/callgrind/callgrind.py @@ -22,7 +22,7 @@ def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) timeout = cfg.valgrindtimeout - super(Callgrind, self).__init__(cfg, crash, outfile, timeout) + Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True self.missing_output_ok = True diff --git a/src/certfuzz/analyzers/cw_gmalloc.py b/src/certfuzz/analyzers/cw_gmalloc.py index 6f0074b..65bcd57 100644 --- a/src/certfuzz/analyzers/cw_gmalloc.py +++ b/src/certfuzz/analyzers/cw_gmalloc.py @@ -32,7 +32,7 @@ def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) timeout = cfg.debugger_timeout - super(CrashWranglerGmalloc, self).__init__(cfg, crash, outfile, timeout) + Analyzer.__init__(self, cfg, crash, outfile, timeout) def go(self): if not _platform_is_supported: diff --git a/src/certfuzz/analyzers/pin_calltrace.py b/src/certfuzz/analyzers/pin_calltrace.py index 9556671..2057590 100644 --- a/src/certfuzz/analyzers/pin_calltrace.py +++ b/src/certfuzz/analyzers/pin_calltrace.py @@ -23,7 +23,7 @@ def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) timeout = cfg.valgrindtimeout * 10 - super(Pin_calltrace, self).__init__(cfg, crash, outfile, timeout) + Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True self.missing_output_ok = True diff --git a/src/certfuzz/analyzers/stderr.py b/src/certfuzz/analyzers/stderr.py index 17f1093..57a6884 100644 --- a/src/certfuzz/analyzers/stderr.py +++ b/src/certfuzz/analyzers/stderr.py @@ -21,7 +21,7 @@ def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) timeout = cfg.progtimeout - super(StdErr, self).__init__(cfg, crash, outfile, timeout, stderr=outfile) + Analyzer.__init__(self, cfg, crash, outfile, timeout, stderr=outfile) # need to set the stderr_redirect flag on the base class self.empty_output_ok = True diff --git a/src/certfuzz/analyzers/valgrind.py b/src/certfuzz/analyzers/valgrind.py index 77f32ad..8642868 100644 --- a/src/certfuzz/analyzers/valgrind.py +++ b/src/certfuzz/analyzers/valgrind.py @@ -22,7 +22,7 @@ def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) timeout = cfg.valgrindtimeout - super(Valgrind, self).__init__(cfg, crash, outfile, timeout) + Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True self.missing_output_ok = True diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index 4d1ad32..411cd82 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -41,7 +41,7 @@ class CrashWrangler(Debugger): _ext = 'cw' def __init__(self, program, cmd_args, outfile, timeout, killprocname, template=None, exclude_unmapped_frames=True, **options): - super(CrashWrangler, self).__init__(program, cmd_args, outfile, timeout, killprocname) + Debugger.__init__(self, program, cmd_args, outfile, timeout, killprocname) def _get_crashwrangler_cmdline(self): if (self.program == cwapp): diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 25f44dd..3193922 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -31,7 +31,7 @@ class MsecDebugger(DebuggerBase): _ext = 'msec' def __init__(self, program, cmd_args, outfile_base, timeout, killprocname, watchcpu, exception_depth=0, **options): - super(MsecDebugger, self).__init__(program, cmd_args, outfile_base, timeout, killprocname, **options) + DebuggerBase.__init__(self, program, cmd_args, outfile_base, timeout, killprocname, **options) self.exception_depth = exception_depth self.watchcpu = watchcpu diff --git a/src/certfuzz/debuggers/output_parsers/abrtfile.py b/src/certfuzz/debuggers/output_parsers/abrtfile.py index 5c4fcb1..5976027 100644 --- a/src/certfuzz/debuggers/output_parsers/abrtfile.py +++ b/src/certfuzz/debuggers/output_parsers/abrtfile.py @@ -35,7 +35,7 @@ def __init__(self, path, exclude_unmapped_frames=True): self.crashing_thread = False self.has_proc_map = False - super(self.__class__, self).__init__(path, exclude_unmapped_frames) + DebuggerFile.__init__(self, path, exclude_unmapped_frames) def backtrace_line(self, idx, l): self._look_for_crashing_thread(l) diff --git a/src/certfuzz/debuggers/output_parsers/konqifile.py b/src/certfuzz/debuggers/output_parsers/konqifile.py index 8c8f461..236f263 100644 --- a/src/certfuzz/debuggers/output_parsers/konqifile.py +++ b/src/certfuzz/debuggers/output_parsers/konqifile.py @@ -33,7 +33,7 @@ def __init__(self, path, exclude_unmapped_frames=True): self.has_proc_map = False self.dataformat = 'gdb' - super(self.__class__, self).__init__(path, exclude_unmapped_frames) + DebuggerFile.__init__(self, path, exclude_unmapped_frames) def backtrace_line(self, idx, l): self._look_for_crashing_thread(l) From be68036f70048cfa1903901315857bbadc8d68ce Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 7 Nov 2014 14:41:44 -0500 Subject: [PATCH 0536/1169] refactor pipeline creation --- .../testcase_pipeline/tc_pipeline_base.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index c961d31..803501f 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -24,6 +24,7 @@ class TestCasePipelineBase(object): Implements a pipeline for filtering and processing a testcase ''' __metaclass__ = abc.ABCMeta + pipes = ['verify', 'minimize', 'analyze', 'report'] def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, outdir=None, workdirbase=None): @@ -63,11 +64,21 @@ def _setup_analyzers(self): def _setup_analysis_pipeline(self): # build up the pipeline: # verify | minimize | analyze | report - r = self.report() - a = self.analyze(r) - m = self.minimize(a) - self.analysis_pipeline = self.verify(m) + logger.debug('Construct analysis pipeline') + setup_order = list(self.pipes).reverse() + + pipeline = self.analysis_pipeline + + for pipe_name in setup_order: + pipe_method = getattr(self, pipe_name) + logger.debug('Add {}() to pipeline'.format(pipe_name)) + if pipeline is None: + # we haven't initialized it yet + pipeline = pipe_method() + else: + # prepend pipe_method upstream of pipeline + pipeline = pipe_method(target=pipeline) @coroutine def verify(self, *targets): From 731efb13892174f195eacd46acddc3841ac73edf Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 10 Nov 2014 09:30:58 -0500 Subject: [PATCH 0537/1169] return bff.go instead of bff then calling bff.go() --- src/certfuzz/bff/common.py | 4 ++-- src/certfuzz/bff/linux.py | 2 +- src/certfuzz/bff/windows.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/bff/common.py b/src/certfuzz/bff/common.py index 725ea3a..bc23571 100644 --- a/src/certfuzz/bff/common.py +++ b/src/certfuzz/bff/common.py @@ -59,8 +59,8 @@ def __enter__(self): if self.args.debug: setup_debugging() - return self - + return self.go + def __exit__(self, etype, value, traceback): pass diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 211adf5..5d23b1a 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -20,4 +20,4 @@ def main(): cfg = os.path.abspath(os.path.join(os.getcwd(), 'conf.d', 'bff.cfg')) with BFF(config_path=cfg, campaign_class=LinuxCampaign) as bff: - bff.go() + bff() diff --git a/src/certfuzz/bff/windows.py b/src/certfuzz/bff/windows.py index 03a2821..815c5bb 100644 --- a/src/certfuzz/bff/windows.py +++ b/src/certfuzz/bff/windows.py @@ -19,4 +19,4 @@ def main(): cfg = os.path.abspath(os.path.join(os.getcwd(), 'configs', 'bff.yaml')) with BFF(config_path=cfg, campaign_class=WindowsCampaign) as bff: - bff.go() + bff() From 36af4c27a550120938a4bc5f7d444a9809c255ad Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Nov 2014 10:37:05 -0500 Subject: [PATCH 0538/1169] reorder imports --- src/certfuzz/fuzztools/filetools.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 2964e3c..7b1e6b6 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -5,17 +5,17 @@ @organization: cert.org ''' -import os +import StringIO import errno -import time -import shutil +import fnmatch import hashlib import logging -import tempfile -import fnmatch +import os +import shutil import stat +import tempfile +import time import zipfile -import StringIO MAXDEPTH = 5 From 03a0c0ee1f0855c07ce121f5ff51fc1ac7f717c1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Nov 2014 10:37:32 -0500 Subject: [PATCH 0539/1169] refactor out local var --- src/certfuzz/fuzztools/filetools.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 7b1e6b6..1914b95 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -214,8 +214,7 @@ def copy_file2(src=None, targets=[]): def mkdtemp(base_dir=None): - path = tempfile.mkdtemp(prefix='BFF-', dir=base_dir) - return path + return tempfile.mkdtemp(prefix='BFF-', dir=base_dir) def write_oneline_to_file(line, dst, mode): From b2eebdea9b93aaa2373a5e51a7311d42530642b6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Nov 2014 10:38:58 -0500 Subject: [PATCH 0540/1169] refactor watchdog call --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/fuzztools/watchdog.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index c0245ce..7b2570b 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -140,7 +140,7 @@ def _setup_watchdog(self): # set up the watchdog timeout within the VM and restart the daemon with WatchDog(self.config.watchdogfile, self.config.watchdogtimeout) as watchdog: - watchdog.go() + watchdog() def _check_for_script(self): logger.debug('check for script') diff --git a/src/certfuzz/fuzztools/watchdog.py b/src/certfuzz/fuzztools/watchdog.py index 23137dc..3b21658 100644 --- a/src/certfuzz/fuzztools/watchdog.py +++ b/src/certfuzz/fuzztools/watchdog.py @@ -33,7 +33,7 @@ def __init__(self, f, timeout): def __enter__(self): self._set_cmdline() - return self + return self.go def __exit__(self, etype, value, traceback): handled = False From 2936a92714b30c36414b2ecde5485888e268f46f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 11 Nov 2014 10:42:47 -0500 Subject: [PATCH 0541/1169] fix call to read config --- src/certfuzz/campaign/campaign_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 6814364..718ba74 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -97,7 +97,7 @@ def __init__(self, config_file, result_dir=None, debug=False): if result_dir: self.outdir_base = os.path.abspath(result_dir) - self._read_config_file(self.config_file) + self._read_config_file() @abc.abstractmethod def _read_config_file(self): From 7fe728210924eb9ed8d747354c45090fabd88ae4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Nov 2014 08:30:57 -0500 Subject: [PATCH 0542/1169] first try at replacing .cfg with .yaml on linux --- src/certfuzz/bff/linux.py | 2 +- src/certfuzz/campaign/config/config_linux.py | 178 +++++++++++++++++- .../test/campaign/config/test_bff_config.py | 134 ------------- .../test/campaign/config/test_config_linux.py | 153 +++++++++++++++ src/certfuzz/tools/linux/bff_stats.py | 13 +- .../tools/linux/create_crasher_script.py | 13 +- src/certfuzz/tools/linux/minimize.py | 26 +-- src/certfuzz/tools/linux/minimizer_plot.py | 14 +- src/certfuzz/tools/linux/repro.py | 7 +- src/linux/conf.d/{bff.cfg => bff.yaml} | 70 ++++--- 10 files changed, 407 insertions(+), 203 deletions(-) delete mode 100644 src/certfuzz/test/campaign/config/test_bff_config.py create mode 100644 src/certfuzz/test/campaign/config/test_config_linux.py rename src/linux/conf.d/{bff.cfg => bff.yaml} (88%) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 5d23b1a..43dd63b 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -17,7 +17,7 @@ def main(): - cfg = os.path.abspath(os.path.join(os.getcwd(), 'conf.d', 'bff.cfg')) + cfg = os.path.abspath(os.path.join(os.getcwd(), 'conf.d', 'bff.yaml')) with BFF(config_path=cfg, campaign_class=LinuxCampaign) as bff: bff() diff --git a/src/certfuzz/campaign/config/config_linux.py b/src/certfuzz/campaign/config/config_linux.py index dbb887b..d5b9127 100644 --- a/src/certfuzz/campaign/config/config_linux.py +++ b/src/certfuzz/campaign/config/config_linux.py @@ -3,16 +3,188 @@ @organization: cert.org ''' +from functools import partial +import logging +import os +import re +import shlex +import shutil + from certfuzz.campaign.config.config_base import ConfigBase +from certfuzz.fuzztools import filetools + + +logger = logging.getLogger(__name__) + +_DTYPES = {'timeouts': {'killproctimeout': int, + 'watchdogtimeout': int, + 'debugger_timeout': float, + 'progtimeout': float, + 'valgrindtimeout': int, + 'minimizertimeout': int, + }, + 'zzuf': {'copymode': bool, + 'start_seed': int, + 'seed_interval': int, + }, + 'verifier': {'backtracelevels': int, + 'keep_duplicates': bool, + 'exclude_unmapped_frames': bool, + 'minimizecrashers': bool, + 'minimize_to_string': bool, + 'use_valgrind': bool, + 'use_pin_calltrace': bool, + 'savefailedasserts': bool, + 'recycle_crashers': bool, + }, + 'directories': {'remote_dir': str, + 'seedfile_origin_dir': str, + 'debugger_template_dir': str, + 'local_dir': str, + 'output_dir': str, + 'watchdog_file': str, + }, + 'target': { + 'killprocname': str, + 'cmdline': str, + } + } +UNIQ_LOG = "uniquelog.txt" +LAST_SEEDFILE = 'lastseed' + +MINIMIZED_EXT = "minimal" +ZZUF_LOG_FILE = 'zzuf_log.txt' +RANGE_LOG = 'rangelog.txt' +CRASH_EXIT_CODE_FILE = "crashexitcodes" +CACHED_CONFIG_OBJECT_FILE = 'config.pkl' +CACHED_SEEDRANGE_OBJECT_FILE = 'seedrange.pkl' +CACHED_RANGEFINDER_OBJECT_FILE = 'rangefinder.pkl' +CACHED_SEEDFILESET_OBJECT_FILE = 'seedfile_set.pkl' +SEEDFILE_REPLACE_STRING = '\$SEEDFILE' class LinuxConfig(ConfigBase): ''' - classdocs + Defines a linux-specific configuration file format ''' + def _set_derived_options(self): + ConfigBase._set_derived_options(self) + + # [campaign] + campaign_id = re.sub('\s+', '_', self.config['campaign']['id']) + self.campaign_id = campaign_id + + # unroll ~ & relative paths + dir_dict = self.config['directories'] + for k, path in dir_dict.iteritems(): + dir_dict[k] = os.path.abspath(os.path.expanduser(path)) + + # cast the data to the expected type + for k, dtypes in _DTYPES.iteritems(): + # pick the top-level config block to work with + cfgblock = self.config[k] + for key, dtype in dtypes.iteritems(): + # get the value if it's present + # otherwise take the default type value (e.g., int() = 0, str() = '') + val = cfgblock.get(key, dtype()) + # set the attribute, casting it to the expected type + setattr(self, key, dtype(val)) + + self.cmd_list = shlex.split(self.cmdline) + for index, cmd_part in enumerate(self.cmd_list): + self.cmd_list[index] = os.path.expanduser(cmd_part) + if re.search(' ', self.cmd_list[0]): + self.cmd_list[0] = '"' + self.cmd_list[0] + '"' + self.program = self.cmd_list[0] + self._cmd = self.cmd_list + self._args = self.cmd_list[1:] + + # derived properties + self.program_basename = os.path.basename(self.program).replace('"', '') + self.uniq_log = os.path.join(self.output_dir, UNIQ_LOG) + self.crashexitcodesfile = os.path.join(self.local_dir, CRASH_EXIT_CODE_FILE) + self.zzuf_log_file = os.path.join(self.local_dir, ZZUF_LOG_FILE) + + # derived cached paths +# self.cached_config_file = os.path.join(self.local_dir, CACHED_CONFIG_OBJECT_FILE) +# self.cached_seedrange_file = os.path.join(self.local_dir, CACHED_SEEDRANGE_OBJECT_FILE) +# self.cached_rangefinder_file = os.path.join(self.local_dir, CACHED_RANGEFINDER_OBJECT_FILE) +# self.cached_seedfile_set = os.path.join(self.local_dir, CACHED_SEEDFILESET_OBJECT_FILE) + + +# self.tmpdir = None + + def get_command(self, filepath): + return ' '.join(self.get_command_list(filepath)) + + def get_command_list(self, seedfile): + cmdlst = [self.program] + cmdlst.extend(self.get_command_args_list(seedfile)) + return cmdlst + def get_command_args_list(self, seedfile): + arglist = [] + for arg in self._args: + arglist.append(re.sub(SEEDFILE_REPLACE_STRING, seedfile, arg)) + return arglist - def __init__(self, params): + def zzuf_log_out(self, mydir): + return os.path.join(mydir, ZZUF_LOG_FILE) + + def full_path_local_fuzz_dir(self, seedfile): + ''' + Returns // + @param seedfile: + ''' + return os.path.join(self.local_dir, self.program_basename, seedfile) + + def full_path_original(self, seedfile): ''' - Constructor + Returns / + @param seedfile: ''' + return os.path.join(self.full_path_local_fuzz_dir(seedfile), seedfile) + + def get_minimized_file(self, outfile): + ''' + @rtype: string + @return: -. + ''' + (head, tail) = os.path.split(outfile) + (root, ext) = os.path.splitext(tail) + new_filename = '%s-%s%s' % (root, MINIMIZED_EXT, ext) + return os.path.join(head, new_filename) + + def create_tmpdir(self): + # TODO: this should become part of campaign object + if not self.tmpdir: + self.tmpdir = filetools.mkdtemp(self.testscase_tmp_dir) + logger.debug("Created temp dir %s", self.tmpdir) + assert os.path.isdir(self.tmpdir) + + def clean_tmpdir(self): + # TODO: this should become part of campaign object + if self.tmpdir is None: + return + + if os.path.exists(self.tmpdir): + shutil.rmtree(self.tmpdir) + logger.debug("Removed temp dir %s", self.tmpdir) + assert not os.path.exists(self.tmpdir) + self.tmpdir = None + self.create_tmpdir() + + def get_testcase_outfile(self, seedfile, s1): + # TODO: this should become part of campaign object + ''' + @rtype: string + @return: the path to the output file for this seed: . + ''' + (dirname, basename) = os.path.split(seedfile) # @UnusedVariable + (root, ext) = os.path.splitext(basename) + new_root = '%s-%d' % (root, s1) + new_basename = '%s%s' % (new_root, ext) + self.create_tmpdir() + return os.path.join(self.tmpdir, new_basename) + + diff --git a/src/certfuzz/test/campaign/config/test_bff_config.py b/src/certfuzz/test/campaign/config/test_bff_config.py deleted file mode 100644 index dab9724..0000000 --- a/src/certfuzz/test/campaign/config/test_bff_config.py +++ /dev/null @@ -1,134 +0,0 @@ -''' -Created on Apr 8, 2011 - -@organization: cert.org -''' -import unittest -import os -import ConfigParser -from certfuzz.campaign.config.bff_config import ConfigHelper -from certfuzz.campaign.config.bff_config import MINIMIZED_EXT -import tempfile -from certfuzz.campaign.config.bff_config import read_config_options -import shutil - - -class Test(unittest.TestCase): - def delete_file(self, f): - os.remove(f) - self.assertFalse(os.path.exists(f)) - - def setUp(self): - self.tmpdir = tempfile.mkdtemp() - # build a config - self.config = ConfigParser.RawConfigParser() - - self.config.add_section("campaign") - self.config.set('campaign', 'id', 'campaign_id') - self.config.set('campaign', 'distributed', 'False') - - self.config.add_section("directories") - self.config.set('directories', 'remote_dir', 'remote_dir') - self.config.set('directories', 'crashers_dir', 'crashers_dir') - self.config.set('directories', 'seedfile_origin_dir', 'seedfile_origin_dir') - - self.config.set('directories', 'output_dir', 'output_dir') - self.config.set('directories', 'local_dir', 'local_dir') - self.config.set('directories', 'seedfile_output_dir', 'seedfile_output_dir') - self.config.set('directories', 'seedfile_local_dir', 'seedfile_local_dir') - self.config.set('directories', 'cached_objects_dir', 'cached_objects_dir') - self.config.set('directories', 'temp_working_dir', 'temp_working_dir') - self.config.set('directories', 'watchdog_file', 'watchdog_file') - self.config.set('directories', 'debugger_template_dir', 'debugger_template_dir') - - self.config.add_section("target") - self.config.set('target', 'cmdline', '/path/to/program $SEEDFILE outfile.ext') - self.config.set('target', 'killprocname', '/path/to/killprocname') - - self.config.add_section('timeouts') - self.config.set('timeouts', 'killproctimeout', '1') - self.config.set('timeouts', 'watchdogtimeout', '2') - self.config.set('timeouts', 'debugger_timeout', '4') - self.config.set('timeouts', 'progtimeout', '3.4') - self.config.set('timeouts', 'valgrindtimeout', '6') - self.config.set('timeouts', 'minimizertimeout', '6') - - self.config.add_section('zzuf') - self.config.set('zzuf', 'copymode', '1') - self.config.set('zzuf', 'ratiomin', '0.0001') - self.config.set('zzuf', 'ratiomax', '0.01') - self.config.set('zzuf', 'start_seed', '1000') - self.config.set('zzuf', 'seed_interval', '500') - self.config.set('zzuf', 'max_seed', '100000') - - self.config.add_section('verifier') - self.config.set('verifier', 'backtracelevels', '17') - self.config.set('verifier', 'minimizecrashers', '1') - self.config.set('verifier', 'manualcutoff', '10') -# self.config.set('verifier', 'keepduplicates', '1') - self.config.set('verifier', 'minimize_to_string', '1') - self.config.set('verifier', 'use_valgrind', '1') - - # create a ConfigHelper object - self.cfg = ConfigHelper(self.config) - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - def test_init(self): - self.assertEqual(self.cfg.killprocname, 'killprocname') - self.assertEqual(self.cfg.killproctimeout, 1) - self.assertEqual(self.cfg.watchdogtimeout, 2) - self.assertEqual(self.cfg.copymode, 1) - self.assertEqual(self.cfg.progtimeout, 3.4) - self.assertEqual(self.cfg.output_dir, 'output_dir') - self.assertEqual(self.cfg.local_dir, 'local_dir') - self.assertEqual(self.cfg.debugger_timeout, 4) - self.assertEqual(self.cfg.backtracelevels, 17) - self.assertEqual(self.cfg.minimizecrashers, 1) - self.assertEqual(self.cfg.valgrindtimeout, 6) - - def test_program_is_script(self): - pass - - def test_get_minimized_file(self): - self.assertEqual(self.cfg.get_minimized_file('foo.txt'), 'foo-%s.txt' % MINIMIZED_EXT) - - def test_uniquelog(self): - self.assertEqual(self.cfg.uniq_log, os.path.join('output_dir', 'uniquelog.txt')) - - def test_crashexitcodesfile(self): - self.assertEqual(self.cfg.crashexitcodesfile, os.path.join('local_dir', 'crashexitcodes')) - - def test_zzuf_log_file(self): - self.assertEqual(self.cfg.zzuf_log_file, os.path.join('local_dir', 'zzuf_log.txt')) - - def test_zzuf_log_out(self): - self.assertEqual(self.cfg.zzuf_log_out('seedfile'), os.path.join('seedfile', 'zzuf_log.txt')) - - def test_read_config_options(self): - (fd, f) = tempfile.mkstemp(text=True) - os.close(fd) - - with open(f, 'wb') as configfile: - self.config.write(configfile) - - cfg = read_config_options(f) - - self.assertEqual(cfg.killprocname, 'killprocname') - self.assertEqual(cfg.killproctimeout, 1) - self.assertEqual(cfg.watchdogtimeout, 2) - self.assertEqual(cfg.copymode, 1) - self.assertEqual(cfg.progtimeout, 3.4) - self.assertEqual(cfg.output_dir, 'output_dir') - self.assertEqual(cfg.local_dir, 'local_dir') - self.assertEqual(cfg.debugger_timeout, 4) - self.assertEqual(cfg.backtracelevels, 17) - self.assertEqual(cfg.minimizecrashers, 1) - self.assertEqual(cfg.valgrindtimeout, 6) - - self.delete_file(f) - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/campaign/config/test_config_linux.py b/src/certfuzz/test/campaign/config/test_config_linux.py new file mode 100644 index 0000000..b229ac6 --- /dev/null +++ b/src/certfuzz/test/campaign/config/test_config_linux.py @@ -0,0 +1,153 @@ +''' +Created on Nov 11, 2014 + +@author: adh +''' +import os +from pprint import pprint +import shutil +import tempfile +import unittest + +import yaml + +from certfuzz.campaign.config.config_linux import LinuxConfig, MINIMIZED_EXT +import certfuzz.campaign.config.config_linux as cl +from certfuzz.campaign.config.errors import ConfigError + + +CFG = ''' +campaign: + id: convert v5.2.0 +target: + cmdline: ~/convert $SEEDFILE /dev/null + killprocname: convert +directories: + remote_dir: ~/bff &remote_dir + seedfile_origin_dir: ~/bff/seedfiles/examples + debugger_template_dir: ~/bff/certfuzz/debuggers/templates + output_dir: ~/results + local_dir: ~/fuzzing + watchdog_file: /tmp/bff_watchdog +zzuf: + copymode: 1 + start_seed: 0 + seed_interval: 20 +verifier: + backtracelevels: 5 + exclude_unmapped_frames: True + savefailedasserts: False + use_valgrind: True + use_pin_calltrace: False + minimizecrashers: True + minimize_to_string: False + recycle_crashers: False +timeouts: + progtimeout: 5 + killproctimeout: 130 + debugger_timeout: 60 + valgrindtimeout: 120 + watchdogtimeout: 3600 + minimizertimeout: 3600 +''' + +class Test(unittest.TestCase): + + def setUp(self): + self.c = yaml.load(CFG) + self.tmpdir = tempfile.mkdtemp() + fd, self.yamlfile = tempfile.mkstemp(suffix='.yaml', dir=self.tmpdir, text=True) + os.close(fd) + with open(self.yamlfile, 'w') as stream: + yaml.dump(self.c, stream) + + self.cfg = LinuxConfig(self.yamlfile) + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def test_init(self): + self.assertEqual(self.yamlfile, self.cfg.file) + self.assertEqual(None, self.cfg.config) + self.assertEqual(None, self.cfg.configdate) + for key in self.c.iterkeys(): + self.assertFalse(hasattr(self.cfg, key)) + + def test_contextmgr(self): + # expect to throw an error if the file doesn't exist + bogusfile = os.path.join(self.tmpdir, 'nonexistent_file') + cfg = LinuxConfig(bogusfile) + self.assertRaises(ConfigError, cfg.__enter__) + + with self.cfg: + self.assertNotEqual(None, self.cfg.config) + self.assertEqual(dict, type(self.cfg.config)) + self.assertNotEqual(None, self.cfg.configdate) + for key in self.c.iterkeys(): + self.assertTrue(hasattr(self.cfg, key)) + + def test_set_derived_options(self): + with self.cfg: + for blockname, dtypes in cl._DTYPES.iteritems(): + for k, dtype in dtypes.iteritems(): + self.assertTrue(hasattr(self.cfg, k)) + attr_val = getattr(self.cfg, k) + self.assertEqual(dtype, type(attr_val)) + if blockname == 'directories': + continue + self.assertEqual(self.c[blockname][k], attr_val) + self.assertTrue(self.cfg.uniq_log.endswith(cl.UNIQ_LOG)) + self.assertTrue(self.cfg.crashexitcodesfile.endswith(cl.CRASH_EXIT_CODE_FILE)) + self.assertTrue(self.cfg.zzuf_log_file.endswith(cl.ZZUF_LOG_FILE)) + self.assertEqual(self.cfg.zzuf_log_out('seedfile'), os.path.join('seedfile', 'zzuf_log.txt')) + + def test_get_command(self): + with self.cfg: + self.assertIn('foo', self.cfg.get_command('foo')) + + def test_get_command_list(self): + with self.cfg: + result = self.cfg.get_command_list('foo') + self.assertTrue(result[0].endswith('convert')) + self.assertEqual(result[1], 'foo') + self.assertEqual(result[2], '/dev/null') + + def test_get_command_args_list(self): + with self.cfg: + result = self.cfg.get_command_args_list('foo') + self.assertEqual(result[0], 'foo') + self.assertEqual(result[1], '/dev/null') + + def test_zzuf_log_out(self): + with self.cfg: + result = self.cfg.zzuf_log_out('foo') + self.assertEqual(str, type(result)) + self.assertTrue(result.startswith('foo')) + self.assertTrue(result.endswith(cl.ZZUF_LOG_FILE)) + + def test_full_path_local_fuzz_dir(self): + with self.cfg: + result = self.cfg.full_path_local_fuzz_dir('foo') + self.assertEqual(str, type(result)) + self.assertTrue(result.endswith('foo')) + + def test_full_path_original(self): + with self.cfg: + result = self.cfg.full_path_original('foo') + self.assertEqual(str, type(result)) + self.assertTrue(result.endswith(os.path.join('foo', 'foo'))) + + def test_get_minimized_file(self): + with self.cfg: + self.assertEqual(self.cfg.get_minimized_file('foo.txt'), 'foo-%s.txt' % MINIMIZED_EXT) + + def test_get_filenames(self): + with self.cfg: + result = self.cfg.get_filenames('foo.bar.baz', use_minimized_as_root=True) + + print result + result = self.cfg.get_filenames('foo.bar.baz', use_minimized_as_root=False) + print result +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/certfuzz/tools/linux/bff_stats.py b/src/certfuzz/tools/linux/bff_stats.py index d18cf00..c72fd41 100755 --- a/src/certfuzz/tools/linux/bff_stats.py +++ b/src/certfuzz/tools/linux/bff_stats.py @@ -4,12 +4,13 @@ @organization: cert.org ''' -import os import logging -import sys -import re from optparse import OptionParser -from certfuzz.campaign.config.bff_config import read_config_options +import os +import re +import sys +from certfuzz.campaign.config.config_linux import LinuxConfig + parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, parent_path) @@ -100,7 +101,9 @@ def main(): cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') logger.debug('Using config file: %s', cfg_file) - cfg = read_config_options(cfg_file) + cfg = LinuxConfig(cfg_file) + with cfg: + pass result_dir = cfg.crashers_dir logger.debug('Reading results from %s', result_dir) diff --git a/src/certfuzz/tools/linux/create_crasher_script.py b/src/certfuzz/tools/linux/create_crasher_script.py index 2dc3a52..e0d3224 100755 --- a/src/certfuzz/tools/linux/create_crasher_script.py +++ b/src/certfuzz/tools/linux/create_crasher_script.py @@ -5,17 +5,17 @@ ''' -from optparse import OptionParser import logging -import sys +from optparse import OptionParser import os import re +import sys +from certfuzz.campaign.config.config_linux import LinuxConfig + parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, parent_path) -from certfuzz.campaign.config.bff_config import read_config_options - logger = logging.getLogger(__name__) # set default logging level (override with command line options) logger.setLevel(logging.INFO) @@ -73,7 +73,10 @@ def main(): cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') logger.debug('Using config file: %s', cfg_file) - cfg = read_config_options(cfg_file) + + cfg = LinuxConfig(cfg_file) + with cfg: + pass result_dir = cfg.crashers_dir logger.debug('Reading results from %s', result_dir) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 67f853f..d2818a1 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -7,18 +7,20 @@ import os import sys +from certfuzz import debuggers +from certfuzz.campaign.config.config_linux import LinuxConfig +from certfuzz.crash.bff_crash import BffCrash +from certfuzz.debuggers import crashwrangler # @UnusedImport +from certfuzz.debuggers import gdb # @UnusedImport +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.fuzztools import filetools, text +from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer + + mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) -from certfuzz import debuggers -from certfuzz.fuzztools import filetools, text -from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.campaign.config.bff_config import read_config_options -from certfuzz.crash.bff_crash import BffCrash -from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer -from certfuzz.debuggers import gdb # @UnusedImport -from certfuzz.debuggers import crashwrangler # @UnusedImport logger = logging.getLogger() @@ -123,7 +125,9 @@ def main(): else: parser.error('fuzzedfile must be specified') - cfg = read_config_options(cfg_file) + cfg = LinuxConfig(cfg_file) + with cfg: + pass if options.target: seedfile = BasicFile(options.target) @@ -160,11 +164,11 @@ def main(): logger.debug('x character substitution') length = len(minimize.fuzzed_content) if options.prefer_x_target: - #We minimized to 'x', so we attempt to get metasploit as a freebie + # We minimized to 'x', so we attempt to get metasploit as a freebie targetstring = list(text.metasploit_pattern_orig(length)) filename_modifier = '-mtsp' else: - #We minimized to metasploit, so we attempt to get 'x' as a freebie + # We minimized to metasploit, so we attempt to get 'x' as a freebie targetstring = list('x' * length) filename_modifier = '-x' diff --git a/src/certfuzz/tools/linux/minimizer_plot.py b/src/certfuzz/tools/linux/minimizer_plot.py index d882d5a..13bae84 100755 --- a/src/certfuzz/tools/linux/minimizer_plot.py +++ b/src/certfuzz/tools/linux/minimizer_plot.py @@ -4,18 +4,19 @@ @organization: cert.org ''' -from optparse import OptionParser import logging -import sys +from optparse import OptionParser import os import re +import sys + +from certfuzz.campaign.config.config_linux import LinuxConfig import matplotlib.pyplot as plt + parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, parent_path) -from certfuzz.campaign.config.bff_config import read_config_options - logger = logging.getLogger(__name__) # set default logging level (override with command line options) logger.setLevel(logging.INFO) @@ -147,7 +148,10 @@ def main(): result_dir = options.dir else: logger.info('Using config file: %s', cfg_file) - cfg = read_config_options(cfg_file) + cfg = LinuxConfig(cfg_file) + with cfg: + pass + result_dir = cfg.crashers_dir logger.info('Reading results from %s', result_dir) diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index fa2e537..256a2f5 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -7,12 +7,12 @@ import os import re from subprocess import Popen +from certfuzz.campaign.config.config_linux import LinuxConfig try: from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options from certfuzz.debuggers import gdb, crashwrangler # @UnusedImport except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH @@ -23,7 +23,6 @@ from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.campaign.config.bff_config import ConfigHelper, read_config_options from certfuzz.debuggers import gdb, crashwrangler # @UnusedImport logger = logging.getLogger() @@ -104,7 +103,9 @@ def main(): os.path.join(iterationdir, iterationfile)) fullpath_fuzzed_file = iterationpath - config = read_config_options(cfg_file) + config = LinuxConfig(cfg_file) + with config: + pass cmd_as_args = config.get_command_list(fullpath_fuzzed_file) program = cmd_as_args[0] diff --git a/src/linux/conf.d/bff.cfg b/src/linux/conf.d/bff.yaml similarity index 88% rename from src/linux/conf.d/bff.cfg rename to src/linux/conf.d/bff.yaml index 3dd4d6b..6fc6e13 100644 --- a/src/linux/conf.d/bff.cfg +++ b/src/linux/conf.d/bff.yaml @@ -6,15 +6,12 @@ ################################################################ # FUZZ CAMPAIGN SETTINGS ################################################################ -[campaign] -# A string that uniquely identifies this campaign -id=Default BFF Campaign - +campaign: + id: convert v5.2.0 ################################################################ # TARGET APPLICATION INVOCATION OPTIONS: ################################################################ -[target] # # Command-line arguments will be split using python shlex.split() so # be sure to add quotes where needed @@ -22,8 +19,6 @@ id=Default BFF Campaign # seed file name. # Use quotes if the target application has spaces in the path # examples: -cmdline=~/convert $SEEDFILE /dev/null - # Name of process to monitor for hangs. This is done by greping the # process list, so choose carefully! Usually the same name as "program" # will suffice, but in cases where the program is started from a script @@ -31,42 +26,42 @@ cmdline=~/convert $SEEDFILE /dev/null # This process name is also used to kill the target process when BFF # launches the target application from an analyzer, such as gdb or valgrind # Use quotes if the target application has spaces in its name -killprocname=convert - +target: + cmdline: ~/convert $SEEDFILE /dev/null + killprocname: convert ################################################################ # LOCATIONS FOR FUZZ RUN FILES: # Output files are placed in [outputdir]/[seedfile] ################################################################ -[directories] # Location of the fuzzing scripts # (VMware shared directory if using UbuFuzz VM) -remote_dir=~/bff # The location of the seed files -seedfile_origin_dir=%(remote_dir)s/seedfiles/examples # location of debugger templates -debugger_template_dir=%(remote_dir)s/certfuzz/debuggers/templates # location of results: # If results are stored in a shared location, # this directory needs to be unique for each fuzzing machine -output_dir=~/results # dirs local to the fuzzing machine # Local directory for fuzzing run mutated files. -local_dir=~/fuzzing # Location of file used checked by Linux watchdog to determine # if fuzzer is still running -watchdog_file=/tmp/bff_watchdog +directories: + remote_dir: ~/bff &remote_dir + seedfile_origin_dir: ~/bff/seedfiles/examples + debugger_template_dir: ~/bff/certfuzz/debuggers/templates + output_dir: ~/results + local_dir: ~/fuzzing + watchdog_file: /tmp/bff_watchdog ################################################################ # ZZUF FUZZER PARAMETERS ################################################################ -[zzuf] # Use zzuf's "copy" mode, which creates a temporary fuzzed file # Default is 1, where zzuf will determine the file to fuzz, mangle it as # a randomly-named file in $TMPDIR and then open that with the target app. @@ -76,50 +71,44 @@ watchdog_file=/tmp/bff_watchdog # than creating a temporary file. Note that this mode can interfere with # some target applications. # OSX Cocoa applications require copymode=1 -copymode=1 # The zzuf seed number to start with -start_seed=0 # How many iterations you want zzuf to try per seedfile/range selection # If you have a large number of seed files and/or the target application # is slow, you may wish to make this value smaller to get better coverage # in a reasonable amount of time. -seed_interval=20 +zzuf: + copymode: 1 + start_seed: 0 + seed_interval: 20 ################################################################ # VERIFIER PARAMETERS ################################################################ -[verifier] # # number of backtrace levels to hash for uniqueness. # Increase this number for more crash uniqueness granularity. # Decrease this number if you think that you are getting too many # duplicate crashes. -backtracelevels=5 # Include backtrace frames that aren't part of a loaded library? # Set this value to False if you wish to consider unmapped stack # frames in the crash hashes. This can be useful for target application # that perform JIT compilation -exclude_unmapped_frames=True # Save cases that cause failed ASSERTs? If set to 1, then __assert_fail termination, # e.g. via assert(), it will be considered a crash. -savefailedasserts=False # Use valgrind (and callgrind) # Note that valgrind can be slow. Disabling this option can improve throughput -use_valgrind=True # Use PIN to get call traces for every crash, as opposed to just those # that result in total stack corruption. # PIN is even slower than valgrind -use_pin_calltrace=False # Obtain minimally-different testcase for each unique crash -minimizecrashers=True # Minimize to a metasploit string # Note: this is in addition to minimize to seedfile if minimize_crashers=True @@ -127,13 +116,21 @@ minimizecrashers=True # for all subsequent analyses (i.e., valgrind, callgrind, etc.) # Disabled by default due to amount of time that the minimization takes. # Stand-alone string minimization can be done using tools/minimize.py. -minimize_to_string = False # Recycle crashing testcases as seed files for further fuzzing. # This can improve the number of unique crash hashes found, however this may # just demonstrate weaknesses in backtrace-based uniqueness determination # rather than finding new underlying vulnerabilities. -recycle_crashers = False + +verifier: + backtracelevels: 5 + exclude_unmapped_frames: True + savefailedasserts: False + use_valgrind: True + use_pin_calltrace: False + minimizecrashers: True + minimize_to_string: False + recycle_crashers: False # Save crashes even when they are unique? If set to True, all # crashing inputs are saved, even when their backtraces are not @@ -143,33 +140,34 @@ keep_duplicates = False ################################################################ # APPLICATION TIMEOUTS ################################################################ -[timeouts] # # maximum program execution time (seconds) that BFF will allow: -progtimeout=5 # maximum time (seconds) that process specified by "killprocname" option # can run before it's killed via killproc.sh, # which is used to kill stray processes. Normally, zzuf will kill # the target process if the above timeout is reached. -killproctimeout=130 # maximum time (seconds) to let the program run to capture debugger and # CERT Triage Tools exploitable output. -debugger_timeout=60 # maximum time (seconds) to let the program run to capture valgrind output: -valgrindtimeout=120 # Set value for the Linux watchdog timer. If watchdog_file is not touched # for a period longer than this value (seconds), then the vm is rebooted # by the watchdog. # Set to 0 to disable watchdog functionality. -watchdogtimeout=3600 # Minimization can sometimes take a long time, and time spent minimizing # is time not spent fuzzing. If a minimization run exceeds this time # (in seconds) the minimization will terminate (keeping whatever progress # it has made at that point) and return to fuzzing. -minimizertimeout=3600 + +timeouts: + progtimeout: 5 + killproctimeout: 130 + debugger_timeout: 60 + valgrindtimeout: 120 + watchdogtimeout: 3600 + minimizertimeout: 3600 From 7b4992db8bba52c9ea5514d2fd47d863ef36ab28 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 12 Nov 2014 08:55:08 -0500 Subject: [PATCH 0543/1169] move certfuzz.campaign.config to certfuzz.config --- src/certfuzz/campaign/campaign_linux.py | 10 +++---- src/certfuzz/campaign/campaign_windows.py | 6 ++-- .../{campaign => }/config/__init__.py | 0 .../{campaign => }/config/config_base.py | 2 +- .../{campaign => }/config/config_linux.py | 2 +- .../{campaign => }/config/config_windows.py | 4 +-- src/certfuzz/{campaign => }/config/errors.py | 0 src/certfuzz/crash/crash_windows.py | 6 ++-- src/certfuzz/helpers/misc.py | 7 +++-- src/certfuzz/iteration/iteration_windows.py | 4 +-- src/certfuzz/runners/winrun.py | 2 +- .../test/campaign/config/test_config_pkg.py | 30 ------------------- .../test/campaign/test_campaign_windows.py | 9 +++--- .../test/{campaign => }/config/__init__.py | 0 .../{campaign => }/config/test_config_base.py | 17 ++++++----- .../config/test_config_linux.py | 6 ++-- .../config/test_config_windows.py | 17 ++++++----- .../testcase_pipeline/tc_pipeline_windows.py | 4 +-- src/certfuzz/tools/linux/bff_stats.py | 2 +- .../tools/linux/create_crasher_script.py | 2 +- src/certfuzz/tools/linux/minimize.py | 2 +- src/certfuzz/tools/linux/minimizer_plot.py | 2 +- src/certfuzz/tools/linux/repro.py | 2 +- src/certfuzz/tools/windows/clean_windows.py | 9 +++--- src/certfuzz/tools/windows/minimize.py | 7 +++-- src/certfuzz/tools/windows/repro.py | 8 ++--- 26 files changed, 70 insertions(+), 90 deletions(-) rename src/certfuzz/{campaign => }/config/__init__.py (100%) rename src/certfuzz/{campaign => }/config/config_base.py (96%) rename src/certfuzz/{campaign => }/config/config_linux.py (99%) rename src/certfuzz/{campaign => }/config/config_windows.py (96%) rename src/certfuzz/{campaign => }/config/errors.py (100%) delete mode 100644 src/certfuzz/test/campaign/config/test_config_pkg.py rename src/certfuzz/test/{campaign => }/config/__init__.py (100%) rename src/certfuzz/test/{campaign => }/config/test_config_base.py (94%) rename src/certfuzz/test/{campaign => }/config/test_config_linux.py (96%) rename src/certfuzz/test/{campaign => }/config/test_config_windows.py (93%) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 7b2570b..6584cb9 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -12,17 +12,17 @@ from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.campaign.errors import CampaignScriptError -from certfuzz.debuggers import crashwrangler #@UnusedImport -from certfuzz.debuggers import gdb #@UnusedImport +from certfuzz.config.config_linux import LinuxConfig +from certfuzz.debuggers import crashwrangler # @UnusedImport +from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.debuggers.registration import verify_supported_platform from certfuzz.file_handlers.tmp_reaper import TmpReaper +from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools import subprocess_helper as subp +from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.watchdog import WatchDog -from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file -from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.iteration.iteration_linux import LinuxIteration -from certfuzz.campaign.config.config_linux import LinuxConfig logger = logging.getLogger(__name__) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 2a015b3..4b1285a 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -11,11 +11,11 @@ from threading import Timer from certfuzz.campaign.campaign_base import CampaignBase +from certfuzz.config.config_windows import WindowsConfig from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.fuzzers.errors import FuzzerExhaustedError -from certfuzz.runners.killableprocess import Popen -from certfuzz.campaign.config.config_windows import WindowsConfig from certfuzz.iteration.iteration_windows import WindowsIteration +from certfuzz.runners.killableprocess import Popen logger = logging.getLogger(__name__) @@ -66,7 +66,7 @@ def __init__(self, config_file, result_dir=None, debug=False): def _read_config_file(self): CampaignBase._read_config_file(self) - #read configs + # read configs with WindowsConfig(self.config_file) as cfgobj: self.config = cfgobj.config self.configdate = cfgobj.configdate diff --git a/src/certfuzz/campaign/config/__init__.py b/src/certfuzz/config/__init__.py similarity index 100% rename from src/certfuzz/campaign/config/__init__.py rename to src/certfuzz/config/__init__.py diff --git a/src/certfuzz/campaign/config/config_base.py b/src/certfuzz/config/config_base.py similarity index 96% rename from src/certfuzz/campaign/config/config_base.py rename to src/certfuzz/config/config_base.py index c4c52f1..a18b76f 100644 --- a/src/certfuzz/campaign/config/config_base.py +++ b/src/certfuzz/config/config_base.py @@ -7,7 +7,7 @@ import os.path import yaml -from certfuzz.campaign.config.errors import ConfigError +from certfuzz.config.errors import ConfigError logger = logging.getLogger(__name__) diff --git a/src/certfuzz/campaign/config/config_linux.py b/src/certfuzz/config/config_linux.py similarity index 99% rename from src/certfuzz/campaign/config/config_linux.py rename to src/certfuzz/config/config_linux.py index d5b9127..c3fd2df 100644 --- a/src/certfuzz/campaign/config/config_linux.py +++ b/src/certfuzz/config/config_linux.py @@ -10,7 +10,7 @@ import shlex import shutil -from certfuzz.campaign.config.config_base import ConfigBase +from certfuzz.config.config_base import ConfigBase from certfuzz.fuzztools import filetools diff --git a/src/certfuzz/campaign/config/config_windows.py b/src/certfuzz/config/config_windows.py similarity index 96% rename from src/certfuzz/campaign/config/config_windows.py rename to src/certfuzz/config/config_windows.py index 7bac383..d2597a1 100644 --- a/src/certfuzz/campaign/config/config_windows.py +++ b/src/certfuzz/config/config_windows.py @@ -7,8 +7,8 @@ import shlex from string import Template -from certfuzz.campaign.config.config_base import ConfigBase -from certfuzz.campaign.config.errors import ConfigError +from certfuzz.config.config_base import ConfigBase +from certfuzz.config.errors import ConfigError from certfuzz.helpers import quoted diff --git a/src/certfuzz/campaign/config/errors.py b/src/certfuzz/config/errors.py similarity index 100% rename from src/certfuzz/campaign/config/errors.py rename to src/certfuzz/config/errors.py diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/crash/crash_windows.py index 4760fb8..c83e01a 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/crash/crash_windows.py @@ -7,7 +7,7 @@ import logging import os -from certfuzz.campaign.config.config_windows import get_command_args_list +from certfuzz.config.config_windows import get_command_args_list from certfuzz.crash.crash_base import Testcase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import best_effort_move @@ -159,9 +159,9 @@ def debug_once(self): # add debugger results to our own attributes self.is_crash = self.parsed_outputs[0].is_crash self.dbg_type = self.parsed_outputs[0]._key - #self.exp = self.parsed_outputs[0].exp + # self.exp = self.parsed_outputs[0].exp self.faddr = self.parsed_outputs[0].faddr - #self.crash_hash = self.parsed_outputs[0].crash_hash + # self.crash_hash = self.parsed_outputs[0].crash_hash def get_signature(self): self.signature = self.crash_hash diff --git a/src/certfuzz/helpers/misc.py b/src/certfuzz/helpers/misc.py index 6bdbb05..1e94c07 100644 --- a/src/certfuzz/helpers/misc.py +++ b/src/certfuzz/helpers/misc.py @@ -3,11 +3,12 @@ @organization: cert.org ''' +import logging import platform +from pprint import pformat, pprint import random import string -from pprint import pformat, pprint -import logging + my_os = platform.system() @@ -32,7 +33,7 @@ def random_str(length=1): def bitswap(input_byte): - bits = [1, 2, 4, 8, 16, 32, 64, 128] + bits = [2 ** y for y in range(8)] backwards = list(bits) backwards.reverse() # 1 -> 128 diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index f036fe8..ee92eb8 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -8,7 +8,7 @@ import os import string -from certfuzz.campaign.config.config_windows import get_command_args_list +from certfuzz.config.config_windows import get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers.output_parsers import DebuggerFileError from certfuzz.file_handlers.basicfile import BasicFile @@ -22,7 +22,7 @@ from certfuzz.testcase_pipeline.tc_pipeline_windows import WindowsTestCasePipeline -#from certfuzz.iteration.iteration_base import IterationBase2 +# from certfuzz.iteration.iteration_base import IterationBase2 logger = logging.getLogger(__name__) IOERROR_COUNT = 0 diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 6e8255a..1d7e48a 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -19,7 +19,7 @@ import time from certfuzz.runners.errors import RunnerArchitectureError, RunnerRegistryError from certfuzz.runners.errors import RunnerError -from certfuzz.campaign.config.config_windows import get_command_args_list +from certfuzz.config.config_windows import get_command_args_list from certfuzz.fuzztools.filetools import find_or_create_dir logger = logging.getLogger(__name__) diff --git a/src/certfuzz/test/campaign/config/test_config_pkg.py b/src/certfuzz/test/campaign/config/test_config_pkg.py deleted file mode 100644 index 56565c6..0000000 --- a/src/certfuzz/test/campaign/config/test_config_pkg.py +++ /dev/null @@ -1,30 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.campaign.config - - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.campaign.config - api_list = ['parse_yaml', - 'Config', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/campaign/test_campaign_windows.py b/src/certfuzz/test/campaign/test_campaign_windows.py index 439c036..0fe3525 100644 --- a/src/certfuzz/test/campaign/test_campaign_windows.py +++ b/src/certfuzz/test/campaign/test_campaign_windows.py @@ -3,11 +3,12 @@ @author: adh ''' +import shutil +import tempfile import unittest + from certfuzz.campaign.campaign_windows import WindowsCampaign -import tempfile -import shutil -from certfuzz.campaign.config.errors import ConfigError +from certfuzz.config.errors import ConfigError class Test(unittest.TestCase): @@ -22,5 +23,5 @@ def test_init_without_config(self): self.assertRaises(ConfigError, WindowsCampaign, cfgfile) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/campaign/config/__init__.py b/src/certfuzz/test/config/__init__.py similarity index 100% rename from src/certfuzz/test/campaign/config/__init__.py rename to src/certfuzz/test/config/__init__.py diff --git a/src/certfuzz/test/campaign/config/test_config_base.py b/src/certfuzz/test/config/test_config_base.py similarity index 94% rename from src/certfuzz/test/campaign/config/test_config_base.py rename to src/certfuzz/test/config/test_config_base.py index 5c00201..93fd5e1 100644 --- a/src/certfuzz/test/campaign/config/test_config_base.py +++ b/src/certfuzz/test/config/test_config_base.py @@ -4,14 +4,17 @@ @organization: cert.org ''' -import unittest -import tempfile -import shutil -import yaml import os -from certfuzz.campaign import config import pprint -from certfuzz.campaign.config.errors import ConfigError +import shutil +import tempfile +import unittest + +import yaml + +from certfuzz import config +from certfuzz.config.errors import ConfigError + _count = 0 def _counter(): @@ -92,5 +95,5 @@ def test_load(self): self.assertEqual(c.__getattribute__(k), v) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/campaign/config/test_config_linux.py b/src/certfuzz/test/config/test_config_linux.py similarity index 96% rename from src/certfuzz/test/campaign/config/test_config_linux.py rename to src/certfuzz/test/config/test_config_linux.py index b229ac6..d0e76bc 100644 --- a/src/certfuzz/test/campaign/config/test_config_linux.py +++ b/src/certfuzz/test/config/test_config_linux.py @@ -11,9 +11,9 @@ import yaml -from certfuzz.campaign.config.config_linux import LinuxConfig, MINIMIZED_EXT -import certfuzz.campaign.config.config_linux as cl -from certfuzz.campaign.config.errors import ConfigError +from certfuzz.config.config_linux import LinuxConfig, MINIMIZED_EXT +import certfuzz.config.config_linux as cl +from certfuzz.config.errors import ConfigError CFG = ''' diff --git a/src/certfuzz/test/campaign/config/test_config_windows.py b/src/certfuzz/test/config/test_config_windows.py similarity index 93% rename from src/certfuzz/test/campaign/config/test_config_windows.py rename to src/certfuzz/test/config/test_config_windows.py index d74fed2..2e1dd3f 100644 --- a/src/certfuzz/test/campaign/config/test_config_windows.py +++ b/src/certfuzz/test/config/test_config_windows.py @@ -3,14 +3,17 @@ @organization: cert.org ''' -import unittest -from certfuzz.campaign.config.config_windows import WindowsConfig +import logging import os -import yaml -import tempfile import shutil -from certfuzz.campaign.config.errors import ConfigError -import logging +import tempfile +import unittest + +import yaml + +from certfuzz.config.config_windows import WindowsConfig +from certfuzz.config.errors import ConfigError + logger = logging.getLogger() hdlr = logging.FileHandler(os.devnull) @@ -89,5 +92,5 @@ def test_validation(self): self.assertEqual(10, self.counter) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index a95d5c4..58e7638 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -8,12 +8,12 @@ import shutil from certfuzz import debuggers -from certfuzz.campaign.config.config_windows import get_command_args_list +from certfuzz.config.config_windows import get_command_args_list from certfuzz.fuzztools import filetools from certfuzz.iteration.errors import IterationError from certfuzz.minimizer import WindowsMinimizer as Minimizer -from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.testcase_pipeline.errors import TestCasePipelineError +from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase logger = logging.getLogger(__name__) diff --git a/src/certfuzz/tools/linux/bff_stats.py b/src/certfuzz/tools/linux/bff_stats.py index c72fd41..4a0c363 100755 --- a/src/certfuzz/tools/linux/bff_stats.py +++ b/src/certfuzz/tools/linux/bff_stats.py @@ -9,7 +9,7 @@ import os import re import sys -from certfuzz.campaign.config.config_linux import LinuxConfig +from certfuzz.config.config_linux import LinuxConfig parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) diff --git a/src/certfuzz/tools/linux/create_crasher_script.py b/src/certfuzz/tools/linux/create_crasher_script.py index e0d3224..b7e9ab3 100755 --- a/src/certfuzz/tools/linux/create_crasher_script.py +++ b/src/certfuzz/tools/linux/create_crasher_script.py @@ -10,7 +10,7 @@ import os import re import sys -from certfuzz.campaign.config.config_linux import LinuxConfig +from certfuzz.config.config_linux import LinuxConfig parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index d2818a1..9938d70 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -8,7 +8,7 @@ import sys from certfuzz import debuggers -from certfuzz.campaign.config.config_linux import LinuxConfig +from certfuzz.config.config_linux import LinuxConfig from certfuzz.crash.bff_crash import BffCrash from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport diff --git a/src/certfuzz/tools/linux/minimizer_plot.py b/src/certfuzz/tools/linux/minimizer_plot.py index 13bae84..925ffeb 100755 --- a/src/certfuzz/tools/linux/minimizer_plot.py +++ b/src/certfuzz/tools/linux/minimizer_plot.py @@ -10,7 +10,7 @@ import re import sys -from certfuzz.campaign.config.config_linux import LinuxConfig +from certfuzz.config.config_linux import LinuxConfig import matplotlib.pyplot as plt diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index 256a2f5..22aa845 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -7,7 +7,7 @@ import os import re from subprocess import Popen -from certfuzz.campaign.config.config_linux import LinuxConfig +from certfuzz.config.config_linux import LinuxConfig try: from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file diff --git a/src/certfuzz/tools/windows/clean_windows.py b/src/certfuzz/tools/windows/clean_windows.py index d831aae..3b8276f 100644 --- a/src/certfuzz/tools/windows/clean_windows.py +++ b/src/certfuzz/tools/windows/clean_windows.py @@ -5,9 +5,10 @@ ''' import os -import time -import tempfile import pprint +import tempfile +import time + defaults = {'config': 'configs/bff.yaml', 'remove_results': False, @@ -25,7 +26,7 @@ def main(): import optparse try: from certfuzz.fuzztools.filetools import delete_contents_of - from certfuzz.campaign.config.config_base import ConfigBase + from certfuzz.config.config_base import ConfigBase except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH import sys @@ -33,7 +34,7 @@ def main(): parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) from certfuzz.fuzztools.filetools import delete_contents_of - from certfuzz.campaign.config.config_base import ConfigBase + from certfuzz.config.config_base import ConfigBase if not os.path.exists(defaults['config']): defaults['config'] = '../configs/bff.yaml' diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 16eaf40..eac4fd6 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -6,15 +6,16 @@ import logging import os -import sys import string +import sys + try: from certfuzz import debuggers from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer import WindowsMinimizer as Minimizer - from certfuzz.campaign.config.config_windows import WindowsConfig, get_command_args_list + from certfuzz.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport except ImportError: @@ -27,7 +28,7 @@ from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer import WindowsMinimizer as Minimizer - from certfuzz.campaign.config.config_windows import WindowsConfig, get_command_args_list + from certfuzz.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index 4248c64..a465907 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -5,15 +5,15 @@ ''' import logging import os -import string import re +import string from subprocess import Popen -from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file from certfuzz import debuggers -from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.campaign.config.config_windows import WindowsConfig, get_command_args_list +from certfuzz.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.debuggers import msec # @UnusedImport +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file logger = logging.getLogger() From 0d1d1b8fee5c91a26d024ef7183154c5e8ef633f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Nov 2014 14:05:25 -0500 Subject: [PATCH 0544/1169] refactor to push rng derivation down into the fuzzer class --- src/certfuzz/campaign/campaign_linux.py | 2 ++ src/certfuzz/campaign/campaign_windows.py | 7 +++---- src/certfuzz/fuzzers/fuzzer_base.py | 5 ++--- src/certfuzz/iteration/iteration_linux.py | 18 ++++++------------ src/certfuzz/iteration/iteration_windows.py | 4 +--- 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 6584cb9..84181ea 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -201,6 +201,8 @@ def _do_interval(self): r = sf.rangefinder.next_item() qf = not self.first_chunk + rng_seed = int(sf.md5, 16) + interval_limit = self.current_seed + self.seed_interval # start an iteration interval diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 4b1285a..4f43be5 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -190,7 +190,6 @@ def _do_interval(self): sf = self.seedfile_set.next_item() logger.info('Selected seedfile: %s', sf.basename) - rng_seed = int(sf.md5, 16) if self.current_seed % self.status_interval == 0: # cache our current state @@ -202,7 +201,7 @@ def _do_interval(self): # note that range does not include interval_limit logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) for seednum in xrange(self.current_seed, interval_limit): - self._do_iteration(sf, rng_seed, seednum) + self._do_iteration(sf, seednum) del sf # manually collect garbage @@ -211,11 +210,11 @@ def _do_interval(self): self.current_seed = interval_limit self.first_chunk = False - def _do_iteration(self, sf, rng_seed, seednum): + def _do_iteration(self, sf, seednum): # use a with...as to ensure we always hit # the __enter__ and __exit__ methods of the # newly created WindowsIteration() - with WindowsIteration(sf, rng_seed, seednum, self.config, self.fuzzer_cls, + with WindowsIteration(sf, seednum, self.config, self.fuzzer_cls, self.runner, self.debugger_module, self.dbg_class, self.keep_heisenbugs, self.keep_duplicates, self.cmd_template, self._crash_is_unique, diff --git a/src/certfuzz/fuzzers/fuzzer_base.py b/src/certfuzz/fuzzers/fuzzer_base.py index 19f04b3..a4e1cae 100644 --- a/src/certfuzz/fuzzers/fuzzer_base.py +++ b/src/certfuzz/fuzzers/fuzzer_base.py @@ -52,12 +52,11 @@ class Fuzzer(object): # child classes that are can set it themselves is_minimizable = False - def __init__(self, seedfile_obj, outdir_base, rng_seed, iteration, options): + def __init__(self, seedfile_obj, outdir_base, iteration, options): ''' Parameters get converted to attributes. @param local_seed_path: @param fuzz_output_path: - @param rng_seed: @param iteration: @param options: ''' @@ -65,7 +64,7 @@ def __init__(self, seedfile_obj, outdir_base, rng_seed, iteration, options): self.sf = seedfile_obj # TODO: rename tmpdir -> working_dir self.tmpdir = outdir_base - self.rng_seed = rng_seed + self.rng_seed = int(self.sf.md5, 16) self.iteration = iteration self.options = options diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 55ae0b3..6b8c2f0 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -27,6 +27,7 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No sf_set=None, rf=None, outdir=None): IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, sf_set, rf, uniq_func, cfg, r) + self.quiet_flag = quiet self.testcase_base_dir = os.path.join(self.outdir, 'crashers') @@ -56,19 +57,12 @@ def __enter__(self): check_ppid() return self.go - def __exit__(self, etype, value, traceback): - handled = IterationBase3.__exit__(self, etype, value, traceback) - - self.cfg.clean_tmpdir() - return handled - def _pre_fuzz(self): - fuzz_opts = self.config['fuzzer'] - self.fuzzer = ZzufFuzzer(self.sf, - self.working_dir, - self.rng_seed, - self.current_seed, - fuzz_opts) + fuzz_opts = self.cfg.config['fuzzer'] + self.fuzzer = ZzufFuzzer(self.seedfile, + self.working_dir, + self.seednum, + fuzz_opts) def _post_fuzz(self): self.r = self.fuzzer.range diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index ee92eb8..25b069e 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -32,13 +32,12 @@ class WindowsIteration(IterationBase3): tcpipeline_cls = WindowsTestCasePipeline - def __init__(self, seedfile, rng_seed, seednum, config, fuzzer_cls, + def __init__(self, seedfile, seednum, config, fuzzer_cls, runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, cmd_template, uniq_func, workdirbase, outdir, debug, sf_set, rf): IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, sf_set, rf, uniq_func, config, None) - self.rng_seed = rng_seed self.fuzzer_cls = fuzzer_cls self.runner = runner self.debugger_module = debugger @@ -131,7 +130,6 @@ def _pre_fuzz(self): fuzz_opts = self.cfg['fuzzer'] self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, - self.rng_seed, self.seednum, fuzz_opts) def _post_fuzz(self): From ba88f5d790506dd977a873186975384d9cbff4d7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Nov 2014 14:06:30 -0500 Subject: [PATCH 0545/1169] tune up for yaml config --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/config/config_linux.py | 22 +++------------------- src/certfuzz/iteration/iteration_linux.py | 3 +-- src/linux/conf.d/bff.yaml | 10 ++++++++++ 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 84181ea..39dd124 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -85,7 +85,7 @@ def _read_config_file(self): CampaignBase._read_config_file(self) with LinuxConfig(self.config_file) as cfgobj: - self.config = cfgobj.config + self.config = cfgobj self.configdate = cfgobj.configdate diff --git a/src/certfuzz/config/config_linux.py b/src/certfuzz/config/config_linux.py index c3fd2df..9dd9e83 100644 --- a/src/certfuzz/config/config_linux.py +++ b/src/certfuzz/config/config_linux.py @@ -99,6 +99,9 @@ def _set_derived_options(self): self._cmd = self.cmd_list self._args = self.cmd_list[1:] + # for backwards compatibility + self.watchdogfile = self.watchdog_file + # derived properties self.program_basename = os.path.basename(self.program).replace('"', '') self.uniq_log = os.path.join(self.output_dir, UNIQ_LOG) @@ -155,25 +158,6 @@ def get_minimized_file(self, outfile): new_filename = '%s-%s%s' % (root, MINIMIZED_EXT, ext) return os.path.join(head, new_filename) - def create_tmpdir(self): - # TODO: this should become part of campaign object - if not self.tmpdir: - self.tmpdir = filetools.mkdtemp(self.testscase_tmp_dir) - logger.debug("Created temp dir %s", self.tmpdir) - assert os.path.isdir(self.tmpdir) - - def clean_tmpdir(self): - # TODO: this should become part of campaign object - if self.tmpdir is None: - return - - if os.path.exists(self.tmpdir): - shutil.rmtree(self.tmpdir) - logger.debug("Removed temp dir %s", self.tmpdir) - assert not os.path.exists(self.tmpdir) - self.tmpdir = None - self.create_tmpdir() - def get_testcase_outfile(self, seedfile, s1): # TODO: this should become part of campaign object ''' diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 6b8c2f0..9962390 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -42,8 +42,7 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No self._analysis_needed = True - self.pipeline_options = { - 'use_valgrind': self.cfg.use_valgrind, + self.pipeline_options = {'use_valgrind': self.cfg.use_valgrind, 'use_pin_calltrace': self.cfg.use_pin_calltrace, 'minimize_crashers': self.cfg.minimizecrashers, 'minimize_to_string': self.cfg.minimize_to_string, diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 6fc6e13..8786f59 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -171,3 +171,13 @@ timeouts: watchdogtimeout: 3600 minimizertimeout: 3600 +# adding these sections for compatibility with FOE config style +fuzzer: + fuzzer: zzuf + fuzz_zip_container: False +runner: + runner: zzufrun + runtimeout: 5 +debugger: + debugger: gdb + runtimeout: 60 From d0a094dbb4149f4a805d3024135a551610d3d22a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Nov 2014 14:36:51 -0500 Subject: [PATCH 0546/1169] get the right config params --- src/certfuzz/iteration/iteration_linux.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 9962390..36acdad 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -72,10 +72,10 @@ def _post_fuzz(self): # decide if we can minimize this case later # do this here (and not sooner) because the fuzzer could # decide at runtime whether it is or is not minimizable - self.minimizable = self.fuzzer.is_minimizable and self.config['runoptions']['minimize'] + self.minimizable = self.fuzzer.is_minimizable and self.cfg.config['runoptions']['minimize'] def _pre_run(self): - options = self.options + options = self.cfg.config['runner'] cmd_template = '' fuzzed_file = self.fuzzer.output_file_path workingdir_base = self.working_dir From e194294d7fc23ddcfd21db0b2ca65df32e69e3b8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Nov 2014 14:37:09 -0500 Subject: [PATCH 0547/1169] use config opt consistent with windows --- src/certfuzz/runners/zzufrun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index c043b64..36b6f9a 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -22,7 +22,7 @@ def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): self._zzuf_args = None self._zzuf_log_basename = 'zzuf_log.txt' self._zzuf_log = os.path.join(self.workingdir, self._zzuf_log_basename) - self._quiet = options.get('quiet', True) + self._quiet = options.get('hideoutput', True) self._zzuf_basename = 'zzuf' def _get_zzuf_args(self): From 255903a84b828407a7d788a8e28f868fd008f6ef Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Nov 2014 14:37:31 -0500 Subject: [PATCH 0548/1169] correct calling of pipeline --- src/certfuzz/testcase_pipeline/tc_pipeline_base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 803501f..2d3411d 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -66,7 +66,8 @@ def _setup_analysis_pipeline(self): # verify | minimize | analyze | report logger.debug('Construct analysis pipeline') - setup_order = list(self.pipes).reverse() + setup_order = list(self.pipes) + setup_order.reverse() pipeline = self.analysis_pipeline @@ -78,7 +79,8 @@ def _setup_analysis_pipeline(self): pipeline = pipe_method() else: # prepend pipe_method upstream of pipeline - pipeline = pipe_method(target=pipeline) + targets = [pipeline] + pipeline = pipe_method(*targets) @coroutine def verify(self, *targets): From f5b247ed9bcc8bbc51fca2d2121f9d55227a164b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Nov 2014 14:38:00 -0500 Subject: [PATCH 0549/1169] catch up config --- src/linux/conf.d/bff.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 8786f59..5899aa0 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -172,6 +172,15 @@ timeouts: minimizertimeout: 3600 # adding these sections for compatibility with FOE config style + +runoptions: +# first_iteration: 0 +# seed_interval: 1 + minimize: True +# minimizer_timeout: 3600 +# keep_unique_faddr: False +# keep_all_duplicates: False +# recycle_crashers: False fuzzer: fuzzer: zzuf fuzz_zip_container: False From 3c9159eabca120ec8ef5b478ae3479713cb93eb3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Nov 2014 15:40:28 -0500 Subject: [PATCH 0550/1169] only process test cases when we have at least one --- src/certfuzz/iteration/iteration_base3.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index d6a9ad2..be55713 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -147,4 +147,5 @@ def go(self): logger.debug('go') self.fuzz() self.run() - self.process_testcases() + if len(self.testcases): + self.process_testcases() From d5fcb622fa56e5f0d9f221c2fb131aa51cb921e8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Nov 2014 15:41:03 -0500 Subject: [PATCH 0551/1169] use bytemut instead of zzuf for the fuzzing part --- src/certfuzz/iteration/iteration_linux.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 36acdad..2b1b8cb 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -15,6 +15,7 @@ from certfuzz.testcase_pipeline.tc_pipeline_linux import LinuxTestCasePipeline from certfuzz.runners.zzufrun import ZzufRunner from certfuzz.fuzzers.zzuf import ZzufFuzzer +from certfuzz.fuzzers.bytemut import ByteMutFuzzer logger = logging.getLogger(__name__) @@ -58,7 +59,7 @@ def __enter__(self): def _pre_fuzz(self): fuzz_opts = self.cfg.config['fuzzer'] - self.fuzzer = ZzufFuzzer(self.seedfile, + self.fuzzer = ByteMutFuzzer(self.seedfile, self.working_dir, self.seednum, fuzz_opts) From 6b1d4bdd6f758cd49ee6afd5300bd59a0852d705 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 08:19:44 -0500 Subject: [PATCH 0552/1169] use quoted method --- src/certfuzz/config/config_linux.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/config/config_linux.py b/src/certfuzz/config/config_linux.py index 9dd9e83..bab773c 100644 --- a/src/certfuzz/config/config_linux.py +++ b/src/certfuzz/config/config_linux.py @@ -12,6 +12,7 @@ from certfuzz.config.config_base import ConfigBase from certfuzz.fuzztools import filetools +from certfuzz.helpers.misc import quoted logger = logging.getLogger(__name__) @@ -94,7 +95,7 @@ def _set_derived_options(self): for index, cmd_part in enumerate(self.cmd_list): self.cmd_list[index] = os.path.expanduser(cmd_part) if re.search(' ', self.cmd_list[0]): - self.cmd_list[0] = '"' + self.cmd_list[0] + '"' + self.cmd_list[0] = quoted(self.cmd_list[0]) self.program = self.cmd_list[0] self._cmd = self.cmd_list self._args = self.cmd_list[1:] From 813a48745df1991fcd29a7e6862c21a0e84f9ae0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 08:35:37 -0500 Subject: [PATCH 0553/1169] use the correct command line from config, add some debugging --- src/certfuzz/iteration/iteration_linux.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 2b1b8cb..414d423 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -77,9 +77,11 @@ def _post_fuzz(self): def _pre_run(self): options = self.cfg.config['runner'] - cmd_template = '' + cmd_template = self.cfg.config['target']['cmdline'] fuzzed_file = self.fuzzer.output_file_path workingdir_base = self.working_dir + from pprint import pformat + logger.debug(pformat(self.__dict__)) self.runner = ZzufRunner(options, cmd_template, fuzzed_file, workingdir_base) def _run(self): From 1cd38a6fbdf3fbb7c1125b21fd02d9a0fc2d593d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 08:35:53 -0500 Subject: [PATCH 0554/1169] refactor last line getter --- src/certfuzz/fuzztools/zzuflog.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuflog.py b/src/certfuzz/fuzztools/zzuflog.py index 55cffaf..deecc75 100644 --- a/src/certfuzz/fuzztools/zzuflog.py +++ b/src/certfuzz/fuzztools/zzuflog.py @@ -66,15 +66,12 @@ def _get_last_line(self): range, result, and complete line from the last line of the file. @return: string, string, string, string ''' - f = open(self.infile, 'r') - last_line = "" - try: - for l in f: - last_line = l - finally: - f.close() - - return last_line.strip() + with open(self.infile, 'r') as f: + for line in f: + # don't do anything, just iterate through until we're done with lines + pass + # when you get here line contains the last line read from the file + return line.strip() def _parse_line(self): seed = False From 7bbe7ffb8ccab8a9b679fb01284e2df3f935330f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 08:49:01 -0500 Subject: [PATCH 0555/1169] pep8 cleanup --- src/certfuzz/iteration/iteration_linux.py | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 414d423..a2eb1b6 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -114,22 +114,22 @@ def _post_run(self): def _construct_testcase(self): with ZzufTestCase(seedfile=self.seedfile, seed=self.seednum, - range=self._zzuf_range, - working_dir=self.working_dir) as ztc: + range=self._zzuf_range, + working_dir=self.working_dir) as ztc: ztc.generate() fuzzedfile = BasicFile(ztc.outfile) logger.info('Building testcase object') with BffCrash(cfg=self.cfg, - seedfile=self.seedfile, - fuzzedfile=fuzzedfile, - program=self.cfg.program, - debugger_timeout=self.cfg.debugger_timeout, - killprocname=self.cfg.killprocname, - backtrace_lines=self.cfg.backtracelevels, - crashers_dir=self.testcase_base_dir, - workdir_base=self.working_dir, - seednum=self.seednum, - range=self.r) as testcase: + seedfile=self.seedfile, + fuzzedfile=fuzzedfile, + program=self.cfg.program, + debugger_timeout=self.cfg.debugger_timeout, + killprocname=self.cfg.killprocname, + backtrace_lines=self.cfg.backtracelevels, + crashers_dir=self.testcase_base_dir, + workdir_base=self.working_dir, + seednum=self.seednum, + range=self.r) as testcase: # put it on the list for the analysis pipeline self.testcases.append(testcase) From 83b81c850b153bd001c230c5fdac067e92104581 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 08:49:32 -0500 Subject: [PATCH 0556/1169] clean up debug logging --- src/certfuzz/iteration/iteration_linux.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index a2eb1b6..ed4fe47 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -16,6 +16,7 @@ from certfuzz.runners.zzufrun import ZzufRunner from certfuzz.fuzzers.zzuf import ZzufFuzzer from certfuzz.fuzzers.bytemut import ByteMutFuzzer +from pprint import pformat logger = logging.getLogger(__name__) @@ -80,8 +81,9 @@ def _pre_run(self): cmd_template = self.cfg.config['target']['cmdline'] fuzzed_file = self.fuzzer.output_file_path workingdir_base = self.working_dir - from pprint import pformat - logger.debug(pformat(self.__dict__)) + + for line in pformat(self.__dict__).splitlines(): + logger.debug(line) self.runner = ZzufRunner(options, cmd_template, fuzzed_file, workingdir_base) def _run(self): From 00e73f0f2fa011f703083bbceaf362d1978955a1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 09:26:20 -0500 Subject: [PATCH 0557/1169] fix how we deal with zzuf output --- src/certfuzz/config/config_linux.py | 2 -- src/certfuzz/fuzztools/zzuflog.py | 14 +------------- src/certfuzz/iteration/iteration_linux.py | 2 +- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/certfuzz/config/config_linux.py b/src/certfuzz/config/config_linux.py index bab773c..1130cf1 100644 --- a/src/certfuzz/config/config_linux.py +++ b/src/certfuzz/config/config_linux.py @@ -132,8 +132,6 @@ def get_command_args_list(self, seedfile): arglist.append(re.sub(SEEDFILE_REPLACE_STRING, seedfile, arg)) return arglist - def zzuf_log_out(self, mydir): - return os.path.join(mydir, ZZUF_LOG_FILE) def full_path_local_fuzz_dir(self, seedfile): ''' diff --git a/src/certfuzz/fuzztools/zzuflog.py b/src/certfuzz/fuzztools/zzuflog.py index deecc75..1f57558 100644 --- a/src/certfuzz/fuzztools/zzuflog.py +++ b/src/certfuzz/fuzztools/zzuflog.py @@ -6,7 +6,6 @@ @organization: cert.org ''' import logging -import os import re import filetools @@ -17,13 +16,12 @@ class ZzufLog: - def __init__(self, infile, outfile): + def __init__(self, infile): ''' Reads in and parses *the last line*. @param logfile: the zzuf log file to analyze ''' self.infile = infile - self.outfile = outfile self.line = self._get_last_line() # parsed will get set True in _parse_line if we successfully parse the line @@ -32,17 +30,7 @@ def __init__(self, infile, outfile): self.was_killed = self._was_killed() self.was_out_of_memory = self._was_out_of_memory() - - try: - fp = open(self.outfile, 'a') - fp.write("%s\n" % self.line) - except Exception, e: - logger.warning('Error writing to %s: %s', self.outfile, e) - finally: - fp.close() - filetools.delete_files(self.infile) - assert not os.path.exists(self.infile) self.exitcode = '' self._set_exitcode() diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index ed4fe47..2db823d 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -99,7 +99,7 @@ def _post_run(self): # we must have seen a crash # get the results - zzuf_log = ZzufLog(self.cfg.zzuf_log_file, self.cfg.zzuf_log_out(self.seedfile.output_dir)) + zzuf_log = ZzufLog(self.runner.zzuf_log_path) # Don't generate cases for killed process or out-of-memory # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will From 02ca48dd34a957328418a19b1f443f6a3154cca9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 09:26:59 -0500 Subject: [PATCH 0558/1169] refactor kill & out of memory indicator detection --- src/certfuzz/fuzztools/zzuflog.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuflog.py b/src/certfuzz/fuzztools/zzuflog.py index 1f57558..c5d5679 100644 --- a/src/certfuzz/fuzztools/zzuflog.py +++ b/src/certfuzz/fuzztools/zzuflog.py @@ -14,6 +14,9 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) +KILL_INDICATORS = ['signal 9', 'SIGXFSZ', 'Killed', 'exit 137'] +OUT_OF_MEMORY_INDICATORS = ['signal 15', 'exit 143'] + class ZzufLog: def __init__(self, infile): @@ -28,8 +31,6 @@ def __init__(self, infile): self.parsed = False (self.seed, self.range, self.result) = self._parse_line() - self.was_killed = self._was_killed() - self.was_out_of_memory = self._was_out_of_memory() filetools.delete_files(self.infile) self.exitcode = '' @@ -95,14 +96,14 @@ def crash_logged(self, checkexit): # if you got here, consider it a crash return True - def _was_killed(self): - for kill_indicator in ['signal 9', 'SIGXFSZ', 'Killed', 'exit 137']: - if kill_indicator in self.result: - return True - return False - - def _was_out_of_memory(self): - for out_of_memory_indicator in ['signal 15', 'exit 143']: - if out_of_memory_indicator in self.result: - return True - return False + + @property + def was_killed(self): + return self._any_indicators_in_result(KILL_INDICATORS) + + @property + def was_out_of_memory(self): + return self._any_indicators_in_result(OUT_OF_MEMORY_INDICATORS) + + def _any_indicators_in_result(self, indicator_list): + return(any(indicator in self.result for indicator in indicator_list)) From 12527fb8ba5027ac9348709335d69cc563812ac9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 09:27:34 -0500 Subject: [PATCH 0559/1169] pep 8 cleanup --- src/certfuzz/iteration/iteration_base3.py | 10 +++++----- src/certfuzz/iteration/iteration_linux.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index be55713..43dd2c5 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -136,11 +136,11 @@ def record_tries(self): def process_testcases(self): # hand it off to our pipeline class with self.tcpipeline_cls(testcases=self.testcases, - uniq_func=self.uniq_func, - cfg=self.cfg, - options=self.pipeline_options, - outdir=self.outdir, - workdirbase=self.working_dir) as pipeline: + uniq_func=self.uniq_func, + cfg=self.cfg, + options=self.pipeline_options, + outdir=self.outdir, + workdirbase=self.working_dir) as pipeline: pipeline() def go(self): diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 2db823d..75d642a 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -61,9 +61,9 @@ def __enter__(self): def _pre_fuzz(self): fuzz_opts = self.cfg.config['fuzzer'] self.fuzzer = ByteMutFuzzer(self.seedfile, - self.working_dir, - self.seednum, - fuzz_opts) + self.working_dir, + self.seednum, + fuzz_opts) def _post_fuzz(self): self.r = self.fuzzer.range From 74d79ee11765a6dbff989ac96963dc96f3df5556 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 09:28:13 -0500 Subject: [PATCH 0560/1169] move conditional to process_testcases method --- src/certfuzz/iteration/iteration_base3.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 43dd2c5..fcbc32b 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -134,6 +134,10 @@ def record_tries(self): self.rf.record_tries(key=self.r.id, tries=1) def process_testcases(self): + if not len(self.testcases): + # short circuit if nothing to do + return + # hand it off to our pipeline class with self.tcpipeline_cls(testcases=self.testcases, uniq_func=self.uniq_func, @@ -147,5 +151,4 @@ def go(self): logger.debug('go') self.fuzz() self.run() - if len(self.testcases): - self.process_testcases() + self.process_testcases() From c4941ece6ee47c7f28cc0eb4f71b519ec4edfff0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 09:29:20 -0500 Subject: [PATCH 0561/1169] clean up analysis_needed --- src/certfuzz/iteration/iteration_linux.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 75d642a..787ae97 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -36,12 +36,11 @@ def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=No self._zzuf_range = None self._zzuf_line = None + # analysis is required in two cases: # 1) runner is not defined (self.runner == None) # 2) runner is defined, and detects crash (runner.saw_crash == True) # this takes care of case 1 by default - analysis_needed = True - self._analysis_needed = True self.pipeline_options = {'use_valgrind': self.cfg.use_valgrind, @@ -70,7 +69,6 @@ def _post_fuzz(self): if self.r: logger.info('Selected r: %s', self.r) - fuzzed_file_full_path = self.fuzzer.output_file_path # decide if we can minimize this case later # do this here (and not sooner) because the fuzzer could # decide at runtime whether it is or is not minimizable @@ -106,7 +104,7 @@ def _post_run(self): # report the exit code in its output log. The exit code is 128 + the signal number. self._analysis_needed = zzuf_log.crash_logged(self.cfg.copymode) - if not self.analysis_needed: + if not self._analysis_needed: return # store a few things for use downstream From 123a145807e519e512704a24d903660f1d7af465 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 13:12:19 -0500 Subject: [PATCH 0562/1169] rename function (+1 squashed commit) Squashed commits: [c31b053] refactor zzufrun module --- src/certfuzz/fuzztools/zzuf.py | 4 +- src/certfuzz/runners/zzufrun.py | 73 ++++++++++++++---------- src/certfuzz/test/fuzztools/test_zzuf.py | 8 +-- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index 5143bca..598b957 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -79,7 +79,7 @@ def __init__(self, dir, s1, s2, cmd, seedfile, file, copymode, ratiomin, ratioma self.timeout = timeout self.quiet = quiet - self.zzuf_args = self._get_zzuf_args() + self.zzuf_args = self._construct_zzuf_args() self.saw_crash = False def _get_go_fuzz_cmdline(self): @@ -108,7 +108,7 @@ def go(self): return self.saw_crash - def _get_zzuf_args(self): + def _construct_zzuf_args(self): ''' Builds an argument string for zzuf based on the passed parameters. @rtype: string diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 36b6f9a..af5c8d7 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -11,53 +11,68 @@ import logging from collections import deque from certfuzz.runners.errors import RunnerNotFoundError +import cmd +import shlex +from string import Template +from certfuzz.helpers.misc import quoted logger = logging.getLogger(__name__) +_zzuf_basename = 'zzuf' +_zzuf_loc = None + + +def _find_zzuf(): + global _zzuf_loc + _zzuf_loc = find_executable(_zzuf_basename) + + class ZzufRunner(Runner): def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): Runner.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) - self._zzuf_loc = None - self._zzuf_args = None + self._zzuf_log_basename = 'zzuf_log.txt' - self._zzuf_log = os.path.join(self.workingdir, self._zzuf_log_basename) + self.zzuf_log_path = os.path.join(self.workingdir, self._zzuf_log_basename) self._quiet = options.get('hideoutput', True) - self._zzuf_basename = 'zzuf' - def _get_zzuf_args(self): - self._zzuf_args = deque(['--signal', - '--ratio=0.0', - '--seed=0', - '--max-crashes=1', - '--max-usertime=5.00', - ]) + self._cmd_template = Template(cmd_template) + self._cmd = self._cmd_template.substitute(SEEDFILE=quoted(fuzzed_file)) + self._cmd_parts = shlex.split(self._cmd) + self._cmd_parts[0] = os.path.expanduser(self._cmd_parts[0]) - if self._quiet: - self._zzuf_args.appendleft('--quiet') - - def _find_zzuf(self): - self._zzuf_loc = find_executable(self._zzuf_basename) - if self._zzuf_loc is None: - raise RunnerNotFoundError('Unable to locate {}, $PATH={}'.format(self._zzuf_basename, os.environ['PATH'])) - - def __enter__(self): - self = Runner.__enter__(self) - - self._find_zzuf() - self._get_zzuf_args() - self._zzuf_args.appendleft(self._zzuf_loc) + self._zzuf_args = None + self._construct_zzuf_args() logger.debug('_zzuf_args=%s', self._zzuf_args) - return self + def _construct_zzuf_args(self): + if _zzuf_loc is None: + _find_zzuf() + # if it's still None, we have a problem + if _zzuf_loc is None: + raise RunnerNotFoundError('Unable to locate {}, $PATH={}'.format(_zzuf_basename, os.environ['PATH'])) + + args = [_zzuf_loc] + if self._quiet: + args.append('--quiet') + args.extend(['--signal', + '--ratio=0.0', + '--seed=0', + '--max-crashes=1', + '--max-usertime=5.00', + ]) + self._zzuf_args = args def _run(self): if not len(self._zzuf_args): raise RunnerError('_zzuf_args is empty') - with open(self.fuzzed_file, 'rb') as ff, open(self._zzuf_log, 'ab') as zo: - p = subprocess.Popen(self._zzuf_args, cwd=self.workingdir, stdin=ff, stderr=zo) + with open(self.fuzzed_file, 'rb') as ff, open(self.zzuf_log_path, 'wb') as zo: + cmd2run = self._zzuf_args + self._cmd_parts + logger.debug('RUN_CMD: {}'.format(' '.join(cmd2run))) + p = subprocess.Popen(cmd2run, cwd=self.workingdir, stdin=ff, stderr=zo) rc = p.wait() if rc != 0: - raise RunnerError('zzuf returncode: {}'.format(rc)) + self.saw_crash = True +# raise RunnerError('zzuf returncode: {}'.format(rc)) diff --git a/src/certfuzz/test/fuzztools/test_zzuf.py b/src/certfuzz/test/fuzztools/test_zzuf.py index a27c859..f6ce3df 100644 --- a/src/certfuzz/test/fuzztools/test_zzuf.py +++ b/src/certfuzz/test/fuzztools/test_zzuf.py @@ -56,7 +56,7 @@ def test_go_fuzz(self): def test_get_zzuf_args(self): - zzuf_args = self.z._get_zzuf_args() + zzuf_args = self.z._construct_zzuf_args() splitparts = lambda L: [re.sub('^--', '', s) for s in L.split(' ')] @@ -79,9 +79,9 @@ def test_get_zzuf_args(self): max_usertime = max_usertime_item.split('=')[1] self.assertEqual(float(max_usertime), 100.00) - # call _get_zzuf_args() again with copymode=False + # call _construct_zzuf_args() again with copymode=False self.z.copymode = False - zzuf_args = self.z._get_zzuf_args() + zzuf_args = self.z._construct_zzuf_args() # strip out the leading '--' from args to make it easier to verify parts = splitparts(zzuf_args) @@ -92,7 +92,7 @@ def test_get_zzuf_args(self): # check case where quiet is False self.z.quiet = False # strip out the leading '--' from args to make it easier to verify - parts = splitparts(self.z._get_zzuf_args()) + parts = splitparts(self.z._construct_zzuf_args()) self.assertFalse('quiet' in parts) if __name__ == "__main__": From f7b4aa10e9b9d98c746115e315d479faf4bd4027 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 13:12:42 -0500 Subject: [PATCH 0563/1169] fix how pipelines get built --- src/certfuzz/testcase_pipeline/tc_pipeline_base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 2d3411d..54f0242 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -69,8 +69,7 @@ def _setup_analysis_pipeline(self): setup_order = list(self.pipes) setup_order.reverse() - pipeline = self.analysis_pipeline - + pipeline = None for pipe_name in setup_order: pipe_method = getattr(self, pipe_name) logger.debug('Add {}() to pipeline'.format(pipe_name)) @@ -82,6 +81,8 @@ def _setup_analysis_pipeline(self): targets = [pipeline] pipeline = pipe_method(*targets) + self.analysis_pipeline = pipeline + @coroutine def verify(self, *targets): ''' @@ -215,6 +216,7 @@ def _post_report(self, testcase): def go(self): while not self.tc_candidate_q.empty(): testcase = self.tc_candidate_q.get() + logger.debug('Sending testcase %s to pipeline', testcase.signature) self.analysis_pipeline.send(testcase) def _copy_files(self, testcase): From 6f1e7cc8b20c93b8110804aae2052c3d8d6a0ede Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 12 Dec 2014 14:07:31 -0500 Subject: [PATCH 0564/1169] reinstate keep_duplicates regression from merge --- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 4 +++- src/linux/conf.d/bff.yaml | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 69c3e8c..f5575a7 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -67,7 +67,9 @@ def _verify(self, testcase): # crash_dir_found = filetools.find_or_create_dir(tc.result_dir) - tc.should_proceed_with_analysis = is_new_to_campaign and not crash_dir_found + keep_all = self.cfg.config['verifier'].get('keep_duplicates', False) + + tc.should_proceed_with_analysis = keep_all or (is_new_to_campaign and not crash_dir_found) if tc.should_proceed_with_analysis: logger.info('%s first seen at %d', tc.signature, tc.seednum) diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 5899aa0..c3c1d0e 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -117,6 +117,10 @@ zzuf: # Disabled by default due to amount of time that the minimization takes. # Stand-alone string minimization can be done using tools/minimize.py. +# Save crashes even when they are unique? If set to True, all +# crashing inputs are saved, even when their backtraces are not +# unique. + # Recycle crashing testcases as seed files for further fuzzing. # This can improve the number of unique crash hashes found, however this may # just demonstrate weaknesses in backtrace-based uniqueness determination @@ -131,11 +135,8 @@ verifier: minimizecrashers: True minimize_to_string: False recycle_crashers: False + keep_duplicates: False -# Save crashes even when they are unique? If set to True, all -# crashing inputs are saved, even when their backtraces are not -# unique. -keep_duplicates = False ################################################################ # APPLICATION TIMEOUTS From 8b971354433766617477d032bd2d1bd351108d18 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 15 Dec 2014 11:09:41 -0500 Subject: [PATCH 0565/1169] fix bug in logic when in debug mode so we don't delete campaign temp dir --- src/certfuzz/campaign/campaign_base.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 718ba74..f26dac1 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -225,12 +225,14 @@ def __exit__(self, etype, value, mytraceback): # if you got here, nothing has handled the error # so log it and keep going self._log_unhandled_exception(etype, value, mytraceback) - if self.debug: - cleanup = False - logger.debug('Skipping cleanup since we are in debug mode.') - if cleanup: - self._cleanup_workdir() + if self.debug and etype: + # short out if in debug mode and an error occurred + logger.debug('Skipping cleanup since we are in debug mode.') + return handled + + # debug not set, so we should clean up + self._cleanup_workdir() return handled From 21ad2e47b0c318e0cfc81325b38d985d95bd0d30 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 15 Dec 2014 13:51:18 -0500 Subject: [PATCH 0566/1169] set up zzuf before we hit the __enter__ method --- src/certfuzz/fuzztools/zzuf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index 598b957..7abba6b 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -29,10 +29,10 @@ def __init__(self, seedfile, seed, range, working_dir): self.range = range self.working_dir = working_dir self.outfile = None - - def __enter__(self): self._set_outfile() self._set_cmdline() + + def __enter__(self): return self def __exit__(self, etype, value, traceback): From 30abc8d3c7350794bd656c8460090016bde79a96 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 15 Dec 2014 13:52:07 -0500 Subject: [PATCH 0567/1169] refactor test for analysis needed to outside of target loop --- src/certfuzz/testcase_pipeline/tc_pipeline_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 54f0242..93a6232 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -99,10 +99,10 @@ def verify(self, *targets): self._verify(testcase) self._post_verify(testcase) - for target in targets: - if testcase.should_proceed_with_analysis: - # we're ready to proceed with this testcase - # so send it downstream + if testcase.should_proceed_with_analysis: + # we're ready to proceed with this testcase + # so send it downstream + for target in targets: target.send(testcase) @coroutine From 748372854ea114dbea8cd93db01378590c34bdd0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 15 Dec 2014 13:52:30 -0500 Subject: [PATCH 0568/1169] minor refactor --- src/certfuzz/iteration/iteration_base3.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index fcbc32b..6df75b2 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -74,13 +74,14 @@ def __exit__(self, etype, value, traceback): else: self.record_failure() - if etype and self.debug: + if self.debug and etype: # leave it behind if we're in debug mode # and there's a problem logger.debug('Skipping cleanup since we are in debug mode.') - else: - rm_rf(self.working_dir) + return handled + # clean up + rm_rf(self.working_dir) return handled def _pre_fuzz(self): From f93c3df6bec0fe7015122e52cfcc956db937d14f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 15 Dec 2014 13:53:38 -0500 Subject: [PATCH 0569/1169] we can safely get rid of the zzuf test case stuff since our fuzzer object already dumped the fuzzed file to disk --- src/certfuzz/fuzztools/zzuf.py | 39 ----------------------- src/certfuzz/iteration/iteration_linux.py | 17 ++++------ 2 files changed, 6 insertions(+), 50 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py index 7abba6b..c6d2387 100644 --- a/src/certfuzz/fuzztools/zzuf.py +++ b/src/certfuzz/fuzztools/zzuf.py @@ -9,50 +9,11 @@ import subprocess from subprocess import CalledProcessError -import os - logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -class ZzufTestCase: - def __init__(self, seedfile, seed, range, working_dir): - ''' - @param seedfile: The original seed file to use - @param seed: The zzuf seed number to use - @param range: - @param outfile: - ''' - self.seedfile = seedfile - self.seed = seed - self.range = range - self.working_dir = working_dir - self.outfile = None - self._set_outfile() - self._set_cmdline() - - def __enter__(self): - return self - - def __exit__(self, etype, value, traceback): - return - - def _set_outfile(self): - # generate the test case file name - (root, ext) = os.path.splitext(self.seedfile.basename) - new_root = '{}-{}'.format(root, self.seed) - new_basename = new_root + ext - self.outfile = os.path.join(self.working_dir, new_basename) - logger.debug('Output file is %s', self.outfile) - - def _set_cmdline(self): - self.cmdline = 'cat %s | zzuf -s%s -r%s > %s' % (self.seedfile.path, self.seed, self.range, self.outfile) - - def generate(self): - subprocess.check_call(self.cmdline, shell=True) - - class Zzuf: def __init__(self, dir, s1, s2, cmd, seedfile, file, copymode, ratiomin, ratiomax, timeout, quiet=True): ''' diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 787ae97..0d03c35 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -9,14 +9,11 @@ from certfuzz.crash.bff_crash import BffCrash from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.ppid_observer import check_ppid -from certfuzz.fuzztools.zzuf import ZzufTestCase from certfuzz.fuzztools.zzuflog import ZzufLog from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.testcase_pipeline.tc_pipeline_linux import LinuxTestCasePipeline from certfuzz.runners.zzufrun import ZzufRunner -from certfuzz.fuzzers.zzuf import ZzufFuzzer from certfuzz.fuzzers.bytemut import ByteMutFuzzer -from pprint import pformat logger = logging.getLogger(__name__) @@ -80,8 +77,6 @@ def _pre_run(self): fuzzed_file = self.fuzzer.output_file_path workingdir_base = self.working_dir - for line in pformat(self.__dict__).splitlines(): - logger.debug(line) self.runner = ZzufRunner(options, cmd_template, fuzzed_file, workingdir_base) def _run(self): @@ -98,6 +93,10 @@ def _post_run(self): # we must have seen a crash # get the results zzuf_log = ZzufLog(self.runner.zzuf_log_path) + logger.debug("ZzufLog:") + from pprint import pformat + for line in pformat(zzuf_log.__dict__).splitlines(): + logger.debug(line) # Don't generate cases for killed process or out-of-memory # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will @@ -113,16 +112,12 @@ def _post_run(self): self._construct_testcase() def _construct_testcase(self): - with ZzufTestCase(seedfile=self.seedfile, seed=self.seednum, - range=self._zzuf_range, - working_dir=self.working_dir) as ztc: - ztc.generate() - fuzzedfile = BasicFile(ztc.outfile) + fuzzed_file = BasicFile(self.fuzzer.output_file_path) logger.info('Building testcase object') with BffCrash(cfg=self.cfg, seedfile=self.seedfile, - fuzzedfile=fuzzedfile, + fuzzedfile=fuzzed_file, program=self.cfg.program, debugger_timeout=self.cfg.debugger_timeout, killprocname=self.cfg.killprocname, From 46bac73eb627dc08070b82d064b201a77f78fab4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 17 Dec 2014 08:53:34 -0500 Subject: [PATCH 0570/1169] remove dead code --- src/certfuzz/fuzztools/zzuf.py | 102 --------------------------------- 1 file changed, 102 deletions(-) delete mode 100644 src/certfuzz/fuzztools/zzuf.py diff --git a/src/certfuzz/fuzztools/zzuf.py b/src/certfuzz/fuzztools/zzuf.py deleted file mode 100644 index c6d2387..0000000 --- a/src/certfuzz/fuzztools/zzuf.py +++ /dev/null @@ -1,102 +0,0 @@ -''' -Created on Oct 25, 2010 - -Provides wrapper facilities for zzuf - -@organization: cert.org -''' -import logging - -import subprocess -from subprocess import CalledProcessError - -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - - -class Zzuf: - def __init__(self, dir, s1, s2, cmd, seedfile, file, copymode, ratiomin, ratiomax, timeout, quiet=True): - ''' - - @param dir: - @param s1: The starting seed - @param s2: The ending seed - @param cmd: The command to run - @param file: The zzuf output file - @param copymode: - @param ratiomin: - @param ratiomax: - @param timeout: A float timeout - ''' - self.dir = dir - self.s1 = s1 - self.s2 = s2 - self.cmd = cmd - self.include = seedfile - self.file = file - self.copymode = copymode - self.ratiomin = ratiomin - self.ratiomax = ratiomax - self.timeout = timeout - self.quiet = quiet - - self.zzuf_args = self._construct_zzuf_args() - self.saw_crash = False - - def _get_go_fuzz_cmdline(self): - if self.quiet: - # if we are in quiet mode (default), redirect stderr to self.file - template = "cd %s && zzuf %s %s 2> %s" - else: - # if we are not in quiet mode, then we want both stderr and stdout - # on the console, but only stderr goes to self.file - template = "cd %s && zzuf %s %s 3>&1 1>&2 2>&3 | tee %s" - cmdline = template % (self.dir, self.zzuf_args, self.cmd, self.file) - logger.info(cmdline) - return cmdline - - def go(self): - ''' - Changes directory to then starts a zzuf run with the - given parameters. - ''' - command = self._get_go_fuzz_cmdline() - - try: - subprocess.check_call(command, shell=True) - except CalledProcessError: - self.saw_crash = True - - return self.saw_crash - - def _construct_zzuf_args(self): - ''' - Builds an argument string for zzuf based on the passed parameters. - @rtype: string - ''' - parts = [] - if self.quiet: - parts.append("quiet") - parts.append('include=%s' % self.include) - parts.append("signal") - parts.append("max-crashes=1") - parts.append("ratio=%6f:%6f" % (self.ratiomin, self.ratiomax)) - parts.append("max-usertime=%.2f" % self.timeout) - - # zzuf supports a "copy" mode, where LD_PRELOAD is not used to hook into the - # target process. If zzuf.cfg specifies copy mode, then the appropriate options - # will be added to the zzuf command line to enable this mode. - # Some applications do not behave properly with zzuf loaded via LD_PRELOAD. - # Those applications should be fuzzed in copy mode, which also specifies the option - # to look at the process exit code to indicate failures. This works well for - # programs that are launched by a shell script. - if (self.copymode): - parts.append("opmode=copy") - - if self.s1 == self.s2: - parts.append("seed=%d" % self.s1) - else: - parts.append("seed=%d:%d" % (self.s1, self.s2)) - - # prefix everything with a "--" then build the string - return " ".join(["--%s" % p for p in parts]) From 03f8e0dadcd50ca58d4cba8de6354b82e08d1220 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 17 Dec 2014 08:53:49 -0500 Subject: [PATCH 0571/1169] bff uses yaml now --- src/linux/batch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index 155350f..8d907bf 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -94,7 +94,7 @@ cd $scriptlocation echo "Using python interpreter: $mypython" if [[ -f "$scriptlocation/bff.py" ]]; then - $mypython $scriptlocation/bff.py --config=$scriptlocation/conf.d/bff.cfg "$@" + $mypython $scriptlocation/bff.py --config=$scriptlocation/conf.d/bff.yaml "$@" else read -p "Cannot find $scriptlocation/bff.py Please verify script locations." fi From f8d0604a1b8f22e40ca29edefa89334ce057ba05 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 18 Dec 2014 09:08:10 -0500 Subject: [PATCH 0572/1169] give test case objects a reasonable __repr__ --- src/certfuzz/crash/testcase_base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/crash/testcase_base.py b/src/certfuzz/crash/testcase_base.py index 589ea9a..5b7f546 100644 --- a/src/certfuzz/crash/testcase_base.py +++ b/src/certfuzz/crash/testcase_base.py @@ -8,6 +8,7 @@ import tempfile from certfuzz.fuzztools import hamming +from pprint import pformat logger = logging.getLogger(__name__) @@ -44,6 +45,9 @@ def _setup_workdir(self): prefix=self._tmp_pfx, dir=self.workdir_base) + def __repr__(self): + return pformat(self.__dict__) + def _teardown_workdir(self): shutil.rmtree(self.working_dir) self.working_dir = None From ff91c4dc351d30e32b396bad87d607f16a9ea3f4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 18 Dec 2014 09:09:11 -0500 Subject: [PATCH 0573/1169] move logging into _report method --- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index f5575a7..79c787e 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -140,14 +140,13 @@ def _pre_report(self, testcase): uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) logger.info('%s first seen at %d', testcase.signature, testcase.seednum) + def _report(self, testcase): + self._copy_files(testcase) # whether it was unique or not, record some details for posterity # record the details of this crash so we can regenerate it later if needed testcase.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', testcase.seedfile.basename, testcase.seednum, testcase.range, testcase.fuzzedfile.path) testcase.logger.info('PC=%s', testcase.pc) - def _report(self, testcase): - self._copy_files(testcase) - def _post_report(self, testcase): # always clean up after yourself testcase.clean_tmpdir() From cd17f7083f5265823e0cc4e6d3e5048fd59672cd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 18 Dec 2014 09:18:21 -0500 Subject: [PATCH 0574/1169] organize imports --- src/certfuzz/testcase_pipeline/tc_pipeline_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 93a6232..08616c3 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -6,14 +6,14 @@ import Queue import abc import logging +import os +import shutil from certfuzz.analyzers.errors import AnalyzerEmptyOutputError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file +from certfuzz.fuzztools import filetools from certfuzz.helpers.coroutine import coroutine from certfuzz.testcase_pipeline.errors import TestCasePipelineError -import os -from certfuzz.fuzztools import filetools -import shutil logger = logging.getLogger(__name__) From 46b7e461f5498e7be1ba262865f3d8c10dc37ada Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 19 Dec 2014 11:47:28 -0500 Subject: [PATCH 0575/1169] fix file copy of test cases --- .../testcase_pipeline/tc_pipeline_base.py | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 08616c3..12cf42d 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -35,6 +35,7 @@ def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, self.options = options self.uniq_func = uniq_func self.outdir = outdir + self.tc_dir = os.path.join(self.outdir, 'crashers') self.working_dir = workdirbase @@ -52,6 +53,7 @@ def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, def __enter__(self): self._setup_analysis_pipeline() + filetools.mkdir_p(self.tc_dir) return self.go def __exit__(self, etype, value, traceback): @@ -99,6 +101,10 @@ def verify(self, *targets): self._verify(testcase) self._post_verify(testcase) + logger.debug('Testcase data:') + for line in testcase.__repr__().splitlines(): + logger.debug(line) + if testcase.should_proceed_with_analysis: # we're ready to proceed with this testcase # so send it downstream @@ -208,7 +214,9 @@ def _pre_report(self, testcase): @abc.abstractmethod def _report(self, testcase): - pass + logger.debug('Testcase contents:') + for line in testcase.__repr__().splitlines(): + logger.debug(line) def _post_report(self, testcase): pass @@ -220,21 +228,16 @@ def go(self): self.analysis_pipeline.send(testcase) def _copy_files(self, testcase): - if not self.outdir: - raise TestCasePipelineError('No outdir set') - - logger.debug('target_base=%s', self.outdir) - - target_dir = testcase.result_dir + dst_dir = os.path.join(self.tc_dir, testcase.signature) + # ensure target dir exists already (it might because of crash logging) + filetools.mkdir_p(dst_dir) - if os.path.exists(target_dir): - logger.debug('Repeat crash, will not copy to %s', target_dir) - return + src_dir = testcase.tempdir + if not os.path.exists(src_dir): + raise TestCasePipelineError('Testcase tempdir not found: %s', src_dir) - # make sure target_base exists already - filetools.find_or_create_dir(self.outdir) - logger.debug('Copying to %s', target_dir) - shutil.copytree(testcase.tempdir, target_dir) + src_paths = [os.path.join(src_dir, f) for f in os.listdir(src_dir)] - if not os.path.exists(target_dir): - raise TestCasePipelineError('Failed to create target dir %s', target_dir) + for f in src_paths: + logger.debug('Copy %s -> %s', f, dst_dir) + shutil.copy2(f, dst_dir) From 053ae2329a164e6dbe2a506d6fe6641df55675f7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 19 Dec 2014 14:38:23 -0500 Subject: [PATCH 0576/1169] no more zzuf.py, no need to test it --- src/certfuzz/test/fuzztools/test_zzuf.py | 100 ----------------------- 1 file changed, 100 deletions(-) delete mode 100644 src/certfuzz/test/fuzztools/test_zzuf.py diff --git a/src/certfuzz/test/fuzztools/test_zzuf.py b/src/certfuzz/test/fuzztools/test_zzuf.py deleted file mode 100644 index f6ce3df..0000000 --- a/src/certfuzz/test/fuzztools/test_zzuf.py +++ /dev/null @@ -1,100 +0,0 @@ -''' -Created on Apr 8, 2011 - -@organization: cert.org -''' -import unittest -import re - -from certfuzz.fuzztools.zzuf import Zzuf -from certfuzz.fuzztools.zzuf import ZzufTestCase - - -class Test(unittest.TestCase): - - def setUp(self): - self.z = Zzuf('a', 1, 2, 'd', 'e', 'f', - True, - 0.01, - 0.1, - 100 - ) - pass - - def tearDown(self): - pass - - def test_testcase_set_cmdline(self): - expected = "cat a | zzuf -sb -rc > x" - - class MockSf(object): - path = 'a' - sf = MockSf() - - testcase = ZzufTestCase(sf, 'b', 'c', 'd') - testcase.outfile = 'x' - testcase._set_cmdline() - self.assertEqual(testcase.cmdline, expected) - - def test_generate_test_case(self): - # can not test without real data - # see test_testcase_set_cmdline() - pass - - def test_get_go_fuzz_cmdline(self): - self.z.dir = 'dir' - self.z.zzuf_args = 'args' - self.z.get_command = 'get_command' - self.z.file = 'file' - expected = "cd dir && zzuf args d 2> file" - self.assertEqual(self.z._get_go_fuzz_cmdline(), expected) - - def test_go_fuzz(self): - # cannot test nondestructively - # see test_get_go_fuzz_cmdline() - pass - - def test_get_zzuf_args(self): - - zzuf_args = self.z._construct_zzuf_args() - - splitparts = lambda L: [re.sub('^--', '', s) for s in L.split(' ')] - - # strip out the leading '--' from args to make it easier to verify - parts = splitparts(zzuf_args) - - [self.assertTrue(s in parts, s) for s in ('signal', 'quiet')] - self.assertTrue('max-crashes=1' in parts) - self.assertTrue('opmode=copy' in parts) - - # check for presence of ratiomin and ratiomax - ratio_item = [x for x in parts if "ratio" in x].pop() - ratio_item = ratio_item.split('=')[1] # take the part after the equals sign - (rmin, rmax) = ratio_item.split(':') - self.assertEqual(float(rmin), 0.01) - self.assertEqual(float(rmax), 0.1) - - # TODO check for presence of timeout - max_usertime_item = [x for x in parts if "usertime" in x].pop() - max_usertime = max_usertime_item.split('=')[1] - self.assertEqual(float(max_usertime), 100.00) - - # call _construct_zzuf_args() again with copymode=False - self.z.copymode = False - zzuf_args = self.z._construct_zzuf_args() - - # strip out the leading '--' from args to make it easier to verify - parts = splitparts(zzuf_args) - - self.assertFalse('check-exit' in parts) - self.assertFalse('opmode=copy' in parts) - - # check case where quiet is False - self.z.quiet = False - # strip out the leading '--' from args to make it easier to verify - parts = splitparts(self.z._construct_zzuf_args()) - self.assertFalse('quiet' in parts) - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From a24b3ec7c29edda17778dc378360a35d37b3a294 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 19 Dec 2014 14:38:47 -0500 Subject: [PATCH 0577/1169] refactor seed file set creation --- src/certfuzz/campaign/campaign_base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index f26dac1..f844203 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -293,13 +293,15 @@ def _cleanup_workdir(self): logger.debug('Removed campaign working dir: %s', self.working_dir) def _create_seedfile_set(self): + if self.seedfile_set is not None: + return + logger.info('Building seedfile set') - if self.seedfile_set is None: - with SeedfileSet(campaign_id=self.campaign_id, - originpath=self.seed_dir_in, - localpath=self.seed_dir_local, - outputpath=self.sf_set_out) as sfset: - self.seedfile_set = sfset + with SeedfileSet(campaign_id=self.campaign_id, + originpath=self.seed_dir_in, + localpath=self.seed_dir_local, + outputpath=self.sf_set_out) as sfset: + self.seedfile_set = sfset @abc.abstractmethod def __getstate__(self): From 68a210227881391a46dc1e4562f58006590e8e1f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 19 Dec 2014 14:52:37 -0500 Subject: [PATCH 0578/1169] clean up --- src/certfuzz/campaign/campaign_linux.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 39dd124..d2d031f 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -88,7 +88,6 @@ def _read_config_file(self): self.config = cfgobj self.configdate = cfgobj.configdate - def _pre_enter(self): self._start_process_killer() self._set_unbuffered_stdout() @@ -201,7 +200,7 @@ def _do_interval(self): r = sf.rangefinder.next_item() qf = not self.first_chunk - rng_seed = int(sf.md5, 16) +# rng_seed = int(sf.md5, 16) interval_limit = self.current_seed + self.seed_interval @@ -217,9 +216,14 @@ def _do_interval(self): def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() - with LinuxIteration(cfg=self.config, seednum=seednum, seedfile=seedfile, r=range_obj, workdirbase=self.working_dir, quiet=quiet_flag, - uniq_func=self._crash_is_unique, - sf_set=self.seedfile_set, - rf=seedfile.rangefinder, - outdir=self.outdir) as iteration: + with LinuxIteration(cfg=self.config, + seednum=seednum, + seedfile=seedfile, + r=range_obj, + workdirbase=self.working_dir, + quiet=quiet_flag, + uniq_func=self._crash_is_unique, + sf_set=self.seedfile_set, + rf=seedfile.rangefinder, + outdir=self.outdir) as iteration: iteration() From e857c62bd35e44a1bd1a29b9fe02f8811c2acd5b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 19 Dec 2014 14:56:05 -0500 Subject: [PATCH 0579/1169] get rid of rangefinder log We don't really need a rangefinder log for every single seed file. --- src/certfuzz/file_handlers/seedfile.py | 11 +---------- src/certfuzz/file_handlers/seedfile_set.py | 3 +-- src/certfuzz/fuzztools/rangefinder.py | 13 +------------ src/certfuzz/test/fuzztools/test_rangefinder.py | 6 +++--- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/src/certfuzz/file_handlers/seedfile.py b/src/certfuzz/file_handlers/seedfile.py index 71d298e..c99bbe0 100644 --- a/src/certfuzz/file_handlers/seedfile.py +++ b/src/certfuzz/file_handlers/seedfile.py @@ -38,7 +38,6 @@ def __init__(self, output_base_dir, path): if not self.len > 0: raise SeedFileError('You cannot do bitwise fuzzing on a zero-length file: %s' % self.path) - self.output_dir = os.path.join(output_base_dir, self.md5) # use len for bytewise, bitlen for bitwise if self.len > 1: self.range_min = 1.0 / self.len @@ -47,15 +46,7 @@ def __init__(self, output_base_dir, path): self.range_min = 0 self.range_max = 1 - # output_dir might not exist, so create it - if not os.path.exists(self.output_dir): - filetools.make_directories(self.output_dir) - - self.rangefinder = self._get_rangefinder() - - def _get_rangefinder(self): - rf_log = os.path.join(self.output_dir, 'rangefinder.log') - return RangeFinder(self.range_min, self.range_max, rf_log) + self.rangefinder = RangeFinder(self.range_min, self.range_max) def __getstate__(self): ''' diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 4f0d9a3..e4e3b73 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -94,7 +94,6 @@ def copy_file_from_origin(self, f): filetools.copy_file(f.path, *targets) for target in targets: filetools.make_writable(target) - return 1 def paths(self): for x in self.things.values(): @@ -138,7 +137,7 @@ def next_item(self): # for k, old_sf in oldthings.iteritems(): # # update the seedfiles for ones that are still present # if k in self.things: -## print "%s in things..." % k +# # print "%s in things..." % k # self.things[k].__setstate__(old_sf) # self.sfcount += 1 diff --git a/src/certfuzz/fuzztools/rangefinder.py b/src/certfuzz/fuzztools/rangefinder.py index b0c3ef3..9cda110 100644 --- a/src/certfuzz/fuzztools/rangefinder.py +++ b/src/certfuzz/fuzztools/rangefinder.py @@ -23,7 +23,7 @@ class RangeFinder(MultiArmedBandit): 3. a probability distribution across all ranges as well as a picker method to randomly choose a range based on the probability distribution. ''' - def __init__(self, low, high, logfile): + def __init__(self, low, high): MultiArmedBandit.__init__(self) self.min = low @@ -34,11 +34,6 @@ def __init__(self, low, high, logfile): if self.max < self.min: raise RangeFinderError('max cannot be less than min') - self.logfile = logfile - logger.debug('Rangefinder log: %s', self.logfile) - - self._set_logger() - self._set_ranges() # def __getstate__(self): @@ -55,10 +50,6 @@ def __init__(self, low, high, logfile): # # recover the logger we had to drop in __getstate__ # self._set_logger() - def _set_logger(self): - self.logger = logging.getLogger(self.logfile) - self.logger.setLevel(logging.INFO) - def _exp_range(self, low, factor): high = low * factor # don't overshoot the high @@ -91,7 +82,5 @@ def _set_ranges(self): for r in ranges: self.add_item(r.id, r) - self.logger.debug('Ranges: [%s]', ', '.join([str(r) for r in self.things.keys()])) - def next_item(self): return self.next() diff --git a/src/certfuzz/test/fuzztools/test_rangefinder.py b/src/certfuzz/test/fuzztools/test_rangefinder.py index 208aab1..591d800 100644 --- a/src/certfuzz/test/fuzztools/test_rangefinder.py +++ b/src/certfuzz/test/fuzztools/test_rangefinder.py @@ -22,7 +22,7 @@ def setUp(self): (fd, f) = tempfile.mkstemp(text=True) os.close(fd) self.tmpfile = f - self.r = RangeFinder(self.min, self.max, self.tmpfile) + self.r = RangeFinder(self.min, self.max) def tearDown(self): self.delete_file(self.tmpfile) @@ -43,7 +43,7 @@ def test_get_ranges(self): # Ranges would be 0.375-0.601, 0.601-0.981, 0.981-0.999 # if it weren't for the fix that merges the last two # so we should only see two ranges - r = RangeFinder(0.375, 0.999, self.tmpfile) + r = RangeFinder(0.375, 0.999) self.assertEqual(len(r.things), 2) mins = sorted([thing.min for thing in r.things.itervalues()]) maxs = sorted([thing.max for thing in r.things.itervalues()]) @@ -98,5 +98,5 @@ def test_range_mean(self): # self.assertEqual(dict, type(state)) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From f2a397fb488775276a913f891f6ba525a2d774d8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 19 Dec 2014 15:34:44 -0500 Subject: [PATCH 0580/1169] refactor _do_interval into base class --- src/certfuzz/campaign/campaign_base.py | 34 ++++++++++++++++++++++- src/certfuzz/campaign/campaign_linux.py | 25 ----------------- src/certfuzz/campaign/campaign_windows.py | 28 +------------------ 3 files changed, 34 insertions(+), 53 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index f844203..bad6704 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -21,6 +21,8 @@ from certfuzz.runners.errors import RunnerArchitectureError, \ RunnerPlatformVersionError from certfuzz.version import __version__ +from certfuzz.file_handlers.tmp_reaper import TmpReaper +import gc logger = logging.getLogger(__name__) @@ -365,11 +367,41 @@ def _keep_going(self): ''' return True - @abc.abstractmethod def _do_interval(self): ''' Implements a loop over a set of iterations ''' + # wipe the tmp dir clean to try to avoid filling the VM disk + TmpReaper().clean_tmp() + + # choose seedfile + sf = self.seedfile_set.next_item() + logger.info('Selected seedfile: %s', sf.basename) + +# TODO: restore this +# if self.current_seed % self.status_interval == 0: +# # cache our current state +# self._save_state() + + r = sf.rangefinder.next_item() + qf = not self.first_chunk + +# rng_seed = int(sf.md5, 16) + + interval_limit = self.current_seed + self.seed_interval + + # start an iteration interval + # note that range does not include interval_limit + logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) + for seednum in xrange(self.current_seed, interval_limit): + self._do_iteration(sf, r, qf, seednum) + + del sf + # manually collect garbage + gc.collect() + + self.current_seed = interval_limit + self.first_chunk = False @abc.abstractmethod def _do_iteration(self): diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index d2d031f..cec99a0 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -16,7 +16,6 @@ from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.debuggers.registration import verify_supported_platform -from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools import subprocess_helper as subp from certfuzz.fuzztools.ppid_observer import check_ppid @@ -189,30 +188,6 @@ def _save_state(self): ''' pass - def _do_interval(self): - # wipe the tmp dir clean to try to avoid filling the VM disk - TmpReaper().clean_tmp() - - # choose seedfile - sf = self.seedfile_set.next_item() - logger.info('Selected seedfile: %s', sf.basename) - - r = sf.rangefinder.next_item() - qf = not self.first_chunk - -# rng_seed = int(sf.md5, 16) - - interval_limit = self.current_seed + self.seed_interval - - # start an iteration interval - # note that range does not include interval_limit - logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) - for seednum in xrange(self.current_seed, interval_limit): - self._do_iteration(sf, r, qf, seednum) - - self.current_seed = interval_limit - self.first_chunk = False - def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 4f43be5..25b883f 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -3,7 +3,6 @@ @author: adh ''' -import gc import logging import os import platform @@ -185,32 +184,7 @@ def _stop_buttonclicker(self): if self.use_buttonclicker: os.system('taskkill /im buttonclicker.exe') - def _do_interval(self): - # choose seedfile - sf = self.seedfile_set.next_item() - - logger.info('Selected seedfile: %s', sf.basename) - - if self.current_seed % self.status_interval == 0: - # cache our current state - self._save_state() - - interval_limit = self.current_seed + self.seed_interval - - # start an iteration interval - # note that range does not include interval_limit - logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) - for seednum in xrange(self.current_seed, interval_limit): - self._do_iteration(sf, seednum) - - del sf - # manually collect garbage - gc.collect() - - self.current_seed = interval_limit - self.first_chunk = False - - def _do_iteration(self, sf, seednum): + def _do_iteration(self, sf, range_obj, quiet_flag, seednum): # use a with...as to ensure we always hit # the __enter__ and __exit__ methods of the # newly created WindowsIteration() From 419f82d6fa29d844b81402b036344f30d8c22136 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 Jan 2015 11:00:48 -0500 Subject: [PATCH 0581/1169] fix unit tests --- src/certfuzz/config/config_base.py | 9 +++---- src/certfuzz/test/config/test_config_base.py | 26 +++++--------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/certfuzz/config/config_base.py b/src/certfuzz/config/config_base.py index a18b76f..3b81b90 100644 --- a/src/certfuzz/config/config_base.py +++ b/src/certfuzz/config/config_base.py @@ -14,7 +14,9 @@ def parse_yaml(yaml_file): - return yaml.load(open(yaml_file, 'r')) + with open(yaml_file, 'r') as f: + stuff = yaml.load(f) + return stuff class ConfigBase(object): @@ -30,7 +32,6 @@ def __init__(self, config_file): def __enter__(self): self.load() - self._verify_load() self._set_derived_options() self._add_validations() self.validate() @@ -50,10 +51,6 @@ def load(self): if self.config: self.__dict__.update(self.config) - def _verify_load(self): - if self.config is None: - raise ConfigError('ConfigBase load failed for {}'.format(self.file)) - def validate(self): for validation in self.validations: validation() diff --git a/src/certfuzz/test/config/test_config_base.py b/src/certfuzz/test/config/test_config_base.py index 93fd5e1..27361a2 100644 --- a/src/certfuzz/test/config/test_config_base.py +++ b/src/certfuzz/test/config/test_config_base.py @@ -12,7 +12,7 @@ import yaml -from certfuzz import config +import certfuzz.config.config_base as config from certfuzz.config.errors import ConfigError @@ -31,7 +31,7 @@ def tearDown(self): def _write_yaml(self, thing=None): if thing is None: - thing = dict(a=1, b=2, c=3, d=4) + thing = dict([(y, x) for x, y in enumerate("abcd")]) fd, f = tempfile.mkstemp(suffix='yaml', dir=self.tempdir) os.close(fd) with open(f, 'wb') as fd: @@ -49,10 +49,9 @@ def test_parse_yaml(self): self.assertEqual(thing, from_yaml) def test_config_init(self): - thing, f = self._write_yaml() + _junk, f = self._write_yaml() c = config.ConfigBase(f) self.assertEqual(f, c.file) - self.assertEqual(thing, c.config) def test_validate(self): dummy, f = self._write_yaml() @@ -67,26 +66,13 @@ def test_validate(self): c.validate() self.assertEqual(3, _count) - def test_init_fails_if_load_fails(self): - dummy, f = self._write_yaml() - os.remove(f) - self.assertRaises(ConfigError, config.ConfigBase, f) - - def test_verify_load(self): - # write another yaml file - _thing, f = self._write_yaml() - # sub the new file name - c = config.ConfigBase(f) - c.config = None - self.assertRaises(ConfigError, c._verify_load) - def test_load(self): # write another yaml file thing, f = self._write_yaml() # sub the new file name - c = config.ConfigBase(f) - # we should get the thing back again - self.assertEqual(thing, c.config) + with config.ConfigBase(f) as c: + # we should get the thing back again + self.assertEqual(thing, c.config) # load should add each of the things as # config attributes From c5285f14d56397e6d6ce3128724457f4c3f9e949 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 Jan 2015 11:42:00 -0500 Subject: [PATCH 0582/1169] fix unit tests --- .../test_tc_pipeline_base.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py index f969662..528960f 100644 --- a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py +++ b/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py @@ -43,17 +43,20 @@ def test_abstract_class(self): self.assertRaises(TypeError, cls) try: - TCPL_Impl() + TCPL_Impl(outdir=self.tmpdir) except TypeError as e: self.fail('Dummy implementation class failed to instantiate: {}'.format(e)) def test_pipeline_coroutines(self): - tcpl = TCPL_Impl() + tcpl = TCPL_Impl(outdir=self.tmpdir) results = [] def func(tc): results.append(tc) + class MockTestCase(object): + should_proceed_with_analysis = True + tcpl._verify = func tcpl._minimize = func tcpl._analyze = func @@ -64,23 +67,26 @@ def func(tc): tcpl.report, ] + tc = MockTestCase() # if this test works, results will get [0,1,2,...] # because each of the above functions will call # its corresponding _function which will append # i to results for i, plfunc in enumerate(funcs2check): - #setup the pipeline coroutine + # setup the pipeline coroutine testfunc = plfunc() self.assertEqual(i, len(results)) - testfunc.send(i) + testfunc.send(tc) self.assertEqual(i + 1, len(results)) - self.assertEqual(i, results[-1]) + self.assertEqual(tc, results[-1]) def test_copy_files(self): - tcpl = TCPL_Impl() + tcpl = TCPL_Impl(outdir=self.tmpdir) class MockTestCase(object): - result_dir = tempfile.mkdtemp(dir=self.tmpdir) + result_dir = tempfile.mkdtemp(prefix="tc_out_", dir=self.tmpdir) + signature = "tcsignature" + tempdir = tempfile.mkdtemp(prefix="tc_in_", dir=self.tmpdir) tc = MockTestCase() fd, fname = tempfile.mkstemp(dir=tc.result_dir) @@ -102,7 +108,7 @@ class TCPL_Impl2(TCPL_Impl): def _analyze(self, testcase): certfuzz.testcase_pipeline.tc_pipeline_base.TestCasePipelineBase._analyze(self, testcase) - tcpl = TCPL_Impl2() + tcpl = TCPL_Impl2(outdir=self.tmpdir) class MockTestCase(object): pass @@ -132,5 +138,5 @@ def inc_cc(): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 867be03361e615ad318f473a60ff45730407e582 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 Jan 2015 12:09:21 -0500 Subject: [PATCH 0583/1169] fix unit tests --- src/certfuzz/config/config_windows.py | 3 +++ src/certfuzz/test/config/test_config_windows.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/config/config_windows.py b/src/certfuzz/config/config_windows.py index d2597a1..9a61820 100644 --- a/src/certfuzz/config/config_windows.py +++ b/src/certfuzz/config/config_windows.py @@ -36,6 +36,9 @@ def _add_validations(self): self.validations.append(self._validate_new_options) def _set_derived_options(self): + if self.config is None: + raise ConfigError('No config found (or config file empty?)') + # interpolate program name # add quotes around $SEEDFILE t = Template(self.config['target']['cmdline_template']) diff --git a/src/certfuzz/test/config/test_config_windows.py b/src/certfuzz/test/config/test_config_windows.py index 2e1dd3f..97cf620 100644 --- a/src/certfuzz/test/config/test_config_windows.py +++ b/src/certfuzz/test/config/test_config_windows.py @@ -31,7 +31,8 @@ def _get_minimal_config(self): with open(f, 'w') as fd: yaml.dump(self.cfg_in, fd) - return WindowsConfig(f) + with WindowsConfig(f) as wcfg: + return wcfg def setUp(self): self.tempdir = tempfile.mkdtemp() @@ -46,7 +47,8 @@ def test_empty_cfg_raises_exception(self): with open(f, 'w') as fd: yaml.dump(self.cfg_in, fd) - self.assertRaises(KeyError, WindowsConfig, f) + wcfg = WindowsConfig(f) + self.assertRaises(ConfigError, wcfg._set_derived_options) def test_minimal_config(self): try: From 36cb00dd288e664f125285f306cc3af22716bf7f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 Jan 2015 12:52:26 -0500 Subject: [PATCH 0584/1169] bff_config is superseded by config_linux --- src/certfuzz/campaign/config/bff_config.py | 232 --------------------- 1 file changed, 232 deletions(-) delete mode 100644 src/certfuzz/campaign/config/bff_config.py diff --git a/src/certfuzz/campaign/config/bff_config.py b/src/certfuzz/campaign/config/bff_config.py deleted file mode 100644 index 2ac2953..0000000 --- a/src/certfuzz/campaign/config/bff_config.py +++ /dev/null @@ -1,232 +0,0 @@ -''' -Created on Oct 19, 2010 - -@organization: cert.org -@contact: adh@cert.org - -This module provides the ConfigHelper class to assist with converting a bff.cfg file into -a collection of attributes and methods to facilitate a fuzzing run. -''' -import ConfigParser -import logging -import os -import re -import shlex -import shutil - -from certfuzz.fuzztools import filetools - - -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - -UNIQ_LOG = "uniquelog.txt" -LAST_SEEDFILE = 'lastseed' - -MINIMIZED_EXT = "minimal" -ZZUF_LOG_FILE = 'zzuf_log.txt' -RANGE_LOG = 'rangelog.txt' -CRASH_EXIT_CODE_FILE = "crashexitcodes" -CACHED_CONFIG_OBJECT_FILE = 'config.pkl' -CACHED_SEEDRANGE_OBJECT_FILE = 'seedrange.pkl' -CACHED_RANGEFINDER_OBJECT_FILE = 'rangefinder.pkl' -CACHED_SEEDFILESET_OBJECT_FILE = 'seedfile_set.pkl' -SEEDFILE_REPLACE_STRING = '\$SEEDFILE' - - -def read_config_options(cfg_file): - ''' - Reads and parses , returning a ConfigHelper object - @rtype: ConfigHelper - ''' - config = ConfigParser.ConfigParser() - with open(cfg_file) as fp: - config.readfp(fp) - return ConfigHelper(config) - - -class ConfigHelper: - '''ConfigHelper takes a generic ConfigParser raw object and provides the properties and - helper methods necessary to configure a fuzz run.''' - - def __init__(self, cfg): - '''Initialize a ConfigHelper object by passing in a ConfigParser object representing your bff.cfg file. - - @rtype: ConfigHelper''' - self.cfg = cfg - # [campaign] - campaign_id = re.sub('\s+', '_', self.cfg.get('campaign', 'id')) - self.campaign_id = campaign_id - - # [target] - self.killprocname = os.path.basename(self.cfg.get('target', 'killprocname')) - self.cmdline = self.cfg.get('target', 'cmdline') - - self.cmd_list = shlex.split(self.cmdline) - for index, cmd_part in enumerate(self.cmd_list): - self.cmd_list[index] = os.path.expanduser(cmd_part) - if re.search(' ', self.cmd_list[0]): - self.cmd_list[0] = '"' + self.cmd_list[0] + '"' - self.program = self.cmd_list[0] - self._cmd = self.cmd_list - self._args = self.cmd_list[1:] - - # [timeouts] - self.killproctimeout = self.cfg.getint('timeouts', 'killproctimeout') - self.watchdogtimeout = self.cfg.getint('timeouts', 'watchdogtimeout') - self.debugger_timeout = self.cfg.getfloat('timeouts', 'debugger_timeout') - self.progtimeout = self.cfg.getfloat('timeouts', 'progtimeout') - self.valgrindtimeout = self.cfg.getint('timeouts', 'valgrindtimeout') - self.minimizertimeout = self.cfg.getint('timeouts', 'minimizertimeout') - - # [zzuf] - self.copymode = self.cfg.getint('zzuf', 'copymode') - self.start_seed = self.cfg.getint('zzuf', 'start_seed') - self.seed_interval = self.cfg.getint('zzuf', 'seed_interval') - - # [verifier] - self.backtracelevels = self.cfg.getint('verifier', 'backtracelevels') - if self.cfg.has_option('verifier', 'exclude_unmapped_frames'): - self.exclude_unmapped_frames = self.cfg.get('verifier', 'exclude_unmapped_frames') - else: - self.exclude_unmapped_frames = True - self.minimizecrashers = self.cfg.getboolean('verifier', 'minimizecrashers') - self.minimize_to_string = self.cfg.getboolean('verifier', 'minimize_to_string') - if self.cfg.has_option('verifier', 'use_valgrind'): - self.use_valgrind = self.cfg.getboolean('verifier', 'use_valgrind') - else: - self.use_valgrind = True - if self.cfg.has_option('verifier', 'use_pin_calltrace'): - self.use_pin_calltrace = self.cfg.getboolean('verifier', 'use_pin_calltrace') - else: - self.use_pin_calltrace = False - if self.cfg.has_option('verifier', 'savefailedasserts'): - self.savefailedasserts = self.cfg.getboolean('verifier', 'savefailedasserts') - else: - self.savefailedasserts = False - if self.cfg.has_option('verifier', 'recycle_crashers'): - self.recycle_crashers = self.cfg.getboolean('verifier', 'recycle_crashers') - else: - self.recycle_crashers = False - if self.cfg.has_option('verifier', 'keep_duplicates'): - self.keep_duplicates = self.cfg.getboolean('verifier', 'keep_duplicates') - else: - self.keep_duplicates = False - - # [directories] - self.remote_dir = os.path.expanduser(self.cfg.get('directories', 'remote_dir')) - self.seedfile_origin_dir = os.path.expanduser(self.cfg.get('directories', 'seedfile_origin_dir')) - self.debugger_template_dir = os.path.expanduser(self.cfg.get('directories', 'debugger_template_dir')) - self.local_dir = os.path.expanduser(self.cfg.get('directories', 'local_dir')) - self.output_dir = os.path.expanduser(self.cfg.get('directories', 'output_dir')) - - self.watchdogfile = os.path.expanduser(self.cfg.get('directories', 'watchdog_file')) - - # derived properties - self.program_basename = os.path.basename(self.program).replace('"', '') - - self.uniq_log = os.path.join(self.output_dir, UNIQ_LOG) - - self.crashexitcodesfile = os.path.join(self.local_dir, CRASH_EXIT_CODE_FILE) - self.zzuf_log_file = os.path.join(self.local_dir, ZZUF_LOG_FILE) - - self.tmpdir = None - - # derived cached paths -# self.cached_config_file = os.path.join(self.local_dir, CACHED_CONFIG_OBJECT_FILE) -# self.cached_seedrange_file = os.path.join(self.local_dir, CACHED_SEEDRANGE_OBJECT_FILE) -# self.cached_rangefinder_file = os.path.join(self.local_dir, CACHED_RANGEFINDER_OBJECT_FILE) -# self.cached_seedfile_set = os.path.join(self.local_dir, CACHED_SEEDFILESET_OBJECT_FILE) - - def get_command(self, filepath): - return ' '.join(self.get_command_list(filepath)) - - def get_command_list(self, seedfile): - cmdlst = [self.program] - cmdlst.extend(self.get_command_args_list(seedfile)) - return cmdlst - - def get_command_args_list(self, seedfile): - arglist = [] - for arg in self._args: - arglist.append(re.sub(SEEDFILE_REPLACE_STRING, seedfile, arg)) - return arglist - - def zzuf_log_out(self, mydir): - return os.path.join(mydir, ZZUF_LOG_FILE) - - def full_path_local_fuzz_dir(self, seedfile): - ''' - Returns // - @param seedfile: - ''' - return os.path.join(self.local_dir, self.program_basename, seedfile) - - def full_path_original(self, seedfile): - ''' - Returns / - @param seedfile: - ''' - return os.path.join(self.full_path_local_fuzz_dir(seedfile), seedfile) - - def get_minimized_file(self, outfile): - ''' - @rtype: string - @return: -. - ''' - (head, tail) = os.path.split(outfile) - (root, ext) = os.path.splitext(tail) - new_filename = '%s-%s%s' % (root, MINIMIZED_EXT, ext) - return os.path.join(head, new_filename) - - def get_filenames(self, outfile, use_minimized_as_root=True): - ''' - @rtype: list - @return: a list of filenames for minidump, stderr, gdb, valgrind, minimized - ''' - minfile = self.get_minimized_file(outfile) - if use_minimized_as_root: - file_root = minfile - else: - file_root = outfile - - files = [] - for f in (self.get_minidump_file, self.get_stderr_file, self.get_gdb_file, self.get_valgrind_file): - files.append(f(file_root)) - - files.append(minfile) - - return files - - - def create_tmpdir(self): - # TODO: this should become part of campaign object - if not self.tmpdir: - self.tmpdir = filetools.mkdtemp(self.testscase_tmp_dir) - logger.debug("Created temp dir %s", self.tmpdir) - assert os.path.isdir(self.tmpdir) - - def clean_tmpdir(self): - # TODO: this should become part of campaign object - if self.tmpdir is None: - return - - if os.path.exists(self.tmpdir): - shutil.rmtree(self.tmpdir) - logger.debug("Removed temp dir %s", self.tmpdir) - assert not os.path.exists(self.tmpdir) - self.tmpdir = None - self.create_tmpdir() - - def get_testcase_outfile(self, seedfile, s1): - # TODO: this should become part of campaign object - ''' - @rtype: string - @return: the path to the output file for this seed: . - ''' - (dirname, basename) = os.path.split(seedfile) # @UnusedVariable - (root, ext) = os.path.splitext(basename) - new_root = '%s-%d' % (root, s1) - new_basename = '%s%s' % (new_root, ext) - self.create_tmpdir() - return os.path.join(self.tmpdir, new_basename) From 4ae4d05910e67b641e7687728be171af36e0fcc5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 Jan 2015 12:55:46 -0500 Subject: [PATCH 0585/1169] fix unit tests --- src/certfuzz/config/config_base.py | 3 ++- src/certfuzz/config/config_windows.py | 3 +-- src/certfuzz/test/config/test_config_base.py | 5 +++++ src/certfuzz/test/config/test_config_linux.py | 18 ++---------------- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/certfuzz/config/config_base.py b/src/certfuzz/config/config_base.py index 3b81b90..9c81a9e 100644 --- a/src/certfuzz/config/config_base.py +++ b/src/certfuzz/config/config_base.py @@ -56,7 +56,8 @@ def validate(self): validation() def _set_derived_options(self): - pass + if self.config is None: + raise ConfigError('No config found (or config file empty?)') def _add_validations(self): pass diff --git a/src/certfuzz/config/config_windows.py b/src/certfuzz/config/config_windows.py index 9a61820..b329bd3 100644 --- a/src/certfuzz/config/config_windows.py +++ b/src/certfuzz/config/config_windows.py @@ -36,8 +36,7 @@ def _add_validations(self): self.validations.append(self._validate_new_options) def _set_derived_options(self): - if self.config is None: - raise ConfigError('No config found (or config file empty?)') + ConfigBase._set_derived_options(self) # interpolate program name # add quotes around $SEEDFILE diff --git a/src/certfuzz/test/config/test_config_base.py b/src/certfuzz/test/config/test_config_base.py index 27361a2..0e78bb4 100644 --- a/src/certfuzz/test/config/test_config_base.py +++ b/src/certfuzz/test/config/test_config_base.py @@ -80,6 +80,11 @@ def test_load(self): self.assertTrue(hasattr(c, k)) self.assertEqual(c.__getattribute__(k), v) + def test_set_derived_options(self): + c = config.ConfigBase('foo') + self.assertEqual(None, c.config) + self.assertRaises(ConfigError, c._set_derived_options) + if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/config/test_config_linux.py b/src/certfuzz/test/config/test_config_linux.py index d0e76bc..2251369 100644 --- a/src/certfuzz/test/config/test_config_linux.py +++ b/src/certfuzz/test/config/test_config_linux.py @@ -35,6 +35,7 @@ seed_interval: 20 verifier: backtracelevels: 5 + keep_duplicates: False exclude_unmapped_frames: True savefailedasserts: False use_valgrind: True @@ -60,7 +61,7 @@ def setUp(self): os.close(fd) with open(self.yamlfile, 'w') as stream: yaml.dump(self.c, stream) - + self.cfg = LinuxConfig(self.yamlfile) def tearDown(self): @@ -99,7 +100,6 @@ def test_set_derived_options(self): self.assertTrue(self.cfg.uniq_log.endswith(cl.UNIQ_LOG)) self.assertTrue(self.cfg.crashexitcodesfile.endswith(cl.CRASH_EXIT_CODE_FILE)) self.assertTrue(self.cfg.zzuf_log_file.endswith(cl.ZZUF_LOG_FILE)) - self.assertEqual(self.cfg.zzuf_log_out('seedfile'), os.path.join('seedfile', 'zzuf_log.txt')) def test_get_command(self): with self.cfg: @@ -118,13 +118,6 @@ def test_get_command_args_list(self): self.assertEqual(result[0], 'foo') self.assertEqual(result[1], '/dev/null') - def test_zzuf_log_out(self): - with self.cfg: - result = self.cfg.zzuf_log_out('foo') - self.assertEqual(str, type(result)) - self.assertTrue(result.startswith('foo')) - self.assertTrue(result.endswith(cl.ZZUF_LOG_FILE)) - def test_full_path_local_fuzz_dir(self): with self.cfg: result = self.cfg.full_path_local_fuzz_dir('foo') @@ -141,13 +134,6 @@ def test_get_minimized_file(self): with self.cfg: self.assertEqual(self.cfg.get_minimized_file('foo.txt'), 'foo-%s.txt' % MINIMIZED_EXT) - def test_get_filenames(self): - with self.cfg: - result = self.cfg.get_filenames('foo.bar.baz', use_minimized_as_root=True) - - print result - result = self.cfg.get_filenames('foo.bar.baz', use_minimized_as_root=False) - print result if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 5538b96c3b26355fb86890927767de9f9e43094b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 Jan 2015 13:16:45 -0500 Subject: [PATCH 0586/1169] handle empty zzuflog files more gracefully, fix unit tests --- src/certfuzz/fuzztools/zzuflog.py | 11 +++--- src/certfuzz/test/fuzztools/test_zzuflog.py | 37 ++++++++++----------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuflog.py b/src/certfuzz/fuzztools/zzuflog.py index c5d5679..64f1a7a 100644 --- a/src/certfuzz/fuzztools/zzuflog.py +++ b/src/certfuzz/fuzztools/zzuflog.py @@ -55,10 +55,13 @@ def _get_last_line(self): range, result, and complete line from the last line of the file. @return: string, string, string, string ''' - with open(self.infile, 'r') as f: - for line in f: - # don't do anything, just iterate through until we're done with lines - pass + try: + with open(self.infile, 'r') as f: + line = list(f)[-1] + except IndexError: + # e.g., if infile is empty + line = '' + # when you get here line contains the last line read from the file return line.strip() diff --git a/src/certfuzz/test/fuzztools/test_zzuflog.py b/src/certfuzz/test/fuzztools/test_zzuflog.py index 0e5d964..e2714c8 100644 --- a/src/certfuzz/test/fuzztools/test_zzuflog.py +++ b/src/certfuzz/test/fuzztools/test_zzuflog.py @@ -8,6 +8,7 @@ ''' import unittest +import shutil class Test(unittest.TestCase): def delete_file(self, f): @@ -16,19 +17,15 @@ def delete_file(self, f): self.assertFalse(os.path.exists(f)) def tearDown(self): - self.delete_file(self.infile) - self.delete_file(self.outfile) + shutil.rmtree(self.tmpdir) def setUp(self): - (fd1, f1) = tempfile.mkstemp(text=True) + self.tmpdir = tempfile.mkdtemp(prefix='test_zzuflog_') + (fd1, f1) = tempfile.mkstemp(text=True, dir=self.tmpdir) os.close(fd1) self.infile = f1 - (fd2, f2) = tempfile.mkstemp(text=True) - os.close(fd2) - self.outfile = f2 - - self.log = ZzufLog(self.infile, self.outfile) + self.log = ZzufLog(self.infile) def test_get_last_line(self): open(self.infile, 'w') @@ -40,7 +37,7 @@ def test_get_last_line(self): os.write(fd, "thirdline\n") os.close(fd) - log = ZzufLog(f, self.outfile) + log = ZzufLog(f) # log.line gets the result of _get_last_line before the infile is wiped out self.assertEqual(log.line, 'thirdline') self.delete_file(f) @@ -72,28 +69,28 @@ def test_parse_line(self): def test_was_out_of_memory(self): # should be true self.log.result = "signal 15" - self.assertTrue(self.log._was_out_of_memory()) + self.assertTrue(self.log.was_out_of_memory) self.log.result = "exit 143" - self.assertTrue(self.log._was_out_of_memory()) + self.assertTrue(self.log.was_out_of_memory) # should be false self.log.result = "signal 8" - self.assertFalse(self.log._was_out_of_memory()) + self.assertFalse(self.log.was_out_of_memory) self.log.result = "exit 18" - self.assertFalse(self.log._was_out_of_memory()) + self.assertFalse(self.log.was_out_of_memory) def test_was_killed(self): # should be true self.log.result = "signal 9" - self.assertTrue(self.log._was_killed()) + self.assertTrue(self.log.was_killed) self.log.result = "exit 137" - self.assertTrue(self.log._was_killed()) + self.assertTrue(self.log.was_killed) # should be false self.log.result = "signal 8" - self.assertFalse(self.log._was_killed()) + self.assertFalse(self.log.was_killed) self.log.result = "exit 18" - self.assertFalse(self.log._was_killed()) + self.assertFalse(self.log.was_killed) def test_read_zzuf_log(self): (fd, f) = tempfile.mkstemp(text=True) @@ -102,7 +99,7 @@ def test_read_zzuf_log(self): os.write(fd, line % (85, "0.01-0.02", "bar")) os.close(fd) - log = ZzufLog(f, self.outfile) + log = ZzufLog(f) self.assertEqual(log.seed, 85) self.assertEqual(log.range, "0.01-0.02") @@ -138,7 +135,7 @@ def test_crash_logged(self): # should be true self.log.result = "a" self.log._set_exitcode() - self.log.parsed = True # have to fake it since infile is empty + self.log.parsed = True # have to fake it since infile is empty self.assertTrue(self.log.crash_logged(False)) # def test_crash_exit(self): @@ -169,5 +166,5 @@ def test_crash_logged(self): # self.assertFalse(self.log._crash_exit(crash_exit_code_list)) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 9622baf63353a82b87f223a6787d94f455da22bf Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 7 Jan 2015 15:55:00 -0500 Subject: [PATCH 0587/1169] fix unit tests --- src/certfuzz/test/fuzzers/test_bitmut.py | 6 ++---- src/certfuzz/test/fuzzers/test_bytemut.py | 5 ++--- src/certfuzz/test/fuzzers/test_crlfmut.py | 7 +++---- src/certfuzz/test/fuzzers/test_crmut.py | 5 ++--- src/certfuzz/test/fuzzers/test_drop.py | 5 ++--- src/certfuzz/test/fuzzers/test_fuzzer_base.py | 3 +-- src/certfuzz/test/fuzzers/test_insert.py | 5 ++--- src/certfuzz/test/fuzzers/test_nullmut.py | 3 +-- src/certfuzz/test/fuzzers/test_swap.py | 5 ++--- src/certfuzz/test/fuzzers/test_truncate.py | 5 ++--- src/certfuzz/test/fuzzers/test_wave.py | 5 ++--- src/certfuzz/test/fuzzers/test_zzuf.py | 6 +++--- src/certfuzz/test/mocks.py | 2 ++ 13 files changed, 26 insertions(+), 36 deletions(-) diff --git a/src/certfuzz/test/fuzzers/test_bitmut.py b/src/certfuzz/test/fuzzers/test_bitmut.py index 84c1f1c..901ca50 100644 --- a/src/certfuzz/test/fuzzers/test_bitmut.py +++ b/src/certfuzz/test/fuzzers/test_bitmut.py @@ -17,11 +17,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 self.options = {} -# self.options = {'min_ratio': 0.1, 'max_ratio': 0.2} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, self.options) + self.args = (seedfile_obj, outdir_base, iteration, self.options) def tearDown(self): shutil.rmtree(self.tempdir) @@ -50,5 +48,5 @@ def test_fuzz(self): # self.assertAlmostEqual(f.ratio, f.fuzzed_bit_ratio(), 2) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/fuzzers/test_bytemut.py b/src/certfuzz/test/fuzzers/test_bytemut.py index 775d531..dbb8721 100644 --- a/src/certfuzz/test/fuzzers/test_bytemut.py +++ b/src/certfuzz/test/fuzzers/test_bytemut.py @@ -23,10 +23,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 self.options = {'min_ratio': 0.1, 'max_ratio': 0.2} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, self.options) + self.args = (seedfile_obj, outdir_base, iteration, self.options) def tearDown(self): shutil.rmtree(self.tempdir) @@ -136,7 +135,7 @@ def test_consistency(self): last_result = None last_x = x for _ in range(20): - with ByteMutFuzzer(self.sf, self.outdir, x, x, self.options) as f: + with ByteMutFuzzer(self.sf, self.outdir, x, self.options) as f: f._fuzz() result = str(f.output) if last_result: diff --git a/src/certfuzz/test/fuzzers/test_crlfmut.py b/src/certfuzz/test/fuzzers/test_crlfmut.py index 967f726..af3d88b 100644 --- a/src/certfuzz/test/fuzzers/test_crlfmut.py +++ b/src/certfuzz/test/fuzzers/test_crlfmut.py @@ -38,14 +38,13 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 self.options = {'min_ratio': 0.1, 'max_ratio': 0.2} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, self.options) + self.args = (seedfile_obj, outdir_base, iteration, self.options) def tearDown(self): shutil.rmtree(self.tempdir) - + def _fail_if_not_fuzzed(self, fuzzed): for c in fuzzed: if c == 'A' or c == 0x0D or c == 0x0A: @@ -143,7 +142,7 @@ def test_consistency(self): last_result = None last_x = x for _ in range(20): - with CRLFMutFuzzer(self.sf, self.outdir, x, x, self.options) as f: + with CRLFMutFuzzer(self.sf, self.outdir, x, self.options) as f: f._fuzz() result = str(f.output) if last_result: diff --git a/src/certfuzz/test/fuzzers/test_crmut.py b/src/certfuzz/test/fuzzers/test_crmut.py index 704f5c3..7bdb299 100644 --- a/src/certfuzz/test/fuzzers/test_crmut.py +++ b/src/certfuzz/test/fuzzers/test_crmut.py @@ -27,10 +27,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 self.options = {'min_ratio': 0.1, 'max_ratio': 0.2} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, self.options) + self.args = (seedfile_obj, outdir_base, iteration, self.options) def tearDown(self): shutil.rmtree(self.tempdir) @@ -142,7 +141,7 @@ def test_consistency(self): last_result = None last_x = x for _ in range(20): - with CRMutFuzzer(self.sf, self.outdir, x, x, self.options) as f: + with CRMutFuzzer(self.sf, self.outdir, x, self.options) as f: f._fuzz() result = str(f.output) if last_result: diff --git a/src/certfuzz/test/fuzzers/test_drop.py b/src/certfuzz/test/fuzzers/test_drop.py index 604a50d..6158592 100644 --- a/src/certfuzz/test/fuzzers/test_drop.py +++ b/src/certfuzz/test/fuzzers/test_drop.py @@ -23,10 +23,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 options = {} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, options) + self.args = (seedfile_obj, outdir_base, iteration, options) def tearDown(self): shutil.rmtree(self.tempdir, ignore_errors=True) @@ -56,5 +55,5 @@ def test_is_not_minimizable(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/fuzzers/test_fuzzer_base.py b/src/certfuzz/test/fuzzers/test_fuzzer_base.py index 4565b80..f1c62d0 100644 --- a/src/certfuzz/test/fuzzers/test_fuzzer_base.py +++ b/src/certfuzz/test/fuzzers/test_fuzzer_base.py @@ -20,10 +20,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 options = {} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, options) + self.args = (seedfile_obj, outdir_base, iteration, options) def tearDown(self): shutil.rmtree(self.tempdir, ignore_errors=True) diff --git a/src/certfuzz/test/fuzzers/test_insert.py b/src/certfuzz/test/fuzzers/test_insert.py index f14fe83..06cdb59 100644 --- a/src/certfuzz/test/fuzzers/test_insert.py +++ b/src/certfuzz/test/fuzzers/test_insert.py @@ -23,10 +23,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 options = {} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, options) + self.args = (seedfile_obj, outdir_base, iteration, options) def tearDown(self): shutil.rmtree(self.tempdir, ignore_errors=True) @@ -55,5 +54,5 @@ def test_is_not_minimizable(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/fuzzers/test_nullmut.py b/src/certfuzz/test/fuzzers/test_nullmut.py index 1633616..b90e162 100644 --- a/src/certfuzz/test/fuzzers/test_nullmut.py +++ b/src/certfuzz/test/fuzzers/test_nullmut.py @@ -25,10 +25,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 self.options = {'min_ratio': 0.1, 'max_ratio': 0.2} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, self.options) + self.args = (seedfile_obj, outdir_base, iteration, self.options) def tearDown(self): shutil.rmtree(self.tempdir) diff --git a/src/certfuzz/test/fuzzers/test_swap.py b/src/certfuzz/test/fuzzers/test_swap.py index 01762ab..b0fa70b 100644 --- a/src/certfuzz/test/fuzzers/test_swap.py +++ b/src/certfuzz/test/fuzzers/test_swap.py @@ -18,10 +18,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 options = {} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, options) + self.args = (seedfile_obj, outdir_base, iteration, options) def tearDown(self): shutil.rmtree(self.tempdir, ignore_errors=True) @@ -55,5 +54,5 @@ def test_fuzz_out_of_range(self): self.assertRaises(FuzzerExhaustedError, f._fuzz) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/fuzzers/test_truncate.py b/src/certfuzz/test/fuzzers/test_truncate.py index 31080c6..fb4f323 100644 --- a/src/certfuzz/test/fuzzers/test_truncate.py +++ b/src/certfuzz/test/fuzzers/test_truncate.py @@ -23,10 +23,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 options = {} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, options) + self.args = (seedfile_obj, outdir_base, iteration, options) def tearDown(self): shutil.rmtree(self.tempdir, ignore_errors=True) @@ -53,5 +52,5 @@ def test_is_not_minimizable(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/fuzzers/test_wave.py b/src/certfuzz/test/fuzzers/test_wave.py index 51aa3ba..16616cd 100644 --- a/src/certfuzz/test/fuzzers/test_wave.py +++ b/src/certfuzz/test/fuzzers/test_wave.py @@ -23,10 +23,9 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 iteration = 0 options = {} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, options) + self.args = (seedfile_obj, outdir_base, iteration, options) def tearDown(self): shutil.rmtree(self.tempdir, ignore_errors=True) @@ -54,5 +53,5 @@ def test_is_not_minimizable(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/fuzzers/test_zzuf.py b/src/certfuzz/test/fuzzers/test_zzuf.py index 31278d9..9fef2d2 100644 --- a/src/certfuzz/test/fuzzers/test_zzuf.py +++ b/src/certfuzz/test/fuzzers/test_zzuf.py @@ -28,11 +28,11 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() self.outdir = outdir_base = tempfile.mkdtemp(prefix='outdir_base', dir=self.tempdir) - rng_seed = 0 + iteration = 0 self.options = {} # self.options = {'min_ratio': 0.1, 'max_ratio': 0.2} - self.args = (seedfile_obj, outdir_base, rng_seed, iteration, self.options) + self.args = (seedfile_obj, outdir_base, iteration, self.options) def tearDown(self): shutil.rmtree(self.tempdir) @@ -70,5 +70,5 @@ def test_fuzz(self): self.skipTest("zzuf not found in path") if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/mocks.py b/src/certfuzz/test/mocks.py index dc1d8df..67fdc53 100644 --- a/src/certfuzz/test/mocks.py +++ b/src/certfuzz/test/mocks.py @@ -3,6 +3,7 @@ @organization: cert.org ''' +import hashlib class Mock(object): pass @@ -24,6 +25,7 @@ def __init__(self, sz=1000): self.value = 'A' * sz self.len = len(self.value) self.rangefinder = MockRangefinder() + self.md5 = hashlib.md5(self.value).hexdigest() def read(self): return self.value From 3250f067f47684b5d4593e60a0711f60523f9c23 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 Jan 2015 08:38:50 -0500 Subject: [PATCH 0588/1169] fix unit tests --- src/certfuzz/test/file_handlers/test_seedfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/test/file_handlers/test_seedfile.py b/src/certfuzz/test/file_handlers/test_seedfile.py index c5cef6c..bc740a4 100644 --- a/src/certfuzz/test/file_handlers/test_seedfile.py +++ b/src/certfuzz/test/file_handlers/test_seedfile.py @@ -26,7 +26,7 @@ def tearDown(self): assert not os.path.exists(self.file) def test_init(self): - self.assertEqual(self.sf.output_dir, os.path.join(self.dir, self.sf.md5)) + pass # def test_getstate(self): # self.assertEqual(RangeFinder, type(self.sf.rangefinder)) @@ -42,5 +42,5 @@ def test_init(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From e68c39b751c8dc6d67fc73d15e225de8887f540d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 Jan 2015 08:39:03 -0500 Subject: [PATCH 0589/1169] fix unit tests --- src/certfuzz/test/runners/test_zzufrun.py | 35 +++++++++++------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/certfuzz/test/runners/test_zzufrun.py b/src/certfuzz/test/runners/test_zzufrun.py index 6010191..19554f3 100644 --- a/src/certfuzz/test/runners/test_zzufrun.py +++ b/src/certfuzz/test/runners/test_zzufrun.py @@ -4,6 +4,7 @@ @organization: cert.org ''' import unittest +import certfuzz.runners.zzufrun from certfuzz.runners.zzufrun import ZzufRunner import shutil import tempfile @@ -31,46 +32,44 @@ def setUp(self): self.ff = fname + self._basename_saved = certfuzz.runners.zzufrun._zzuf_basename + + def tearDown(self): shutil.rmtree(self.tmpdir) + certfuzz.runners.zzufrun._zzuf_basename = self._basename_saved def test_quiet_flag(self): - zr = ZzufRunner(options={'quiet': True}, cmd_template=None, + cmd_template = 'foo bar' + zr = ZzufRunner(options={'hideoutput': True}, cmd_template=cmd_template, fuzzed_file=None, workingdir_base=self.tmpdir) self.assertTrue(zr._quiet) with zr: self.assertTrue('--quiet' in zr._zzuf_args) - zr = ZzufRunner(options={'quiet': False}, cmd_template=None, + zr = ZzufRunner(options={'hideoutput': False}, cmd_template=cmd_template, fuzzed_file=None, workingdir_base=self.tmpdir) self.assertFalse(zr._quiet) with zr: self.assertFalse('--quiet' in zr._zzuf_args) def test_find_zzuf(self): + cmd_template = 'foo bar' (fd, fname) = tempfile.mkstemp(prefix='zzufrun_test_', dir=self.tmpdir) os.close(fd) os.remove(fname) self.assertFalse(os.path.exists(fname)) - for exe in ['/bin/ls', fname]: - zr = ZzufRunner(options={}, cmd_template=None, - fuzzed_file=None, workingdir_base=self.tmpdir) - self.assertEqual(zr._zzuf_basename, 'zzuf') - - _basename = os.path.basename(exe) - zr._zzuf_basename = _basename - - self.assertEqual(None, zr._zzuf_loc) - if os.path.exists(exe): - zr._find_zzuf() - self.assertEqual(exe, zr._zzuf_loc) - else: - self.assertRaises(RunnerNotFoundError, zr._find_zzuf) + certfuzz.runners.zzufrun._zzuf_basename = '/bin/ls' + if os.path.exists(certfuzz.runners.zzufrun._zzuf_basename): + certfuzz.runners.zzufrun._find_zzuf() + self.assertEqual(certfuzz.runners.zzufrun._zzuf_basename, certfuzz.runners.zzufrun._zzuf_loc) + else: + self.assertRaises(RunnerNotFoundError, certfuzz.runners.zzufrun._find_zzuf) def test_run(self): options = {} - cmd_template = '' + cmd_template = 'foo bar' touch = '/usr/bin/touch' if not os.path.exists(touch): @@ -92,5 +91,5 @@ def test_run(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 260d66244bc91bbedd8850a1b76ddab7c68f9ed0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 Jan 2015 08:54:08 -0500 Subject: [PATCH 0590/1169] fix unit tests --- src/certfuzz/test/iteration/test_iteration_windows.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/test/iteration/test_iteration_windows.py b/src/certfuzz/test/iteration/test_iteration_windows.py index d7463e7..a353fc2 100644 --- a/src/certfuzz/test/iteration/test_iteration_windows.py +++ b/src/certfuzz/test/iteration/test_iteration_windows.py @@ -11,8 +11,13 @@ class Test(unittest.TestCase): def setUp(self): - args = list('0123456789ABCDE') - args[3] = {'runoptions': {'keep_unique_faddr': False}} + # args: +# seedfile, seednum, config, fuzzer_cls, +# runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, +# cmd_template, uniq_func, workdirbase, outdir, debug, +# sf_set, rf + args = list('0123456789ABCDEF') + args[2] = {'runoptions': {'keep_unique_faddr': False}} self.iteration = WindowsIteration(*args) def tearDown(self): @@ -22,5 +27,5 @@ def testName(self): pass if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 3022b99b71593853bedb06e5e5e12cc9fc686b86 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 Jan 2015 10:25:59 -0500 Subject: [PATCH 0591/1169] fix unit tests --- src/certfuzz/campaign/campaign_linux.py | 9 +++--- src/certfuzz/debuggers/registration.py | 7 ++--- .../test/campaign/test_campaign_base.py | 4 ++- .../test/campaign/test_campaign_linux.py | 28 ++++++++++++------- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index cec99a0..bf58460 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -75,10 +75,6 @@ def __init__(self, config_file=None, result_dir=None, debug=False): # must occur after work_dir_base, outdir_base, and campaign_id are set self._common_init() - # give up if we don't have a debugger - verify_supported_platform() - # give up if prog is a script - self._check_for_script() def _read_config_file(self): CampaignBase._read_config_file(self) @@ -88,6 +84,11 @@ def _read_config_file(self): self.configdate = cfgobj.configdate def _pre_enter(self): + # give up if we don't have a debugger + verify_supported_platform() + # give up if prog is a script + self._check_for_script() + self._start_process_killer() self._set_unbuffered_stdout() self._check_for_script() diff --git a/src/certfuzz/debuggers/registration.py b/src/certfuzz/debuggers/registration.py index b5cc5e6..81eb5ea 100644 --- a/src/certfuzz/debuggers/registration.py +++ b/src/certfuzz/debuggers/registration.py @@ -22,10 +22,9 @@ system = platform.system() # the keys for debugger_for should match strings returned by platform.system() -debugger_for = { - # platform: key -# 'Linux': 'gdb', -# 'Darwin': 'crashwrangler', +debugger_for = { # platform: key + # 'Linux': 'gdb', + # 'Darwin': 'crashwrangler', # 'Windows': 'msec', } diff --git a/src/certfuzz/test/campaign/test_campaign_base.py b/src/certfuzz/test/campaign/test_campaign_base.py index 668d7f8..28504a4 100644 --- a/src/certfuzz/test/campaign/test_campaign_base.py +++ b/src/certfuzz/test/campaign/test_campaign_base.py @@ -52,6 +52,8 @@ def _set_fuzzer(self): def _set_runner(self): pass + def _read_config_file(self): + pass class Test(unittest.TestCase): def setUp(self): @@ -140,5 +142,5 @@ def test_keep_going(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/campaign/test_campaign_linux.py b/src/certfuzz/test/campaign/test_campaign_linux.py index 06d33e2..0ca4bac 100644 --- a/src/certfuzz/test/campaign/test_campaign_linux.py +++ b/src/certfuzz/test/campaign/test_campaign_linux.py @@ -9,26 +9,34 @@ import os import shutil from ConfigParser import NoSectionError +from certfuzz.config.errors import ConfigError class Test(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() - _fd, cfgfile = tempfile.mkstemp(suffix=".cfg", dir=self.tmpdir, text=True) - try: - self.campaign = LinuxCampaign(cfgfile) - except TypeError as e: - self.fail('LinuxCampaign does not match requirements: {}'.format(e)) - except NoSectionError as e: - pass + fd, cfgfile = tempfile.mkstemp(suffix=".yaml", dir=self.tmpdir, text=True) + os.close(fd) + import yaml + data = {'campaign': {'id': 'foo'}, + 'directories': {}, + 'timeouts': {}, + 'zzuf': {}, + 'verifier': {}, + 'target': {'cmdline': 'bar baz quux'}, + } + with open(cfgfile, 'wb') as stream: + yaml.dump(data, stream) + + self.campaign = LinuxCampaign(cfgfile) def tearDown(self): shutil.rmtree(self.tmpdir) def test_init_without_config(self): - _fd, cfgfile = tempfile.mkstemp(suffix=".cfg", dir=self.tmpdir, text=True) - self.assertRaises(NoSectionError, LinuxCampaign, cfgfile) + _fd, cfgfile = tempfile.mkstemp(suffix=".yaml", dir=self.tmpdir, text=True) + self.assertRaises(ConfigError, LinuxCampaign, cfgfile) def test_check_program_file_type(self): fd, fname = tempfile.mkstemp(dir=self.tmpdir, text=True) @@ -37,5 +45,5 @@ def test_check_program_file_type(self): self.assertTrue(check_program_file_type('text', fname)) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 79aa8ef7c88a4529687d596bb55d44596ca367a3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 Jan 2015 10:28:30 -0500 Subject: [PATCH 0592/1169] change log level for range selection --- src/certfuzz/iteration/iteration_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 0d03c35..38adbe7 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -64,7 +64,7 @@ def _pre_fuzz(self): def _post_fuzz(self): self.r = self.fuzzer.range if self.r: - logger.info('Selected r: %s', self.r) + logger.debug('Selected r: %s', self.r) # decide if we can minimize this case later # do this here (and not sooner) because the fuzzer could From c7dd1226d0b2c206149b660d06e7efb40386ebbc Mon Sep 17 00:00:00 2001 From: "Edward J. Schwartz" Date: Mon, 29 Dec 2014 14:25:17 -0500 Subject: [PATCH 0593/1169] Add error message when suffix is not known --- src/certfuzz/tools/linux/debuggerfile.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/tools/linux/debuggerfile.py b/src/certfuzz/tools/linux/debuggerfile.py index 1aee794..51b8a91 100755 --- a/src/certfuzz/tools/linux/debuggerfile.py +++ b/src/certfuzz/tools/linux/debuggerfile.py @@ -43,6 +43,10 @@ def main(): g = GDBfile(f) elif debugger == '.cw': g = CWfile(f) + elif debugger == '': + parser.error('No file suffix found, but \'.gdb\' or \'.cw\' expected') + else: + parser.error('Unknown file suffix \'%s\' found, but \'.gdb\' or \'.cw\' expected' % debugger) print 'Signature=%s' % g.get_crash_signature(5) if g.registers_hex.get(g.pc_name): print 'PC=%s' % g.registers_hex[g.pc_name] From fc8df6efebfc1463a04ac22606888b7bd77fdab1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 Jan 2015 11:47:26 -0500 Subject: [PATCH 0594/1169] eliminate content of certfuzz.__init__.py --- src/certfuzz/__init__.py | 5 ----- src/certfuzz/test/test_certfuzz_pkg.py | 23 ----------------------- 2 files changed, 28 deletions(-) delete mode 100644 src/certfuzz/test/test_certfuzz_pkg.py diff --git a/src/certfuzz/__init__.py b/src/certfuzz/__init__.py index 9a6234c..e69de29 100644 --- a/src/certfuzz/__init__.py +++ b/src/certfuzz/__init__.py @@ -1,5 +0,0 @@ -''' -The certfuzz package forms the basis of the CERT Basic Fuzzing Framework (BFF). -''' -from certfuzz.errors import CERTFuzzError -from certfuzz.version import __version__ diff --git a/src/certfuzz/test/test_certfuzz_pkg.py b/src/certfuzz/test/test_certfuzz_pkg.py deleted file mode 100644 index a4378ab..0000000 --- a/src/certfuzz/test/test_certfuzz_pkg.py +++ /dev/null @@ -1,23 +0,0 @@ -''' -Created on Apr 10, 2012 - -@organization: cert.org -''' -import unittest -import certfuzz - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - self.assertTrue(hasattr(certfuzz, '__version__')) - self.assertEqual(str, type(certfuzz.__version__)) - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From 112c48dffece4c6cb88e90d8fdc730d0dd44b846 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 Jan 2015 12:08:05 -0500 Subject: [PATCH 0595/1169] clean out __init__.py (+2 squashed commits) Squashed commits: [779350d] clean out certfuzz.crash.__init__.py [7e3e5ba] clean out __init__.py --- src/certfuzz/crash/__init__.py | 5 --- src/certfuzz/debuggers/__init__.py | 13 ------ .../debuggers/output_parsers/__init__.py | 4 -- .../debuggers/output_parsers/gdbfile.py | 2 +- src/certfuzz/fuzzers/__init__.py | 1 - src/certfuzz/fuzzers/bitmut.py | 2 +- src/certfuzz/fuzzers/bytemut.py | 2 +- src/certfuzz/fuzzers/drop.py | 4 +- src/certfuzz/fuzzers/insert.py | 4 +- src/certfuzz/fuzzers/swap.py | 2 +- src/certfuzz/fuzzers/truncate.py | 2 +- src/certfuzz/fuzzers/wave.py | 4 +- src/certfuzz/iteration/iteration_windows.py | 2 +- src/certfuzz/test/__init__.py | 4 -- src/certfuzz/test/crash/test_bff_crash.py | 2 +- src/certfuzz/test/crash/test_crash_pkg.py | 21 ---------- .../output_parsers/test_debugger_file_base.py | 16 ++++---- .../debuggers/output_parsers/test_gdbfile.py | 6 +-- .../output_parsers/test_output_parsers_pkg.py | 36 ----------------- .../test/debuggers/test_debuggers_pkg.py | 40 ------------------- src/certfuzz/test/fuzzers/test_bitmut.py | 2 +- src/certfuzz/test/fuzzers/test_bytemut.py | 2 +- src/certfuzz/test/fuzzers/test_crlfmut.py | 2 +- src/certfuzz/test/fuzzers/test_crmut.py | 2 +- src/certfuzz/test/fuzzers/test_drop.py | 2 +- src/certfuzz/test/fuzzers/test_fuzzer_base.py | 6 +-- src/certfuzz/test/fuzzers/test_fuzzers_pkg.py | 30 -------------- src/certfuzz/test/fuzzers/test_insert.py | 2 +- src/certfuzz/test/fuzzers/test_nullmut.py | 2 +- src/certfuzz/test/fuzzers/test_swap.py | 2 +- src/certfuzz/test/fuzzers/test_truncate.py | 2 +- src/certfuzz/test/fuzzers/test_wave.py | 2 +- 32 files changed, 37 insertions(+), 191 deletions(-) delete mode 100644 src/certfuzz/test/crash/test_crash_pkg.py delete mode 100644 src/certfuzz/test/debuggers/output_parsers/test_output_parsers_pkg.py delete mode 100644 src/certfuzz/test/debuggers/test_debuggers_pkg.py delete mode 100644 src/certfuzz/test/fuzzers/test_fuzzers_pkg.py diff --git a/src/certfuzz/crash/__init__.py b/src/certfuzz/crash/__init__.py index 6d48b4d..e69de29 100644 --- a/src/certfuzz/crash/__init__.py +++ b/src/certfuzz/crash/__init__.py @@ -1,5 +0,0 @@ -from certfuzz.crash.testcase_base import TestCaseBase -from certfuzz.crash.crash_base import Testcase -from certfuzz.crash.bff_crash import BffCrash -from certfuzz.crash.errors import CrashError -from certfuzz.crash.errors import TestCaseError diff --git a/src/certfuzz/debuggers/__init__.py b/src/certfuzz/debuggers/__init__.py index 95637b7..e69de29 100644 --- a/src/certfuzz/debuggers/__init__.py +++ b/src/certfuzz/debuggers/__init__.py @@ -1,13 +0,0 @@ -from certfuzz.debuggers.errors import UndefinedDebuggerError -from certfuzz.debuggers.errors import DebuggerNotFoundError -from certfuzz.debuggers.errors import DebuggerError -from certfuzz.debuggers.registration import get_debug_file -from certfuzz.debuggers.registration import register -from certfuzz.debuggers.registration import verify_supported_platform -from certfuzz.debuggers.registration import get -from certfuzz.debuggers.registration import result_fields -from certfuzz.debuggers.registration import allowed_exploitability_values -from certfuzz.debuggers.registration import debugger -from certfuzz.debuggers.registration import debug_class -from certfuzz.debuggers.registration import debug_ext -from certfuzz.debuggers.debugger_base import Debugger diff --git a/src/certfuzz/debuggers/output_parsers/__init__.py b/src/certfuzz/debuggers/output_parsers/__init__.py index c8a3214..e69de29 100644 --- a/src/certfuzz/debuggers/output_parsers/__init__.py +++ b/src/certfuzz/debuggers/output_parsers/__init__.py @@ -1,4 +0,0 @@ -from certfuzz.debuggers.output_parsers.debugger_file_base import DebuggerFile, \ - blacklist, check_thread_type, detect_format, regex, registers -from certfuzz.debuggers.output_parsers.errors import DebuggerError, \ - DebuggerFileError, UnknownDebuggerError diff --git a/src/certfuzz/debuggers/output_parsers/gdbfile.py b/src/certfuzz/debuggers/output_parsers/gdbfile.py index 5df1fd7..5dbc84b 100644 --- a/src/certfuzz/debuggers/output_parsers/gdbfile.py +++ b/src/certfuzz/debuggers/output_parsers/gdbfile.py @@ -5,7 +5,7 @@ @organization: cert.org ''' -from certfuzz.debuggers.output_parsers import DebuggerFile +from certfuzz.debuggers.output_parsers.debugger_file_base import DebuggerFile class GDBfile(DebuggerFile): diff --git a/src/certfuzz/fuzzers/__init__.py b/src/certfuzz/fuzzers/__init__.py index b305044..e69de29 100644 --- a/src/certfuzz/fuzzers/__init__.py +++ b/src/certfuzz/fuzzers/__init__.py @@ -1 +0,0 @@ -from certfuzz.fuzzers.fuzzer_base import Fuzzer, MinimizableFuzzer diff --git a/src/certfuzz/fuzzers/bitmut.py b/src/certfuzz/fuzzers/bitmut.py index 319fd5e..c20d2f2 100644 --- a/src/certfuzz/fuzzers/bitmut.py +++ b/src/certfuzz/fuzzers/bitmut.py @@ -1,4 +1,4 @@ -from certfuzz.fuzzers import MinimizableFuzzer +from certfuzz.fuzzers.fuzzer_base import MinimizableFuzzer from random import jumpahead, sample, uniform, seed import logging diff --git a/src/certfuzz/fuzzers/bytemut.py b/src/certfuzz/fuzzers/bytemut.py index 41640ec..b075736 100644 --- a/src/certfuzz/fuzzers/bytemut.py +++ b/src/certfuzz/fuzzers/bytemut.py @@ -1,7 +1,7 @@ import logging import random -from certfuzz.fuzzers import MinimizableFuzzer +from certfuzz.fuzzers.fuzzer_base import MinimizableFuzzer from certfuzz.fuzzers.fuzzer_base import is_fuzzable as _fuzzable diff --git a/src/certfuzz/fuzzers/drop.py b/src/certfuzz/fuzzers/drop.py index 2db91c5..1b399e1 100644 --- a/src/certfuzz/fuzzers/drop.py +++ b/src/certfuzz/fuzzers/drop.py @@ -2,7 +2,7 @@ """ import logging -from certfuzz.fuzzers import Fuzzer +from certfuzz.fuzzers.fuzzer_base import Fuzzer from certfuzz.fuzzers.errors import FuzzerExhaustedError @@ -34,7 +34,7 @@ def _fuzz(self): if byte_pos < len(bytes_to_fuzz): del self.input[byte_pos] else: - #indicate we didn't fuzz the file for this iteration + # indicate we didn't fuzz the file for this iteration raise FuzzerExhaustedError('Iteration exceeds available values') logger.debug('%s - dropped byte 0x%02x', self.sf.basename, byte_pos) diff --git a/src/certfuzz/fuzzers/insert.py b/src/certfuzz/fuzzers/insert.py index c3077a4..7a63266 100644 --- a/src/certfuzz/fuzzers/insert.py +++ b/src/certfuzz/fuzzers/insert.py @@ -3,7 +3,7 @@ import logging from random import getrandbits -from certfuzz.fuzzers import Fuzzer +from certfuzz.fuzzers.fuzzer_base import Fuzzer from certfuzz.fuzzers.errors import FuzzerExhaustedError @@ -38,7 +38,7 @@ def _fuzz(self): if byte_pos < len(bytes_to_fuzz): self.input.insert(byte_pos, byte_to_insert) else: - #indicate we didn't fuzz the file for this iteration + # indicate we didn't fuzz the file for this iteration raise FuzzerExhaustedError('Iteration exceeds available values') logger.debug('%s - inserted byte 0x%02x at 0x%02x', self.sf.basename, diff --git a/src/certfuzz/fuzzers/swap.py b/src/certfuzz/fuzzers/swap.py index 3bc498d..36aea1c 100644 --- a/src/certfuzz/fuzzers/swap.py +++ b/src/certfuzz/fuzzers/swap.py @@ -3,7 +3,7 @@ """ import logging -from certfuzz.fuzzers import MinimizableFuzzer +from certfuzz.fuzzers.fuzzer_base import MinimizableFuzzer from certfuzz.fuzzers.errors import FuzzerExhaustedError diff --git a/src/certfuzz/fuzzers/truncate.py b/src/certfuzz/fuzzers/truncate.py index 3de1a81..c1a01db 100644 --- a/src/certfuzz/fuzzers/truncate.py +++ b/src/certfuzz/fuzzers/truncate.py @@ -5,7 +5,7 @@ ''' import logging -from certfuzz.fuzzers import Fuzzer +from certfuzz.fuzzers.fuzzer_base import Fuzzer from certfuzz.fuzzers.errors import FuzzerExhaustedError diff --git a/src/certfuzz/fuzzers/wave.py b/src/certfuzz/fuzzers/wave.py index ae09bc9..b4d313f 100644 --- a/src/certfuzz/fuzzers/wave.py +++ b/src/certfuzz/fuzzers/wave.py @@ -3,7 +3,7 @@ """ import logging -from certfuzz.fuzzers import MinimizableFuzzer +from certfuzz.fuzzers.fuzzer_base import MinimizableFuzzer from certfuzz.fuzzers.errors import FuzzerExhaustedError @@ -31,7 +31,7 @@ def _fuzz(self): if q < len(bytes_to_fuzz): self.input[bytes_to_fuzz[q]] = r else: - #indicate we didn't fuzz the file for this iteration + # indicate we didn't fuzz the file for this iteration raise FuzzerExhaustedError('Iteration exceeds available values') logger.debug('%s - set byte 0x%02x to 0x%02x', self.sf.basename, q, r) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 25b069e..90df43f 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -10,7 +10,7 @@ from certfuzz.config.config_windows import get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash -from certfuzz.debuggers.output_parsers import DebuggerFileError +from certfuzz.debuggers.output_parsers.errors import DebuggerFileError from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.fuzzers.errors import FuzzerError diff --git a/src/certfuzz/test/__init__.py b/src/certfuzz/test/__init__.py index ff39d05..e69de29 100644 --- a/src/certfuzz/test/__init__.py +++ b/src/certfuzz/test/__init__.py @@ -1,4 +0,0 @@ -from .mocks import Mock -from .mocks import MockRange -from .mocks import MockRangefinder -from .mocks import MockSeedfile diff --git a/src/certfuzz/test/crash/test_bff_crash.py b/src/certfuzz/test/crash/test_bff_crash.py index 894ce09..da8cb5c 100644 --- a/src/certfuzz/test/crash/test_bff_crash.py +++ b/src/certfuzz/test/crash/test_bff_crash.py @@ -6,7 +6,7 @@ import unittest # from certfuzz.crash.bff_crash import Crash -from certfuzz.test import Mock +from certfuzz.test.mocks import Mock import tempfile import os import shutil diff --git a/src/certfuzz/test/crash/test_crash_pkg.py b/src/certfuzz/test/crash/test_crash_pkg.py deleted file mode 100644 index 9704909..0000000 --- a/src/certfuzz/test/crash/test_crash_pkg.py +++ /dev/null @@ -1,21 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.crash - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/debuggers/output_parsers/test_debugger_file_base.py b/src/certfuzz/test/debuggers/output_parsers/test_debugger_file_base.py index 966f49e..89fa1eb 100644 --- a/src/certfuzz/test/debuggers/output_parsers/test_debugger_file_base.py +++ b/src/certfuzz/test/debuggers/output_parsers/test_debugger_file_base.py @@ -8,14 +8,14 @@ import glob import os import logging -from certfuzz.debuggers.output_parsers import detect_format -from certfuzz.debuggers.output_parsers import UnknownDebuggerError +from certfuzz.debuggers.output_parsers.debugger_file_base import detect_format +from certfuzz.debuggers.output_parsers.errors import UnknownDebuggerError -#logger = logging.getLogger() -#hdlr = logging.StreamHandler() -#logger.addHandler(hdlr) -#logger.setLevel(logging.WARNING) -#debuggers.debug_file.logger.setLevel(logging.DEBUG) +# logger = logging.getLogger() +# hdlr = logging.StreamHandler() +# logger.addHandler(hdlr) +# logger.setLevel(logging.WARNING) +# debuggers.debug_file.logger.setLevel(logging.DEBUG) class Test(unittest.TestCase): @@ -58,5 +58,5 @@ def test_formats_that_should_fail(self): self.detect_format_fail(self.expect2fail) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/debuggers/output_parsers/test_gdbfile.py b/src/certfuzz/test/debuggers/output_parsers/test_gdbfile.py index a469530..c336e7c 100644 --- a/src/certfuzz/test/debuggers/output_parsers/test_gdbfile.py +++ b/src/certfuzz/test/debuggers/output_parsers/test_gdbfile.py @@ -7,7 +7,7 @@ import tempfile import hashlib from certfuzz.debuggers.output_parsers.gdbfile import GDBfile -from certfuzz.debuggers.output_parsers import registers +from certfuzz.debuggers.output_parsers.debugger_file_base import registers import unittest class _Test(unittest.TestCase): @@ -159,7 +159,7 @@ def test_registers(self): self.assertEqual(gdbf.registers_hex[r], '0xf00') self.assertEqual(gdbf.registers[r], 'bar') - #TODO: finish writing this test + # TODO: finish writing this test def test_backtrace_without_questionmarks(self): (fd, f) = tempfile.mkstemp(text=True) @@ -201,5 +201,5 @@ def test_received_signal(self): self.delete_file(f) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/debuggers/output_parsers/test_output_parsers_pkg.py b/src/certfuzz/test/debuggers/output_parsers/test_output_parsers_pkg.py deleted file mode 100644 index 9aae86b..0000000 --- a/src/certfuzz/test/debuggers/output_parsers/test_output_parsers_pkg.py +++ /dev/null @@ -1,36 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.debuggers.output_parsers - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.debuggers.output_parsers - api_list = ['DebuggerError', - 'DebuggerFileError', - 'UnknownDebuggerError', - 'regex', - 'DebuggerFile', - 'detect_format', - 'check_thread_type', - 'registers', - 'blacklist', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/debuggers/test_debuggers_pkg.py b/src/certfuzz/test/debuggers/test_debuggers_pkg.py deleted file mode 100644 index 8f223e2..0000000 --- a/src/certfuzz/test/debuggers/test_debuggers_pkg.py +++ /dev/null @@ -1,40 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.debuggers - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.debuggers - api_list = ['UndefinedDebuggerError', - 'DebuggerNotFoundError', - 'DebuggerError', - 'get_debug_file', - 'register', - 'verify_supported_platform', - 'get', - 'result_fields', - 'allowed_exploitability_values', - 'Debugger', - 'debugger', - 'debug_class', - 'debug_ext', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/fuzzers/test_bitmut.py b/src/certfuzz/test/fuzzers/test_bitmut.py index 901ca50..42da10f 100644 --- a/src/certfuzz/test/fuzzers/test_bitmut.py +++ b/src/certfuzz/test/fuzzers/test_bitmut.py @@ -6,7 +6,7 @@ import unittest import tempfile import os -from certfuzz.test import MockSeedfile +from certfuzz.test.mocks import MockSeedfile import shutil from certfuzz.fuzzers.bitmut import BitMutFuzzer diff --git a/src/certfuzz/test/fuzzers/test_bytemut.py b/src/certfuzz/test/fuzzers/test_bytemut.py index dbb8721..e6c1c4c 100644 --- a/src/certfuzz/test/fuzzers/test_bytemut.py +++ b/src/certfuzz/test/fuzzers/test_bytemut.py @@ -9,7 +9,7 @@ import shutil from certfuzz.fuzzers.bytemut import fuzz, _fuzzable from certfuzz.fuzzers.bytemut import ByteMutFuzzer -from certfuzz.test import MockSeedfile, MockRange +from certfuzz.test.mocks import MockSeedfile, MockRange import tempfile from certfuzz.fuzztools.hamming import bytewise_hd diff --git a/src/certfuzz/test/fuzzers/test_crlfmut.py b/src/certfuzz/test/fuzzers/test_crlfmut.py index af3d88b..ea986a6 100644 --- a/src/certfuzz/test/fuzzers/test_crlfmut.py +++ b/src/certfuzz/test/fuzzers/test_crlfmut.py @@ -9,7 +9,7 @@ import shutil from certfuzz.fuzzers.bytemut import fuzz from certfuzz.fuzzers.crlfmut import CRLFMutFuzzer -from certfuzz.test import MockSeedfile, MockRange +from certfuzz.test.mocks import MockSeedfile, MockRange import tempfile from certfuzz.fuzztools.hamming import bytewise_hd import copy diff --git a/src/certfuzz/test/fuzzers/test_crmut.py b/src/certfuzz/test/fuzzers/test_crmut.py index 7bdb299..7f246e0 100644 --- a/src/certfuzz/test/fuzzers/test_crmut.py +++ b/src/certfuzz/test/fuzzers/test_crmut.py @@ -9,7 +9,7 @@ import shutil from certfuzz.fuzzers.bytemut import fuzz from certfuzz.fuzzers.crmut import CRMutFuzzer -from certfuzz.test import MockSeedfile, MockRange +from certfuzz.test.mocks import MockSeedfile, MockRange import tempfile from certfuzz.fuzztools.hamming import bytewise_hd import copy diff --git a/src/certfuzz/test/fuzzers/test_drop.py b/src/certfuzz/test/fuzzers/test_drop.py index 6158592..d243c41 100644 --- a/src/certfuzz/test/fuzzers/test_drop.py +++ b/src/certfuzz/test/fuzzers/test_drop.py @@ -10,7 +10,7 @@ import shutil from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging -from certfuzz.test import MockSeedfile +from certfuzz.test.mocks import MockSeedfile import tempfile certfuzz.fuzzers.drop.logger.setLevel(logging.WARNING) diff --git a/src/certfuzz/test/fuzzers/test_fuzzer_base.py b/src/certfuzz/test/fuzzers/test_fuzzer_base.py index f1c62d0..a02aff8 100644 --- a/src/certfuzz/test/fuzzers/test_fuzzer_base.py +++ b/src/certfuzz/test/fuzzers/test_fuzzer_base.py @@ -6,10 +6,10 @@ import unittest import os -from certfuzz.fuzzers import Fuzzer -from certfuzz.test import MockSeedfile +from certfuzz.fuzzers.fuzzer_base import Fuzzer +from certfuzz.test.mocks import MockSeedfile import shutil -from certfuzz.fuzzers import MinimizableFuzzer +from certfuzz.fuzzers.fuzzer_base import MinimizableFuzzer import tempfile from certfuzz.fuzzers.fuzzer_base import is_fuzzable as _fuzzable diff --git a/src/certfuzz/test/fuzzers/test_fuzzers_pkg.py b/src/certfuzz/test/fuzzers/test_fuzzers_pkg.py deleted file mode 100644 index 7843f04..0000000 --- a/src/certfuzz/test/fuzzers/test_fuzzers_pkg.py +++ /dev/null @@ -1,30 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.fuzzers - - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.fuzzers - api_list = ['Fuzzer', - 'MinimizableFuzzer', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/certfuzz/test/fuzzers/test_insert.py b/src/certfuzz/test/fuzzers/test_insert.py index 06cdb59..9b1c6df 100644 --- a/src/certfuzz/test/fuzzers/test_insert.py +++ b/src/certfuzz/test/fuzzers/test_insert.py @@ -10,7 +10,7 @@ import shutil from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging -from certfuzz.test import MockSeedfile +from certfuzz.test.mocks import MockSeedfile import tempfile certfuzz.fuzzers.insert.logger.setLevel(logging.WARNING) diff --git a/src/certfuzz/test/fuzzers/test_nullmut.py b/src/certfuzz/test/fuzzers/test_nullmut.py index b90e162..6ef244d 100644 --- a/src/certfuzz/test/fuzzers/test_nullmut.py +++ b/src/certfuzz/test/fuzzers/test_nullmut.py @@ -8,7 +8,7 @@ import os import shutil from certfuzz.fuzzers.nullmut import NullMutFuzzer -from certfuzz.test import MockSeedfile, MockRange +from certfuzz.test.mocks import MockSeedfile, MockRange import tempfile from certfuzz.fuzztools.hamming import bytewise_hd diff --git a/src/certfuzz/test/fuzzers/test_swap.py b/src/certfuzz/test/fuzzers/test_swap.py index b0fa70b..88acecc 100644 --- a/src/certfuzz/test/fuzzers/test_swap.py +++ b/src/certfuzz/test/fuzzers/test_swap.py @@ -5,7 +5,7 @@ ''' import unittest from certfuzz.fuzzers.swap import SwapFuzzer -from certfuzz.test import MockSeedfile +from certfuzz.test.mocks import MockSeedfile from certfuzz.fuzzers.errors import FuzzerExhaustedError import shutil import tempfile diff --git a/src/certfuzz/test/fuzzers/test_truncate.py b/src/certfuzz/test/fuzzers/test_truncate.py index fb4f323..ccb4d58 100644 --- a/src/certfuzz/test/fuzzers/test_truncate.py +++ b/src/certfuzz/test/fuzzers/test_truncate.py @@ -10,7 +10,7 @@ import shutil from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging -from certfuzz.test import MockSeedfile +from certfuzz.test.mocks import MockSeedfile import tempfile certfuzz.fuzzers.drop.logger.setLevel(logging.WARNING) diff --git a/src/certfuzz/test/fuzzers/test_wave.py b/src/certfuzz/test/fuzzers/test_wave.py index 16616cd..c3a0070 100644 --- a/src/certfuzz/test/fuzzers/test_wave.py +++ b/src/certfuzz/test/fuzzers/test_wave.py @@ -10,7 +10,7 @@ import shutil from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging -from certfuzz.test import MockSeedfile +from certfuzz.test.mocks import MockSeedfile import tempfile certfuzz.fuzzers.wave.logger.setLevel(logging.WARNING) From da72d5ec7071dd226e9302b9b21170001ba07e77 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 8 Jan 2015 12:25:50 -0500 Subject: [PATCH 0596/1169] fix imports --- src/certfuzz/minimizer/minimizer_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index c551d33..fa2c6f9 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -13,7 +13,7 @@ import tempfile import time -from certfuzz import debuggers +from certfuzz.debuggers.registration import get as debugger_get from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.fuzztools import hamming, filetools, probability, text @@ -108,7 +108,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, if not os.path.isdir(self.crash_dst): raise MinimizerError("%s is not a directory" % self.crash_dst) - self.debugger = debuggers.get() + self.debugger = debugger_get() self._logger_setup() self.logger.info("Minimizer initializing for %s", self.crash.fuzzedfile.path) @@ -402,7 +402,7 @@ def is_same_crash(self): self.crash_sigs_found[newfuzzed_hash] = 1 self.logger.info('crash=%s signal=%s', newfuzzed_hash, dbg.signal) - if self.save_others and not newfuzzed_hash in self.crash_hashes: + if self.save_others and newfuzzed_hash not in self.crash_hashes: # the crash is not one of the crashes we're looking for # so add it to the other_crashes dict in case our # caller wants to do something with it From 9b1e6585fc24d3c3e5d25ebd86772752f1973f40 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 9 Jan 2015 08:28:31 -0500 Subject: [PATCH 0597/1169] fix unit test --- src/certfuzz/test/minimizer/test_minimizer_base.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/test/minimizer/test_minimizer_base.py b/src/certfuzz/test/minimizer/test_minimizer_base.py index 8e17455..3d9bcb9 100644 --- a/src/certfuzz/test/minimizer/test_minimizer_base.py +++ b/src/certfuzz/test/minimizer/test_minimizer_base.py @@ -6,11 +6,11 @@ import os import tempfile from certfuzz.fuzztools import hamming -from certfuzz.minimizer import Minimizer +from certfuzz.minimizer.minimizer_base import Minimizer import shutil from certfuzz.file_handlers.basicfile import BasicFile -import certfuzz import unittest +import certfuzz.minimizer.minimizer_base class Mock(object): def __init__(self, *args, **kwargs): @@ -50,6 +50,9 @@ def get(self): def go(self): return MockDbgOut() +def _mock_dbg_get(): + return MockDebugger + class Test(unittest.TestCase): def delete_file(self, f): @@ -59,6 +62,9 @@ def delete_file(self, f): def setUp(self): self.cfg = MockCfg() self.crash = MockCrasher() + + certfuzz.minimizer.minimizer_base.debugger_get = _mock_dbg_get + self.tempdir = tempfile.mkdtemp(prefix='minimizer_test_') self.crash_dst_dir = tempfile.mkdtemp(prefix='crash_', dir=self.tempdir) (fd, self.logfile) = tempfile.mkstemp(dir=self.tempdir) @@ -124,5 +130,5 @@ def test_update_probabilities(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From f67f3ad24a9224413f3ff91aa0d897efc4922c68 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 2 Feb 2015 14:41:49 -0500 Subject: [PATCH 0598/1169] Update NSIS inetc plugin --- build/distmods/windows/nsis/inetc.dll | Bin 24576 -> 22016 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/build/distmods/windows/nsis/inetc.dll b/build/distmods/windows/nsis/inetc.dll index 1420582e3be626102ef0d21bc00a2fefa7e22044..cca1550d58a25b25dafeae38cdec508103bdef29 100644 GIT binary patch literal 22016 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 literal 24576 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 Jz<&V^{BL Date: Tue, 28 Apr 2015 13:56:35 -0400 Subject: [PATCH 0599/1169] Use latest exploitable from github --- src/linux/CERT_triage_tools/.gitignore | 33 ++ src/linux/CERT_triage_tools/.travis.yml | 32 ++ src/linux/CERT_triage_tools/AUTHORS.txt | 27 +- src/linux/CERT_triage_tools/LICENSE.md | 106 +++++ src/linux/CERT_triage_tools/LICENSE.txt | 51 --- src/linux/CERT_triage_tools/README.md | 146 +++++++ src/linux/CERT_triage_tools/README.txt | 41 -- .../CERT_triage_tools/exploitable/AUTHORS.txt | 3 - .../CERT_triage_tools/exploitable/README.txt | 54 --- .../exploitable/exploitable.py | 267 ++++++------ .../exploitable/lib/analyzers.py | 296 ------------- .../exploitable/lib/analyzers/__init__.py | 0 .../exploitable/lib/analyzers/arm.py | 189 +++++++++ .../exploitable/lib/analyzers/asan.py | 99 +++++ .../exploitable/lib/analyzers/x86.py | 355 ++++++++++++++++ .../CERT_triage_tools/exploitable/lib/arch.py | 116 ++++++ .../exploitable/lib/classifier.py | 126 +++--- .../exploitable/lib/gdb_wrapper/__init__.py | 0 .../exploitable/lib/gdb_wrapper/arm.py | 303 ++++++++++++++ .../exploitable/lib/gdb_wrapper/asan.py | 354 ++++++++++++++++ .../exploitable/lib/gdb_wrapper/elf.py | 81 ++++ .../lib/{versions.py => gdb_wrapper/qnx.py} | 35 +- .../tests/x86_unit_tests.py} | 357 ++++++++-------- .../{gdb_wrapper.py => gdb_wrapper/x86.py} | 392 +++++++++++------- .../exploitable/lib/rules.py | 66 ++- .../exploitable/lib/tools.py | 141 ++++--- .../exploitable/tests/Makefile | 4 +- .../exploitable/tests/Makefile.arm | 34 ++ .../exploitable/tests/testBlockMoveAv.c | 3 + .../exploitable/tests/testBranchAv.c | 8 +- .../exploitable/tests/testBranchAvNearNull.c | 8 +- .../exploitable/tests/testDeepStack.c | 19 + .../exploitable/tests/testSourceAv.c | 2 +- .../CERT_triage_tools/gdb_install_stub.py | 19 + src/linux/CERT_triage_tools/setup.py | 71 ++++ .../CERT_triage_tools/test/arm-expected.json | 197 +++++++++ src/linux/CERT_triage_tools/test/arm.sh | 203 +++++++++ .../CERT_triage_tools/test/x86-expected.json | 122 ++++++ src/linux/CERT_triage_tools/test/x86.sh | 129 ++++++ src/linux/CERT_triage_tools/triage.py | 64 +-- 40 files changed, 3469 insertions(+), 1084 deletions(-) create mode 100644 src/linux/CERT_triage_tools/.gitignore create mode 100644 src/linux/CERT_triage_tools/.travis.yml create mode 100644 src/linux/CERT_triage_tools/LICENSE.md delete mode 100644 src/linux/CERT_triage_tools/LICENSE.txt create mode 100644 src/linux/CERT_triage_tools/README.md delete mode 100644 src/linux/CERT_triage_tools/README.txt delete mode 100644 src/linux/CERT_triage_tools/exploitable/AUTHORS.txt delete mode 100644 src/linux/CERT_triage_tools/exploitable/README.txt delete mode 100644 src/linux/CERT_triage_tools/exploitable/lib/analyzers.py create mode 100644 src/linux/CERT_triage_tools/exploitable/lib/analyzers/__init__.py create mode 100644 src/linux/CERT_triage_tools/exploitable/lib/analyzers/arm.py create mode 100644 src/linux/CERT_triage_tools/exploitable/lib/analyzers/asan.py create mode 100644 src/linux/CERT_triage_tools/exploitable/lib/analyzers/x86.py create mode 100644 src/linux/CERT_triage_tools/exploitable/lib/arch.py create mode 100644 src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/__init__.py create mode 100644 src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/arm.py create mode 100644 src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/asan.py create mode 100644 src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/elf.py rename src/linux/CERT_triage_tools/exploitable/lib/{versions.py => gdb_wrapper/qnx.py} (72%) rename src/linux/CERT_triage_tools/exploitable/lib/{tests/unit_tests.py => gdb_wrapper/tests/x86_unit_tests.py} (79%) rename src/linux/CERT_triage_tools/exploitable/lib/{gdb_wrapper.py => gdb_wrapper/x86.py} (53%) create mode 100644 src/linux/CERT_triage_tools/exploitable/tests/Makefile.arm create mode 100644 src/linux/CERT_triage_tools/exploitable/tests/testDeepStack.c create mode 100644 src/linux/CERT_triage_tools/gdb_install_stub.py create mode 100644 src/linux/CERT_triage_tools/setup.py create mode 100644 src/linux/CERT_triage_tools/test/arm-expected.json create mode 100644 src/linux/CERT_triage_tools/test/arm.sh create mode 100644 src/linux/CERT_triage_tools/test/x86-expected.json create mode 100644 src/linux/CERT_triage_tools/test/x86.sh diff --git a/src/linux/CERT_triage_tools/.gitignore b/src/linux/CERT_triage_tools/.gitignore new file mode 100644 index 0000000..84c2c9b --- /dev/null +++ b/src/linux/CERT_triage_tools/.gitignore @@ -0,0 +1,33 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + diff --git a/src/linux/CERT_triage_tools/.travis.yml b/src/linux/CERT_triage_tools/.travis.yml new file mode 100644 index 0000000..f47df87 --- /dev/null +++ b/src/linux/CERT_triage_tools/.travis.yml @@ -0,0 +1,32 @@ +# This is a travis-ci.org configuration script. This script performs +# integration tests for the GDB 'exploitable' plugin on x86 and ARM target +# architectures when the associated git repo is checked into github. +# The majority of the testing logic is contained in bash scripts +# rather than this configuration file to support running the tests outside +# of travis-ci. +# +# Jonathan Foote +# jmfoote@loyola.edu + +language: python +python: + - "2.7" + +# These are AWS keys. See http://docs.travis-ci.com/user/encryption-keys/ +env: + global: + - secure: "N5Je6PZWWK8Ti2/siLWU3xtLPfvx6VZLwDc+F3Q2YEwNdEs0aNztyMYGJOwVCi0HGBA22biwl5I7lDxfD9kKPRCWCoyqKegTvuLe2kwrSomICnMKNx9OpJFMPdWwgAe+RhuHETusi6i1TJFLvDJ6gfBMtFqTHhiIGGv18KidW30=" + - secure: "TQ0kydD24Bwssp6ftb1KHh3dizlWFs000W95vi+hEpWPT7IsphWsFLIzH8p6rmqcGop+2iY1UWco1TFZnqn/xLwzwYRIBHhFzwSsizzaTT/9WVnn78CuHNlRNZxuRtwH//kqk4LQMN2LXg1F+RnMckSsTWmnYFXsz9IzY3uRLQs=" +# whitelist +branches: + only: + - master + +install: + pip install boto + +# command to run tests +# Note: the ARM test hack needs some work due to changes in the Travis +# environment; disabling for now to support pull requests for x86 +# script: '${TRAVIS_BUILD_DIR}/test/x86.sh && ${TRAVIS_BUILD_DIR}/test/arm.sh' +script: '${TRAVIS_BUILD_DIR}/test/x86.sh' diff --git a/src/linux/CERT_triage_tools/AUTHORS.txt b/src/linux/CERT_triage_tools/AUTHORS.txt index 9039ac6..4905030 100644 --- a/src/linux/CERT_triage_tools/AUTHORS.txt +++ b/src/linux/CERT_triage_tools/AUTHORS.txt @@ -1,3 +1,26 @@ -Developer: Jonathan Foote (jmfoote at cert dot org) +Thanks to everyone that has contributed to this project! -Test cases in ./exploitable/tests were orignally written by Josh Bressers of Red Hat, Inc. (bressers at redhat dot com) \ No newline at end of file +Jonathan Foote of CERT at Carnegie Mellon University + Primary developer and maintainer + jmfoote at andrew dot cmu dot edu + https://github.com/jfoote + +Josh Bressers of Red Hat, Inc. + Created test cases in ./exploitable/tests + bressers at redhat dot com + https://github.com/bressers + +Jesse Schwartzentruber of Blackberry, Inc. + Added support for ARM/QNX, additional methods for obtaining process address space mappings, ASAN support, cleaned up code, and made many other contributions. + https://github.com/jschwartzentruber + +allanlw + Contributed bugfixes and optimizations + https://github.com/allanlw + +Will Dormann of CERT at Carnegie Mellon University + Tested code and contributed bug reports + +Allen Householder of CERT at Carnegie Mellon University + Contributed updated CMU license text + https://github.com/ahouseholder diff --git a/src/linux/CERT_triage_tools/LICENSE.md b/src/linux/CERT_triage_tools/LICENSE.md new file mode 100644 index 0000000..693e29c --- /dev/null +++ b/src/linux/CERT_triage_tools/LICENSE.md @@ -0,0 +1,106 @@ +Use of the triage tools and related source code is subject to the terms +of multiple licenses. The licenses are appended below. See individual source +files for the licenses that apply to their code, respectively. + +License for code written by the author: +============================================ + +The MIT License (MIT) + +Copyright (c) 2013 Jonathan Foote + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License for CMU code (code written while I was at CMU/by CMU employees): +============================================ + +------------------------------------------------------------------------ +Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +------------------------------------------------------------------------ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following acknowledgments + and disclaimers. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. The names "Department of Homeland Security," "Carnegie Mellon + University," "CERT" and/or "Software Engineering Institute" shall + not be used to endorse or promote products derived from this software + without prior written permission. For written permission, please + contact permission@sei.cmu.edu. + +4. Products derived from this software may not be called "CERT" nor + may "CERT" appear in their names without prior written permission of + permission@sei.cmu.edu. + +5. Redistributions of any form whatsoever must retain the following + acknowledgment: + + "This product includes software developed by CERT with funding + and support from the Department of Homeland Security under + Contract No. FA 8721-05-C-0003." + +THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. + +License for code contributed by BlackBerry employees: +============================================ + +------------------------------------------------------------------------ +Copyright (C) 2013 BlackBerry Ltd. All Rights Reserved. +------------------------------------------------------------------------ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of BlackBerry Ltd. nor the names of its contributors +may be used to endorse or promote products derived from this software +without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLACKBERRY LTD BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + diff --git a/src/linux/CERT_triage_tools/LICENSE.txt b/src/linux/CERT_triage_tools/LICENSE.txt deleted file mode 100644 index b7f871c..0000000 --- a/src/linux/CERT_triage_tools/LICENSE.txt +++ /dev/null @@ -1,51 +0,0 @@ -Use of the triage tools and related source code is subject to the terms -of the license below. - ------------------------------------------------------------------------- -Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. ------------------------------------------------------------------------- -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following acknowledgments - and disclaimers. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. All advertising materials for third-party software mentioning - features or use of this software must display the following - disclaimer: - - "Neither Carnegie Mellon University nor its Software Engineering - Institute have reviewed or endorsed this software" - -4. The names "Department of Homeland Security," "Carnegie Mellon - University," "CERT" and/or "Software Engineering Institute" shall - not be used to endorse or promote products derived from this software - without prior written permission. For written permission, please - contact permission@sei.cmu.edu. - -5. Products derived from this software may not be called "CERT" nor - may "CERT" appear in their names without prior written permission of - permission@sei.cmu.edu. - -6. Redistributions of any form whatsoever must retain the following - acknowledgment: - - "This product includes software developed by CERT with funding - and support from the Department of Homeland Security under - Contract No. FA 8721-05-C-0003." - -THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND -CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER -EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE -EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, -CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND -RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, -RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND -COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. diff --git a/src/linux/CERT_triage_tools/README.md b/src/linux/CERT_triage_tools/README.md new file mode 100644 index 0000000..7ec8374 --- /dev/null +++ b/src/linux/CERT_triage_tools/README.md @@ -0,0 +1,146 @@ +GDB 'exploitable' plugin +==== + +Jonathan Foote + +jmfoote@loyola.edu + +15 April 2015 + +Requirements +==== + +- Compatible x86/x86_64/ARM Linux +- Compatible GDB 7.2 or later +- Python 2.7 or later (for triage.py) + + +The 'exploitable' plugin (exploitable/exploitable.py) +==== + +'exploitable' is a GDB extension that classifies Linux application bugs by severity. The extension inspects the state of a Linux application that has crashed and outputs a summary of how difficult it might be for an attacker to exploit the underlying software bug to gain control of the system. The extension can be used to prioritize bugs for software developers so that they can address the most severe ones first. + +The extension implements a GDB command called 'exploitable'. The command uses heuristics to describe the exploitability of the state of the application that is currently being debugged in GDB. The command is designed to be used on Linux platforms and versions of GDB that include the GDB Python API. Note that the command will not operate correctly on core file targets at this time. + +WARNING: This is an engineering tool. It has not been exhaustively tested, and has not been executed on many flavors of Linux! Please read and understand the classification rules (lib/rules.py) before use and modify the source code to suit your specific testing needs if necessary. + +Usage +---- + +### Global installation and usage +1. Optionally, run integration tests: + + $ python setup.py test + +2. Install script to GDB data directory (probably as root) + + \# python setup.py install + +3. Run the command + + (gdb) exploitable + +### Local usage +1. Copy all files in this directory and its sub-directories to a sub-directory that is accessible from GDB + +2. Source the exploitable file as a script and run a command + + (gdb) source my-exploitable-dir/exploitable.py + (gdb) exploitable + +In many cases exploitable makes guesses and calculates values based on GDB's stack unwind. The extension will work best if GDB can find the debug symbols for binaries, especially libc. + +Note that the extension's classification capability is significantly degraded when run in a GDB session with a core file target. When GDB is run against a core file, much of the information that the extension uses for classification is not present via conventional GDB APIs. + +Testing +---- + +### Smoke testing + +This project includes test cases (tests/) that can be used as a starting point for testing the 'exploitable' command on new Linux platforms. A Makefile is included. Note that test case filenames generally correspond to the most exploitabile tag that should be applied to the test case, however, because Linux platforms handle errors differently, an exact correspondence will not exist for all of the test cases on most Linux platforms. + +A few unit tests have been implemented in lib/gdb_wrapper/tests/x86\_unit\_tests.py. unit\_tests.py is meant to be invoked from GDB -- see comments in the file for details. + +### Integration testing + +Integration tests for x86 and ARM platforms are located in test/x86.sh and test/arm.sh, respectively. These tests are designed for use with travis-ci.org, but can (and should) be run locally on an up-to-date Ubuntu x86_64 any time functional changes are made to the code. Note that, if not modified, arm.sh requires access to a private AWS S3 bucket to install dependencies. Please contact the author if you require access to the S3 bucket. + +To run the integration x86 tests, try this from the project root directory: + $ ./tests/x86.sh + +Note that these tests are quite fragile and will (hopefully improve over time). + +#### Tested platforms + +At the time of this writing integration tests pass on the following platforms: + +Ubuntu 13.10 32-bit, GDB 7.2 +Ubuntu 13.10 64-bit, GDB 7.4 +Ubunut 13.10 64-bit, GDB 7.6 +Travis-ci Ubuntu 64-bit, GDB 7.4 + +The authors of the GDB Python API tend to break backwards compatibility regularly, so beware. + +Internals Overview +---- + +exploitable runs in GDB's Python interpreter (which depends on the Python C API) and uses GDB's Python API. For details, see: +http://sourceware.org/gdb/onlinedocs/gdb/Python-API.html + +exploitable iterates over a list of ordered "rules" (lib/rules.py) to generate a Classification (lib/classifier.py). If the state of the application running in GDB matches a rule, exploitable adds a corresponding "tag" to the Classification. The result of an exploitable invocation is a Classification-- either printed to the GDB's stdout or stored to a pickle file, depending on command parameters. + +The entry point for the GDB command is defined in exploitable.py. Iteration over the rules is implemented by a Classifier object (lib/classifier.py). The methods that determine whether a rule matches or not are contained in per-platform "analyzers" (lib/analyzers/). The state of the application is queried via a set of GDB API wrapper objects and methods (see lib/gdb_wrapper/x86.py for details). A Classification (lib/classifier.py) retains attributes for the "most exploitable" (lowest ordered) tag (matching rule), but it also includes an ordered list of all other matching tags. + +Classification rule definitions, located in lib/rules.py, can be re-prioritized by simple cut/paste. + +Contributing +---- + +Please contribute your changes, fixes, and issues to the master branch at https://github.com/jfoote/exploitable ! To help things go smoothly, please ensure that the integration tests (see test/ in the home directory) pass before you submit your code. Feel free to change the integration tests themselves if you are fixing bugs or adding features. Feel free to contact the author (jmfoote@loyola.edu) if you have any questions or feedback. + +triage.py +==== + +This package consists of a triage script and a GNU Debugger (GDB) extension named 'exploitable'. The triage script is a simple batch wrapper for the 'exploitable' GDB extension. The triage script is designed to prioritize bugs for software developers so that they can address the most severe ones first. For more information on the 'exploitable' GDB extension, see the 'exploitable' section below. + +The triage script automates invocations of GDB and the 'exploitable' GDB extension. The script invokes a target application one or more times via GDB. Each invocation includes execution of the exploitable command. Results of the exploitable command are accumulated and a summary is printed to stdout. + +WARNING: The triage script was written to address some specific testing needs, so it is not particularly robust or extensible. The script is being distributed as a starting point and example for writing a custom wrapper for the 'exploitable' extension. + +In practice the triage script is meant to run an application with a set of crashing inputs that have been discovered via other means. If an application invocation does not cause GDB to break, the triage script will hang (CTRL-C to stop). + +Note that some output from the application under test may be printed to the console as the triage script runs (particularly output from libc_message). Also note that the 'exploitable' extension will not operate correctly on core file targets at this time. + +Usage +---- + +The triage script is designed to be invoked from this directory. + +### Print help + +1. From this directory, invoke triage + + python triage.py --help + +### Running included exploitable tests (Hello exploitable!) + +1. Build exploitable tests + + cd exploitable/tests && make && cd ../.. + +2. Invoke triage with the tests as arguments + + python triage.py \$sub `find exploitable/tests/bin -type f` + +3. Cleanup test binaries + + cd exploitable/tests && make clean && cd ../.. + +### Example application usage + +1. Invoke triage with application and crashing inputs as arguments. For example: + + python triage.py "jasper --input \$sub --output /dev/null" `find /mnt/foo/crashers -type f` + +This example will invoke japser for each file in /mnt/foo/crashers. + diff --git a/src/linux/CERT_triage_tools/README.txt b/src/linux/CERT_triage_tools/README.txt deleted file mode 100644 index ebc8545..0000000 --- a/src/linux/CERT_triage_tools/README.txt +++ /dev/null @@ -1,41 +0,0 @@ -CERT post-mortem triage tools version 1.04 -October 4, 2012 - -===== Requirements ===== - -Compatible 32-bit or 64-bit Linux -GDB 7.2 or later -Python 2.6 or later - -===== About ===== - -The CERT post-mortem triage tools consist of a triage script and a GNU Debugger (GDB) extension named 'exploitable'. The triage script is a simple batch wrapper for the 'exploitable' GDB extension. The triage script is designed to prioritize bugs for software developers so that they can address the most severe ones first. For more information on the 'exploitable' GDB extension, see exploitable/readme.txt. - -The triage script automates invocations of GDB and the 'exploitable' GDB extension. The script invokes a target application one or more times via GDB. Each invocation includes execution of the exploitable command. Results of the exploitable command are accumulated and a summary is printed to stdout. - -WARNING: The triage script was written to address some specific testing needs at CERT, so it is not particularly robust or extensible. The script is being distributed as a starting point and example for writing a custom wrapper for the 'exploitable' extension. - -In practice the triage script is meant to run an application with a set of crashing inputs that have been discovered via other means. If an application invocation does not cause GDB to break, the triage script will hang (CTRL-C to stop). - -Note that some output from the application under test may be printed to the console as the triage script runs (particularly output from libc_message). Also note that the 'exploitable' extension will not operate correctly on core file targets at this time. - -===== Usage ===== - -The triage script is designed to be invoked from this directory. - -Print help - 1. From this directory, invoke triage - python triage.py --help -Running exploitable tests - 1. Build exploitable tests - cd exploitable/tests && make && cd ../.. - 2. Invoke triage with the tests as arguments - python triage.py \$sub `find exploitable/tests/bin -type f` - 3. Cleanup test binaries - cd exploitable/tests && make clean && cd ../.. -Example application usage - 1. Invoke triage with application and crashing inputs as arguments - ex: python triage.py "jasper --input \$sub --output /dev/null" \ - `find /mnt/foo/crashers -type f` - The example will invoke japser for each file in /mnt/foo/crashers. - diff --git a/src/linux/CERT_triage_tools/exploitable/AUTHORS.txt b/src/linux/CERT_triage_tools/exploitable/AUTHORS.txt deleted file mode 100644 index 196ca30..0000000 --- a/src/linux/CERT_triage_tools/exploitable/AUTHORS.txt +++ /dev/null @@ -1,3 +0,0 @@ -Developer: Jonathan Foote (jmfoote at cert dot org) - -Test cases in ./tests were orignally written by Josh Bressers of Red Hat, Inc. (bressers at redhat dot com) \ No newline at end of file diff --git a/src/linux/CERT_triage_tools/exploitable/README.txt b/src/linux/CERT_triage_tools/exploitable/README.txt deleted file mode 100644 index e0c1ebd..0000000 --- a/src/linux/CERT_triage_tools/exploitable/README.txt +++ /dev/null @@ -1,54 +0,0 @@ -CERT 'exploitable' GNU Debugger (GDB) extension version 1.04 -October 4, 2012 - -===== Requirements ===== - -Compatible 32-bit or 64-bit Linux -GDB 7.2 or later -Python 2.6 or later - -===== About ===== - -The CERT 'exploitable' engineering tool is a GDB extension that classifies Linux application bugs by severity. The extension inspects the state of a Linux application that has crashed and outputs a summary of how difficult it might be for an attacker to exploit the underlying software bug to gain control of the system. The extension can be used to prioritize bugs for software developers so that they can address the most severe ones first. - -The extension implements a GDB command called 'exploitable'. The command uses heuristics to describe the exploitability of the state of the application that is currently being debugged in GDB. The command is designed to be used on Linux platforms and versions of GDB that include the GDB Python API. Note that the command will not operate correctly on core file targets at this time. - -WARNING: This is an engineering tool. It has not been has not been exhaustively tested, and has not been executed on many flavors of Linux! Please read and understand the classification rules (lib/rules.py) before use and modify the source code to suit your specific testing needs if necessary. - -===== Usage ===== - -Global installation and usage - 1. Copy all files in this directory and its sub-directories to your - GDB Python data directory, which is probably /usr/share/gdb/python. - ex: sudo cp -r * /usr/share/gdb/python/ - To check your data directory, run gdb -ex "show data-dir" - 2. Import the exploitable file as a module and run a command - (gdb) python import exploitable - (gdb) exploitable -Local usage - 1. Copy all files in this directory and its sub-directories to a - sub-directory that is accessible from GDB - 2. Source the exploitable file as a script and run a command - (gdb) source my-exploitable-dir/exploitable.py - (gdb) exploitable - -In many cases exploitable makes guesses and calculates values based on GDB's stack unwind. The extension will work best if GDB can find the debug symbols for binaries, especially libc. - -Note that the extension's classification capability is significantly degraded when run in a GDB session with a core file target. When GDB is run against a core file, much of the information that the extension uses for classification is not present via conventional GDB APIs. - -===== Testing ===== - -This project includes test cases (tests/) that can be used as a starting point for testing the 'exploitable' command on new Linux platforms. A Makefile is included. Note that test case filenames generally correspond to the most exploitabile tag that should be applied to the test case, however, because Linux platforms handle errors differently, an exact correspondence will not exist for all of the test cases on most Linux platforms. - -A few unit tests have been implemented in lib/unit_tests.py. unit_tests.py is meant to be invoked from GDB -- see comments in the file for details. - -===== Internals Overview ===== - -exploitable runs in GDB's Python interpreter (which depends on the Python C API) and uses GDB's Python API. For details, see: -http://sourceware.org/gdb/onlinedocs/gdb/Python-API.html - -exploitable iterates over a list of ordered "rules" (lib/rules.py) to generate a Classification (lib/classifier.py). If the state of the application running in GDB matches a rule, exploitable adds a corresponding "tag" to the Classification. The result of an exploitable invocation is a Classification-- either printed to the GDB's stdout or stored to a pickle file, depending on command parameters. - -The entry point for the GDB command is defined in exploitable.py. Iteration over the rules is implemented by a Classifier object (lib/classifier.py). The functions that determine whether a rule matches or not are called "analyzers" (lib/analyzers.py). The state of the application is queried via a set of GDB API wrapper objects and methods (lib/gdb_wrapper.py). A Classification (lib/classifier.py) retains attributes for the "most exploitable" (lowest ordered) tag (matching rule), but it also includes an ordered list of all other matching tags. - -Classification rule definitions, located in lib/rules.py, can be re-prioritized by simple cut/paste. diff --git a/src/linux/CERT_triage_tools/exploitable/exploitable.py b/src/linux/CERT_triage_tools/exploitable/exploitable.py index d57c789..471a2a7 100644 --- a/src/linux/CERT_triage_tools/exploitable/exploitable.py +++ b/src/linux/CERT_triage_tools/exploitable/exploitable.py @@ -1,56 +1,55 @@ ### BEGIN LICENSE ### -### Use of the CERT Basic Fuzzing Framework and related source code -### is subject to the terms of the following licenses: +### Use of the triage tools and related source code is subject to the terms +### of the license below. ### -### GNU Public License (GPL) Rights pursuant to Version 2, June 1991 -### Government Purpose License Rights (GPLR) pursuant to DFARS -### 252.227.7013 +### ------------------------------------------------------------------------ +### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### ------------------------------------------------------------------------ +### Redistribution and use in source and binary forms, with or without +### modification, are permitted provided that the following conditions are +### met: ### -### NO WARRANTY +### 1. Redistributions of source code must retain the above copyright +### notice, this list of conditions and the following acknowledgments +### and disclaimers. ### -### ANY INFORMATION, MATERIALS, SERVICES, INTELLECTUAL -### PROPERTY OR OTHER PROPERTY OR RIGHTS GRANTED OR -### PROVIDED BY CARNEGIE MELLON UNIVERSITY PURSUANT TO -### THIS LICENSE (HEREINAFTER THE "DELIVERABLES") ARE ON -### AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES -### NO WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED -### AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, -### WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE, -### MERCHANTABILITY, INFORMATIONAL CONTENT, -### NONINFRINGEMENT, OR ERROR-FREE OPERATION. CARNEGIE -### MELLON UNIVERSITY SHALL NOT BE LIABLE FOR INDIRECT, -### SPECIAL OR CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF -### PROFITS OR INABILITY TO USE SAID INTELLECTUAL -### PROPERTY, UNDER THIS LICENSE, REGARDLESS OF WHETHER -### SUCH PARTY WAS AWARE OF THE POSSIBILITY OF SUCH -### DAMAGES. LICENSEE AGREES THAT IT WILL NOT MAKE ANY -### WARRANTY ON BEHALF OF CARNEGIE MELLON UNIVERSITY, -### EXPRESS OR IMPLIED, TO ANY PERSON CONCERNING THE -### APPLICATION OF OR THE RESULTS TO BE OBTAINED WITH -### THE DELIVERABLES UNDER THIS LICENSE. +### 2. Redistributions in binary form must reproduce the above copyright +### notice, this list of conditions and the following disclaimer in the +### documentation and/or other materials provided with the distribution. ### -### Licensee hereby agrees to defend, indemnify, and hold harmless -### Carnegie Mellon University, its trustees, officers, employees, -### and agents from all claims or demands made against them (and any -### related losses, expenses, or attorney's fees) arising out of, or -### relating to Licensee's and/or its sub licensees' negligent use or -### willful misuse of or negligent conduct or willful misconduct -### regarding the Software, facilities, or other rights or assistance -### granted by Carnegie Mellon University under this License, -### including, but not limited to, any claims of product liability, -### personal injury, death, damage to property, or violation of any -### laws or regulations. +### 3. The names "Department of Homeland Security," "Carnegie Mellon +### University," "CERT" and/or "Software Engineering Institute" shall +### not be used to endorse or promote products derived from this software +### without prior written permission. For written permission, please +### contact permission@sei.cmu.edu. ### -### Carnegie Mellon University Software Engineering Institute -### authored documents are sponsored by the U.S. Department of -### Defense under Contract F19628-00-C-0003. Carnegie Mellon -### University retains copyrights in all material produced under this -### contract. The U.S. Government retains a non-exclusive, -### royalty-free license to publish or reproduce these documents, or -### allow others to do so, for U.S. Government purposes only -### pursuant to the copyright license under the contract clause at -### 252.227.7013. +### 4. Products derived from this software may not be called "CERT" nor +### may "CERT" appear in their names without prior written permission of +### permission@sei.cmu.edu. +### +### 5. Redistributions of any form whatsoever must retain the following +### acknowledgment: +### +### "This product includes software developed by CERT with funding +### and support from the Department of Homeland Security under +### Contract No. FA 8721-05-C-0003." +### +### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. ### END LICENSE ### +### +### Portions Copyright 2013, BlackBerry Ltd. All Rights Reserved. +### + +# Jonathan Foote +# jmfoote@loyola.edu ''' Implements GDB entry point for GDB 'exploitable' command @@ -65,30 +64,27 @@ except ImportError as e: raise ImportError("This script must be run in GDB: ", str(e)) -# Code below adds directory containing this file to python path iff this script -# is being invoked standalone (i.e. not when it is imported). Allows script -# to be executed via GDB when not on GDB's PYTHONPATH without increasing -# vulnerability to carpet bombing attacks beyond that of executing a Python script -# outside GDB. -import sys, os -if __name__ == "__main__": - # Code below contains a workaround for a bug in GDB's Python API: - # os.path.abspath returns an incorrect string when this script is sourced - # from a path containing "~" - abspath = os.path.abspath(__file__) - pos = abspath.find("/~/") - if pos != -1: - abspath = abspath[pos+1:] - abspath = os.path.expanduser(abspath) - sys.path.append(os.path.dirname(abspath)) - -import warnings -from optparse import OptionParser -import cPickle as pickle +import argparse +import os import re -import lib.versions as versions -import lib.gdb_wrapper as gdb_wrapper +import sys +import warnings + +version = "1.32" + +# Code below contains a workaround for a bug in GDB's Python API: +# os.path.abspath returns an incorrect string when this script is sourced +# from a path containing "~" +abspath = os.path.abspath(__file__) +pos = abspath.find("/~/") +if pos != -1: + abspath = abspath[pos+1:] +abspath = os.path.expanduser(abspath) +sys.path.append(os.path.dirname(abspath)) + import lib.classifier as classifier +import lib.arch as arch +from lib.tools import print_machine_string def check_version(): ''' @@ -97,25 +93,30 @@ def check_version(): If the current operation environment is not Linux, an error is generated. ''' - if not sys.platform.startswith('linux'): - raise NotImplementedError("unsupported platform: %s" % sys.platform) # check GDB ver - if gdb_ver() < versions.min_gdb_version: - warnings.warn("GDB v%s may not support required Python API" % gdb_ver()) + if gdb_ver() < "7.2": + warnings.warn("GDB v{} may not support required Python API".format(gdb_ver())) -ver_regex = re.compile("^.*?([\d]\.[\d]).*$") +_re_gdb_version = re.compile(r"\d+\.\d+") def gdb_ver(): ''' Gets the GDB version number as a string. ''' gdbstr = gdb.execute("show version", False, True).splitlines()[0] - groups = re.match(ver_regex, gdbstr).groups() - if len(groups) != 1: - warnings.warn("Error while parsing gdb version string: %s", gdbstr) - return groups[0] + version = _re_gdb_version.search(gdbstr) + if version is None: + warnings.warn("Error while parsing gdb version string: {}".format(gdbstr)) + return version.group() + +class NiceArgParserExit(RuntimeError): + pass + +class NiceArgParser(argparse.ArgumentParser): + def exit(self, *args): + raise NiceArgParserExit(*args) -class ExploitableCommand (gdb.Command): +class ExploitableCommand(gdb.Command): ''' A GDB Command that determines how exploitable the current state of the Inferior (the program being debugged) is. Either prints the result to @@ -126,17 +127,17 @@ class ExploitableCommand (gdb.Command): of GDB have been issued. WARNING: This command may change the underlying state of GDB (ex: changing the disassembler flavor). - Type -h for options. Note specifying incorrect command options may - cause GDB to exit. + Type -h for options. ''' + _cmdstr = "exploitable" - _pc_regex = re.compile("^=>.*$") - def __init__ (self): + _re_gdb_pc = re.compile(r"^=>.*$") + def __init__(self): ''' Specifies the command string that invokes this Command from the GDB shell. See GDB Python API documentation for details ''' - super (ExploitableCommand, self).__init__ (self._cmdstr, gdb.COMMAND_OBSCURE) + gdb.Command.__init__(self, self._cmdstr, gdb.COMMAND_OBSCURE) def print_disassembly(self): ''' @@ -157,21 +158,23 @@ def print_disassembly(self): try: gdb.execute("disas $pc", False, True) except RuntimeError as e: - warnings.warn(e) + warnings.warn(str(e)) return try: disas = gdb.execute("disas $pc", False, True).splitlines() - pos = 0 - for line in disas: - if re.match(self._pc_regex, line): - break - pos += 1 - print "\n".join(disas[max(pos-5,0):pos+5]) except RuntimeError as e: - warnings.warn(e) + warnings.warn(str(e)) + return - def invoke (self, argstr, from_tty): + pos = 0 + for line in disas: + if self._re_gdb_pc.match(line): + break + pos += 1 + print("\n".join(disas[max(pos-5, 0):pos+5])) + + def invoke(self, argstr, from_tty): ''' Called when this Command is invoked from GDB. Prints classification of Inferior to GDB's STDOUT. @@ -179,52 +182,54 @@ def invoke (self, argstr, from_tty): Note that sys.stdout is automatically redirected to GDB's STDOUT. See GDB Python API documentation for details ''' + check_version() + + op = NiceArgParser(prog=self._cmdstr, description=self.__doc__) + op.add_argument("-v", "--verbose", action="store_true", + help="print analysis info from the Inferior") + op.add_argument("-m", "--machine", action="store_true", + help="Print output in a machine parsable format") + op.add_argument("-p", "--pkl-file", type=argparse.FileType("wb"), + help="pickle exploitability classification object and store to PKL_FILE") + op.add_argument("-a", "--asan-log", type=argparse.FileType(), + help="Symbolize and analyze AddressSanitizer output (assumes " + "executable is loaded) (WARNING: untested).") + op.add_argument("-b", "--backtrace-limit", type=int, + help="Limit number of stack frames in backtrace to supplied value. " + "0 means no limit.", default=1000) - # Note that OptionParser is configured to work without - # sys.argv and to minimize the cases where OptionParser - # calls sys.exit(), which kills the parent GDB process - op = OptionParser(prog=self._cmdstr, add_help_option=False, - description="type 'help exploitable' for " - "description. WARNING: typing an invalid " - "option string may cause GDB to exit.") - op.add_option("-v", "--verbose", action="store_true", - dest="verbose", default=False, - help="print analysis info from the Inferior") - op.add_option("-p", "--pkl-file", dest="pkl_file", - help="pickle exploitability classification object and " - "store to PKL_FILE") - op.add_option("-h", "--help", action="store_true", - dest="help", default=False, help="Print this message") - (opts, args) = op.parse_args(gdb.string_to_argv(argstr)) - if opts.help: - # Print help manually b/c Default OptionParser help calls sys.exit - op.print_help() + try: + args = op.parse_args(gdb.string_to_argv(argstr)) + except NiceArgParserExit: return - self._options = opts + import logging try: - target = gdb_wrapper.getTarget() - except gdb_wrapper.GdbWrapperError as e: - raise gdb.GdbError(e) - - c = classifier.getClassification(target) - if self._options.pkl_file: - path = os.path.expanduser(self._options.pkl_file) - pickle.dump(c, file(path, "wb")) + target = arch.getTarget(args.asan_log, args.backtrace_limit) + c = classifier.Classifier().getClassification(target) + except Exception as e: + logging.exception(e) + raise e + + if args.pkl_file: + import pickle as pickle + pickle.dump(c, args.pkl_file, 2) return - if self._options.verbose: - print "'exploitable' version %s" % versions.exploitable_version - print " ".join([str(i) for i in os.uname()]) - print "Signal si_signo: %s Signal si_addr: %s" % \ - (target.si_signo(), target.si_addr()) - print "Nearby code:" + if args.verbose: + print("'exploitable' version {}".format(version)) + print(" ".join([str(i) for i in os.uname()])) + print("Signal si_signo: {} Signal si_addr: {}".format(target.si_signo(), target.si_addr())) + print("Nearby code:") self.print_disassembly() - print "Stack trace:" - print target.backtrace() - print "Faulting frame: %s" % target.faulting_frame() + print("Stack trace:") + print(str(target.backtrace())) + print("Faulting frame: {}".format(target.faulting_frame())) - print c + if args.machine: + print_machine_string(c, target) + else: + gdb.write(str(c)) + gdb.flush() -check_version() ExploitableCommand() diff --git a/src/linux/CERT_triage_tools/exploitable/lib/analyzers.py b/src/linux/CERT_triage_tools/exploitable/lib/analyzers.py deleted file mode 100644 index a9c33cf..0000000 --- a/src/linux/CERT_triage_tools/exploitable/lib/analyzers.py +++ /dev/null @@ -1,296 +0,0 @@ -### BEGIN LICENSE ### -### Use of the triage tools and related source code is subject to the terms -### of the license below. -### -### ------------------------------------------------------------------------ -### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. -### ------------------------------------------------------------------------ -### Redistribution and use in source and binary forms, with or without -### modification, are permitted provided that the following conditions are -### met: -### -### 1. Redistributions of source code must retain the above copyright -### notice, this list of conditions and the following acknowledgments -### and disclaimers. -### -### 2. Redistributions in binary form must reproduce the above copyright -### notice, this list of conditions and the following disclaimer in the -### documentation and/or other materials provided with the distribution. -### -### 3. All advertising materials for third-party software mentioning -### features or use of this software must display the following -### disclaimer: -### -### "Neither Carnegie Mellon University nor its Software Engineering -### Institute have reviewed or endorsed this software" -### -### 4. The names "Department of Homeland Security," "Carnegie Mellon -### University," "CERT" and/or "Software Engineering Institute" shall -### not be used to endorse or promote products derived from this software -### without prior written permission. For written permission, please -### contact permission@sei.cmu.edu. -### -### 5. Products derived from this software may not be called "CERT" nor -### may "CERT" appear in their names without prior written permission of -### permission@sei.cmu.edu. -### -### 6. Redistributions of any form whatsoever must retain the following -### acknowledgment: -### -### "This product includes software developed by CERT with funding -### and support from the Department of Homeland Security under -### Contract No. FA 8721-05-C-0003." -### -### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND -### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER -### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING -### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE -### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, -### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND -### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, -### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND -### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. -### END LICENSE ### -''' -Contains analyzers used to match rules that are used to classify the state -of a GDB inferior and some helper functions. -''' - -import signal -import re -from tools import * - -''' -Analyzers -If an analyzer function returns True the rule is considered a match -to the GDB inferior's state, otherwise the rule is not considered a match. -''' - -# EXPLOITABLE - -def isBranchAvNotNearNull(target): - return isBranchAv(target) and \ - not isNearNull(faultingAddress(target)) - -def isReturnAv(target): - rets = ["iret", "ret"] - return isAccessViolationSignal(target) and \ - target.current_instruction() and\ - (target.current_instruction().mnemonic in rets) - -def isSegFaultOnPcNotNearNull(target): - return isSegFaultOnPc(target) and not isFaNearNull(target) - -def isErrorWhileExecutingFromStack(target): - if isBenign(target): - return False - map = target.procmaps().findByAddr(target.pc()) - if map and map.name == "[stack]": # maybe check threadstacks too? - return True - return False - -def isStackBufferOverflow(target): - frames = ["__fortify_fail", - "__stack_chk_fail"] - # Some versions of libc attempt to print a backtrace when - # __stack_chk_fail is invoked, which may cause a SIGSEGV in the SIGABRT - # handler. This is a a known issue, see CVE 2010-3192. - # Thus the isAbortSignal(target) check must be omitted. - return isInBacktrace(target, frames) - -def isPossibleStackCorruption(target): - if isBenign(target): - return False - - if isStackOverflow(target): - return False - - bt = target.backtrace() - if bt.abnormal_termination: - return True - - for fr in bt[1:]: - if not fr.mapped_region: - return True - - pm = target.procmaps().findByAddr(target.stack_pointer()) - if not pm or pm.name != "[stack]": - return True - - return False - -def isDestAvNotNearNull(target): - return isDestAv(target) and not isFaNearNull(target) - -def isHeapError(target): - libc_bts = [ ["abort", "__libc_message", "malloc_printerr"], # mcheck_print - ["abort", "malloc_printerr"], # mcheck_noprint - ["free"], - ["malloc"], - ["__malloc_assert"] ] - for seq in libc_bts: - if isInBacktrace(target, seq, "/libc"): - return True - return False - -# PROBABLY_EXPLOITABLE -def isStackOverflow(target): - if not isAccessViolationSignal(target) or not target.current_instruction(): - return False - - # verify this is a push* instruction or - # a call instruction where the AV is due to the "push" - if "push" not in target.current_instruction().mnemonic and not ( \ - target.current_instruction().mnemonic == "call" and \ - faultingAddress(target) != target.current_instruction().dest.eval() and \ - faultingAddress(target) + target.pointer_size() == target.stack_pointer()): - return False - - # verify the stack pointer is outside the default stack region - pm = target.procmaps().findByAddr(target.stack_pointer()) - if pm and pm.name == "[stack]": - return False - - return True - -def isMalformedInstructionSignal(target): - siglist = ["SIGILL", "SIGSYS"] - return isSignalInList(target, siglist) - -def isSegFaultOnPcNearNull(target): - return isSegFaultOnPc(target) and isFaNearNull(target) - -def isBranchAvNearNull(target): - return isBranchAv(target) and isNearNull(faultingAddress(target)) - -blk_mv_regex = re.compile("^rep.*mov.*$") -def isBlockMove(target): - if isBenign(target) or not target.current_instruction(): - return False - m = target.current_instruction().mnemonic - if re.match(blk_mv_regex, m): - return True - return False - -def isDestAvNearNull(target): - return isDestAv(target) and isFaNearNull(target) - -# PROBABLY_NOT_EXPLOITABLE - -def isBenignSignal(target): - siglist = ["SIGTERM", "SIGINT", "SIGQUIT", "SIGKILL", "SIGHUP", - "SIGALRM", "SIGVTALRM", "SIGPROF", "SIGIO", "SIGURG", - "SIGPOLL", "SIGUSR1", "SIGUSR2", "SIGWINCH", "SIGINFO", - "SIGCHLD", "SIGCONT", "SIGSTOP", "SIGTSTP"] - return isSignalInList(target, siglist) - -def isSourceAvNotNearNull(target): - return isSourceAv(target) and not isFaNearNull(target) - -def isFloatingPointException(target): - siglist = ["SIGFPE"] - return isSignalInList(target, siglist) - -# UNKNOWN - -def isSourceAvNearNull(target): - return isSourceAv(target) and isFaNearNull(target) - -def isAbortSignal(target): - return isOnSignal(target) and target.si_signo() == signal.SIGABRT - -def isAccessViolationSignal(target): - return isSignalInList(target, ["SIGSEGV", "SIGBUS"]) - -def isUncategorizedSignal(target): - return not (isAccessViolationSignal(target) or isAbortSignal(target) \ - or isBenignSignal(target) or isFloatingPointException(target) \ - or isMalformedInstructionSignal(target)) - -''' -Helpers -These functions are called by multiple analyzers and are not directly -associated with a rule. -''' - -def isOnSignal(target): - return bool(target.si_signo()) - -def isSignalInList(target, siglist): - ''' - Returns True if target's signo is in siglist, False otherwise - ''' - if not isOnSignal(target): - return False - for s in siglist: - signo = getattr(signal, s, None) # not all sigs may be defined - if signo: - if signo == target.si_signo(): - return True - return False - -def isNearNull(addr): - ''' - Returns True of addr is near NULL, False otherwise - ''' - if addr < 64 * 1024: # same as !exploitable - return True - return False - -def isInBacktrace(target, fnames, region=None): - i = 0 - for fr in target.backtrace(): - if fr.name() and fnames[i] == fr.name() and (not region or region in fr.mapped_region): - i = i + 1 - if i == len(fnames): - return True - else: - i = 0 - return False - -def isFaNearNull(target): - return isNearNull(faultingAddress(target)) - -def isBenign(target): - return isOnSignal(target) and isBenignSignal(target) - -def isJumpInstruction(target): - jumps = ["ja", "jae", "jb", "jbe", "jc", "jcxz", "je", "jecxz", "jg", - "jge", "jl", "jle", "jmp", "jna", "jnae", "jnb", "jnbe", "jnc", - "jne", "jng", "jnge", "jnl", "jnle", "jno", "jnp", "jns", "jp", - "js", "jz"] - return target.current_instruction() and \ - target.current_instruction().mnemonic in jumps - -def isBranchAv(target): - calls = ["call", "callq"] - if not isAccessViolationSignal(target): - return False - return isJumpInstruction(target) or (target.current_instruction() and \ - target.current_instruction().mnemonic in calls) - -def faultingAddress(target): - if isJumpInstruction(target): - # si_addr does not always contain a valid faulting address, but - # jump instructions always access the dest op and GDB always displays - # the absolute addr, so we can use the dest op instead of si_addr here. - return target.current_instruction().operands[0].eval() - return target.si_addr() - -def isSegFaultOnPc(target): - return isAccessViolationSignal(target) and \ - faultingAddress(target) == target.pc() - -def isDestAv(target): - if not isAccessViolationSignal(target): - return False - dest_op = getattr(target.current_instruction(), "dest", False) - return dest_op and dest_op.is_pointer and \ - dest_op.eval() == faultingAddress(target) - -def isSourceAv(target): - if not isAccessViolationSignal(target): - return False - source_op = getattr(target.current_instruction(), "source", False) - return source_op and source_op.is_pointer and \ - source_op.eval() == faultingAddress(target) diff --git a/src/linux/CERT_triage_tools/exploitable/lib/analyzers/__init__.py b/src/linux/CERT_triage_tools/exploitable/lib/analyzers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linux/CERT_triage_tools/exploitable/lib/analyzers/arm.py b/src/linux/CERT_triage_tools/exploitable/lib/analyzers/arm.py new file mode 100644 index 0000000..a5fb807 --- /dev/null +++ b/src/linux/CERT_triage_tools/exploitable/lib/analyzers/arm.py @@ -0,0 +1,189 @@ +### BEGIN LICENSE ### +### Use of the triage tools and related source code is subject to the terms +### of the license below. +### +### ------------------------------------------------------------------------ +### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. +### ------------------------------------------------------------------------ +### Redistribution and use in source and binary forms, with or without +### modification, are permitted provided that the following conditions are +### met: +### +### 1. Redistributions of source code must retain the above copyright +### notice, this list of conditions and the following acknowledgments +### and disclaimers. +### +### 2. Redistributions in binary form must reproduce the above copyright +### notice, this list of conditions and the following disclaimer in the +### documentation and/or other materials provided with the distribution. +### +### 3. All advertising materials for third-party software mentioning +### features or use of this software must display the following +### disclaimer: +### +### "Neither Carnegie Mellon University nor its Software Engineering +### Institute have reviewed or endorsed this software" +### +### 4. The names "Department of Homeland Security," "Carnegie Mellon +### University," "CERT" and/or "Software Engineering Institute" shall +### not be used to endorse or promote products derived from this software +### without prior written permission. For written permission, please +### contact permission@sei.cmu.edu. +### +### 5. Products derived from this software may not be called "CERT" nor +### may "CERT" appear in their names without prior written permission of +### permission@sei.cmu.edu. +### +### 6. Redistributions of any form whatsoever must retain the following +### acknowledgment: +### +### "This product includes software developed by CERT with funding +### and support from the Department of Homeland Security under +### Contract No. FA 8721-05-C-0003." +### +### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. +### END LICENSE ### +''' +Contains analyzers used to match rules that are used to classify the state +of an ARM Linux GDB inferior and some helper functions. + +NOTE: This logic was contributed by a user that was testing on ARM. As of +17 Jan 2014, continuous integration only ensures that results of analysis +on ARM are consistent, not correct. In other words, please help fix this code! +''' + +import re +import signal + +from lib.tools import memoized +from lib.analyzers.x86 import Analyzer + +class ArmAnalyzer(Analyzer): + ''' + An analyzer for an ARM Linux GDB target. See x86.py for more info on + Analyzers. + ''' + + def __init__(self, target): + Analyzer.__init__(self, target) + + # EXPLOITABLE matching methods follow + + @memoized + def isReturnAv(self): + # not as easy, 'ret' is one of many ways + ins = self.target.current_instruction() + return self.isAccessViolationSignal() and ins and \ + ((self.isJumpInstruction() and "lr" in ins.source.regs) or \ + (ins.source.is_pointer and "pc" in ins.dest.regs)) + + @memoized + def isHeapError(self): + libc_bts = [ ["abort", "__libc_message", "malloc_printerr"], # mcheck_print + ["abort", "malloc_printerr"], # mcheck_noprint + ["free"], + ["malloc"], + ["__malloc_assert"] ] + + for seq in libc_bts: + if self.isInBacktrace(seq, "/libc"): + return True + return False + + # PROBABLY_EXPLOITABLE matching methods follow + + @memoized + def isStackOverflow(self): + if not self.isAccessViolationSignal() or not self.target.current_instruction(): + return False + + # verify this is a push* instruction or + # a call instruction where the AV is due to the "push" + if not self.target.current_instruction().mnemonic.startswith("push") and \ + not self.target.current_instruction().mnemonic.startswith("stm"): + return False + + # verify the stack pointer is outside the default stack region + pm = self.target.procmaps().findByAddr(self.target.stack_pointer()) + if pm and pm.name == "[stack]": + return False + + return True + + @memoized + def isBlockMove(self): + return False + + ''' + Helpers + The following functions are called by multiple analyzers and are not + directly associated with a rule. + ''' + + @memoized + def isJumpInstruction(self): + ins = self.target.current_instruction() + return ins and "pc" in ins.dest.regs and not ins.mnemonic.startswith("bl") + + @memoized + def isBranchAv(self): + if not self.isAccessViolationSignal(): + return False + ins = self.target.current_instruction() + return self.isJumpInstruction() or (ins and ins.mnemonic.startswith("bl")) + + @memoized + def faultingAddress(self): + if self.isJumpInstruction(): + # si_addr does not always contain a valid faulting address, but + # jump instructions always access the dest op and GDB always displays + # the absolute addr, so we can use the dest op instead of si_addr here. + try: + return self.target.current_instruction().source.eval() + except gdb.error: + pass # eval() will fail if we're not running (ie. --asan) + return self.target.si_addr() + + @memoized + def isDestAv(self): + try: + return getattr(self.target, "asan_operation") == "WRITE" + except AttributeError: + pass # not --asan + if not self.isAccessViolationSignal(): + return False + dest_op = getattr(self.target.current_instruction(), "dest", False) + try: + return dest_op and dest_op.is_pointer and \ + dest_op.eval() == self.faultingAddress() + except gdb.error: + return None # allow to fail for --asan + + @memoized + def isSourceAv(self): + try: + return getattr(self.target, "asan_operation") == "READ" + except AttributeError: + pass # not --asan + if not self.isAccessViolationSignal(): + return False + source_op = getattr(self.target.current_instruction(), "source", False) + try: + return source_op and source_op.is_pointer and \ + source_op.eval() == self.faultingAddress() + except gdb.error: + return None # allow to fail for --asan + + @memoized + def isUseAfterFree(self): + return False + diff --git a/src/linux/CERT_triage_tools/exploitable/lib/analyzers/asan.py b/src/linux/CERT_triage_tools/exploitable/lib/analyzers/asan.py new file mode 100644 index 0000000..196a32c --- /dev/null +++ b/src/linux/CERT_triage_tools/exploitable/lib/analyzers/asan.py @@ -0,0 +1,99 @@ +### BEGIN LICENSE ### +### Use of the triage tools and related source code is subject to the terms +### of the license below. +### +### ------------------------------------------------------------------------ +### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. +### ------------------------------------------------------------------------ +### Redistribution and use in source and binary forms, with or without +### modification, are permitted provided that the following conditions are +### met: +### +### 1. Redistributions of source code must retain the above copyright +### notice, this list of conditions and the following acknowledgments +### and disclaimers. +### +### 2. Redistributions in binary form must reproduce the above copyright +### notice, this list of conditions and the following disclaimer in the +### documentation and/or other materials provided with the distribution. +### +### 3. All advertising materials for third-party software mentioning +### features or use of this software must display the following +### disclaimer: +### +### "Neither Carnegie Mellon University nor its Software Engineering +### Institute have reviewed or endorsed this software" +### +### 4. The names "Department of Homeland Security," "Carnegie Mellon +### University," "CERT" and/or "Software Engineering Institute" shall +### not be used to endorse or promote products derived from this software +### without prior written permission. For written permission, please +### contact permission@sei.cmu.edu. +### +### 5. Products derived from this software may not be called "CERT" nor +### may "CERT" appear in their names without prior written permission of +### permission@sei.cmu.edu. +### +### 6. Redistributions of any form whatsoever must retain the following +### acknowledgment: +### +### "This product includes software developed by CERT with funding +### and support from the Department of Homeland Security under +### Contract No. FA 8721-05-C-0003." +### +### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. +### END LICENSE ### +''' +Contains analyzers used to match rules that are used to classify the state +of an "ASAN" Linux GDB inferior and some helper functions. + +NOTE: This logic is used in a workflow by a specific user and has not been +tested by the maintainer. +''' + +import re +import signal + +from lib.tools import memoized + +class ASanAnalyzer(object): + ''' + If an analyzer function returns True the rule is considered a match + to the GDB inferior's state, otherwise the rule is not considered a match. + ''' + + # EXPLOITABLE matching methods follow + + @memoized + def isUseAfterFree(target): + return target.asan_reason == "heap-use-after-free" + + @memoized + def isStackBufferOverflow(target): + return target.asan_reason == "stack-buffer-overflow" + + @memoized + def isHeapError(target): + asan_heap_errs = ["double-free", "bad-free", "alloc-dealloc-mismatch", "unknown-crash"] + + return target.asan_reason in asan_heap_errs + + # PROBABLY_EXPLOITABLE matching methods follow + + @memoized + def isDestAv(target): + return target.asan_operation == "WRITE" + + @memoized + def isSourceAv(target): + return target.asan_operation == "READ" + diff --git a/src/linux/CERT_triage_tools/exploitable/lib/analyzers/x86.py b/src/linux/CERT_triage_tools/exploitable/lib/analyzers/x86.py new file mode 100644 index 0000000..4eb809f --- /dev/null +++ b/src/linux/CERT_triage_tools/exploitable/lib/analyzers/x86.py @@ -0,0 +1,355 @@ +### BEGIN LICENSE ### +### Use of the triage tools and related source code is subject to the terms +### of the license below. +### +### ------------------------------------------------------------------------ +### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. +### ------------------------------------------------------------------------ +### Redistribution and use in source and binary forms, with or without +### modification, are permitted provided that the following conditions are +### met: +### +### 1. Redistributions of source code must retain the above copyright +### notice, this list of conditions and the following acknowledgments +### and disclaimers. +### +### 2. Redistributions in binary form must reproduce the above copyright +### notice, this list of conditions and the following disclaimer in the +### documentation and/or other materials provided with the distribution. +### +### 3. All advertising materials for third-party software mentioning +### features or use of this software must display the following +### disclaimer: +### +### "Neither Carnegie Mellon University nor its Software Engineering +### Institute have reviewed or endorsed this software" +### +### 4. The names "Department of Homeland Security," "Carnegie Mellon +### University," "CERT" and/or "Software Engineering Institute" shall +### not be used to endorse or promote products derived from this software +### without prior written permission. For written permission, please +### contact permission@sei.cmu.edu. +### +### 5. Products derived from this software may not be called "CERT" nor +### may "CERT" appear in their names without prior written permission of +### permission@sei.cmu.edu. +### +### 6. Redistributions of any form whatsoever must retain the following +### acknowledgment: +### +### "This product includes software developed by CERT with funding +### and support from the Department of Homeland Security under +### Contract No. FA 8721-05-C-0003." +### +### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. +### END LICENSE ### +''' +Contains analyzers used to match rules that are used to classify the state +of a GDB inferior and some helper functions. +''' + +import re +import signal + +from lib.tools import memoized + +class Analyzer(object): + ''' + Contains methods that analyze a Target (a Linux GDB inferior state) to + determine properties of the Target (such as how the application crashed). + The methods correspond to "rules" defined in rules.py. + If an analyzer method returns True the rule is considered a match + to the GDB inferior's state, otherwise the rule is not considered a match. + This class contains logic for x86 platforms (the "original" target of the + exploitable plugin), and is inherited to implement logic for other + platforms. + ''' + + def __init__(self, target): + self.target = target + + # EXPLOITABLE matching methods follow + + @memoized + def isBranchAvNotNearNull(self): + return self.isBranchAv() and \ + not self.isNearNull(self.faultingAddress()) + + @memoized + def isReturnAv(self): + rets = ["iret", "ret"] + return self.isAccessViolationSignal() and \ + (self.target.current_instruction() and \ + (self.target.current_instruction().mnemonic in rets)) + + @memoized + def isSegFaultOnPcNotNearNull(self): + return self.isSegFaultOnPc() and not self.isFaNearNull() + + @memoized + def isErrorWhileExecutingFromStack(self): + if self.isBenign(): + return False + sect = self.target.procmaps().findByAddr(self.target.pc()) + if sect and sect.name == "[stack]": # maybe check threadstacks too? + return True + return False + + @memoized + def isStackBufferOverflow(self): + # Some versions of libc attempt to print a backtrace when + # __stack_chk_fail is invoked, which may cause a SIGSEGV in the SIGABRT + # handler. This is a a known issue, see CVE 2010-3192. + # Thus the isAbortSignal(target) check must be omitted. + + sequences = [["__fortify_fail", "__stack_chk_fail"], + ["__GI___fortify_fail", "__stack_chk_fail"]] + for seq in sequences: + if self.isInBacktrace(seq, "/libc"): + return True + return False + + @memoized + def isPossibleStackCorruption(self): + if self.isBenign(): + return False + + if self.isStackOverflow(): + return False + + bt = self.target.backtrace() + was_abnormal_termination = bt.abnormal_termination + + if was_abnormal_termination: + for fr in bt: + if fr.name() is None: + return True + + for fr in bt[1:]: + if not fr.mapped_region: + return True + + pm = self.target.procmaps().findByAddr(self.target.stack_pointer()) + if not pm or pm.name != "[stack]": + return True + + return False + + @memoized + def isDestAvNotNearNull(self): + return self.isDestAv() and not self.isFaNearNull() + + @memoized + def isHeapError(self): + libc_bts = [ ["abort", "__libc_message", "malloc_printerr"], # mcheck_print + ["__GI_abort", "__libc_message", "malloc_printerr"], + ["abort", "malloc_printerr"], # mcheck_noprint + ["free"], + ["malloc"], + ["__malloc_assert"] ] + + for seq in libc_bts: + if self.isInBacktrace(seq, "/libc"): + return True + return False + + # PROBABLY_EXPLOITABLE matching methods follow + + @memoized + def isStackOverflow(self): + if not self.isAccessViolationSignal() or not self.target.current_instruction(): + return False + + # verify this is a push* instruction or + # a call instruction where the AV is due to the "push" + if "push" not in self.target.current_instruction().mnemonic and not ( \ + self.target.current_instruction().mnemonic == "call" and \ + self.faultingAddress() != self.target.current_instruction().dest.eval() and \ + self.faultingAddress() + self.target.pointer_size() == self.target.stack_pointer()): + return False + + # verify the stack pointer is outside the default stack region + pm = self.target.procmaps().findByAddr(self.target.stack_pointer()) + if pm and pm.name == "[stack]": + return False + + return True + + @memoized + def isMalformedInstructionSignal(self): + siglist = ["SIGILL", "SIGSYS"] + return self.isSignalInList(siglist) + + @memoized + def isSegFaultOnPcNearNull(self): + return self.isSegFaultOnPc() and self.isFaNearNull() + + @memoized + def isBranchAvNearNull(self): + return self.isBranchAv() and self.isNearNull(self.faultingAddress()) + + _re_blk_mov = re.compile("^rep.*mov") + @memoized + def isBlockMove(self): + if self.isBenign() or not self.target.current_instruction(): + return False + m = self.target.current_instruction().mnemonic + if self._re_blk_mov.match(m): + return True + return False + + @memoized + def isDestAvNearNull(self): + return self.isDestAv() and self.isFaNearNull() + + # PROBABLY_NOT_EXPLOITABLE matching methods follow + + @memoized + def isBenignSignal(self): + siglist = ["SIGTERM", "SIGINT", "SIGQUIT", "SIGKILL", "SIGHUP", + "SIGALRM", "SIGVTALRM", "SIGPROF", "SIGIO", "SIGURG", + "SIGPOLL", "SIGUSR1", "SIGUSR2", "SIGWINCH", "SIGINFO", + "SIGCHLD", "SIGCONT", "SIGSTOP", "SIGTSTP"] + return self.isSignalInList(siglist) + + @memoized + def isSourceAvNotNearNull(self): + return self.isSourceAv() and not self.isFaNearNull() + + @memoized + def isFloatingPointException(self): + siglist = ["SIGFPE"] + return self.isSignalInList(siglist) + + # UNKNOWN matching methods follow + + @memoized + def isSourceAvNearNull(self): + return self.isSourceAv() and self.isFaNearNull() + + @memoized + def isAbortSignal(self): + return self.isOnSignal() and self.target.si_signo() == signal.SIGABRT + + @memoized + def isAccessViolationSignal(self): + return self.isSignalInList(["SIGSEGV", "SIGBUS"]) + + @memoized + def isUncategorizedSignal(self): + return not (self.isAccessViolationSignal() or self.isAbortSignal() \ + or self.isBenignSignal() or self.isFloatingPointException() \ + or self.isMalformedInstructionSignal()) + + ''' + Helpers + The following functions are called by multiple analyzers and are not + directly associated with a rule. + ''' + + @memoized + def isOnSignal(self): + return bool(self.target.si_signo()) + + @memoized + def isSignalInList(self, siglist): + ''' + Returns True if target's signo is in siglist, False otherwise + ''' + if not self.isOnSignal(): + return False + tsigno = self.target.si_signo() # only get this ONCE per function call + for s in siglist: + signo = getattr(signal, s, None) # not all sigs may be defined + if signo and signo == tsigno: + return True + return False + + def isNearNull(self, addr): + ''' + Returns True of addr is near NULL, False otherwise + ''' + if addr < 64 * 1024: # same as !exploitable + return True + return False + + @memoized + def isInBacktrace(self, fnames, region=None): + i = 0 + for fr in self.target.backtrace(): + if fr.name() and fnames[i] == fr.name() and (not region or not fr.mapped_region or region in fr.mapped_region.name): + i = i + 1 + if i == len(fnames): + return True + else: + i = 0 + return False + + @memoized + def isFaNearNull(self): + return self.isNearNull(self.faultingAddress()) + + @memoized + def isBenign(self): + return self.isOnSignal() and self.isBenignSignal() + + @memoized + def isJumpInstruction(self): + ins = self.target.current_instruction() + jumps = ["ja", "jae", "jb", "jbe", "jc", "jcxz", "je", "jecxz", "jg", + "jge", "jl", "jle", "jmp", "jna", "jnae", "jnb", "jnbe", "jnc", + "jne", "jng", "jnge", "jnl", "jnle", "jno", "jnp", "jns", "jp", + "js", "jz"] + return ins and ins.mnemonic in jumps + + @memoized + def isBranchAv(self): + if not self.isAccessViolationSignal(): + return False + ins = self.target.current_instruction() + calls = ["call", "callq"] + return self.isJumpInstruction() or (ins and ins.mnemonic in calls) + + @memoized + def faultingAddress(self): + if self.isJumpInstruction(): + # si_addr does not always contain a valid faulting address, but + # jump instructions always access the dest op and GDB always displays + # the absolute addr, so we can use the dest op instead of si_addr here. + return self.target.current_instruction().operands[0].eval() + return self.target.si_addr() + + @memoized + def isSegFaultOnPc(self): + return self.isAccessViolationSignal() and \ + self.faultingAddress() == self.target.pc() + + @memoized + def isDestAv(self): + if not self.isAccessViolationSignal(): + return False + dest_op = getattr(self.target.current_instruction(), "dest", False) + return dest_op and dest_op.is_pointer and \ + dest_op.eval() == self.faultingAddress() + + @memoized + def isSourceAv(self): + if not self.isAccessViolationSignal(): + return False + source_op = getattr(self.target.current_instruction(), "source", False) + return source_op and source_op.is_pointer and \ + source_op.eval() == self.faultingAddress() + + @memoized + def isUseAfterFree(self): + return False + diff --git a/src/linux/CERT_triage_tools/exploitable/lib/arch.py b/src/linux/CERT_triage_tools/exploitable/lib/arch.py new file mode 100644 index 0000000..04692b1 --- /dev/null +++ b/src/linux/CERT_triage_tools/exploitable/lib/arch.py @@ -0,0 +1,116 @@ +# The MIT License (MIT) +# +# Copyright (c) 2013 Jonathan Foote +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Jonathan Foote +# jmfoote@loyola.edu + +''' +A collection of Python objects that wrap and extend the GDB Python API based. + +The objects in this file should generally not be instantiated directly-- +they should be accessed via getTarget(), which selects the proper GDB wrapper +objects at runtime based on the architecture of the system executing the code. + +Note that for many objects defined in this file, GDB is queried only when the +object is instantiated: if the state of the Inferior changes, any previously +created instances should be considered stale. +''' +try: + import gdb +except ImportError as e: + raise ImportError("This script must be run in GDB: ", str(e)) + +from lib.gdb_wrapper.arm import ArmTarget +from lib.gdb_wrapper.qnx import QnxTarget +from lib.gdb_wrapper.asan import ASanTarget +from lib.gdb_wrapper.x86 import Target, x86Target + +from lib.analyzers.x86 import Analyzer +from lib.analyzers.asan import ASanAnalyzer +from lib.analyzers.arm import ArmAnalyzer + +class ArmASanTarget(ASanTarget, ArmTarget): + ''' + A wrapper for an ARM Linux GDB Inferior enhanced with ASAN log output. + ''' + pass + +class QnxASanTarget(ASanTarget, QnxTarget): + ''' + A wrapper for an ARM Linux (with QNX Neutrino kernel) GDB Inferior + enhanced with ASAN log output. + ''' + pass + +class ArmASanAnalyzer(ArmAnalyzer, ASanAnalyzer): + ''' + A rule analyzer for ASAN log output from executing on ARM Linux. + ''' + pass + +def getTarget(asan_log_file=None, bt_limit=0): + ''' + Returns the current Target, which is a Python wrapper representing the + current state of the underlying Linux GDB Inferior object. This function + selects a Target and Analyzer based on the architecture of the system at + runtime. + ''' + + # Get OS info. TODO: verify this works on older versions of GDB (7.2) + osabi = Target._re_gdb_osabi.search(str(gdb.execute("show osabi", False, + True))).group(1) + arch = Target._re_gdb_arch.search(str(gdb.execute("show architecture", False, True))).group(1) + + # Instantiate a target based on the params and OS info + # ASAN + i386 + * + if asan_log_file and arch.startswith("i386"): + target = ASanTarget(asan_log_file.read(), bt_limit) + target.analyzer = ASanAnalyzer(target) + return target + # ASAN + ARM + QNX + elif asan_log_file and arch.lower()[:3] == "arm" and osabi == "QNX Neutrino": + target = QnxASanTarget(asan_log_file.read(), bt_limit) + target.analyzer = ASanAnalyzer(target) + return target + # ASAN + ARM + * + elif asan_log_file and arch.lower()[:3] == "arm": + target = ArmASanTarget(asan_log_file.read(), bt_limit) + target.analyzer = ArmASanAnalyzer(target) + return target + # * + ARM + QNX + elif arch.lower()[:3] == "arm" and osabi == "QNX Neutrino": + target = QnxTarget(bt_limit) + target.analyzer = ArmAnalyzer(target) + return target + # * + i386 + * + elif arch.startswith("i386"): + target = x86Target(bt_limit) + target.analyzer = Analyzer(target) + return target + # * + ARM + * + elif arch.lower()[:3] == "arm": + target = ArmTarget(bt_limit) + target.analyzer = ArmAnalyzer(target) + return target + else: + raise NotImplementedError("no support for arch=%s and osabi=%s" % (arch, osabi)) + + return Target(bt_limit) diff --git a/src/linux/CERT_triage_tools/exploitable/lib/classifier.py b/src/linux/CERT_triage_tools/exploitable/lib/classifier.py index d135db2..3a9f6c7 100644 --- a/src/linux/CERT_triage_tools/exploitable/lib/classifier.py +++ b/src/linux/CERT_triage_tools/exploitable/lib/classifier.py @@ -4,6 +4,7 @@ ### ### ------------------------------------------------------------------------ ### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. ### ------------------------------------------------------------------------ ### Redistribution and use in source and binary forms, with or without ### modification, are permitted provided that the following conditions are @@ -17,24 +18,17 @@ ### notice, this list of conditions and the following disclaimer in the ### documentation and/or other materials provided with the distribution. ### -### 3. All advertising materials for third-party software mentioning -### features or use of this software must display the following -### disclaimer: -### -### "Neither Carnegie Mellon University nor its Software Engineering -### Institute have reviewed or endorsed this software" -### -### 4. The names "Department of Homeland Security," "Carnegie Mellon +### 3. The names "Department of Homeland Security," "Carnegie Mellon ### University," "CERT" and/or "Software Engineering Institute" shall ### not be used to endorse or promote products derived from this software ### without prior written permission. For written permission, please ### contact permission@sei.cmu.edu. ### -### 5. Products derived from this software may not be called "CERT" nor +### 4. Products derived from this software may not be called "CERT" nor ### may "CERT" appear in their names without prior written permission of ### permission@sei.cmu.edu. ### -### 6. Redistributions of any form whatsoever must retain the following +### 5. Redistributions of any form whatsoever must retain the following ### acknowledgment: ### ### "This product includes software developed by CERT with funding @@ -55,11 +49,12 @@ A collection of objects used to classify GDB Inferiors (Targets). ''' -from tools import AttrDict -import rules -import versions -from copy import deepcopy -import warnings +import copy +import warnings, traceback +from functools import partial + +import lib.rules as rules +from lib.tools import AttrDict class Tag(object): ''' @@ -69,14 +64,20 @@ class Tag(object): def __init__(self, tag_dict): self.__dict__ = tag_dict - def __cmp__(self, other): + # for python3 + def __lt__(self, other): if type(other) != type(self): raise TypeError("cannot compare type %s to type %s" % \ (type(other),type(self))) + return self.ranking[0] < other.ranking[0] + + def __cmp__(self, other): + if not issubclass(type(other), type(self)): + raise TypeError("cannot compare type {} to type {}".format(type(other), type(self))) return self.ranking[0] - other.ranking[0] def __str__(self): - return "%s (%d/%d)" % ((self.short_desc,) + self.ranking) + return "{0} ({1[0]:d}/{1[1]:d})".format(self.short_desc, self.ranking) class Classification(AttrDict): ''' @@ -85,25 +86,38 @@ class Classification(AttrDict): An instance of this object is returned by a Classifier. ''' - version = versions.exploitable_version - def __init__(self): + def __init__(self, target): + AttrDict.__init__(self) self.tags = [] def __add__(self, tag): - if type(tag) != Tag: - raise TypeError("cannot add type %s to type %s" % \ - (type(tag), type(self))) + if not issubclass(type(tag), Tag): + raise TypeError("cannot add type {} to type {}".format(type(tag), type(self))) self.tags.append(tag) self.tags.sort() - for k, v in self.tags[0].__dict__.iteritems(): + for k, v in self.tags[0].__dict__.items(): self[k] = v return self + # for python3 + def __lt__(self, other): + if not issubclass(type(other), type(self)): + raise TypeError("cannot compare type {} to type {}".format(type(other), type(self))) + + if len(self.tags) == 0 or len(other.tags) == 0: + return len(self.tags) < len(other.tags) + + i = 0 + while i < len(self.tags) and i < len(other.tags): + if self.tags[i] < other.tags[i]: + return True + i += 1 + return False + def __cmp__(self, other): - if type(other) != type(self): - raise TypeError("cannot compare type %s to type %s" % \ - (type(other),type(self))) + if not issubclass(type(other), type(self)): + raise TypeError("cannot compare type {} to type {}".format(type(other), type(self))) if len(self.tags) == 0 or len(other.tags) == 0: return len(self.tags) - len(other.tags) @@ -117,70 +131,66 @@ def __cmp__(self, other): return result def __str__(self): - if len(self.tags) == 0: + if not self.tags: return "No matches" - result = "Description: %s\n" % self.desc - result += "Short description: %s\n" % str(self.tags[0]) - result += "Hash: %s.%s\n" % (self.hash.major, self.hash.minor) - result += "Exploitability Classification: %s\n" % self.category - result += "Explanation: %s\n" % self.explanation + result = ["Description: {}".format(self.desc), + "Short description: {}".format(self.tags[0]), + "Hash: {}.{}".format(self.hash.major, self.hash.minor), + "Exploitability Classification: {}".format(self.category), + "Explanation: {}".format(self.explanation)] if len(self.tags) > 1: - result += "Other tags: %s" % \ - ", ".join([str(r) for r in self.tags[1:]]) - return result + result.append("Other tags: {}".format( + ", ".join(str(r) for r in self.tags[1:]))) + result.append("") + return "\n".join(result) -class Classifier(): +class Classifier(object): ''' A Classifier used for classifying the state of a Target (a Linux GDB Inferior). ''' _major_hash_depth = 5 - def __init__(self): + def getRules(self, target): ''' Organizes the nested list of rules (dicts) for classification The rules specified in rules.py are organized into AttrDicts ("rules"). - Each rule is composed of a tag and an analyzer. + Each rule is composed of a tag and a match_function. ''' + processed_rules = [] - num_rules = sum([len(rl) for c, rl in rules.rules]) + num_rules = sum(len(rl) for (_, rl) in rules.rules) ranking = 1 for cat, user_rule_list in rules.rules: for user_rule in user_rule_list: - analyzer = user_rule['analyzer'] - tag_data = deepcopy(user_rule) - del tag_data ['analyzer'] - tag_data['ranking'] = (ranking, num_rules) - tag_data['category'] = cat - rule = AttrDict(analyzer=analyzer, tag=Tag(tag_data)) + match_function = partial(getattr(target.analyzer, user_rule["match_function"])) + tag_data = copy.deepcopy(user_rule) + del tag_data["match_function"] + tag_data["ranking"] = (ranking, num_rules) + tag_data["category"] = cat + rule = AttrDict(matches=match_function, tag=Tag(tag_data)) processed_rules.append(rule) ranking += 1 - self.rules = processed_rules + return processed_rules def getClassification(self, target): ''' Returns the Classification of target, which is a Classification of the exploitability of a Linux GDB Inferior. ''' - c = Classification() - for rule in self.rules: + c = Classification(target) + for rule in self.getRules(target): try: - match = rule.analyzer(target) + match = rule.matches() if match: c += rule.tag - except RuntimeError as e: - warnings.warn("Error while analyzing rule %s: %s" - % (rule.tag, str(e))) + except Exception as e: + warnings.warn("Error while analyzing rule {}: {}\n{}".format( + rule.tag, e, traceback.format_exc())) c.hash = target.hash() return c -def getClassification(target): - ''' - Returns the exploitability Classification of target. target should be an - instance of Target -- a representation of the state of the GDB Inferior. - ''' - return Classifier().getClassification(target) diff --git a/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/__init__.py b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/arm.py b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/arm.py new file mode 100644 index 0000000..dd6d627 --- /dev/null +++ b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/arm.py @@ -0,0 +1,303 @@ +### BEGIN LICENSE ### +### Use of the triage tools and related source code is subject to the terms +### of the license below. +### +### ------------------------------------------------------------------------ +### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. +### ------------------------------------------------------------------------ +### Redistribution and use in source and binary forms, with or without +### modification, are permitted provided that the following conditions are +### met: +### +### 1. Redistributions of source code must retain the above copyright +### notice, this list of conditions and the following acknowledgments +### and disclaimers. +### +### 2. Redistributions in binary form must reproduce the above copyright +### notice, this list of conditions and the following disclaimer in the +### documentation and/or other materials provided with the distribution. +### +### 3. All advertising materials for third-party software mentioning +### features or use of this software must display the following +### disclaimer: +### +### "Neither Carnegie Mellon University nor its Software Engineering +### Institute have reviewed or endorsed this software" +### +### 4. The names "Department of Homeland Security," "Carnegie Mellon +### University," "CERT" and/or "Software Engineering Institute" shall +### not be used to endorse or promote products derived from this software +### without prior written permission. For written permission, please +### contact permission@sei.cmu.edu. +### +### 5. Products derived from this software may not be called "CERT" nor +### may "CERT" appear in their names without prior written permission of +### permission@sei.cmu.edu. +### +### 6. Redistributions of any form whatsoever must retain the following +### acknowledgment: +### +### "This product includes software developed by CERT with funding +### and support from the Department of Homeland Security under +### Contract No. FA 8721-05-C-0003." +### +### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. +### END LICENSE ### +''' +A collection of Python objects that wrap and extend the GDB Python API. + +See x86.py in this directory for more info. +''' +try: + import gdb +except ImportError as e: + raise ImportError("This script must be run in GDB: ", str(e)) + +import os +import re +import warnings + +from lib.gdb_wrapper.elf import read_elf_sects +from lib.tools import AttrDict, memoized +from lib.gdb_wrapper.x86 import Target, Operand, Instruction + +class ArmInstruction(Instruction): + ''' + A disassembled ARM instruction. See Instruction class comments for more + details. + + This code was graciously contributed by a user and needs some cleanup -- + please help! + ''' + _re_arm_operands = re.compile(r"""(?P0x[A-Fa-f0-9]+) | # addr + (?P{) | # start brace + (?P[cs]psr) | # + (?P\]) | # end bracket + _(?P[cfgnqsvxz]) | # + (?P[aif]) | # + \#(?P-?[0-9]+) | # immediate value (with #) + (?Plsr|lsl|asr|ror|rrx) | # shift/rotate + (?P[bl]e) | # endianness + (?P

[+-]) | # +/- + (?P}) | # end brace + (?Pr\d{1,2}|lr|pc|sp) | # register + (?P\[) | # start bracket + (?P;.*) # semicolon annotation + """, re.VERBOSE) + _re_arm_groups = re.compile(r"[sebq]") + + def __init__(self, gdbstr): + ''' + Parses gdbstr to populate self. + ''' + Instruction.__init__(self, gdbstr) + self.gdbstr = gdbstr + + def split_expr(inst, tok): + com = inst.index(",", tok.end(0)) + return inst[:com], inst[com:] + + # get instruction addr and instruction string + inst = [pt for pt in self.gdbstr.split(":\t", 1)[1].strip().split() if pt] + self.addr = int(self._re_hex_int.search(self.gdbstr).group(), 16) + self.mnemonic = inst[0] + inst = " ".join(inst[1:]) + if inst == "": # handle "ret", "iret", et al. + return + + # tokenize the arguments + toks = [t for t in self._re_arm_operands.finditer(inst)] + form = "".join(t.lastgroup for t in toks) + if form.endswith("x"): + form = form[:-1] + toks = toks[:-1] + form = self._re_arm_groups.sub(lambda c:{"s": "[", "e": "]", "b": "{", "q": "}"}[c.group()], form) + strtoks = [t.group(0) for t in toks] + if form in ["rri", # add, adc, sub, sbc, rsb, rsc, asr, lsl, lsr, ror, and, eor, orr, orn, bic + "rrr", # add, adc, sub, sbc, rsb, rsc, qadd, qdadd, qsub, qdsub, + # (s|q|sh|u|uq|uh)(add|sub|asx|sax), usad8, mul, smul, smuad, smusd, smmul, + # [su]div, asr, lsl, lsr, ror, and, eor, orr, orn, bic, pkhbt, pkhtb, [us]xta[hb], sel + "rrrli", # add, adc, sub, sbc, rsb, rsc, and, eor, orr, orn, bic, pkhbt, pkhtb, [us]xta[hb] + "rrrlr", "rrrl", # add, adc, sub, sbc, rsb, rsc, and, eor, orr, orn, bic + "rir", "rirli", # [us]sat + "ri", # mov, mvn, cmp, cmn, tst, teq, srs + "rr", # mov, mvn, rrx, clz, cmp, cmn, tst, teq, [us]xt[hb], rbit, rev + "rrli", # mov, mvn, cmp, cmn, tst, teq, [us]xt[hb] + "rrl", "rlr", # mov, mvn, cmp, cmn, tst, teq + "rii", # bfc + "rrii", # bfi, [us]bfx + ]: + dst, src = split_expr(inst, toks[0]) + self.dest = ArmOperand(dst, strtoks[:1], form[:1]) + self.source = ArmOperand(src, strtoks[1:], form[1:]) + elif form == "ra": # adr, cbz, cbnz, ldr + short = self.mnemonic[:3] + if short in ["cbz", "cbn"]: + self.dest = ArmOperand("", ["pc"], "r") + self.source = ArmOperand(inst, strtoks, form) + elif short in ["ldr", "adr"]: + dst, src = split_expr(inst, toks[0]) + self.dest = ArmOperand(dst, strtoks[:1], form[:1]) + self.source = ArmOperand(src, strtoks[1:], form[1:], is_pointer=(short =="ldr")) + else: + raise RuntimeError("unexpected mnemonic for form \"{}\": {}".format(form, self.mnemonic)) + elif form == "rrrr": # usada8, mla, mls, ([us]mull|[us]mlal|umaal|smlsld), smla, smlad, smlald, smlsd, smmla, smmls + if self.mnemonic in ("umull", "smull", "umlal", "smlal", "umaal", "smlsld"): + dst, src = split_expr(inst, toks[1]) + dst_r = strtoks[:2] + dst_f = form[:2] + if self.mnemonic in ["umlal", "umaal", "smlal", "smlsld"]: + src = inst + src_r = strtoks + src_f = form + else: + src_r = strtoks[2:] + src_f = form[2:] + else: + dst, src = split_expr(inst, toks[0]) + dst_r = strtoks[:1] + dst_f = form[:1] + src_r = strtoks[1:] + src_f = form[1:] + self.dest = ArmOperand(dst, dst_r, dst_f) + self.source = ArmOperand(src, src_r, src_f) + elif form in ["a", # b, bl, blx, pld, pli + "r"]: # bx, blx, bxj, rfe + if self.mnemonic.startswith("b"): + self.dest = ArmOperand("", ["pc"], "r") # ignore LR so it evaluates properly + self.source = ArmOperand(inst, strtoks, form) + elif self.mnemonic.startswith("r"): + self.dest = ArmOperand("", ["pc"], "r") + self.source = ArmOperand(inst, strtoks, form, is_pointer=True) + else: + # pli/pld shouldn't fault + raise RuntimeError("unexpected mnemonic for form \"{}\": {}".format(form, self.mnemonic)) + elif form in ["[rr]", # tbb, pli, pld + "[rrli]"]: # tbh, pli, pld + if self.mnemonic.startswith("p"): + # pli/pld shouldn't fault + raise RuntimeError("unexpected mnemonic for form \"{}\": {}".format(form, self.mnemonic)) + self.dest = ArmOperand("", ["pc"], "r") + self.source = ArmOperand(inst, strtoks, form, is_pointer=True) + elif form == "rc": # mrs + self.dest = ArmOperand(inst, strtoks[:1], form[:1]) + elif form in ["cfr", # msr + "cfi"]: # msr + self.source = ArmOperand(inst, strtoks[-1:], form[-1:]) + elif form in ["g", "gi", # cpsi[de] + "i", # cps, bkpt, smc, svc + "n"]: # setend + pass + elif form in ["r[r]", # ldr, str, ldrex + "r[ri]", "r[rr]", "r[rpr]", "r[rrli]", "r[rprli]", "r[rrl]", "r[rprl]", # ldr, str + "rr[r]", # ldrd, strd, ldrexd, strex, swp + "rr[ri]", "rr[rr]", "rr[rpr]", "rr[rrli]", "rr[rprli]", "rr[rrl]", "rr[rprl]", # ldrd, strd + "rra", # ldrd + "rrr[r]"]: # strexd + regs = len(form) - len(form.lstrip("r")) + dst, src = split_expr(inst, toks[regs-1]) + if self.mnemonic.startswith("l"): + order = ["dest", "source"] + else: + order = ["source", "dest"] + setattr(self, order[0], ArmOperand(dst, strtoks[:regs], form[:regs])) + setattr(self, order[1], ArmOperand(src, strtoks[regs:], form[regs:], is_pointer=True)) + elif form in ["r[r]i", "r[r]r", "r[r]pr", "r[r]rli", "r[r]prli", "r[r]rl", "r[r]prl", # ldr, str + "rr[r]i", # ldrd, strd + "rr[r]r", "rr[r]pr", "rr[r]rli", "rr[r]prli", "rr[r]rl", "rr[r]prl"]: # ldrd, strd + regs = len(form) - len(form.lstrip("r")) + dst, src = split_expr(inst, toks[regs-1]) + if self.mnemonic.startswith("l"): + order = ["dest", "source"] + else: + order = ["source", "dest"] + setattr(self, order[0], ArmOperand(dst, strtoks[:regs], form[:regs])) + setattr(self, order[1], ArmOperand(src, strtoks[regs:regs+3], form[regs:regs+3], is_pointer=True)) + #elif form == "[r]": # pli, pld + #elif form == "[ri]": # pli, pld + #elif form == "[rpr]": # pli, pld + #elif form == "[rprli]": # pli, pld + #elif form == "[rrl]": # pli, pld + #elif form == "[rprl]": # pli, pld + elif form[:2] in ["r{", # ldm, stm + "{r"]: # pop, push + mne = self.mnemonic + if mne.startswith("p"): + form = "r" + form + strtoks.insert(0, "sp") + if mne[1] == "u": # push + mne = "stm" + else: + mne = "ldm" + dst, src = "", inst + else: + dst, src = split_expr(inst, toks[0]) + if mne.startswith("s"): + order = ["dest", "source"] + else: + order = ["source", "dest"] + setattr(self, order[0], ArmOperand(dst, strtoks[:1], form[:1], is_pointer=True)) + setattr(self, order[1], ArmOperand(src, strtoks[1:], form[1:])) + elif self.mnemonic in ("cdp", "mrc", "mrrc", "mcr", "ldc", "stc"): + # coprocessor writes. very special! + raise RuntimeError("coprocessor instructions are unhandled, and you can too!") + else: + raise RuntimeError("unexpected mnemonic for form \"{}\": {}".format(form, self.mnemonic)) + self.operands = [o.regs for o in (self.dest, self.source, self.aux) if o] + +class ArmOperand(Operand): + ''' + A disassembled ARM instruction operand that can be evaluated. See Operand + class comments for more details. + ''' + def __init__(self, gdbstr, toklist, form, is_pointer=False): + self.gdbstr = gdbstr + self.regs = [] + self.is_pointer = is_pointer + self.expr = "" + assert len(form) == len(toklist), "form=%s, toklist=%s" % (form, toklist) + eat = False + for f, t in zip(form, toklist): + if f in "[]{}cfgn": + continue + if f in "ai": + if not eat and self.expr: + self.expr += "+" + if f == "a": + self.expr += "{}".format(int(t, 16)) + else: + self.expr += "{}".format(int(t[1:])) + eat = False + elif f == "l": + self.expr += {"lsl": "<<", + "lsr": ">>", + "asr": ">>", + "ror": ">>", + "rrx": ">>1"}[t] + if t != "rrx": + eat = True + elif f == "p" and t == "-": + self.expr += "-" + eat = True + elif f == "r": + if not eat and self.expr: + self.expr += "+" + self.expr += "$" + t + eat = False + self.regs.append(t) + +class ArmTarget(Target): + ''' + A wrapper for an ARM Linux GDB Inferior. + ''' + def _getInstruction(self, gdbstr): + return ArmInstruction(gdbstr) diff --git a/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/asan.py b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/asan.py new file mode 100644 index 0000000..62f54dd --- /dev/null +++ b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/asan.py @@ -0,0 +1,354 @@ +### BEGIN LICENSE ### +### Use of the triage tools and related source code is subject to the terms +### of the license below. +### +### ------------------------------------------------------------------------ +### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. +### ------------------------------------------------------------------------ +### Redistribution and use in source and binary forms, with or without +### modification, are permitted provided that the following conditions are +### met: +### +### 1. Redistributions of source code must retain the above copyright +### notice, this list of conditions and the following acknowledgments +### and disclaimers. +### +### 2. Redistributions in binary form must reproduce the above copyright +### notice, this list of conditions and the following disclaimer in the +### documentation and/or other materials provided with the distribution. +### +### 3. All advertising materials for third-party software mentioning +### features or use of this software must display the following +### disclaimer: +### +### "Neither Carnegie Mellon University nor its Software Engineering +### Institute have reviewed or endorsed this software" +### +### 4. The names "Department of Homeland Security," "Carnegie Mellon +### University," "CERT" and/or "Software Engineering Institute" shall +### not be used to endorse or promote products derived from this software +### without prior written permission. For written permission, please +### contact permission@sei.cmu.edu. +### +### 5. Products derived from this software may not be called "CERT" nor +### may "CERT" appear in their names without prior written permission of +### permission@sei.cmu.edu. +### +### 6. Redistributions of any form whatsoever must retain the following +### acknowledgment: +### +### "This product includes software developed by CERT with funding +### and support from the Department of Homeland Security under +### Contract No. FA 8721-05-C-0003." +### +### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. +### END LICENSE ### +''' +A collection of Python objects that wrap and extend the GDB Python API. + +These objects are designed to be used when an AddressSanitizer log is passed +as an argument to the 'exploitable' command. They are used by a specified user, +who graciously contributed the code, but have not been tested. + +See x86.py in this directory for more info. +''' +try: + import gdb +except ImportError as e: + raise ImportError("This script must be run in GDB: ", str(e)) + +import os +import re + +from lib.tools import AttrDict, memoized + +from lib.gdb_wrapper.x86 import Backtrace, Frame, Target, ProcMaps + +class ASanFrame(Frame): + ''' + Wrapper for gdb.Frame. Content for this frame is generated from + AddressSanitizer log data rather than the GDB API. + ''' + def __init__(self, target, position=None): + self.target = target + self.position = position + self.blacklisted = False + if position is not None: + self._pc = target.asan_stack[position].addr + self._name = target.asan_stack[position].name + self.offset = target.asan_stack[position].off + self.mapped_region = target.procmaps().findByAddr(self._pc) + + @classmethod + def create(cls, target, addr, name, offset): + o = cls(target) + o._pc = addr + o._name = name + o.offset = offset + o.mapped_region = target.procmaps().findByAddr(addr) + return o + + def name(self): + return self._name + + def pc(self): + return self._pc + + def older(self): + try: + return self.target.asan_stack[self.position+1] + except IndexError: + raise RuntimeError() + + def unwind_stop_reason(self): + return 0 + + def type(self): + return 0 + + def __getattribute__(self, name): + return object.__getattribute__(self, name) + +class ASanBacktrace(Backtrace): + ''' + A backtrace composed of a list of GDB Frames (ordered from innermost to + outermost) and other attributes generated from an ASan log. + ''' + + def _next_frame(target=None, frame=None, i=None): + ''' + Gets the next frame (the frame after 'frame'). + If called without params (or if target is None, at least) then True + is returned (so that the backtrace loop in the calling function is + entered). + ''' + if not target: + return True + if i >= len(target.asan_stack): + return None + return ASanFrame(target, i) + +class ASanProcMaps(ProcMaps): + ''' + A list of process address mappings. This object should be instantiated + when the inferior is not running, but an ASan log file has been + specified. + ''' + + def __init__(self): + self._common_init() + self._add_by_target() + + def add_file(self, filepath, offset): + ''' + For the ELF file at filepath, adds a mapping to self and loads debug + symbols into GDB. offset is added to the start and end addresses for + the mapping (offset is the offset that the ELF is loaded to) + ''' + bn = os.path.basename(filepath) + if bn in self._files: + return + elf_sects = read_elf_sects(filepath) + sects = {} + for sect, (start, size) in elf_sects.items(): + start += offset + sects[sect] = start + ad = AttrDict(start=start, end=start + size, size=size, offset=0, name=bn, sect=sect) + self.append(ad) + cmd = "add-symbol-file '{}' {:#x} {}".format(filepath, sects[".text"], + " ".join("-s {} {:#x}".format(nm, st) + for (nm, st) in sects.items() + if nm != ".text")) + gdb.execute(cmd, False, True) + self._files.add(bn) + + def _add_by_target(self, num_initial_stacks=0): + ''' + Searches for thread stacks and adds corresponding ranges to self. + Also sets self.tgt_img to the name associated with the first thread + stack. + + The first num_initial_stacks sections in the target (such as a core + file) are treated as thread stacks. This logic is currently only + used by the ASan logic, which is used by a specific user and + largely untested. + ''' + + # Collect thread stack info + mapstr = str(gdb.execute("info target", False, True)) + ranges = [] + fn = None + for m in self._re_info_target.finditer(mapstr): + if m.group("file") is not None: + fn = os.path.basename(m.group("file")) + else: + lib = m.group("lib") + if lib: + lib = os.path.basename(lib) + ranges.append((int(m.group("start"), 16), int(m.group("end"), 16), fn, lib, m.group("section"))) + + # Append thread stack info to self + for i, (st, en, fn, lib, sect) in enumerate(ranges): + if i < num_initial_stacks: + # We could correlate thread stack pointer to one of the ranges, + # but we'll just assume for now that for num_initial_stacks, + # the first num_intial_stacks sections in the core are the stacks + name = "[stack]" + elif lib is not None: + name = lib + else: + name = fn + ad = AttrDict(start=st, end=en, size=(en-st), offset=0, name=name, sect=sect) + self.append(ad) + #gdb.write("{}: {:#x}-{:#x} {}".format(file, ad.start, ad.end, sect)) + self._files.add(os.path.basename(fn)) + if ranges: + self.tgt_img = ranges[0][2] + + +class ASanTarget(Target): + ''' + A wrapper for a Linux GDB Inferior. This wrapper enhances the state of + the Linux GDB Inferior with output from AddressSanitizer. + Note: This code was contributed by BlackBerry, and has not been thoroughly + tested outside of their environment. + ''' + _re_asan_bt = re.compile(r"""^(\033\133[0-9]+m)* # ANSI colour code + (?P\s*\#(?P[0-9]+)\s* + (?P0x[A-Fa-f0-9]+)\s*\() + (?P.*?) + \+(?P0x[A-Fa-f0-9]+)\)$""", re.VERBOSE|re.MULTILINE) +#==19027== ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f0fa35f1bf1 sp 0x7fffbcc88a48 bp 0x7fffbcc89290 T0) +#==16758== ERROR: AddressSanitizer: attempting double-free on 0x60040000dff0: +#==16769== ERROR: AddressSanitizer: heap-use-after-free on address 0x60040000dff0 at pc 0x4025b1 bp 0x7ffff1dbe6c0 sp 0x7ffff1dbe6b8 +#READ of size 1 at 0x60040000dff0 thread T0 + _re_asan_fault = re.compile(r"""^(\033\133[0-9]+m)* # ANSI colour code + =+[0-9]+=+\s*ERROR:\s*AddressSanitizer:\s* + (attempting\s)?(?P[A-Za-z0-9_-]+)\s + on\s((unknown\s)?address\s)?(?P0x[A-Fa-f0-9]+) + (:$ | \s(\(|at\s) + pc\s(?P0x[A-Fa-f0-9]+)\s + (?P[bs]p)\s(?P0x[A-Fa-f0-9]+)\s + (?P[bs]p)\s(?P0x[A-Fa-f0-9]+)([\r\n]+ + (\033\133[0-9]+m)* # ANSI colour code + (?P[A-Za-z0-9_-]+))?)""", re.VERBOSE|re.MULTILINE) + _re_asan_report_sym = re.compile(r"^(__asan_report_error|__asan::ReportDoubleFree)") + _re_asan_printloop = re.compile(r"^(__sanitizer::RawWrite|__sanitizer::Abort|[A-Za-z_0-9]*puts)") + _re_symline_trim = re.compile(r" starts at address.*\s*") + + _asan_reasons = ["double-free", "bad-free", "alloc-dealloc-mismatch", "unknown-crash", + "heap-buffer-overflow", "global-buffer-overflow", "stack-use-after-scope", + "use-after-poison", "stack-use-after-return", "stack-buffer-overflow", + "initialization-order-fiasco", "stack-buffer-underflow", "heap-use-after-free", "SEGV"] + + def __init__(self, asan_output, bt_limit=0): + self.__memo__ = {"isPossibleStackCorruption()": False, + "isStackCorruption()": False, + "isStackOverflow()": False, + "si_signo()": 11} + if not asan_output: + raise GdbWrapperError("no ASan data to analyze") + + # symbolize asan_message + self.asan_stack = [] + out = [] + last = 0 + all_frames = [] + maps = self.procmaps() + i = 0 + for m in self._re_asan_bt.finditer(asan_output): + frame, addr, img, offset = m.group("frame", "addr", "img", "offset") + frame = int(frame) + addr = int(addr, 16) #+ 1 + if img: + maps.add_file(img, addr - offset) + out.append(asan_output[last:m.end("all")]) + all_frames.append((frame, addr, offset, img, len(out))) + out.append(None) + last = m.end() + + i += 1 + if i >= bt_limit: + break + + if not all_frames: + raise GdbWrapperError("No frames found in address sanitizer log") + + out.append(asan_output[last:]) + frame = -1 + for num, addr, offset, img, outpos in all_frames: + region = maps.findByAddr(addr) + symbol = gdb.execute("info symbol {:#x}".format(addr), False, True) + symline = gdb.execute("info line *{:#x}".format(addr), False, True) + if symline and symline.startswith("Line"): + symline = "\n\t{}".format(self._re_symline_trim.sub("", symline)) + else: + symline = "" + symbol_m = self._re_gdb_info_sym.search(symbol) + if img: + lib = img + elif region: + lib = region.name + else: + lib = None + if symbol_m is None: + sym = None + off = offset + else: + sym = symbol_m.group("sym") + off = int(symbol_m.group("off")) + if frame == -1: + self.asan_pc_img = lib, offset + if frame is not None and num > frame: + frame = num + if lib: + lib = os.path.basename(lib) + self.asan_stack.append(AttrDict(addr=addr, + lib=lib, + off=off, + name=sym)) + else: + frame = None + out[outpos] = "{}){}".format(ASanFrame.create(self, addr, sym, off).terse(), symline) + asan_output = "".join(out) + gdb.write(asan_output) + gdb.flush() + # parse ASAN's analysis + m = self._re_asan_fault.search(asan_output) + self.__memo__["si_addr()"] = int(m.group("fault"), 16) + self.asan_reason = m.group("desc") + if self.asan_reason == "double-free": + self.__memo__["pc()"] = self.asan_stack[1].addr + self.__memo__["stack_pointer()"] = None # what to do? .... + else: + self.__memo__["pc()"] = int(m.group("pc"), 16) + if m.group("bspid1") == "sp": + self.__memo__["stack_pointer()"] = int(m.group("bsp1"), 16) + else: + self.__memo__["stack_pointer()"] = int(m.group("bsp2"), 16) + if self.asan_reason != "SEGV": + self.asan_operation = m.group("operation") + + @memoized + def current_instruction(self): + img, addr = getattr(self, "asan_pc_img", (None, self.pc())) + if img and os.path.basename(img) != getattr(self.procmaps(), "tgt_img", None): + gdb.execute("file {}".format(img), False, True) + try: + gdbstr = gdb.execute("x/i {:#x}".format(addr), False, True).splitlines()[0] + self._getInstruction(gdbstr) + except RuntimeError: + return None + + @memoized + def procmaps(self): + return ASanProcMaps() diff --git a/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/elf.py b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/elf.py new file mode 100644 index 0000000..c815676 --- /dev/null +++ b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/elf.py @@ -0,0 +1,81 @@ +### ------------------------------------------------------------------------ +### Copyright (C) 2013 BlackBerry Ltd. All Rights Reserved. +### ------------------------------------------------------------------------ +### Redistribution and use in source and binary forms, with or without +### modification, are permitted provided that the following conditions +### are met: +### +### * Redistributions of source code must retain the above copyright +### notice, this list of conditions and the following disclaimer. +### +### * Redistributions in binary form must reproduce the above copyright +### notice, this list of conditions and the following disclaimer in the +### documentation and/or other materials provided with the distribution. +### +### * Neither the name of BlackBerry Ltd. nor the names of its contributors +### may be used to endorse or promote products derived from this software +### without specific prior written permission. +### +### THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +### "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +### TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +### PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLACKBERRY LTD BE LIABLE FOR ANY +### DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +### (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +### SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +### CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +### LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +### OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +### SUCH DAMAGE. +__all__ = ["read_elf_sects"] +import collections +import struct + +ElfHeaderCommon = collections.namedtuple("ElfHeaderCommon", + ("ei_magic", "ei_class", "ei_data", "ei_version", "ei_osabi", + "ei_abiversion")) +ElfHeader = collections.namedtuple("ElfHeader", + ("e_type", "e_machine", "e_version", "e_entry", "e_phoff", "e_shoff", + "e_flags", "e_ehsize", "e_phentsize", "e_phnum", "e_shentsize", + "e_shnum", "e_shstrndx")) +ElfSectionHeader = collections.namedtuple("ElfSectionHeader", + ("sh_name", "sh_type", "sh_flags", "sh_addr", "sh_offset", "sh_size", + "sh_link", "sh_info", "sh_addralign", "sh_entsize")) + +def read_elf_sects(filename): + """ + Lists the sections in an ELF file. Returns a dictionary mapping section + names to (address, size) tuples. Only sections with non-zero sh_address + and sh_size will be returned. + """ + with open(filename, "rb") as f: + hdr_s = struct.Struct("=4s5B7x") + hdr = ElfHeaderCommon(*hdr_s.unpack_from(f.read(hdr_s.size))) + if hdr.ei_magic != "\x7FELF": + raise RuntimeError("Unrecognized file format") + try: + bits = {1: "I", 2: "Q"}[hdr.ei_class] + except KeyError: + raise RuntimeError("Unrecognized value for word size ({})".format(hdr.ei_class)) + try: + endian = {1: "<", 2: ">"}[hdr.ei_data] + except KeyError: + raise RuntimeError("Unrecognized value for endianness ({})".format(hdr.ei_data)) + hdr_s = struct.Struct("{}HHI3{}I6H".format(endian, bits)) + hdr = ElfHeader(*hdr_s.unpack_from(f.read(hdr_s.size))) + if hdr.e_shoff == 0: + raise RuntimeError("No section info") + f.seek(hdr.e_shoff) + shdr_s = struct.Struct("{0}II4{1}II2{1}".format(endian, bits)) + first = ElfSectionHeader(*shdr_s.unpack_from(f.read(shdr_s.size))) + if hdr.e_shnum == 0: + hdr = hdr._replace(e_shnum=first.sh_size) + if hdr.e_shstrndx == 0xFFFF: + hdr = hdr._replace(e_shstrndx=first.sh_link) + sects = [ElfSectionHeader(*shdr_s.unpack_from(f.read(shdr_s.size))) + for _ in range(hdr.e_shnum-1)] + f.seek(sects[hdr.e_shstrndx-1].sh_offset) + strings = f.read(sects[hdr.e_shstrndx-1].sh_size) + return {strings[s.sh_name:strings.index("\0", s.sh_name)]: (s.sh_addr, s.sh_size) + for s in sects if s.sh_addr and s.sh_size} + diff --git a/src/linux/CERT_triage_tools/exploitable/lib/versions.py b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/qnx.py similarity index 72% rename from src/linux/CERT_triage_tools/exploitable/lib/versions.py rename to src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/qnx.py index 444638e..170d1ae 100644 --- a/src/linux/CERT_triage_tools/exploitable/lib/versions.py +++ b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/qnx.py @@ -4,6 +4,7 @@ ### ### ------------------------------------------------------------------------ ### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. ### ------------------------------------------------------------------------ ### Redistribution and use in source and binary forms, with or without ### modification, are permitted provided that the following conditions are @@ -52,11 +53,35 @@ ### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. ### END LICENSE ### ''' -Version information +A collection of Python objects that wrap and extend the GDB Python API. + +This logic was contributed by a specific user (guess who :) but is not +currently being tested -- please help remedy that situation! + +See x86.py in this directory for more info. ''' +try: + import gdb +except ImportError as e: + raise ImportError("This script must be run in GDB: ", str(e)) + +from lib.tools import memoized +from lib.gdb_wrapper.x86 import Target, ProcMaps + +class QnxProcMaps(ProcMaps): + def __init__(self): + self._common_init() + self._add_by_target(len(gdb.inferiors()[0].threads())) -# The current version of the exploitable command -exploitable_version = "1.04" +class QnxTarget(Target): + ''' + A wrapper for a ARM Linux (with QNX Neutrino kernel) GDB Inferior. + ''' + @memoized + def si_addr(self): + str(gdb.parse_and_eval("$_siginfo.__data.__fault.__addr")) + return gdb.parse_and_eval("$_siginfo.__data.__fault.__addr") -# The minimum version of GDB that the exploitable command has been tested with -min_gdb_version = "7.2" + @memoized + def proc_maps(self): + return QnxProcMaps(self) diff --git a/src/linux/CERT_triage_tools/exploitable/lib/tests/unit_tests.py b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/tests/x86_unit_tests.py similarity index 79% rename from src/linux/CERT_triage_tools/exploitable/lib/tests/unit_tests.py rename to src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/tests/x86_unit_tests.py index 25ae271..fc16b81 100644 --- a/src/linux/CERT_triage_tools/exploitable/lib/tests/unit_tests.py +++ b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/tests/x86_unit_tests.py @@ -1,166 +1,191 @@ -### BEGIN LICENSE ### -### Use of the triage tools and related source code is subject to the terms -### of the license below. -### -### ------------------------------------------------------------------------ -### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. -### ------------------------------------------------------------------------ -### Redistribution and use in source and binary forms, with or without -### modification, are permitted provided that the following conditions are -### met: -### -### 1. Redistributions of source code must retain the above copyright -### notice, this list of conditions and the following acknowledgments -### and disclaimers. -### -### 2. Redistributions in binary form must reproduce the above copyright -### notice, this list of conditions and the following disclaimer in the -### documentation and/or other materials provided with the distribution. -### -### 3. All advertising materials for third-party software mentioning -### features or use of this software must display the following -### disclaimer: -### -### "Neither Carnegie Mellon University nor its Software Engineering -### Institute have reviewed or endorsed this software" -### -### 4. The names "Department of Homeland Security," "Carnegie Mellon -### University," "CERT" and/or "Software Engineering Institute" shall -### not be used to endorse or promote products derived from this software -### without prior written permission. For written permission, please -### contact permission@sei.cmu.edu. -### -### 5. Products derived from this software may not be called "CERT" nor -### may "CERT" appear in their names without prior written permission of -### permission@sei.cmu.edu. -### -### 6. Redistributions of any form whatsoever must retain the following -### acknowledgment: -### -### "This product includes software developed by CERT with funding -### and support from the Department of Homeland Security under -### Contract No. FA 8721-05-C-0003." -### -### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND -### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER -### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING -### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE -### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, -### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND -### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, -### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND -### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. -### END LICENSE ### -''' -Some unit tests for select objects in lib. - -Because these objects expect to be instantiated in GDB's Python interpreter, -the Python-supplied unittest module is not used (it assumes access to -argv). This script is meant to be invoked like this: - -exploitable$ gdb -ex "source lib/tests/unit_tests.py" -ex "quit" -''' -import sys, os -sys.path.append(os.getcwd()) -import lib.gdb_wrapper as gdb_wrapper - -def assertEqual(val1, val2, fmt="\"%s\""): - assert type(val1) == type(val2), "%s != %s" % (type(val1), type(val2)) - assert val1 == val2, ("%s != %s" % (fmt, fmt)) % (val1, val2) - -def testInstruction(): - ''' - Tests that the gdb_wrapper.Instruction string parsing works as expected. - ''' - # single arg test - gdbstr = "=> 0xb97126 :call 0xab9247" - i = gdb_wrapper.Instruction(gdbstr) - assertEqual(i.addr, 0xb97126, "0x%x") - assertEqual(str(i.operands[0]), "0xab9247") - assertEqual(i.mnemonic, "call") - - # trailing symbol test - gdbstr = "=> 0xb97126 :call 0xab9247 " - i = gdb_wrapper.Instruction(gdbstr) - assertEqual(i.addr, 0xb97126, "0x%x") - assertEqual(str(i.operands[0]), "0xab9247 ") - assertEqual(i.mnemonic, "call") - - # no args - gdbstr = " 0x005ab337 <+535>: ret" - i = gdb_wrapper.Instruction(gdbstr) - assertEqual(i.addr, 0x005ab337, "0x%x") - assertEqual(len(i.operands), 0) - assertEqual(i.mnemonic, "ret") - - # prefix, multiple args - gdbstr = " 0x0011098c: repz xor 0xDEADBEEF,0x23" - i = gdb_wrapper.Instruction(gdbstr) - assertEqual(i.addr, 0x0011098c, "0x%x") - assertEqual(str(i.operands[0]), "0xDEADBEEF") - assertEqual(str(i.operands[1]), "0x23") - assertEqual(i.mnemonic, "repz xor") - -def testOperand(): - ''' - Tests that the gdb_wrapper.Operand string parsing works as expected. - Does not test expression evaluation -- would need active registers to - do that. - ''' - # eiz, pointer test - # 0x005ab184 <+100>: lea esi,[esi+eiz*1+0x0] - gdbstr = "[esi+eiz*1+0x0]" - o = gdb_wrapper.Operand(gdbstr) - assertEqual(o.is_pointer, True) - assertEqual(o.expr, "$esi+0*1+0x0") - - # complex, other-pointer-style test - # 0x00110057: add BYTE PTR [edx+edx*8-0x1d43fffe],bh - gdbstr = "BYTE PTR [edx+edx*8-0x1d43fffe]" - o = gdb_wrapper.Operand(gdbstr) - assertEqual(o.is_pointer, True) - assertEqual(o.expr, "$edx+$edx*8-0x1d43fffe") - - # less-common register test - # 0x00110057: add BYTE PTR [edx+edx*8-0x1d43fffe],bh - gdbstr = "bh" - o = gdb_wrapper.Operand(gdbstr) - assertEqual(o.is_pointer, False) - assertEqual(o.expr, "$bh") - - # yet-another-pointer-style test - # 0x001102ab: add BYTE PTR ds:0x2880000,ch - gdbstr = "BYTE PTR ds:0x2880000" - o = gdb_wrapper.Operand(gdbstr) - assertEqual(o.is_pointer, True) - assertEqual(o.expr, "0x2880000") - - # floating point stack test - # 0xb68dc5: fucomi st,st(1) - gdbstr = "st(1)" - o = gdb_wrapper.Operand(gdbstr) - assertEqual(o.is_pointer, False) - assertEqual(o.expr, "st(1)") - - # spacing test - gdbstr = "edi * xmm5 +1" - o = gdb_wrapper.Operand(gdbstr) - assertEqual(o.is_pointer, False) - assertEqual(o.expr.replace(" ",""), "$edi*$xmm5+1") - - # 64-bit registers - gdbstr = "r16 + r8 + r8b" - o = gdb_wrapper.Operand(gdbstr) - assertEqual(o.is_pointer, False) - assertEqual(o.expr.replace(" ",""), "$r16+$r8+$r8b") - - # more 64-bit registers - gdbstr = "[r12w + 0x50]" - o = gdb_wrapper.Operand(gdbstr) - assertEqual(o.is_pointer, True) - assertEqual(o.expr.replace(" ",""), "$r12w+0x50") - -if __name__ == "__main__": - testInstruction() - testOperand() - print "passed all tests" \ No newline at end of file +### BEGIN LICENSE ### +### Use of the triage tools and related source code is subject to the terms +### of the license below. +### +### ------------------------------------------------------------------------ +### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### ------------------------------------------------------------------------ +### Redistribution and use in source and binary forms, with or without +### modification, are permitted provided that the following conditions are +### met: +### +### 1. Redistributions of source code must retain the above copyright +### notice, this list of conditions and the following acknowledgments +### and disclaimers. +### +### 2. Redistributions in binary form must reproduce the above copyright +### notice, this list of conditions and the following disclaimer in the +### documentation and/or other materials provided with the distribution. +### +### 3. All advertising materials for third-party software mentioning +### features or use of this software must display the following +### disclaimer: +### +### "Neither Carnegie Mellon University nor its Software Engineering +### Institute have reviewed or endorsed this software" +### +### 4. The names "Department of Homeland Security," "Carnegie Mellon +### University," "CERT" and/or "Software Engineering Institute" shall +### not be used to endorse or promote products derived from this software +### without prior written permission. For written permission, please +### contact permission@sei.cmu.edu. +### +### 5. Products derived from this software may not be called "CERT" nor +### may "CERT" appear in their names without prior written permission of +### permission@sei.cmu.edu. +### +### 6. Redistributions of any form whatsoever must retain the following +### acknowledgment: +### +### "This product includes software developed by CERT with funding +### and support from the Department of Homeland Security under +### Contract No. FA 8721-05-C-0003." +### +### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. +### END LICENSE ### +''' +Some unit tests for select objects in lib. + +Because these objects expect to be instantiated in GDB's Python interpreter, +the Python-supplied unittest module is not used (it assumes access to +argv). This script is meant to be invoked like this: + +exploitable$ gdb -ex "source lib/gdb_wrapper/tests/x86_unit_tests.py" -ex "quit" +''' +import sys, os +sys.path.append(os.getcwd()) +import lib.gdb_wrapper.x86 as gdb_wrapper + +def assertEqual(val1, val2, fmt="\"%s\""): + assert type(val1) == type(val2), "%s != %s" % (type(val1), type(val2)) + assert val1 == val2, ("%s != %s" % (fmt, fmt)) % (val1, val2) + +def testInstruction(): + ''' + Tests that the gdb_wrapper.Instruction string parsing works as expected. + ''' + # single arg test + gdbstr = "=> 0xb97126 :call 0xab9247" + i = gdb_wrapper.x86Instruction(gdbstr) + assertEqual(i.addr, 0xb97126, "0x%x") + assertEqual(str(i.operands[0]), "0xab9247") + assertEqual(i.mnemonic, "call") + + # trailing symbol test + gdbstr = "=> 0xb97126 :call 0xab9247 " + i = gdb_wrapper.x86Instruction(gdbstr) + assertEqual(i.addr, 0xb97126, "0x%x") + assertEqual(str(i.operands[0]), "0xab9247") + assertEqual(i.mnemonic, "call") + + # no args + gdbstr = " 0x005ab337 <+535>: ret" + i = gdb_wrapper.x86Instruction(gdbstr) + assertEqual(i.addr, 0x005ab337, "0x%x") + assertEqual(len(i.operands), 0) + assertEqual(i.mnemonic, "ret") + + # prefix, multiple args + gdbstr = " 0x0011098c: repz xor 0xDEADBEEF,0x23" + i = gdb_wrapper.x86Instruction(gdbstr) + assertEqual(i.addr, 0x0011098c, "0x%x") + assertEqual(str(i.operands[0]), "0xDEADBEEF") + assertEqual(str(i.operands[1]), "0x23") + assertEqual(i.mnemonic, "repz xor") + + # segment register + gdbstr = "=> 0x4004bf : mov DWORD PTR ds:0x0,eax" + i = gdb_wrapper.x86Instruction(gdbstr) + assertEqual(i.addr, 0x4004bf, "0x%x") + assertEqual(str(i.operands[0]), "DWORD PTR ds:0x0") + assertEqual(str(i.operands[1]), "eax") + assertEqual(i.mnemonic, "mov") + + # C++ template class + gdbstr = "=> 0x4007c9 >()+4>: mov DWORD PTR [eax+ebx*4+0x8],eax" + i = gdb_wrapper.x86Instruction(gdbstr) + assertEqual(i.addr, 0x4007c9, "0x%x") + assertEqual(str(i.operands[0]), "DWORD PTR [eax+ebx*4+0x8]") + assertEqual(str(i.operands[1]), "eax") + assertEqual(i.mnemonic, "mov") + + # C++ test + gdbstr = "=> 0x4211d6 >::rotateWithLeftChild(AvlNode >*&) const+50>:\tmov rdx,QWORD PTR [rax+0x18]" + i = gdb_wrapper.x86Instruction(gdbstr) + assertEqual(i.addr, 0x4211d6, "0x%x") + assertEqual(str(i.operands[0]), "rdx") + assertEqual(str(i.operands[1]), "QWORD PTR [rax+0x18]") + assertEqual(i.mnemonic, "mov") + + +def testOperand(): + ''' + Tests that the gdb_wrapper.Operand string parsing works as expected. + Does not test expression evaluation -- would need active registers to + do that. + ''' + # eiz, pointer test + # 0x005ab184 <+100>: lea esi,[esi+eiz*1+0x0] + gdbstr = "[esi+eiz*1+0x0]" + o = gdb_wrapper.Operand(gdbstr) + assertEqual(o.is_pointer, True) + assertEqual(o.expr, "$esi+0*1+0x0") + + # complex, other-pointer-style test + # 0x00110057: add BYTE PTR [edx+edx*8-0x1d43fffe],bh + gdbstr = "BYTE PTR [edx+edx*8-0x1d43fffe]" + o = gdb_wrapper.Operand(gdbstr) + assertEqual(o.is_pointer, True) + assertEqual(o.expr, "$edx+$edx*8-0x1d43fffe") + + # less-common register test + # 0x00110057: add BYTE PTR [edx+edx*8-0x1d43fffe],bh + gdbstr = "bh" + o = gdb_wrapper.Operand(gdbstr) + assertEqual(o.is_pointer, False) + assertEqual(o.expr, "$bh") + + # yet-another-pointer-style test + # 0x001102ab: add BYTE PTR ds:0x2880000,ch + gdbstr = "BYTE PTR ds:0x2880000" + o = gdb_wrapper.Operand(gdbstr) + assertEqual(o.is_pointer, True) + assertEqual(o.expr, "0x2880000") + + # floating point stack test + # 0xb68dc5: fucomi st,st(1) + gdbstr = "st(1)" + o = gdb_wrapper.Operand(gdbstr) + assertEqual(o.is_pointer, False) + assertEqual(o.expr, "st(1)") + + # spacing test + gdbstr = "edi * xmm5 +1" + o = gdb_wrapper.Operand(gdbstr) + assertEqual(o.is_pointer, False) + assertEqual(o.expr.replace(" ",""), "$edi*$xmm5+1") + + # 64-bit registers + gdbstr = "r16 + r8 + r8b" + o = gdb_wrapper.Operand(gdbstr) + assertEqual(o.is_pointer, False) + assertEqual(o.expr.replace(" ",""), "$r16+$r8+$r8b") + + # more 64-bit registers + gdbstr = "[r12w + 0x50]" + o = gdb_wrapper.Operand(gdbstr) + assertEqual(o.is_pointer, True) + assertEqual(o.expr.replace(" ",""), "$r12w+0x50") + +if __name__ == "__main__": + testInstruction() + testOperand() + print("passed all tests") diff --git a/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper.py b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/x86.py similarity index 53% rename from src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper.py rename to src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/x86.py index a29c6bc..937b899 100644 --- a/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper.py +++ b/src/linux/CERT_triage_tools/exploitable/lib/gdb_wrapper/x86.py @@ -4,6 +4,7 @@ ### ### ------------------------------------------------------------------------ ### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. ### ------------------------------------------------------------------------ ### Redistribution and use in source and binary forms, with or without ### modification, are permitted provided that the following conditions are @@ -17,24 +18,17 @@ ### notice, this list of conditions and the following disclaimer in the ### documentation and/or other materials provided with the distribution. ### -### 3. All advertising materials for third-party software mentioning -### features or use of this software must display the following -### disclaimer: -### -### "Neither Carnegie Mellon University nor its Software Engineering -### Institute have reviewed or endorsed this software" -### -### 4. The names "Department of Homeland Security," "Carnegie Mellon +### 3. The names "Department of Homeland Security," "Carnegie Mellon ### University," "CERT" and/or "Software Engineering Institute" shall ### not be used to endorse or promote products derived from this software ### without prior written permission. For written permission, please ### contact permission@sei.cmu.edu. ### -### 5. Products derived from this software may not be called "CERT" nor +### 4. Products derived from this software may not be called "CERT" nor ### may "CERT" appear in their names without prior written permission of ### permission@sei.cmu.edu. ### -### 6. Redistributions of any form whatsoever must retain the following +### 5. Redistributions of any form whatsoever must retain the following ### acknowledgment: ### ### "This product includes software developed by CERT with funding @@ -54,22 +48,21 @@ ''' A collection of Python objects that wrap and extend the GDB Python API. -The objects in this file should generally not be instantiated directly-- -they should be accessed via getTarget() - -Note that for many objects defined in this file, GDB is queried only when the -object is instantiated: if the state of the Inferior changes, any previously -created instances should be considered stale. +This file contains base class logic for GDB wrapper object as well as +x86-specific logic. Someday these should be separated. ''' try: import gdb except ImportError as e: raise ImportError("This script must be run in GDB: ", str(e)) -import warnings -import re import hashlib -from tools import * +import os +import re +import warnings + +from lib.gdb_wrapper.elf import read_elf_sects +from lib.tools import AttrDict, memoized def gdb_uint(gdb_value): ''' @@ -84,7 +77,12 @@ def gdb_uint(gdb_value): unsigned GDB.Value type). This method can be used to ensure the types agree and that arithmetic/boolean operations evaluate as expected. ''' - return gdb_value.cast(gdb.lookup_type('unsigned long')) + import sys + pyint = int(gdb_value.cast(gdb.lookup_type("unsigned long"))) + # see https://docs.python.org/2/library/platform.html#cross-platform + if sys.maxsize > 2**32: + return pyint & 0xFFFFFFFFFFFFFFFF + return pyint & 0xFFFFFFFF class GdbWrapperError(RuntimeError): ''' @@ -97,42 +95,56 @@ class ProcMaps(list): A list of process address mappings. This object should only be instantiated when the Inferior is running, otherwise the string parsing may fail. ''' + _re_info_target = re.compile(r"""^\s+`(?P.*?)',\s+file\s+type | + ^\s+(?P0x[0-9A-Fa-f]+)\s+-\s+ + (?P0x[0-9A-Fa-f]+)\s+is\s+ + (?P

.*?)(\s+in\s+(?P.*?)(\.so[\.0-9]*)?)?$""", re.MULTILINE|re.VERBOSE) + _re_gdb_sect_info = re.compile(r"^\s+(?P0x[A-Fa-f0-9]+)->(?P0x[A-Fa-f0-9]+) at 0x[A-Fa-f0-9]+: (?P[^\s]+)", re.MULTILINE) + def __init__(self): ''' Queries the GDB Python API for the process address space, parses it, and appends it to self ''' - super (ProcMaps, self).__init__ () - + self._common_init() mapstr = str(gdb.execute("info proc map", False, True)) header_pos = mapstr.find("Start Addr") if header_pos == -1: - raise GdbWrapperError("Unable to parse 'info proc map' string") + raise GdbWrapperError("Unable to parse \"info proc map\" string") maplines = mapstr[header_pos:].splitlines()[1:] for line in maplines: line = line.split() - start, end, size, offset = tuple([int(i,16) for i in line[0:4]]) + start, end, size, offset = tuple(int(i, 16) for i in line[0:4]) name = " ".join(line[4:]) ad = AttrDict(start=start, end=end, size=size, - offset=offset, name=name) + offset=offset, name=name) self.append(ad) + def _common_init(self): + ''' + Performs init common to this class and some subclasses. + ''' + list.__init__(self) + self._files = set() + def __str__(self): - result = "%10s %10s %10s %10s %s\n" % \ - ("start", "end", "size", "offset", "name") + result = ["{:10} {:10} {:10} {:10} {} {}".format("start", "end", + "size", "offset", "name", "sect")] for m in self: - result += "0x%08x 0x%08x 0x%08x 0x%08x %s\n" % \ - (m.start, m.end, m.size, m.offset, m.name) - return result + result.append("{0.start:#08x} {0.end:#08x} {0.size:#08x} " +\ + "{0.offset:#08x} {0.name} {1}".format(m, + getattr(m, "sect", "?"))) + result.append("") + return "\n".join(result) def findByName(self, name): ''' Returns the process address mapping whose name matches name or None if no such mapping is found in self. ''' - for map in self: - if map.name == name: - return map + for sect in self: + if sect.name == name: + return sect return None def findByAddr(self, addr): @@ -140,12 +152,14 @@ def findByAddr(self, addr): Returns the process address mapping that addr falls in, or None if no such mapping is found in self. ''' - for map in self: - if map.start <= addr < map.end: - return map - return None + result = None + for sect in self: + if sect.start <= addr < sect.end: + if result is None or sect.size < result.size: + result = sect + return result -class Instruction(): +class Instruction(object): ''' A disassembled instruction. Notable attributes include: @@ -161,46 +175,83 @@ class Instruction(): source - the source Operand of this instruction (optional) aux - the aux Operand of this instruction (optional) ''' - operand_order = ["dest", "source", "aux"] - x86_prefixes = ["rep", "repe", "repz", "repne", "repnz"] - regexs = {'hex_int' : re.compile(".*(0x\S*).*"), - 'dis_fail' : re.compile(".*.*|" - ".*No function contains specified address.*")} + _re_hex_int = re.compile(r"(0x[A-Fa-f0-9]+)") + _re_dis_fail = re.compile(r""".*(| + No\sfunction\scontains\sspecified\saddress).*""", re.VERBOSE) + _x86_prefixes = ["rep", "repe", "repz", "repne", "repnz"] def __init__(self, gdbstr): ''' - Parses gdbstr to populate self. + Parses gdbstr to populate self; this method only covers parsing that + is common across processor architectures. The remainder of the parsing + is implemented in subclasses. ''' self.gdbstr = gdbstr - if re.match(self.regexs['dis_fail'], self.gdbstr): - raise GdbWrapperError("Disassembler error detected: %s" - % self.gdbstr) + if self._re_dis_fail.match(self.gdbstr): + raise GdbWrapperError("Disassembler error detected: {}".format(self.gdbstr)) self.operands = [] + self.source = None + self.dest = None + self.aux = None + + def __str__(self): + return self.gdbstr - # get instruction addr and instruction string - inst = filter(None, self.gdbstr.rsplit(":",1)[1].lstrip().rstrip().split(" ")) - self.addr = int(re.match(Instruction.regexs["hex_int"], - self.gdbstr.split(":",1)[0]).group(1), 16) +class x86Instruction(Instruction): + ''' + A disassembled Intel instruction. See Instruction class comments for more + details. + ''' + + def __init__(self, gdbstr): + ''' + Parses gdbstr to populate self. + ''' + Instruction.__init__(self, gdbstr) + + # strip indicator + if self.gdbstr[0:2] == "=>": + self.gdbstr = self.gdbstr[2:] + + # strip hints + c = 0 + s = self.gdbstr + self.gdbstr = "" + for i in range(0, len(s)): + if s[i] == "<": + c += 1 + continue + elif s[i] == ">": + c -= 1 + continue + if c > 0: + continue + self.gdbstr += s[i] + + toks = [] + for t in self.gdbstr.split(): + t = t.strip().strip(":") + if t: + toks.append(t) + self.addr = int(toks[0], 16) # get mnemonic - if inst[0] in Instruction.x86_prefixes: # handle rep* prefixes - self.mnemonic = " ".join(inst[0:2]) - inst = " ".join(inst[2:]) + if toks[1] in self._x86_prefixes: # handle rep* prefixes + self.mnemonic = " ".join(toks[1:3]) + self.inst = " ".join(toks[3:]) else: - self.mnemonic = inst[0] - inst = " ".join(inst[1:]) - if inst == "": # handle "ret", "iret", et al. - return + self.mnemonic = toks[1] + self.inst = " ".join(toks[2:]) + + if self.inst == "": # handle "ret", "iret", et al. + return # get operands - [self.operands.append(Operand(e)) for e in inst.split(",")] - for i in range(0, len(self.operands)): - setattr(self, Instruction.operand_order[i], self.operands[i]) - - def __str__(self): - return self.gdbstr - -class Operand(): + self.operands = [Operand(o) for o in self.inst.split(",")] + for a, o in zip(("dest", "source", "aux"), self.operands): + setattr(self, a, o) + +class Operand(object): ''' A disassembled instruction operand that can be evaluated. Notable attributes include: @@ -211,15 +262,17 @@ class Operand(): For example, "DWORD PTR [eax]" is considered a pointer, "eax" is not. regs - a list of strings representing the registers used in this Operand ''' - _regexs = {'addr' : re.compile(".*\[(.*)\]|" # DWORD PTR [eax+0x1234] - ".*\:(.*)"), # DWORD PTR gs:0x18 - 'p_regs' : re.compile("eiz|riz"), - 'regstrs' : re.compile( - "^([a-z]{1,5}\d{0,2}[a-z]{0,1})(?=\s*[\*\+-])|" # "(eax)+0x1234" - "(?<=[\*\+-])\s*([a-z]{1,5}\d{0,2}[a-z]{0,1})(?=[\s*\*\+-])|" # "xmm5+(al)-0x1234" - "(?<=[\*\+-])\s*([a-z]{1,5}\d{0,2}[a-z]{0,1})$|" # "eax+4* (esi)" - "^([a-z]{1,5}\d{0,2}[a-z]{0,1})$"), # "(edi)" - 'fpu_stack' : re.compile("(st\(.*\))")} + _re_addr = re.compile(r""".*\[(.*)\] | # DWORD PTR [eax+0x1234] + .*\:(.*) # DWORD PTR gs:0x18 + """, re.VERBOSE) + _re_p_regs = re.compile(r"eiz|riz") + _re_regstrs = re.compile( + r"""^([a-z]{1,5}\d{0,2}[a-z]{0,1})(?=\s*[\*\+-]) | # "(eax)+0x1234" + (?<=[\*\+-])\s*([a-z]{1,5}\d{0,2}[a-z]{0,1})(?=[\s*\*\+-]) | # "xmm5+(al)-0x1234" + (?<=[\*\+-])\s*([a-z]{1,5}\d{0,2}[a-z]{0,1})$ | # "eax+4* (esi)" + ^([a-z]{1,5}\d{0,2}[a-z]{0,1})$ # "(edi)" + """, re.VERBOSE) + _re_fpu_stack = re.compile(r"(st\(.*\))") def __init__(self, gdbstr): ''' @@ -229,35 +282,33 @@ def __init__(self, gdbstr): gdbstr = gdbstr.split("<")[0] # get rid of "" # get addr - addr = re.match(Operand._regexs['addr'], gdbstr) # ignores segment regs + addr = self._re_addr.match(gdbstr) # ignores segment regs if addr: self.is_pointer = True - expr = filter(None, addr.groups())[0] + expr = [m for m in addr.groups() if m][0] else: self.is_pointer = False expr = gdbstr - # eiz(x86) and riz(64) are psuedo-index registers that + # eiz(x86) and riz(64) are pseudo-index registers that # the CPU always evaluates to 0. GDB types them as void (DNE) # which can cause a type error when evaluating expression via GDB, # so we substitute immediate zeros - expr = re.sub(Operand._regexs['p_regs'], "0", expr) + expr = self._re_p_regs.sub("0", expr) - self.regs = ["".join(list(t)) for t in\ - re.findall(Operand._regexs['regstrs'], expr)] + self.regs = [t.group() for t in self._re_regstrs.finditer(expr)] # prep for GDB evaluation. ex: "edx+0x12" becomes "$edx+0x12" - self.expr = re.sub(Operand._regexs['regstrs'], - (lambda mo: "$" + filter(None, mo.groups())[0]), + self.expr = self._re_regstrs.sub( + (lambda mo: "${}".format([mg for mg in mo.groups() if mg][0])), expr) - def eval(self): ''' Returns the integer value of this operand as evaluated by GDB. For example, if eax = 0x5 and this operand is DWORD PTR[eax + 0x100], eval would be 0x105 (a GDB.Value) ''' - if "mm" in " ".join(self.regs) or re.match(self._regexs['fpu_stack'], self.gdbstr): + if "mm" in " ".join(self.regs) or self._re_fpu_stack.match(self.gdbstr): return 0xDEADBEEF else: # Some GDBs (GDB 7.2 Fedora vs. Ubuntu/Debian) don't compare @@ -278,23 +329,26 @@ class Backtrace(list): abnormal_termination - set to True if GDB's backtrace unwind terminated abnormally, such as in the case of stack corruption ''' - def __init__(self, blacklist=None, major_depth=5): + def __init__(self, target, blacklist=None, major_depth=5, limit=0): ''' Uses the GDB API to populate self. Any frames in blacklist are marked as such. The first non-blacklisted, major_depth frames are used to calculate the backtrace's major hash. ''' + list.__init__(self) self.blacklist = blacklist - frame = gdb.selected_frame() + frame = self._next_frame() hc = 0 i = 0 - maj = "0" - min = "0" + major = "0" + minor = "0" self.abnormal_termination = False - while(frame and i < 512): - frame = Frame(frame, i) + while frame: + frame = self._next_frame(target, frame, i) + if not frame: + break # The check below is a workaround for a known libc/gdb runaway # backtrace issue, see @@ -304,9 +358,9 @@ def __init__(self, blacklist=None, major_depth=5): if not self._in_blacklist(frame): if hc < major_depth: - maj = hashlib.md5(maj + str(frame)).hexdigest() - min = hashlib.md5(min + str(frame)).hexdigest() - hc+=1 + major = hashlib.md5((major + str(frame)).encode()).hexdigest() + minor = hashlib.md5((minor + str(frame)).encode()).hexdigest() + hc += 1 else: frame.blacklisted = True self.append(frame) @@ -326,7 +380,20 @@ def __init__(self, blacklist=None, major_depth=5): break i += 1 - self.hash = AttrDict(major=maj, minor=min) + if limit and i >= limit: + break + + self.hash = AttrDict(major=major, minor=minor) + + def _next_frame(self, target=None, frame=None, i=None): + ''' + Gets the next frame (the frame after 'frame'). + If called without params (or if target is None, at least) then the + innermost frame is obtained from GDB. + ''' + if not target: + return gdb.selected_frame() + return Frame(target, frame, i) def _in_blacklist(self, frame): ''' @@ -337,19 +404,19 @@ def _in_blacklist(self, frame): if frame.name() in self.blacklist.functions: return True if frame.mapped_region and \ - re.match(self.blacklist.map_regex, frame.mapped_region): + self.blacklist.map_regex.match(frame.mapped_region.name): return True return False def __str__(self): - result = "\n".join([str(f) for f in self]) + result = [str(f) for f in self] if self.abnormal_termination: - reason = self[len(self) - 1].unwind_stop_reason() - result += "\nabnormal stack unwind termination: %s" % \ - gdb.frame_stop_reason_string(reason) - return result + reason = self[-1].unwind_stop_reason() + result.append("abnormal stack unwind termination: {}".format( + gdb.frame_stop_reason_string(reason))) + return "\n".join(result) -class Frame(): +class Frame(object): ''' Wrapper for gdb.Frame. Adds frame position, pretty string, etc. @@ -358,25 +425,38 @@ class Frame(): some issues with getting/setting this class's state, such as during pickling. ''' - def __init__(self, gdb_frame, position=None): + def __init__(self, target, gdb_frame, position=None): self.gdb_frame = gdb_frame self.position = position self.blacklisted = False - self.mapped_region = getTarget().procmaps().findByAddr(self.pc()) - if self.mapped_region: - self.mapped_region = self.mapped_region.name + self.mapped_region = target.procmaps().findByAddr(self.pc()) + sym_addr = target.sym_addr(self.pc()) + if sym_addr is None: + try: + sym_addr = self.mapped_region.start + except AttributeError: + sym_addr = 0 + self.offset = self.pc() - sym_addr def __getattr__(self, name): return getattr(self.gdb_frame, name) def __str__(self): - desc = "#%3d %s at 0x%x in %s" % \ - (self.position, self.name(), self.pc(), self.mapped_region) + desc = "#{:3d} {} at {:#x} in {}".format(self.position, self.name(), self.pc(), + self.mapped_region.name if self.mapped_region else "?") if self.blacklisted: desc += " (BL)" return desc -class Target(): + def terse(self): + out = self.mapped_region.name if self.mapped_region else "Unknown" + name = self.name() + if name is not None: + out = "{}!{}".format(out, name) + out = "{}+{:#x}".format(out, self.offset) + return out + +class Target(object): ''' A wrapper for a Linux GDB Inferior. Includes of various convenience methods used for classification. @@ -385,40 +465,49 @@ class Target(): example, the disassembly flavor may be left as "intel" after this code is executed. ''' - _regexs = {'info_frame' : re.compile("^ *eip = ([^\s;]*)(?: in )?" # addr - "([^\s;]*)" # fname - "([^\s;]*)") } # source_file:line + _re_info_frame = re.compile(r"""^\s*eip\s=\s([^\s;]*)(?:\sin\s)? # addr + ([^\s;]*) # fname + ([^\s;]*) # source_file:line + """, re.VERBOSE) + _re_gdb_info_sym = re.compile(r"""^\s*(?P.*?)\s+\+\s+(?P[0-9]+)\s+ + in\s+section\s+\.text(\s+ + of\s+(?P.*?)\s*)?$""", re.VERBOSE) + _re_gdb_addr_bit = re.compile(r"^gdbarch_dump: addr_bit = ([0-9]+)$", re.MULTILINE) + _re_gdb_osabi = re.compile(r"\(currently \"(.*)\"\)") + _re_gdb_arch = re.compile(r"\(currently\s+(.+)\)") # these functions and libs are not considered to be at fault for a crash - blacklist = AttrDict(functions=('__kernel_vsyscall', 'abort', 'raise', - 'malloc', 'free', '*__GI_abort', - '*__GI_raise', 'malloc_printerr', - '__libc_message', '_int_malloc', - '_int_free', ), - map_regex=re.compile(".*/libc.*|.*/libm.*")) - - def __init__(self): - self.check_inferior_state() - gdb.execute("set disassembly-flavor intel", False, True) - - def check_inferior_state(self): + blacklist = AttrDict(functions=("__kernel_vsyscall", "abort", "raise", + "malloc", "free", "*__GI_abort", + "*__GI_raise", "malloc_printerr", + "__libc_message", "_int_malloc", + "_int_free"), + map_regex=re.compile(r".*/libc(\.|-).*|.*/libm(\.|-).*")) + + def __init__(self, bt_limit=0): + self._check_inferior_state() + self.bt_limit = bt_limit + + def _check_inferior_state(self): if len(gdb.inferiors()) != 1: - raise GdbWrapperError("Unsupported number of inferiors (%d)" - % len(gdb.inferiors())) + raise GdbWrapperError("Unsupported number of inferiors ({})".format(len(gdb.inferiors()))) if len(gdb.inferiors()[0].threads()) == 0: raise GdbWrapperError("No threads running") if not gdb.inferiors()[0].threads()[0].is_stopped: raise GdbWrapperError("Inferior's primary thread is not stopped") + @memoized def backtrace(self): - return Backtrace(self.blacklist) + return Backtrace(self, self.blacklist, limit=self.bt_limit) def hash(self): return self.backtrace().hash + @memoized def procmaps(self): return ProcMaps() + @memoized def faulting_frame(self): for frame in self.backtrace(): if not frame.blacklisted: @@ -426,45 +515,64 @@ def faulting_frame(self): warnings.warn("All frames blacklisted") return None + @staticmethod + def sym_addr(sym): + try: + return gdb_uint(gdb.parse_and_eval(str(sym))) + except gdb.error: + return None + + @memoized def current_instruction(self): try: - addr = self.pc() - gdbstr = gdb.execute("x/i 0x%x" % addr, - False, True).splitlines()[0] - return Instruction(gdbstr) - except RuntimeError as e: + gdbstr = gdb.execute("x/i 0x%x" % self.pc(), False, True).splitlines()[0] + return self._getInstruction(gdbstr) + except RuntimeError: return None + def _getInstruction(self, gdbstr): + return x86Instruction(gdbstr) + + @memoized def pc(self): return gdb_uint(gdb.parse_and_eval("$pc")) + @memoized def stack_pointer(self): return gdb_uint(gdb.parse_and_eval("$sp")) + @memoized def pid(self): return gdb.inferiors()[0].pid + @memoized def pointer_size(self): - import platform - bits = platform.architecture()[0] - return int(re.match("^([\d]*)", bits).groups()[0]) / 8 + return int(self._re_gdb_addr_bit.search( + gdb.execute("maint print architecture", False, True)).group(1)) / 8 + @memoized def si_signo(self): # This is a workaround to a bug in the GDB Python API: # The only reliable way to cause GDB to raise an exception when # $_siginfo is not available it to call __str__() -- otherwise # (such as when casting the Gdb.Value to another type), GDB may # force Python to abruptly exit rather than raising an exception - str(gdb.parse_and_eval("$_siginfo.si_signo")) - return gdb.parse_and_eval("$_siginfo.si_signo") + signo = gdb.parse_and_eval("$_siginfo.si_signo") + str(signo) + return signo + @memoized def si_addr(self): str(gdb.parse_and_eval("$_siginfo._sifields._sigfault.si_addr")) - return gdb.parse_and_eval("$_siginfo._sifields._sigfault.si_addr") + return gdb_uint(gdb.parse_and_eval( + "$_siginfo._sifields._sigfault.si_addr")) -def getTarget(): +class x86Target(Target): ''' - Returns the current Target, which is a Python wrapper representing the - current state of the underlying Linux GDB Inferior object. + A wrapper for an x86 Linux GDB Inferior. ''' - return Target() + + def __init__(self, bt_limit=0): + Target.__init__(self, bt_limit) + self._check_inferior_state() + gdb.execute("set disassembly-flavor intel", False, True) diff --git a/src/linux/CERT_triage_tools/exploitable/lib/rules.py b/src/linux/CERT_triage_tools/exploitable/lib/rules.py index 8072acc..adeab2c 100644 --- a/src/linux/CERT_triage_tools/exploitable/lib/rules.py +++ b/src/linux/CERT_triage_tools/exploitable/lib/rules.py @@ -4,6 +4,7 @@ ### ### ------------------------------------------------------------------------ ### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. ### ------------------------------------------------------------------------ ### Redistribution and use in source and binary forms, with or without ### modification, are permitted provided that the following conditions are @@ -17,24 +18,17 @@ ### notice, this list of conditions and the following disclaimer in the ### documentation and/or other materials provided with the distribution. ### -### 3. All advertising materials for third-party software mentioning -### features or use of this software must display the following -### disclaimer: -### -### "Neither Carnegie Mellon University nor its Software Engineering -### Institute have reviewed or endorsed this software" -### -### 4. The names "Department of Homeland Security," "Carnegie Mellon +### 3. The names "Department of Homeland Security," "Carnegie Mellon ### University," "CERT" and/or "Software Engineering Institute" shall ### not be used to endorse or promote products derived from this software ### without prior written permission. For written permission, please ### contact permission@sei.cmu.edu. ### -### 5. Products derived from this software may not be called "CERT" nor +### 4. Products derived from this software may not be called "CERT" nor ### may "CERT" appear in their names without prior written permission of ### permission@sei.cmu.edu. ### -### 6. Redistributions of any form whatsoever must retain the following +### 5. Redistributions of any form whatsoever must retain the following ### acknowledgment: ### ### "This product includes software developed by CERT with funding @@ -57,17 +51,21 @@ from "most exploitable" to "least exploitable". ''' -from analyzers import * - rules = [ ('EXPLOITABLE', [ - dict(analyzer=isReturnAv, + dict(match_function="isReturnAv", desc="Access violation during return instruction", short_desc="ReturnAv", explanation="The target crashed on a return instruction, which likely " "indicates stack corruption."), - dict(analyzer=isSegFaultOnPcNotNearNull, + dict(match_function="isUseAfterFree", + desc="Use of previously freed heap buffer", + short_desc="UseAfterFree", + explanation="The target tried to access an address which was within a " + "heap buffer which has already been freed."), + + dict(match_function="isSegFaultOnPcNotNearNull", desc="Segmentation fault on program counter", short_desc="SegFaultOnPc", explanation="The target tried to access data at an address that matches " @@ -77,26 +75,26 @@ "cause. Regardless this likely indicates that the program counter " "contents are tainted and can be controlled by an attacker."), - dict(analyzer=isBranchAvNotNearNull, + dict(match_function="isBranchAvNotNearNull", desc="Access violation during branch instruction", short_desc="BranchAv", explanation="The target crashed on a branch instruction, which may " "indicate that the control flow is tainted."), - dict(analyzer=isErrorWhileExecutingFromStack, + dict(match_function="isErrorWhileExecutingFromStack", desc="Executing from stack", - short_desc="StackCodeExection", + short_desc="StackCodeExecution", explanation="The target stopped on an error while executing code " "within the process's stack region."), - dict(analyzer=isStackBufferOverflow, + dict(match_function="isStackBufferOverflow", desc="Stack buffer overflow", short_desc="StackBufferOverflow", explanation="The target stopped while handling a signal that was " "generated by libc due to detection of a stack buffer overflow. Stack " "buffer overflows are generally considered exploitable."), - dict(analyzer=isPossibleStackCorruption, + dict(match_function="isPossibleStackCorruption", desc="Possible stack corruption", short_desc="PossibleStackCorruption", explanation="GDB generated an error while unwinding the stack " @@ -106,7 +104,7 @@ "conditions likely indicate stack corruption, which is generally " "considered exploitable."), - dict(analyzer=isDestAvNotNearNull, + dict(match_function="isDestAvNotNearNull", desc="Access violation on destination operand", short_desc="DestAv", explanation="The target crashed on an access violation at an address " @@ -114,13 +112,13 @@ "indicates a write access violation, which means the attacker may " "control the write address and/or value."), - dict(analyzer=isMalformedInstructionSignal, + dict(match_function="isMalformedInstructionSignal", desc="Bad instruction", short_desc="BadInstruction", explanation="The target tried to execute a malformed or privileged " "instruction. This may indicate that the control flow is tainted."), - dict(analyzer=isHeapError, + dict(match_function="isHeapError", desc="Heap error", short_desc="HeapError", explanation="The target's backtrace indicates that libc has detected " @@ -132,14 +130,14 @@ ]), ('PROBABLY_EXPLOITABLE', [ - dict(analyzer=isStackOverflow, + dict(match_function="isStackOverflow", desc="Stack overflow", short_desc="StackOverflow", explanation="The target crashed on an access violation where the faulting " "instruction's mnemonic and the stack pointer seem to indicate a stack " "overflow."), - dict(analyzer=isSegFaultOnPcNearNull, + dict(match_function="isSegFaultOnPcNearNull", desc="Segmentation fault on program counter near NULL", short_desc="SegFaultOnPcNearNull", explanation="The target tried to access data at an address that matches " @@ -147,20 +145,20 @@ "counter contents are tainted, however, it may also indicate a simple " "NULL deference."), - dict(analyzer=isBranchAvNearNull, + dict(match_function="isBranchAvNearNull", desc="Access violation near NULL during branch instruction", short_desc="BranchAvNearNull", explanation="The target crashed on a branch instruction, which may " "indicate that the control flow is tainted. However, there is a " "chance it could be a NULL dereference."), - dict(analyzer=isBlockMove, + dict(match_function="isBlockMove", desc="Access violation during block move", short_desc="BlockMoveAv", explanation="The target crashed during a block move, which may indicate " "that the attacker can control a buffer overflow."), - dict(analyzer=isDestAvNearNull, + dict(match_function="isDestAvNearNull", desc="Access violation near NULL on destination operand", short_desc="DestAvNearNull", explanation="The target crashed on an access violation at an address " @@ -171,7 +169,7 @@ ]), ('PROBABLY_NOT_EXPLOITABLE', [ - dict(analyzer=isSourceAvNearNull, + dict(match_function="isSourceAvNearNull", desc="Access violation near NULL on source operand", short_desc="SourceAvNearNull", explanation="The target crashed on an access violation at an address " @@ -180,7 +178,7 @@ "crashed on a simple NULL dereference to data structure that has no " "immediate effect on control of the processor."), - dict(analyzer=isFloatingPointException, + dict(match_function="isFloatingPointException", desc="Floating point exception signal", short_desc="FloatingPointException", explanation="The target crashed on a floating point exception. This " @@ -188,7 +186,7 @@ "errors. It is generally difficult to leverage these types of errors " "to gain control of the processor."), - dict(analyzer=isBenignSignal, + dict(match_function="isBenignSignal", desc="Benign signal", short_desc="BenignSignal", explanation="The target is stopped on a signal that either does not " @@ -197,14 +195,14 @@ ]), ('UNKNOWN', [ - dict(analyzer=isSourceAvNotNearNull, + dict(match_function="isSourceAvNotNearNull", desc="Access violation on source operand", short_desc="SourceAv", explanation="The target crashed on an access violation at an address " "matching the source operand of the current instruction. This likely " "indicates a read access violation."), - dict(analyzer=isAbortSignal, + dict(match_function="isAbortSignal", desc="Abort signal", short_desc="AbortSignal", explanation="The target is stopped on a SIGABRT. SIGABRTs are often " @@ -212,13 +210,13 @@ "exploitable conditions. Unfortunately this command does not yet further " "analyze these crashes."), - dict(analyzer=isAccessViolationSignal, + dict(match_function="isAccessViolationSignal", desc="Access violation", short_desc="AccessViolation", explanation="The target crashed due to an access violation but there is not enough " "additional information available to determine exploitability."), - dict(analyzer=isUncategorizedSignal, + dict(match_function="isUncategorizedSignal", desc="Uncategorized signal", short_desc="UncategorizedSignal", explanation="The target is stopped on a signal. This may be an exploitable " diff --git a/src/linux/CERT_triage_tools/exploitable/lib/tools.py b/src/linux/CERT_triage_tools/exploitable/lib/tools.py index a978839..7abba2f 100644 --- a/src/linux/CERT_triage_tools/exploitable/lib/tools.py +++ b/src/linux/CERT_triage_tools/exploitable/lib/tools.py @@ -4,6 +4,7 @@ ### ### ------------------------------------------------------------------------ ### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +### Portions Copyright 2013 BlackBerry Ltd. All Rights Reserved. ### ------------------------------------------------------------------------ ### Redistribution and use in source and binary forms, with or without ### modification, are permitted provided that the following conditions are @@ -17,24 +18,17 @@ ### notice, this list of conditions and the following disclaimer in the ### documentation and/or other materials provided with the distribution. ### -### 3. All advertising materials for third-party software mentioning -### features or use of this software must display the following -### disclaimer: -### -### "Neither Carnegie Mellon University nor its Software Engineering -### Institute have reviewed or endorsed this software" -### -### 4. The names "Department of Homeland Security," "Carnegie Mellon +### 3. The names "Department of Homeland Security," "Carnegie Mellon ### University," "CERT" and/or "Software Engineering Institute" shall ### not be used to endorse or promote products derived from this software ### without prior written permission. For written permission, please ### contact permission@sei.cmu.edu. ### -### 5. Products derived from this software may not be called "CERT" nor +### 4. Products derived from this software may not be called "CERT" nor ### may "CERT" appear in their names without prior written permission of ### permission@sei.cmu.edu. ### -### 6. Redistributions of any form whatsoever must retain the following +### 5. Redistributions of any form whatsoever must retain the following ### acknowledgment: ### ### "This product includes software developed by CERT with funding @@ -54,64 +48,73 @@ ''' A collection of basic Python objects used throughout this project. ''' -class AttrDict(object): - """ - A dictionary that can be accessed both using indexing and attributes, - i.e., - x = AttrDict() - x.foo = 5 - print x["foo"] - Adapted from Construct v2's AttrDict - (ref: http://pypi.python.org/pypi/construct) 8/23/2011 - """ - __slots__ = ["__dict__"] - def __init__(self, **kw): - self.__dict__ = kw - def __contains__(self, key): - return key in self.__dict__ - def __nonzero__(self): - return bool(self.__dict__) - def __repr__(self): - return repr(self.__dict__) - def __str__(self): - return self.__pretty_str__() - def __pretty_str__(self, nesting = 1, indentation = " "): - if not self: - return "{}" - text = ["{\n"] - ind = nesting * indentation - for k in sorted(self.__dict__.keys()): - v = self.__dict__[k] - text.append(ind) - text.append(repr(k)) - text.append(" : ") - if hasattr(v, "__pretty_str__"): - try: - text.append(v.__pretty_str__(nesting+1, indentation)) - except Exception: - text.append(repr(v)) - else: - text.append(str(v)) - text.append("\n") - text.append((nesting-1) * indentation) - text.append("}") - return "".join(text) - def __delitem__(self, key): - del self.__dict__[key] - def __getitem__(self, key): - return self.__dict__[key] - def __setitem__(self, key, value): - self.__dict__[key] = value - def __copy__(self): - new = self.__class__() - new.__dict__ = self.__dict__.copy() - return new - def __update__(self, other): - if isinstance(other, dict): - self.__dict__.update(other) - else: - self.__dict__.update(other.__dict__) - def __getstate__(self): - return self.__dict__ +import functools + +class AttrDict(dict): + def __getattribute__(self, name): + try: + return dict.__getattribute__(self, name) + except AttributeError: + if name in self: + return self[name] + raise + def __setattr__(self, name, value): + dict.__setattr__(self, name, value) + try: + del self[name] + except KeyError: + pass + +def memoized(func): + @functools.wraps(func) + def _wrapper(tgt, *args): + #start = time.time() + if not hasattr(tgt, "__memo__"): + tgt.__memo__ = {} + key = "{}{}".format(func.__name__, repr(args)) + try: + return tgt.__memo__[key] + except KeyError: + pass + res = func(tgt, *args) + tgt.__memo__[key] = res + #print "{} = {} {:0.2f}s".format(key, res, time.time() - start) + return res + return _wrapper + +def print_machine_string(c, target): + ''' + Returns a machine-parsable string representation of this + Classification. + NOTE: This code is deprecated and may be removed in a future update. + ''' + print("Warning: machine string printing is deprecated and may be " + "removed in a future release.") + if not c.tags: + print("No matches") + else: + print("EXCEPTION_FAULTING_ADDRESS:{:#016x}".format(target.si_addr())) + print("EXCEPTION_CODE:{}".format(target.si_signo())) + try: + print("FAULTING_INSTRUCTION:{}".format(str(target.current_instruction()).split(":\t")[1].strip())) + except IndexError: + print("FAULTING_INSTRUCTION:?") + print("MAJOR_HASH:{}".format(c.hash.major)) + print("MINOR_HASH:{}".format(c.hash.minor)) + bt_result = ["STACK_FRAME:{}".format(i.terse()) for i in target.backtrace() if i.type() != 2] + print("STACK_DEPTH:{}".format(len(bt_result))) + for bt in bt_result: + print(bt) + print("INSTRUCTION_ADDRESS:{:#016x}".format(target.pc())) + try: + print("INVOKING_STACK_FRAME:{}".format(target.faulting_frame().position)) + except AttributeError: + print("INVOKING_STACK_FRAME:?") + print("DESCRIPTION:{}".format(c.desc)) + print("SHORT_DESCRIPTION:{}".format(c.tags[0])) + if len(c.tags) > 1: + print("OTHER_RULES:{}".format(", ".join(str(t) for t in c.tags[1:]))) + print("CLASSIFICATION:{}".format(c.category)) + print("EXPLANATION:{}".format(c.explanation)) diff --git a/src/linux/CERT_triage_tools/exploitable/tests/Makefile b/src/linux/CERT_triage_tools/exploitable/tests/Makefile index 7bc3435..e82357d 100644 --- a/src/linux/CERT_triage_tools/exploitable/tests/Makefile +++ b/src/linux/CERT_triage_tools/exploitable/tests/Makefile @@ -4,8 +4,8 @@ BIN_DIR=./bin TEST_FILE_EXT=.test -CC=gcc -CPPC=g++ +CC?=gcc -g +CPPC?=g++ -g C_SRC=$(wildcard *.c) CPP_SRC=$(wildcard *.cpp) diff --git a/src/linux/CERT_triage_tools/exploitable/tests/Makefile.arm b/src/linux/CERT_triage_tools/exploitable/tests/Makefile.arm new file mode 100644 index 0000000..29fe402 --- /dev/null +++ b/src/linux/CERT_triage_tools/exploitable/tests/Makefile.arm @@ -0,0 +1,34 @@ +## Makefile for building triage test binaries for ARM platform. +## The explicitly declared file targets below may cause Makefile warnings +## due to redefinition -- this is a known issue. They may safely be ignored. +BIN_DIR=./bin +TEST_FILE_EXT=.test + +CC?=arm-none-linux-gnueabi-gcc -g +CXX?=arm-none-linux-gnueabi-g++ -g + +C_SRC=$(wildcard *.c) +CPP_SRC=$(wildcard *.cpp) + +C_TESTS=$(C_SRC:.c=$(TEST_FILE_EXT)) +CPP_TESTS=$(CPP_SRC:.cpp=$(TEST_FILE_EXT)) +EXCLUDE=testBranchAv.test testBlockMoveAv.test testDestAv.test testDestAvNearNull.test testSourceAvNearNull.test +TESTS=$(filter-out $(EXCLUDE), $(C_TESTS) $(CPP_TESTS)) + +all: $(TESTS) + +$(TESTS): | $(BIN_DIR) + +$(BIN_DIR): + mkdir $(BIN_DIR) + +$(C_TESTS): %$(TEST_FILE_EXT): %.c + $(CC) -o $(BIN_DIR)/$@ $< + +$(CPP_TESTS): %$(TEST_FILE_EXT): %.cpp + $(CXX) -o $(BIN_DIR)/$@ $< + +.PHONY: clean +clean: + -rm $(addprefix $(BIN_DIR)/,$(TESTS)) + -rmdir $(BIN_DIR) diff --git a/src/linux/CERT_triage_tools/exploitable/tests/testBlockMoveAv.c b/src/linux/CERT_triage_tools/exploitable/tests/testBlockMoveAv.c index 7383699..f7ffba1 100644 --- a/src/linux/CERT_triage_tools/exploitable/tests/testBlockMoveAv.c +++ b/src/linux/CERT_triage_tools/exploitable/tests/testBlockMoveAv.c @@ -1,7 +1,10 @@ int main(int argc, char *argv[]) { #if defined (__x86_64__) + asm ("mov $512,%ecx"); asm ("rep movsq"); #else + asm ("mov $512,%ecx"); asm ("rep movsd"); #endif } + diff --git a/src/linux/CERT_triage_tools/exploitable/tests/testBranchAv.c b/src/linux/CERT_triage_tools/exploitable/tests/testBranchAv.c index bfcc11c..470347a 100644 --- a/src/linux/CERT_triage_tools/exploitable/tests/testBranchAv.c +++ b/src/linux/CERT_triage_tools/exploitable/tests/testBranchAv.c @@ -1,7 +1,11 @@ int main(int argc, char *argv[]) { - +#if defined(__arm__) + void (*p)() = (void*)0xFFFFFFFF; + p(); + return 0; +#else asm ("call *0xFFFFFFF"); - +#endif return 0; } diff --git a/src/linux/CERT_triage_tools/exploitable/tests/testBranchAvNearNull.c b/src/linux/CERT_triage_tools/exploitable/tests/testBranchAvNearNull.c index 2ad08b3..776eb8e 100644 --- a/src/linux/CERT_triage_tools/exploitable/tests/testBranchAvNearNull.c +++ b/src/linux/CERT_triage_tools/exploitable/tests/testBranchAvNearNull.c @@ -1,7 +1,11 @@ int main(int argc, char *argv[]) { - +#if defined(__arm__) + void (*p)() = (void*)0; + p(); + return 0; +#else asm ("call *0x00"); - +#endif return 0; } diff --git a/src/linux/CERT_triage_tools/exploitable/tests/testDeepStack.c b/src/linux/CERT_triage_tools/exploitable/tests/testDeepStack.c new file mode 100644 index 0000000..cf1d094 --- /dev/null +++ b/src/linux/CERT_triage_tools/exploitable/tests/testDeepStack.c @@ -0,0 +1,19 @@ +#include +#if defined (__x86_64__) + #define WORD_SIZE 8 +#else + #define WORD_SIZE 4 +#endif + +int i; + +int the_overflow() { + char a[WORD_SIZE]; + the_overflow(); + return 0; +} + +int main(int argc, char *argv[]) { + the_overflow(); + return 0; +} diff --git a/src/linux/CERT_triage_tools/exploitable/tests/testSourceAv.c b/src/linux/CERT_triage_tools/exploitable/tests/testSourceAv.c index cba52bd..44b8e2c 100644 --- a/src/linux/CERT_triage_tools/exploitable/tests/testSourceAv.c +++ b/src/linux/CERT_triage_tools/exploitable/tests/testSourceAv.c @@ -1,7 +1,7 @@ int main(int argc, char *argv[]) { - char *a; + char *a = "xx"; char b; int i; for (i = 0; i < 1024 * 1024; i++) { diff --git a/src/linux/CERT_triage_tools/gdb_install_stub.py b/src/linux/CERT_triage_tools/gdb_install_stub.py new file mode 100644 index 0000000..51c4d33 --- /dev/null +++ b/src/linux/CERT_triage_tools/gdb_install_stub.py @@ -0,0 +1,19 @@ +# Jonathan Foote +# jmfoote@loyola.edu + +import os, sys + +# Code below contains a workaround for a bug in GDB's Python API: +# os.path.abspath returns an incorrect string when this script is sourced +# from a path containing "~" +abspath = os.path.abspath(__file__) +pos = abspath.find("/~/") +if pos != -1: + abspath = abspath[pos+1:] +abspath = os.path.expanduser(abspath) +sys.path.append(os.path.dirname(abspath)) + + +from exploitable_lib.exploitable import ExploitableCommand + +ExploitableCommand() diff --git a/src/linux/CERT_triage_tools/setup.py b/src/linux/CERT_triage_tools/setup.py new file mode 100644 index 0000000..cadaae4 --- /dev/null +++ b/src/linux/CERT_triage_tools/setup.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +# The MIT License (MIT) +# +# Copyright (c) 2013 Jonathan Foote +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import os +from optparse import OptionParser + +def run(cmd): + import subprocess, shlex + print cmd + subprocess.check_call(shlex.split(cmd)) + +if __name__ == "__main__": + usage = "usage: %prog install|uninstall|test [path]" + desc = "[Un]installs exploitable gdb plugin to PATH, or GDB data dir if no " +\ + "path is specified." + + op = OptionParser(description=desc, usage=usage) + + (opts, args) = op.parse_args() + if len(args) < 1: + op.error("wrong number of arguments") + if len(args) == 1: + import subprocess, shlex, re + path = subprocess.check_output(shlex.split( + "gdb --batch -ex 'show data-directory'")).strip() + match = re.match("^.*\"(.*)\".*$", path) + if not match: + raise Exception("GDB data directory parse command failed, " + "make sure GDB is installed and try specifying a path " + "manually") + path = match.groups()[0] + elif len(args) == 2: + path = args[1] + path = os.path.join(path, 'python', 'gdb', 'command') + print "Target path is %s" % path + if args[0] == "install": + from shutil import copy, move + run("cp -R exploitable/ %s/exploitable_lib" % path) + run("touch %s/exploitable_lib/__init__.py" % path) + run("cp gdb_install_stub.py %s/exploitable.py" % path) + elif args[0] == "uninstall": + run("rm %s/exploitable.py" % path) + run("rm -rf %s/exploitable_lib" % path) + elif args[0] == "test": + if len(args) == 2: + op.error("y u specify path with test?") + print "testing for x86, for ARM and more args, see scripts in test/ dir" + run("test/x86.sh build run_test clean") + print "done" + else: + op.error("first arg is incorrect") diff --git a/src/linux/CERT_triage_tools/test/arm-expected.json b/src/linux/CERT_triage_tools/test/arm-expected.json new file mode 100644 index 0000000..37b4ff0 --- /dev/null +++ b/src/linux/CERT_triage_tools/test/arm-expected.json @@ -0,0 +1,197 @@ +[ + [ + "exploitable/tests/bin/testReturnAv.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 1, + 22 + ], + "explanation": "The target crashed on a return instruction, which likely indicates stack corruption.", + "short_desc": "ReturnAv", + "desc": "Access violation during return instruction" + } + ], + [ + "exploitable/tests/bin/testStackBufferOverflow.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 3, + 22 + ], + "explanation": "The target tried to access data at an address that matches the program counter. This is likely due to the execution of a branch instruction (ex: 'call') with a bad argument, but it could also be due to execution continuing past the end of a memory region or another cause. Regardless this likely indicates that the program counter contents are tainted and can be controlled by an attacker.", + "short_desc": "SegFaultOnPc", + "desc": "Segmentation fault on program counter" + } + ], + [ + "exploitable/tests/bin/testPossibleStackCorruption.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 7, + 22 + ], + "explanation": "GDB generated an error while unwinding the stack and/or the stack contained return addresses that were not mapped in the inferior's process address space and/or the stack pointer is pointing to a location outside the default stack region. These conditions likely indicate stack corruption, which is generally considered exploitable.", + "short_desc": "PossibleStackCorruption", + "desc": "Possible stack corruption" + } + ], + [ + "exploitable/tests/bin/testStackCodeExecution.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 5, + 22 + ], + "explanation": "The target stopped on an error while executing code within the process's stack region.", + "short_desc": "StackCodeExecution", + "desc": "Executing from stack" + } + ], + [ + "exploitable/tests/bin/testFloatingPointException.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 7, + 22 + ], + "explanation": "GDB generated an error while unwinding the stack and/or the stack contained return addresses that were not mapped in the inferior's process address space and/or the stack pointer is pointing to a location outside the default stack region. These conditions likely indicate stack corruption, which is generally considered exploitable.", + "short_desc": "PossibleStackCorruption", + "desc": "Possible stack corruption" + } + ], + [ + "exploitable/tests/bin/testAbortSignal.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 7, + 22 + ], + "explanation": "GDB generated an error while unwinding the stack and/or the stack contained return addresses that were not mapped in the inferior's process address space and/or the stack pointer is pointing to a location outside the default stack region. These conditions likely indicate stack corruption, which is generally considered exploitable.", + "short_desc": "PossibleStackCorruption", + "desc": "Possible stack corruption" + } + ], + [ + "exploitable/tests/bin/testHeapError.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 7, + 22 + ], + "explanation": "GDB generated an error while unwinding the stack and/or the stack contained return addresses that were not mapped in the inferior's process address space and/or the stack pointer is pointing to a location outside the default stack region. These conditions likely indicate stack corruption, which is generally considered exploitable.", + "short_desc": "PossibleStackCorruption", + "desc": "Possible stack corruption" + } + ], + [ + "exploitable/tests/bin/testBadInstruction.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 9, + 22 + ], + "explanation": "The target tried to execute a malformed or privileged instruction. This may indicate that the control flow is tainted.", + "short_desc": "BadInstruction", + "desc": "Bad instruction" + } + ], + [ + "exploitable/tests/bin/testSegFaultOnPc.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 9, + 22 + ], + "explanation": "The target tried to execute a malformed or privileged instruction. This may indicate that the control flow is tainted.", + "short_desc": "BadInstruction", + "desc": "Bad instruction" + } + ], + [ + "exploitable/tests/bin/testDeepStack.test", + { + "category": "PROBABLY_EXPLOITABLE", + "ranking": [ + 11, + 22 + ], + "explanation": "The target crashed on an access violation where the faulting instruction's mnemonic and the stack pointer seem to indicate a stack overflow.", + "short_desc": "StackOverflow", + "desc": "Stack overflow" + } + ], + [ + "exploitable/tests/bin/testSegFaultOnPcNearNull.test", + { + "category": "PROBABLY_EXPLOITABLE", + "ranking": [ + 12, + 22 + ], + "explanation": "The target tried to access data at an address that matches the program counter. This may indicate that the program counter contents are tainted, however, it may also indicate a simple NULL deference.", + "short_desc": "SegFaultOnPcNearNull", + "desc": "Segmentation fault on program counter near NULL" + } + ], + [ + "exploitable/tests/bin/testUncategorizedSignal.test", + { + "category": "EXPLOITABLE", + "ranking": [ + 7, + 22 + ], + "explanation": "GDB generated an error while unwinding the stack and/or the stack contained return addresses that were not mapped in the inferior's process address space and/or the stack pointer is pointing to a location outside the default stack region. These conditions likely indicate stack corruption, which is generally considered exploitable.", + "short_desc": "PossibleStackCorruption", + "desc": "Possible stack corruption" + } + ], + [ + "exploitable/tests/bin/testBranchAvNearNull.test", + { + "category": "PROBABLY_EXPLOITABLE", + "ranking": [ + 12, + 22 + ], + "explanation": "The target tried to access data at an address that matches the program counter. This may indicate that the program counter contents are tainted, however, it may also indicate a simple NULL deference.", + "short_desc": "SegFaultOnPcNearNull", + "desc": "Segmentation fault on program counter near NULL" + } + ], + [ + "exploitable/tests/bin/testSourceAv.test", + { + "category": "PROBABLY_NOT_EXPLOITABLE", + "ranking": [ + 16, + 22 + ], + "explanation": "The target crashed on an access violation at an address matching the source operand of the current instruction. This likely indicates a read access violation, which may mean the application crashed on a simple NULL dereference to data structure that has no immediate effect on control of the processor.", + "short_desc": "SourceAvNearNull", + "desc": "Access violation near NULL on source operand" + } + ], + [ + "exploitable/tests/bin/testBenignSignal.test", + { + "category": "PROBABLY_NOT_EXPLOITABLE", + "ranking": [ + 18, + 22 + ], + "explanation": "The target is stopped on a signal that either does not indicate an error or indicates an error that is generally not considered exploitable.", + "short_desc": "BenignSignal", + "desc": "Benign signal" + } + ] +] \ No newline at end of file diff --git a/src/linux/CERT_triage_tools/test/arm.sh b/src/linux/CERT_triage_tools/test/arm.sh new file mode 100644 index 0000000..0e202c3 --- /dev/null +++ b/src/linux/CERT_triage_tools/test/arm.sh @@ -0,0 +1,203 @@ +#!/bin/bash +# +# The MIT License (MIT) +# +# Copyright (c) 2013 Jonathan Foote +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# ***************************************************************************** +# Description: +# +# This script performs integration testing of the GDB 'exploitable' plugin +# for ARM targets. It is designed to be executed from the base directory +# of the 'exploitable' project, though another directory may be specified +# via the TRAVIS_BUILD_DIR environment variable. It isn't pretty, but it is +# designed to work both locally and with travis-ci.org VMs. +# +# WARNING: This script relies on access to a private AWS S3 bucket to download +# a custom ARM compiler. Access is granted via the conventional AWS key +# environment variables. If you plan to use or fork this script such that +# the test machine doens't have access to the bucket, you'll need to give it +# access to a bucket with functionally equivalent contents or edit the +# respective portion of this script. +# +# WARNING: ARM support for exploitable was graciously contributed by a user, +# but the results are not totally correct. Thus, this script simply ensures +# that results are consistent. Please help resolve this issue! +# +# Functional overview: +# +# This script downloads test dependencies including qemu-system +# (a bare-metal ARM emulator), the QEMU test ARM image (a linux OS), +# jimdb (a GDB client with ARM target and Python scripting support), and +# custom ARM compiler. The script then builds the exploitable test cases +# using the custom ARM compiler and modifies the initrd for the ARM image to +# include the test cases and a gdbserver. An ARM virtual machine is then +# launched with the modified ARM image via QEMU. The VM is configured to +# run a GDB server on boot and wait for connections. Once the VM is waiting +# for connections, the script runs triage.py over all of the test cases +# (making use of several commandline options), connecting to the gdbserver +# to run a classify each test case in the ARM environment. Finally, the +# result of the triage.py run is stored to a JSON file and compared to +# an expected result. +# +# Jonathan Foote +# jmfoote@loyola.edu + +set -e # exit if any simple command returns non-zero +set -x # this script is not polished/user-friendly, so we debug by default + +PROJECT_DIR=${TRAVIS_BUILD_DIR:-$(pwd)} +BUILD_DIR=$PROJECT_DIR/build + +clone() { + # get project code (travis-ci does this automagically) + git clone https://github.com/jfoote/exploitable -b integration +} + +get_deps() { + # install dependencies + sudo apt-get update + sudo apt-get install gdb gcc python gcc-multilib g++-multilib --yes # essentials for x86 + #sudo apt-get install git vim psmisc --yes # dev tools + sudo apt-get install qemu qemu-system python-pip --yes # arm testing + sudo pip install boto # accessing S3 + + mkdir -p $BUILD_DIR + pushd $BUILD_DIR + + # get arm cross-compile toolchain. the toolchain is stored in a private S3 + # bucket; we use travis-ci encryption to protect the keys to the bucket + # ref: http://about.travis-ci.org/docs/user/encryption-keys/ + set +x # don't log keys! + : ${AWS_ACCESS_KEY_ID:?"Need to set AWS_ACCESS_KEY_ID non-empty"} + : ${AWS_SECRET_ACCESS_KEY:?"Need to set AWS_SECRET_ACCESS_KEY non-empty"} + set -x + python -c 'import boto, os; boto.connect_s3(os.environ["AWS_ACCESS_KEY_ID"], os.environ["AWS_SECRET_ACCESS_KEY"]).get_bucket("exploitable").get_key("arm-toolchain-slim.tar.bz2").get_contents_to_filename("arm-toolchain.tar.bz2")' + tar -xjf arm-toolchain.tar.bz2 # dir is arm-2013.11 + export PATH=$PATH:${BUILD_DIR}/arm-2013.11/bin + cpath=$BUILD_DIR/arm-2013.11/bin/arm-none-linux-gnueabi-gcc + if [ ! -f $cpath ]; then + echo "Compiler not found at $cpath" + else + echo "Compiler found at $cpath" + file $cpath + fi + + # get ARM test VM (see http://wiki.qemu.org/Testing#QEMU_disk_images); + wget http://wiki.qemu.org/download/arm-test-0.2.tar.gz # &>> log-setup.txt # arm disk image + tar -xzf arm-test-0.2.tar.gz # dir is arm-test + + # get python-equipped, ARM-compatible GDB (see https://wiki.mozilla.org/Mobile/Fennec/Android/GDB) + wget http://people.mozilla.org/~nchen/jimdb/jimdb-arm-linux_x64.tar.bz2 + tar -xjf jimdb-arm-linux_x64.tar.bz2 # directory is jimdb-arm + popd +} + +build() { + # build ARM test cases + pushd ${PROJECT_DIR}/exploitable/tests + cpath=$BUILD_DIR/arm-2013.11/bin/arm-none-linux-gnueabi-gcc + echo "Compiler path is $cpath" + file $cpath + CC=$BUILD_DIR/arm-2013.11/bin/arm-none-linux-gnueabi-gcc make -f Makefile.arm + popd + + # patch VM initrd to run GDB server on startup + mkdir ${BUILD_DIR}/initrd + pushd ${BUILD_DIR}/initrd + gunzip -c ${BUILD_DIR}/arm-test/arm_root.img | cpio -i -d -H newc + cp ${PROJECT_DIR}/exploitable/tests/bin/* ${BUILD_DIR}/initrd/root/ # all test binaries + cp ${BUILD_DIR}/arm-2013.11/bench/gdbserver ${BUILD_DIR}/initrd/root # gdbserver + chmod +x ${BUILD_DIR}/initrd/root/* + echo " + cd /root + /root/gdbserver --multi 10.0.2.14:1234 + " >> ${BUILD_DIR}/initrd/etc/init.d/rcS + rm ${BUILD_DIR}/arm-test/arm_root.img + find . | cpio -o -H newc | gzip -9 > ${BUILD_DIR}/arm-test/arm_root.img + popd +} + +run_test() { + # start VM wait for GDB server to start + qemu-system-arm -kernel ${BUILD_DIR}/arm-test/zImage.integrator -initrd ${BUILD_DIR}/arm-test/arm_root.img -nographic -append "console=ttyAMA0" -net nic -net user,tftp=exploitable,host=10.0.2.33 -redir tcp:1234::1234 ${BUILD_DIR}/log-qemu.txt & + until grep "Listening on port" ${BUILD_DIR}/log-qemu.txt + do + echo "Waiting for GDB server to start..." + cat ${BUILD_DIR}/log-qemu.txt + sleep 1 + done + echo "GDB server started" + + # run triage; we pass a bash script that will create a per-file remote-debug GDB script to the "step-script" argument for triage.py + pushd ${PROJECT_DIR} + + cmd="#!/bin/bash + + template=\"set solib-absolute-prefix nonexistantpath + set solib-search-path ${BUILD_DIR}/arm-2013.11/arm-none-linux-gnueabi/libc/lib + file dirname/filename + target extended-remote localhost:1234 + set remote exec-file /root/filename + run + source ${PROJECT_DIR}/exploitable/exploitable.py + exploitable -p /tmp/triage.pkl\" + d=\`dirname \$1\` + f=\`basename \$1\` + sub=\${template//filename/\$f} + sub=\${sub//dirname/\$d} + echo \"\$sub\" > ${BUILD_DIR}/gdb_init" + + echo "$cmd" > ${BUILD_DIR}/pre_run.sh + chmod +x ${BUILD_DIR}/pre_run.sh + python triage.py -o ${BUILD_DIR}/result.json -vs ${BUILD_DIR}/pre_run.sh -g "${BUILD_DIR}/jimdb-arm/bin/gdb --batch -x ${BUILD_DIR}/gdb_init --args " \$sub `find exploitable/tests/bin -type f` + rm ${BUILD_DIR}/pre_run.sh ${BUILD_DIR}/gdb_init + popd + + # kill VM + killall qemu-system-arm + + # check results + python -c "import json, sys; from triage import *; sys.exit(sorted(filter(lambda x: x[1], json.load(file('${BUILD_DIR}/result.json')))) != sorted(filter(lambda x: x[1], json.load(file('${PROJECT_DIR}/test/arm-expected.json')))))" +} + +clean() { + # clean up + pushd ${PROJECT_DIR}/exploitable/tests && make -f Makefile.arm clean && popd + rm -rf ${BUILD_DIR} +} + +echo "starting" + +# Run end-to-end test, or a a list of functions if the user has specified them +if [[ -z $1 ]] ; then + get_deps + build + run_test + clean +else + for cmd in $@ + do + $cmd + done +fi + +echo "done!" +exit diff --git a/src/linux/CERT_triage_tools/test/x86-expected.json b/src/linux/CERT_triage_tools/test/x86-expected.json new file mode 100644 index 0000000..ea3ec2d --- /dev/null +++ b/src/linux/CERT_triage_tools/test/x86-expected.json @@ -0,0 +1,122 @@ +[ + [ + "exploitable/tests/bin/testReturnAv.test", + { + "short_desc": ["SegFaultOnPc"] + } + ], + [ + "exploitable/tests/bin/testSegFaultOnPc.test", + { + "short_desc": ["SegFaultOnPc"] + } + ], + [ + "exploitable/tests/bin/testBranchAv.test", + { + "short_desc": ["BranchAv"] + } + ], + [ + "exploitable/tests/bin/testStackCodeExecution.test", + { + "short_desc": ["StackCodeExecution"] + } + ], + [ + "exploitable/tests/bin/testPossibleStackCorruption.test", + { + "short_desc": ["PossibleStackCorruption"] + } + ], + [ + "exploitable/tests/bin/testStackBufferOverflow.test", + { + "short_desc": ["StackBufferOverflow"] + } + ], + [ + "exploitable/tests/bin/testDestAv.test", + { + "short_desc": ["DestAv"] + } + ], + [ + "exploitable/tests/bin/testBadInstruction.test", + { + "short_desc": ["BadInstruction"] + } + ], + [ + "exploitable/tests/bin/testSegFaultOnPcNearNull.test", + { + "short_desc": ["SegFaultOnPcNearNull"] + } + ], + [ + "exploitable/tests/bin/testBranchAvNearNull.test", + { + "short_desc": ["BranchAvNearNull"] + } + ], + [ + "exploitable/tests/bin/testBlockMoveAv.test", + { + "short_desc": ["BlockMoveAv"] + } + ], + [ + "exploitable/tests/bin/testDestAvNearNull.test", + { + "short_desc": ["DestAvNearNull"] + } + ], + [ + "exploitable/tests/bin/testSourceAvNearNull.test", + { + "short_desc": ["SourceAvNearNull"] + } + ], + [ + "exploitable/tests/bin/testFloatingPointException.test", + { + "short_desc": ["FloatingPointException"] + } + ], + [ + "exploitable/tests/bin/testBenignSignal.test", + { + "short_desc": ["BenignSignal"] + } + ], + [ + "exploitable/tests/bin/testSourceAv.test", + { + "short_desc": ["SourceAv"] + } + ], + [ + "exploitable/tests/bin/testAbortSignal.test", + { + "short_desc": ["AbortSignal"] + } + ], + [ + "exploitable/tests/bin/testHeapError.test", + { + "short_desc": ["HeapError", "AbortSignal"] + } + ], + [ + "exploitable/tests/bin/testUncategorizedSignal.test", + { + "short_desc": ["UncategorizedSignal"] + } + ], + [ + "exploitable/tests/bin/testDeepStack.test", + { + "short_desc": ["PossibleStackCorruption", "BranchAv"] + } + ] +] diff --git a/src/linux/CERT_triage_tools/test/x86.sh b/src/linux/CERT_triage_tools/test/x86.sh new file mode 100644 index 0000000..1df81ea --- /dev/null +++ b/src/linux/CERT_triage_tools/test/x86.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# +# The MIT License (MIT) +# +# Copyright (c) 2013 Jonathan Foote +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# ***************************************************************************** +# Description: +# +# This script performs integration testing of the GDB 'exploitable' plugin +# for x86 targets. It is designed to be executed from the base directory +# of the 'exploitable' project, though another directory may be specified +# via the TRAVIS_BUILD_DIR environment variable. It isn't pretty, but it is +# designed to work both locally and with travis-ci.org VMs. +# +# Functional overview: +# +# This script downloads test dependencies and builds the test +# cases supplied in the exploitable repo. The script then runs triage.py +# over all of the test cases in the and stores the result to a JSON file. +# Finally, the result is compared to an expected result. +# +# Jonathan Foote +# jmfoote@loyola.edu + +set -e # exit if any simple command returns non-zero +set -x # this script is not polished/user-friendly, so we debug by default + +PROJECT_DIR=${TRAVIS_BUILD_DIR:-$(pwd)} +BUILD_DIR=$PROJECT_DIR/build + +clone() { + # get project code (travis-ci does this automagically) + git clone https://github.com/jfoote/exploitable +} + +get_deps() { + + # install dependencies + sudo apt-get update + sudo apt-get install gdb gcc python --yes # essentials for x86 + sudo apt-get install execstack --yes # x86 testing +} + +build() { + mkdir -p $BUILD_DIR + + # build test cases + pushd ${PROJECT_DIR}/exploitable/tests + make + popd +} + +run_test() { + pushd ${PROJECT_DIR} + export PYTHONPATH=$PYTHONPATH:`pwd`/exploitable + failed="" + set +e # disable fail-on-nonzero for tests + set +x + echo "verbose script debugging disabled" + + # check each test cases against expected + for f in `find exploitable/tests/bin -type f` + do + cmd="gdb --batch -ex \"source exploitable/exploitable.py\" -ex run -ex \"exploitable -v -p ${BUILD_DIR}/triage.pkl\" --args $f " + eval $cmd &> /dev/null + # command below compares only short_desc fields + result=$(python -c "import sys, pickle, json; expected = json.load(open('${PROJECT_DIR}/test/x86-expected.json')); expected = [i for i in expected if i[0] == '$f'][0][1]['short_desc']; result = pickle.load(open('${BUILD_DIR}/triage.pkl'))['short_desc']; print('result=%s expected=%s' % (result, expected)); sys.exit(not result in expected)") + if [[ "$?" -ne "0" ]] ; then + failed="$failed$f: + $result + cmd=$cmd + " + fi + echo "`basename $f`: $result" + done + popd + if [[ $failed != "" ]] ; then + echo "" + echo "TESTS FAILED:" + echo "$failed" + echo "Tests failed, exiting." + exit -1 + fi + set -x + set -e # re-enable fail-on-nonzero + echo "verbose script debugging enabled" +} + +clean() { + # clean up + rm -rf ${BUILD_DIR} + pushd ${PROJECT_DIR}/exploitable/tests && make clean && popd +} + +echo "starting" + +# Run end-to-end test, or a a list of functions if the user has specified them +if [[ -z $1 ]] ; then + get_deps + build + run_test + clean +else + for cmd in $@ + do + $cmd + done +fi + +echo "done!" +exit 0 diff --git a/src/linux/CERT_triage_tools/triage.py b/src/linux/CERT_triage_tools/triage.py index 6a31985..95e8f52 100644 --- a/src/linux/CERT_triage_tools/triage.py +++ b/src/linux/CERT_triage_tools/triage.py @@ -19,24 +19,17 @@ ### notice, this list of conditions and the following disclaimer in the ### documentation and/or other materials provided with the distribution. ### -### 3. All advertising materials for third-party software mentioning -### features or use of this software must display the following -### disclaimer: -### -### "Neither Carnegie Mellon University nor its Software Engineering -### Institute have reviewed or endorsed this software" -### -### 4. The names "Department of Homeland Security," "Carnegie Mellon +### 3. The names "Department of Homeland Security," "Carnegie Mellon ### University," "CERT" and/or "Software Engineering Institute" shall ### not be used to endorse or promote products derived from this software ### without prior written permission. For written permission, please ### contact permission@sei.cmu.edu. ### -### 5. Products derived from this software may not be called "CERT" nor +### 4. Products derived from this software may not be called "CERT" nor ### may "CERT" appear in their names without prior written permission of ### permission@sei.cmu.edu. ### -### 6. Redistributions of any form whatsoever must retain the following +### 5. Redistributions of any form whatsoever must retain the following ### acknowledgment: ### ### "This product includes software developed by CERT with funding @@ -53,6 +46,10 @@ ### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND ### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. ### END LICENSE ### + +# Jonathan Foote +# jmfoote@loyola.edu + ''' A simple batch wrapper script for the CERT 'exploitable' GDB extension. ''' @@ -61,7 +58,7 @@ from string import Template import subprocess import shlex -import cPickle as pkl +import pickle as pkl import os import warnings import sys @@ -87,8 +84,7 @@ def __str__(self): result = "" failed = [] last = None - get_class = lambda state: state[1] - triaged = sorted(filter(get_class, self), key=get_class) + triaged = sorted(self, key=lambda tstate: tstate[1]) # sort by classification for sub, classification in triaged: if not classification or len(classification.tags) == 0: failed.append(sub) @@ -103,8 +99,7 @@ def __str__(self): result += "\n" last = classification - failed = filter(lambda state: not state[1] or len(state[1].tags) == 0, - self) + failed = [state for state in self if not state[1] or len(state[1].tags) == 0] if len(failed) > 0: result += "\nFailed to triage:\n"+ "\n".join([s[0] for s in failed]) @@ -119,6 +114,7 @@ class Triager(object): "-ex run -ex \"exploitable -p %s\" --args" tmp_file = "/tmp/triage.pkl" verbose = False + step_script = None def __init__(self): pass @@ -143,11 +139,15 @@ def triage(self, inferior_cmd, inferior_subs, verbose=False): pos = pos + 1 self._cleanup_tmp_file() inferior_cmd = inferior_template.safe_substitute(sub=sub) - call = self.gdb_cmd % self.tmp_file + " " + inferior_cmd + if self.step_script: + call = self.step_script + " " + inferior_cmd + self.vprint("(%d/%d) calling: %s" % (pos, len(inferior_subs), call)) + subprocess.call(shlex.split(call)) + call = self.gdb_cmd.replace("%s", self.tmp_file) + " " + inferior_cmd self.vprint("(%d/%d) calling: %s" % (pos, len(inferior_subs), call)) - subprocess.call(shlex.split(call), stdout=file(os.devnull, 'w')) + subprocess.call(shlex.split(call), stdout=open(os.devnull, 'w')) try: - classification = pkl.load(file(self.tmp_file, "rb")) + classification = pkl.load(open(self.tmp_file, "rb")) except Exception as e: classification = None warnings.warn("triage failed (%s), call=%s" % (e, call)) @@ -158,9 +158,7 @@ def triage(self, inferior_cmd, inferior_subs, verbose=False): def vprint(self, msg): if self.verbose: - print msg - -version = "1.0" + print(msg) if __name__ == "__main__": usage = "usage: %prog [options] CMD [arg1, arg2, ..]" @@ -173,16 +171,24 @@ def vprint(self, msg): op = OptionParser(description=desc, usage=usage) op.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, - help="print verbose messages to stdout and includes " + help="Print verbose messages to stdout and includes " "all matching tags in summary") op.add_option("-g", "--gdb-shell-cmd", action="store", dest="gdb_shell_cmd", default=False, - help="overrides gdb shell command. Default is '%s'" + help="Overrides gdb shell command. Default is '%s'" % Triager.gdb_cmd) op.add_option("-t", "--tmp-filename", action="store", dest="tmp_filename", default=False, - help="overrides temporary pkl filename. Default is '%s'" + help="Overrides temporary pkl filename. Default is '%s'" % Triager.tmp_file) + op.add_option("-s", "--step-script", action="store", + dest="step_script", default=False, + help="run specified bash script between each invocation " + "of GDB. The app invocation string is passed as an " + "argument to the bash script.") + op.add_option("-o", "--output", action="store", + dest="output", default=False, + help="output result as JSON to supplied filepath.") (opts, args) = op.parse_args() if len(args) < 1: op.error("wrong number of arguments") @@ -196,8 +202,16 @@ def vprint(self, msg): Triager.gdb_cmd = opts.gdb_shell_cmd if opts.tmp_filename: Triager.tmp_file = opts.tmp_filename + if opts.step_script: + Triager.step_script = opts.step_script results = Triager().triage(cmd, args, opts.verbose) - print "\n\n\n\n", results + if opts.output: + import json + # sort by classification before dumping + triaged = sorted(results , key=lambda tstate: tstate[1]) + json.dump(triaged, open(opts.output, "wt"), indent=4) + + print("\n\n\n\n", results) From 60276a9768f107381774b6c2149f3232d88d5b09 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 29 Apr 2015 11:24:19 -0400 Subject: [PATCH 0600/1169] Fix BFF-794 and BFF-795 --- .../drillresults/testcasebundle_base.py | 22 +++++++++---------- .../drillresults/testcasebundle_linux.py | 10 ++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index a724218..3a7af64 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -214,17 +214,17 @@ def format_addr(self, faultaddr): # pad faultaddr if it's shorter than 64 bits logger.debug('addr < 64 bits: pad') return faultaddr.zfill(16) - - # if faultaddr is longer than 32 bits, truncate it - if len(faultaddr) > 8: - logger.debug('addr > 32 bits: truncate') - return faultaddr[-8:] - - # if faultaddr is shorter than 32 bits, pad it - if len(faultaddr) < 8: - # pad faultaddr - logger.debug('addr < 32 bits: pad') - return faultaddr.zfill(8) + else: + # if faultaddr is longer than 32 bits, truncate it + if len(faultaddr) > 8: + logger.debug('addr > 32 bits: truncate') + return faultaddr[-8:] + + # if faultaddr is shorter than 32 bits, pad it + if len(faultaddr) < 8: + # pad faultaddr + logger.debug('addr < 32 bits: pad') + return faultaddr.zfill(8) return faultaddr diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index cda3afc..3f81561 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -44,7 +44,7 @@ def _check_64bit(self): start_addr = m.group(1) if len(start_addr) > 10: self._64bit_debugger = True - logger.debug() + logger.debug('Using a 64-bit debugger') def _64bit_addr_fixup(self, faultaddr, instraddr): return faultaddr, instraddr @@ -83,16 +83,16 @@ def get_instr_addr(self): ''' instraddr = None for line in self.reporttext.splitlines(): - #print 'checking: %s' % line + # print 'checking: %s' % line n = re.match(RE_CURRENT_INSTR, line) if n: instraddr = n.group(1) - #print 'Found instruction address: %s' % instraddr + # print 'Found instruction address: %s' % instraddr if not instraddr: for line in self.reporttext.splitlines(): - #No disassembly. Resort to frame 0 address + # No disassembly. Resort to frame 0 address n = re.match(RE_FRAME_0, line) if n: instraddr = n.group(1) - #print 'Found instruction address: %s' % instraddr + # print 'Found instruction address: %s' % instraddr return self.format_addr(instraddr) From df37d82aa90aaf36464b9e1a3cb102b59c2d7e01 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 29 Apr 2015 14:43:26 -0400 Subject: [PATCH 0601/1169] Fix for BFF-797 --- src/linux/welcome.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/linux/welcome.sh b/src/linux/welcome.sh index 9f8ca1a..30f6858 100755 --- a/src/linux/welcome.sh +++ b/src/linux/welcome.sh @@ -13,17 +13,16 @@ echo -e "***** Welcome to the CERT BFF! *****\n\n" echo "Working directory: $PWD" if [[ -f ~/fuzzing/bff.log ]]; then - currentcfg=~/bff.cfg + currentcfg=~/bff.yaml echo -e "\n--- Resuming fuzzing campaign ... ---" echo -e "--- Run ./reset_bff.sh to start a new fuzzing campaign ---\n" else - currentcfg=conf.d/bff.cfg + currentcfg=conf.d/bff.yaml echo "Using configuration file: $currentcfg" fi -echo "Target commandline: " `egrep -m1 '^cmdline' $currentcfg | sed 's/^cmdline=//'` -echo "Output directory: " `egrep -m1 '^output_dir' $currentcfg | sed 's/output_dir=//'` -echo "Please see the README file for details on use. +echo "Target commandline: " `egrep -m1 '^ cmdline' $currentcfg | sed 's/^ cmdline://'` +echo "Output directory: " `egrep -m1 '^ output_dir' $currentcfg | sed 's/^ output_dir://'` " From d19250e99a2b24f3dd33369565ea7d8b5ea049cd Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 29 Apr 2015 14:44:53 -0400 Subject: [PATCH 0602/1169] Fix for BFF-799 (bff.cfg -> bff.yaml) --- src/linux/quickstats.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/linux/quickstats.sh b/src/linux/quickstats.sh index 9a90f3a..5b6c642 100755 --- a/src/linux/quickstats.sh +++ b/src/linux/quickstats.sh @@ -3,14 +3,14 @@ platform=`uname -a` echo "Exploitability Summary for campaign thus far:" if [[ "$platform" =~ "Darwin" ]]; then - exploitable=`find ~/results/crashers -name '*.cw' | xargs grep is_exploitable=y | awk -Fcrashers/ '{print $2}' | awk -F/ '{print $1}' | sort | uniq | wc -l` - total=`find ~/results/crashers -type d | wc -l` + exploitable=`find -L ~/results -name '*.cw' | xargs grep is_exploitable=y | awk -Fcrashers/ '{print $2}' | awk -F/ '{print $1}' | sort | uniq | wc -l` + total=`find -L ~/results -name '*.cw' | wc -l` let total-=1 not_exploitable=$(expr $total - $exploitable) echo $exploitable Exploitable echo $not_exploitable Unknown echo $total Total else - find ~/results/crashers -name '*.gdb' | xargs grep -h "Exploitability Classification" | cut -d" " -f3 | sort | uniq -c - echo `find ~/results/crashers -type d | tail -n +2 | wc -l` TOTAL + find -L ~/results -name '*.gdb' | xargs grep -h "Exploitability Classification" | cut -d" " -f3 | sort | uniq -c + echo `find -L ~/results -name '*.gdb' | wc -l` TOTAL fi \ No newline at end of file From 9d95c36c9c106dc477a34da970dc39f5c70595a9 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 29 Apr 2015 14:45:56 -0400 Subject: [PATCH 0603/1169] Clean up symlinks BFF-792 --- src/certfuzz/file_handlers/tmp_reaper.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/file_handlers/tmp_reaper.py b/src/certfuzz/file_handlers/tmp_reaper.py index 7b33a40..6f5599f 100644 --- a/src/certfuzz/file_handlers/tmp_reaper.py +++ b/src/certfuzz/file_handlers/tmp_reaper.py @@ -42,18 +42,23 @@ def clean_tmp_windows(self, extras=[]): def clean_tmp_unix(self, extras=[]): ''' - Starts at the top level of tmpdir and deletes files or directories - owned by the same uid as the current process. + Starts at the top level of tmpdir and deletes files, directories + and symlinks owned by the same uid as the current process. ''' my_uid = os.getuid() for basename in os.listdir(self.tmp_dir): path = os.path.join(self.tmp_dir, basename) try: - path_uid = os.stat(path).st_uid + if os.path.islink(path): + path_uid = os.lstat(path).st_uid + else: + path_uid = os.stat(path).st_uid if my_uid == path_uid: if os.path.isfile(path): os.remove(path) + elif os.path.islink(path): + os.unlink(path) elif os.path.isdir(path): shutil.rmtree(path) except (IOError, OSError): From be18fe949cb585fdc08009d0158103b8ede79d23 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 29 Apr 2015 14:51:35 -0400 Subject: [PATCH 0604/1169] Minor INSTALL guide tweak --- src/linux/INSTALL | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/linux/INSTALL b/src/linux/INSTALL index e7da4be..ce14ddf 100644 --- a/src/linux/INSTALL +++ b/src/linux/INSTALL @@ -6,9 +6,10 @@ dependencies are met. ===== Dependencies ===== For basic fuzzing functionality, the following packages are required: -Python (2.6 or 2.7) +Python 2.7 Python Numpy Python Scipy +Python Yaml gdb 7.1 or later zzuf (patched by CERT) @@ -82,10 +83,10 @@ To install BFF on a Fedora 16 32-bit system, for example, the following steps can be performed: 1) Install dependencies present in the package system: -yum install numpy scipy valgrind svn automake libtool gcc-c++ ncurses-devel +yum install numpy scipy python-yaml valgrind svn automake libtool gcc-c++ ncurses-devel 2) Install libcaca, which is a dependency for building zzuf: -svn co svn://svn.zoy.org/caca/libcaca/trunk libcaca +svn co https://github.com/cacalabs/libcaca/trunk libcaca cd libcaca ./bootstrap ./configure From 230619896e8262357ca76af1f5fb0487f3c48eb9 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 29 Apr 2015 15:50:49 -0400 Subject: [PATCH 0605/1169] new zzuf version BFF-796 --- src/linux/zzuf-patched.zip | Bin 193974 -> 185892 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/linux/zzuf-patched.zip b/src/linux/zzuf-patched.zip index f3f0602450dfa43000d845af35a135d4b3e0b48d..52f7ff62c0b0ca11b56967321b02fce0ae6c1327 100644 GIT binary patch delta 132779 zcmaI7V{m5Q+Vy+Kwr$(CZQFJ_PVU(1IO*6nI=0cVZJQmv|Gm#S_13fNIeS&DS!;c` zYJC{j9COY!>eqS!S6mIpttw%uVWZnc=Wm(?wA$3@?rx-&YvIb*bxDR%DT^7w_>x+4;awvYoh^reK`v- z<%MPg#`Nk|Y1>DZ3-eL3H5%*;v{yB2YWXOH_q)u(ONL%twOwZdz>_#gdhK+`8*)!9GV#y8a1ds`IwlrIF`#+#VKRR7Wx{T~&f0iK>Yl1>_5 zrp2HDfF&g0AASBq(|-wU^lyO~&220#|E&}$t%yeBKQjM^QgP}IN=3rRUz=aiD0L{X z8;(F4O^2(JD5iAET&pP@CR{OSdQ69)OI0_Ur`{cUCeWJW&4B#NOSXrp2Y5N?pe5V0 zwgSTxLJj$ypv~{v$l-aL(Cj{RJ0$qAy_q_Si+6x?h4^+>QE{86ODX~+8 za7=p_-7(3f(H>5$*a`f(iN>)xRngS7%@OU zjpuwEW&h-sd46a(11cJ4$<*>~w%w5Cp+(7Ahe>5*v1WN<4{o?*95i|1&R@@4UUU+H zjdaSMu5*S)CRAK9bV`fr8Xd1%=Ue0@u`v8*k(w>E&+x*YKWoIhtRBb^EBWt3r8MdI zqLYaqg(J41+u}iXAl?Y41tNqx$Yz00Ww1Fva@O7+_uNkr<{zTQ$4X<&ekYl5IjcIgK<+ohy~z^ik(;S zL~pSrvkNS|zuGR}zT%A2wWB-PzC`kTbGl8V=fAd9AT~DRSf-U4+KurF{5pWL)&-zn z9$-6Wf~#!{lRY{s3Pbn=PfH(=!qa-Bo#s6dIYMWuv3~LJul-5M6zOO_!({0&EKlkV zlcd?mJIRCmkbFNp?y{NDdFq085gc?l~-LkTS;G7jGFOLt;_h zV(0dC7RS^}zhgA?_M18&7Z26Hkswqs$N8s1p}YHba^K*6;oZ%ySEhmg*VD+n{->v* z{Ad5WxtjeyzUKN*)93ut^#7l)DXfP91pYB84k!Qs>wl|qa&~uPH2ZJX_CrI?aYGt; z>j6E?i|mbz2C}gMpKL@r9AT^hd7LGf7+Kd`#XjTm*d2>OB?RCW**>Ii7SBp01N^a& zBqOsIUaeJU=>K={+66EQxcGrGh(oVjfI@Q^Hr^r;8S{jTz{Y=ul#?#Wh#LI;74~3w z^YPvL&2!WJ4M6+ZUxRojf&IIg^gTsgQi{aDp|C8fIT5d$dFuGjDsk`<3qx2x;wDNc zuNa)1OcWz@C~c5|BsHy4P%~BmI1ofDG@4i{S%x&$*f4nPK|S0wm$cY)Ey$@OP??$; zTSiWD22M2bkRvp@V_mqE3@064w1AY|FiU$OIKG=@_BI9jeuqoq^-?15cXlNz!hz|G zj0w4zI;PSd9WGr<@PiuiX63_rapfNyeg&ryvJBXFc&_!()jM9aTv5>!BcOZd+DS~Jyxw$PtFCO@oZ8k5}1`LyzHEmkV0JLibnj>SXt%^!QG3)}VsFzj) z?>HaSNHwb|DlY&JiKA1fz4_fI=&kP7d@2F zuG<=?C8QUWkqB4*B|IT9ihapsa)RUhw}x5s#%G@t&%>qcN!QOrFv5`+q>w%P4-0Oe z--M5L=$_GD3K?o^4@t9*4BO<*9Ig(2&wmn%R15|%n)m}Byh-7A_JIf^VZ#g!Bh6zm zFDQ_Mzb;1S+&OU&@i^{H35@$-y6T!(D?TJQjwgp~!R+^Y^p$Q1aVt)mgy5oT#|(Qx ztmY#8nfsRnu_>_y4`d0Ey+znodQJ?<=+awp9(9^v8YY;lH2j3R>fRY(#2n37hU+@t z9a}uf4Pm+oGRm9`4S>EngI%j3WbzX2Rmre&71Zh(_~*3p=I5A|1x1{vjv@spelTkW zUwE6)tL2NW2_m$y_g?|GY|xwi700L~Ml~JPGYi`|>-da4e7YQWtx$C7zMBjy`@Ues zbzw&7d^OK|hvKbmAW-bRlSFzPj z_OH#x@}e|5C=LKnYxVy#DSdoQ-2eNeycD?d*l11qen5|OBamygpLASqv!lB9aiDmr z=#3Ye_dqiO8fJkqETdOY&;W~{a{VLyqx|FiQ~Z;1Z~)|hB-EqdJTIf?*(0b?Z$neY z0QlLZVJ!Qf4NOLxu?$UmTrm4C6a25Kx5nJCZ;4WwzaU&vScMd|M;7QKYuBu^WA{_a z01D6^2XkVm`bezLLf_NNom)@epMSn}Z)!i$_}n6Zm!qo(fmz)OTYJ6mLkX#oRvWN? z{g8|8Q-)Sxn#1b?26*JmggV{OdIp}^j)%o^8#{^dh7Mxe=fFa=qVOz`2-1c_$?=>= zZrFCyl?jGLWlfgW$OSvxnv%UPnZnJ|hA$2&{^$of^JN4QpKU5-p z^h#>6M_{b{bK6BSOy8HVm^5W_PDXu4e@!U?y&(;n?@9+h>~h0?THn(~b~$gJ*w4g} z3H>(2nIggG+wVg9Al_#_`mj7usEPyB{oO<&oyiW#SwQN3d57wbtA?o~j5MJo|Lr=HuNE66AHmR}cF$R3+$!_{SqBp?d+V4%`1Tt;1~5k*coDWsHB}(QWOxTnKS}<1Ff*F#=F!sz zbie+vo>yuTBo-$$yqkp#dOqpReVnle5(f&$tAR*98e(7*%`ffO+;Kw)apnCrObOfn zDe=B#%@cH0?tp*a**sdcc}eS8jq`x(L9<0{7KN+RnDEoHu$ zCA^}TXB1br#<#r1nn&d~M55bmX7>iS|FynXaS|VB_d?6DJGC-PmTWkRJdjlRGUw$r zj79|xRvwyuQ^ef?yLDPxDFd4W91y9)MO45lq{9xrP2wbFlA%ip%BWOFx$E3dg)fP4 zM6L*u8POJJ4IO3p*r!~{ZBCbX4R}9m{M>)RGG42mXaqcEpGGg=>q1HKwC(6#__ckl z#NBM%{u&LitJ%s0yN?7d(zCOxQVaFS+ zClKI?W{fqmqJYyA*H#h($6(k&A>#rtz+J*P4TUj~yM1cmNg>45AxQpi1!_-{B+1r> z9x?U4i#5W5PkP`pW! zN2~r>v8?M*NzwoVoDttksSt*-$|e1+gWufO*Kglny@B9d){glYsX=LibfoY)bKWuC zdZ5#BwRy@pz&>n{W!%kCZ!7Y|lej~7$ojR;&607L^IYK@<; z9W}9B&u1$2X2=M+B|ocge{)9*57*-w8V8Z^C7wR+{=-eIqPyB1N*a(nXtC%3P%e--zKmS8HxsRmRE%s;tocBkrS1k6`-_kQ22Jp1;@5Yt0FgqtP!tbIvX|;4C;p>I+OfS1+D#V zH^kf&{Wct>$0NUc_37h&y#&J>w!zhhFiAX`ARbKOMYL`d<%H<6rqsBVquB(kw~VOZ z>K{x1!TD{mdh5T}i4j**Q>)i1%h@!iE+EkZg`_mWzq-wnSAA?`6gAt{eAaC5z@EXc zS&vH#gm8PkGVW391>qR`=o?CL2gp?(q8`{~o^U)ot5i{#=oFAs-Wp{y&x^v?nfYQz z?a5wa-4I#UjC5@`eC#$&I1lX57Ol-m>x{^NJIJA(FFT%P8567=phZ{Pb)F?d_iKFO zw_3eZ>N2N;N4?X&#BM?L(_fIl9e0K^x~pPP0u#E#wUBsG3s|KF=`qE>Q}q66rymYR zMCKFFG-)=_SBSx{8Lh`(tT-~;6y^A0)3Ut_zttzGZJLV}jG@J#m2Im6(-X)#Sr}b` zszZB4;48O>gZ%f`lzj;1e@4s9N z5oyqZIGR~ozU;nMujq4(fzO=er6w!@4`PbCW6*G&*Q=M3jMDn(mpcrc+gC1yI$nBi zF1lV6g&uVsdy&ZeU^UT$B!c2?ixb5%!KMS{Y%ihX26{bK)adH%DxN4nTH|_Ib~21H z3n47kdCRx%%nfHjN5tH*DY`|GOxFCs`$6ZLH!ul*bqp|^+Uw&iPlKr11_OBxkfqH@N1cA{R?O|NRi%V z@dTt1f*hhO6YJ@yi0Lv>#nQHcKaZ5+;L5sl1mNKaB)5VxB)V34xS8`}(jjkk>Rytq z#Tjh|M3Q*pM`+B$VycLF7~)4tomOTmUTz>!@a(5X%Qpv%;R&*)QqlECRKqIf`E!xK zEz0C^!7c?CdqZ&L^b>iU8sAdNdj`sT!gOm9PDrD(a08x6NB5jk2D=N>Ji0w*Wh%#Qk{^x3zT=erVJEZrVjet?4owhU8KjQ< zfm!g&l6^1)htfP$TPfHY^SB$L`ZG{#vok2Iv6(FFk>$RhlwV&JD@)#@0A@4-|~Kx^r8gvfmF|H}lhRSM%SGRXDcSuC)$7%R91-B(Cwf$M7-E=nL?cRV4B=zAFSHh|M5j=O_ zY7E{Djg7uGl$8}ai2|Vu1Hv2o^|J;RUzHyMVGQ#ai*|b+ukIWn3%~nw(_q%q^hZGf zNAp;J>nThvS6^Q19!epNi^&X2xdYVKtSfqV5-z&TT3Q2hi)57wt$NO7cJ7l0(u;0! zNNOs^k$p|qf?>=xFv-@3;a&1YMj8*J+jC+b?j)J8Bt?Lyz_iYgXJ(YZ>f~`S^8mmD zgWyISWBLUeH9=Iku`yzb=NKm!QK~sLjLLlfBP%L-;Q^c>cZ$d#9@LanZb_U6t2XPm zNN6qfCXQCt2LLW2FuXmK%S-j0PVFKmG>hAHMyGPT4}42Z=>Cl+Ae@b{nXF{0M!*E^;}v zFl#jO?*uiA#K$>VJ*jSb;wiZ6Q4S)|T!c%W0MP~q;stJmF2}3)=q5p7=v4LT+6%&K zLq&Fn?{zsM*zH4a|0MejLh>Q9dU^!=?9=b+&*w@No;jRGQE$!S;$N9qBcMeG!>OM8 zpt#dX%{8p#9}F1<$GBA{LUe#>%HF2BXtlo_PZPDTv;u?REX%~r7lPAqdudY*_j+{1 zDi?PTY=GfUOB z%YDWzS2wmRR9|Y8Z#QgG&<4bDSc=-|jY@_ARR;Mn3-*9lpu4#98vFJwq&I= z#2kWj<_`{m>Gzol_3cP_jdNyp3mG?rU|jsTk7=P_&l}=L`c;SO!5V5$UNlU#@;}YV|5A^)LGskPalvBV>QC>Rn9gv5P=#X0_ZbR!o3xBqn;> zt5Qjqo(q8-!+=Fhykh=aD=NsO!)3Hz(7ZCH9RuvL(^T3Rq z#Uqv)fb^W;qsA^rr=0wX7{=LPaM7Z#3J7o#?>#{htwbWp`?Y@M+a2;bNaF_sgwZqQ z`8BnOCvWmon4VCjCi%^NWTfjc^%RC*TuCE!L0TvN0sIGs@Sh6ikW@+9s-vQcdD;)K z1izUSRWe~g&OOS?CSMm`63%%+I)oW~sw%&Xpp#E1J}WN(YbKd*7GYYs(> zaKvbtjQ}zHx%pAGY48=%o`!=ZTkQ#R;-vEp*iDhNlLJ{#}#Y^kS~Pk z()g3`aJ9jQJUiYW{9Nq0xVd?M{3z>lZ~6G2RFhNDVhUy8)QT;NR-I6R+b--jnc=q+ z50vWn6j^Q62jHgvEPLoMo=ntgRm)NKPhfhQP0j*Fosp>5!z(I*8a8iVk*U>7_Re$R zm$L+ydBV(IW`J_)CMMBEZ8Td1p2h}|o61f-s0d{gy5T30>U(rJS^wm8a>@sXCYA5V!B)<>zuKfp0?&ZPhXT*m zZ>Qn&4tpd4AdhPu&vkR#MEjSrIuGf58|Xe%2CaQ#+7)fpW9yb>%wvhM8?`OvN?--* zEbHD%TZf2fwb+m%dTOR~x!&M9oEOULVe>Y2&&}qbbTEJ8Z^eUg2fa zp^3iFmt=|Lt}rJ%P1!{*C^DZ$Xw(mv$&Fk+|HhB<=&u*EF!2pRuM!0*J4CneHZ^pb z-7y6Z(Au2rTs$K>r#>9YS&<%6&B6%68n!kbODejdF%B9R!xO_oAHf-BzHq(VnlYx? z_Kg!ZH~PGiRX}H$Bg`8uNAq(LyudhUs>z!1JdXT3TBa>eFAVM(0$AHx^4l>lYKQc# zj88~{&t*p0q#vX_AGG&$NXq4Raaek+p$P;vFz3elWU&lzex*9;5yfY(pqHp3KMrr8 z4BH$!M@v|iHhVhEikZ17UTzIlR~nNnuRe3a<-kD)9EUTYgxhQmkf>9yIP1&yWcig7 z_Yxh9R@Gcgv*ZW#1wQ?bnm?D7pp6mDVlLQ<;w?g)Xd%$V7-hH1H1HtRVeqR+G(~*^ zQo$M28ZygKPs~N!|5*#GDNnAaC8Z^OLbEwrg0FNGLAqp3^I|c4%pT7V=E8q?w`W~9 z4H4M6r5b{BnyYHVf(7;Bj-V7epoZI}<3wQQ#|%>Kvo>kzvy$kc978rOY{Sg7?{HXf6vkX=ucHef5LZ8Bs=L8V7F)_R zo$djQ(525ZIg?vx)p~-h{lJ-;fj%3etZ`H$w4MTB-O0FEPR$T6{Je1K;7OeZHqhkp zT#%>q#VR$cDgPCbYu`nTJX!FbqN0`?h8sVfTrc5pLqTI``B2K%!6SktGeKi;R+(It zp;@1W75u5YA~W~HMeb(RbDe_bhZd#WFikdJntXkaC*xwC3&hL?7;7l68eWPb_YJBEeiJ57nbM(&CHRe~DL6uS3xgFE$ z3gZeB8F0ekorCKt{U=wi5AeL@9V1(*I|7)-_d5AHsT827tcEhb(BT}GbB|(9cWLXF zQ;1)}m^#KlwonV*CVvUK6IsLYETfN}le1&ks5JTVrjHe{r+PUtF=3GjR5>f%2g0K? z3LPROsNo`(lWgyBm+VQ|Qs)$JKBG(d>QZ4A73j&&_QNlJmx4ACyAT+$9TDwEdxhJ1 zy%9u|+v_5yy*$k$_ziXuJiGAr<*M8?ao6S#uih(4^#*9Y0}lFY@LWv_Cfm9nWM8Wk zhb1~*e{#Y1u?gZ^Af2lMg*$3$P}O>qjWX>5$`OjVRzMhnPqYHHNcXFc)le%?yH;2n zM;HW=VI&LQ`@Vu-=KA<9)vmvGlGyqJ25i*{Nu8AqKrz4|c>0}b=7aKpc6MO7zgt4E zL8~Q&J2M`H`7%{JGD9E{qA`qZ`^^-z6&Oi4^XsMjBM~QbnlnIv82hK8*87+tSt2gE z+6uG_{T^S{!Jx0zQQWte33ZI8x6sbT7l4?@!^L1!XVw=NH;_rNs{vd0FvR^w9lar8 zyxD|VG@AkvEqGf7lEa3q)FLoGW=w>@Vbnbh=IW7Ptzm72$=RSBoES*sekeh6uxh|z z|6;U!^>Bt`2I4$0s$mR})o83Ly#|>O)J**j@;_4;mEI0f;6EwMp#lJa^S?!-4&Ihd zu6F-Dhndo|bKQ_m`l?e)S+o021tCl>oK_$Mb7=r{@mfqg+;cvIF%z&2Res=INndNA&%t_jA3;BPG^EgzY*N@LAWpv&M+$-x*i){pC4`T@o7 zr;Xt!MV;n3sEX`FdGM~UFN)N&_xS_A<7vN;0p!i*8EG65^k!j0NQc;L9Hg!;GfLh5+jQ-h=CI41 zY{$E&eIE;%eb)ETw}7O_*a>&*Cz}6_-v(vC$vL36{0y4B5;+51%$z{_eYLme+Fs32 z1kAk2e?(e~&4A}_@-kmljQm7;c}5K5#(KH^YPIYMgBY#lDVq*o5n8J~8^W}rSa7p? zR&X#3mW=shpU&33VCO-_pg{B}l|$#hmv0sbMffqU?~n>0ps1Jj1U*Zt0cptP>Q2C~<{xjzqMc(N z6BRx7-_ZVqEc)(rq_eiMcnQSB9WGRdi#VTq_T4wN5}Z)U4o!Vk`gH`!kuRo6%&er& zHO5D~r+ynJ79UvOnL2q1z3Pfh0>P_GB=s5m@RQhxKinMo(g+*;|CY(Jr~;)B23u+4 z-##Ri*DQuIap*_q9A4>$U4S&lwGa$|9$!5s8z6;`)lM%XXjSwP9a9 zcjte!hZH9QB7UDu(zqCzd!h1$cL|u%-8xy^IQd30^sz&Mynw(}Ps837vy0g2)6|qJVRru1Xd#rr#Tf#Dk)q)uF7!A# z7xkT#hhi~QWF?0r^7Ch>6UzoIt*REUmJ1BTd#Y0!JXvw9p!N9J&6xx-+1m841&-6( zJE*wQxQ1%OeSE3(qX8#zGJ%2bgPk~%9zvkl=Kf<3MqD1GrdsK-r53p2x_P7*%Vt2(sF1`kp)q*n$?hJ=XRHbNvV- zPX~3n4hzXgB#_Vsys;6S+i;Q>3Yp?GAs59WwVM}Iyq1di#IuhNhHm2A*t+@Fg<76m zs+2ZMW4oEdJKTi%mAy0l*2)a-!I#4SQICeKpbZ$u;}!=&Wf7M`tnyRYuGlW38!%dH z4iObuY#c((>v*~WLP1a|M%h^pzhdY*F^Q;g{=>Nc7|55~8|cr^?*Gl41;#_i&;PYh zmAA8qG(Jp3thNl|oej1*1xD5dRfg(SixmOY>KT}s*4!R479v=?w_$6=Y8giE-j{dC zRBRP=D>cnMe)1sWyZEM{+fCRpU-r)j>6?39m=;t)lspmg)}NjL>$P<(V~e@VJJq&I zHL4sZzy=)OKSh@K`OAo*M5o$EvK)IC!*I<4m)!GNXtRk*K4$!c9y3&C4nv}1w-~q_ zZIfGg%G+$iO4jO0=?n3*#!;MqR=v3AlG@^h_ATM-ZuI!s!8RhS;r)G*9kpf<{?7GH zp~*wf*`>CAi50D!+gfYR3sUb$9QCQE)5CaP1D&Hvi^Q3rAd01}=5?N(xl}acV)a)J zQ{MA;=I|7v{AS*h4dE`K)g-#N9J*HCiCRqBSg@^YBF2pc%kqG@cEUlpjCeVBGbU0R zl+C1{dUcIfYJ{+H($Is<;|IRAZop`oZ%~4}loRkwtW{R3`qryM^Wkkf zpnx-x!Y3-bxZ35VK6t=iu$nSEtwci)pBth}g?)t-BtOjE>1!esz5wO+l;6l^(qY_Y zIoqR>ZD41NHF^;dFqcy|vVdU%)%p|>I3x#%^Lnrb2sw>1^rY4pqVV$})9C2jW_4&} zO^xk(^JX*LW-;8bkX(v|D{L6wO-FYrVEsnTc>abBr?HY`9GnGKY7VPSh>$z+rs$NY z8A2RR!mbLSeP~Y3L`H@Eu0)@XC0*quw~bsk7jDh27*dozL&x?vywVSNB|v+7mG7Rs zJ3U$TU6F7C?b(EmzXnsJ{YC_1)8O)Kro3rS4E#k@Ik~l2&5KH5>&$>B_XxfYFbX6e zlt6_hmT%BQnu7mlF7&Ioav&fDCP^GMfF4S;mxt>E%=$%s01vL`HrTha-oU`1n}W44 zt|UkJ1-ke*G61YnO0_1wPzn=4wS%&K>ezMw&0Oyqp6WQom=(!7=|%+xvBG&in)q5W zu^73vmcI=42iTqe*px9D<`7RH@V#%Z7JJyRp6vCf0y|$zg>cFtSOz>l!k^smz)y&c z!lq3}66sS-5q*Qi&~NQ%hI?F$E3u#USRz)^1wagp5_sA9{(_*qxig3Wa~+XRTqL{< zk0w={&n@HfAMbIPbS;8E^pFcA*PW|9`A!B$>WkuJCv5-je&cvrX$M%a0Y@XHu?w6s zuwen{V92cW^axavksIL`JYG=9FdO}3&rn$Y^o7wf3pL5Sq#1!N9ODWJPlR4kbNO?N z!hcK1Uc>N0>Ba`d*c)>u12*Q@<77O${NMf}TV9L$(O}FWnPELKI@Bv|cYTSOhU*-D z|NY1IPR0CtC?}2 zfT2gQnF-EQ;#uB6L>_ujPdX7&KnY532c^=%lpqCb>j5&qh0nWQ|cez#}&7 zHwJlQ&1EqO=RYb12HD{3$_$-hoAO73J`cPEkM+gQgS9WgCK01B)(-@sJUdmFzx8N(t*J*8X2H>j*G`bR^QmAgx*VQdq&xgzPV%x8*LV{0SB7c2 zX>zNi;i|Hd7`6-}5pUSa7f@+X`l3J%L?>lzX4Q}I*b`HZj#!gxZ^$q4xPlMI3E8w&0` zfpxhgW(VcbRzr|i-!n`GjpWIR%^@<%M^=wQL#d%WAh+Q4{PrX_TbUOf!vu>dA`~jY z32shc_ee7W#E{jcdMb(LA8BO}Yw}0iq{EW24os8A+;$WJ;&#N(vOb0`iUNu%mC|OU z7YIpRlaX~+9Rr7_Nky@8wvm;X2$iO^9jKOnuM@XIT#e^+F*|~+j*?zOV>23K^K!aR zzo2MTli<>y?gt+oo%;oPHr$g2)(u(3r!rZ>-CnK&)pq)Lr+qA`?$}jjiP?oEdDr;p zEoR;%{lT4SGR8$NshL0D8?(j7X-A`VU8TVLY0QHY!|RqO`QBc3!EQp42%l(@tcOv!>JVyt0eV#S=qREnboAg-3; zKTWoQqMIGX;t(?6{ILY`E1CM%3|7wXh3>G8&{N)tuwct6LFTg+eJA;k&aZIQ_tler z42Ds(in74+7oY)OC(U~DRO#_pv~lw4vsnb?_)xh&`C$(DD*$!U-MNzaEI&=E9;)yV z-38ek-?J4ff*zuulueF53%N!fLu2G88m$lT`xA68x+Zg-%Qy+|jOsq~yVfGe3@UC1hW~aTE?nM<8JGPkB^jcI9#$0>%;8 zU4f+{TZywzjz1rfgy!gzS@k;>2KvhT^MSNGV~kX_xAA5{NRFYf?8CHJ@ruIFE+JI;Ga`c zPc3QVQQ9LYf*>^2bmE@OgmDpP1ylj2tZ-l_P>-rV|Q|FkmD4g#e?}=EC?H1Bza3@BsxqpaTP1`#~(zgsjwBX&kuf|_f zg9ZSv{{+^nuIYNz!-mZ`Z_z%Skn1@XevWCdx96;>!6YVB_Z+11l^UJK#VlW>uhIF| z6Qq1?0fLpWIX^lS3L&h=ZvSJ(MY>Q?4szeC#S$B0Aw!m2%%WxF90Nxwp zBrkk4BJ#-c)#|G9Q0JU~_k+4=e8Qg+v_8L{Fb%WM?3~^=jYEwO_HC%+`0EtSIU3dJ zUf7Mz=Bu(GkM{zpwI{1CG@2q2XCt|(p;(iOlFRPcV(W+;P7bQx5r%JVv52OR9SOiJ z1lIf-BoCB2ZTflwjr;>g#vO4%)-Et{ceoe@Qv0{uYqz^<`A;32rGDdGpYPl4at*K1k;{ee6RtF7qd2#^iI+>#ziN=tk@a2El`h(nArMfi}nI zM(nnTDy4c3N$4TkLqw}}+2k^cxu$+cjwByQ{hs+d`Q2Ol@PyeUnr@?Qcjc|HIMZL* ztnlad$6k~3HHaVJ#$BCTojc963=~i0Qzfs^560+$;;kUeQM?^Fo0IPYg^>IXzsbOq z)*XKXUorrAdO?~O_Uqsc&-VeFt6^H8`ijm=zlMUGAwaeww59}0yZ$iwWA>&j^9B)G zTfoKPM?`3mC0K4ew}G*h(lQbvV|$lyl&2~RVu%QRJ-EWoU(k=~pW(8&>EQ$_mG}{S zPlWGtFog8cN_ARnhP)cEf0>}9#}>wvwHOAHIXmqB@)7kaobN(6FP)?|YOXlaNF+TokI5#Mkv4^2YEEo1W@4Ta!sjP9LEy);4!%cp1S`{!l8q1WJ zyBjsr4BOCEn0jyMs{GQ^L4O&VH%`B!7H_-~hMp#_pgdnd#I$KN9oArXgK3(5h1InJ zW~P)Ex*j_C2u1T7owm{U|6EQkjG);}veTY%=p(YQ_a<#|MZ?7O_dmI7)oGUhh{d1z zyjKx0@{n35Q=$79LJFbNf5BNsp#m~#=`7l5EaZri`a-UOS~Dkn%xhA7k*0$-`9>?6XX=IOX)cdQhB7^~ zjE|Pz`-N<^jUda1&2%+UTsaZCMG{z5O#V$m+Zj>~3B1L=la$ z;SpI;^K*(cH*`|7EtH!rtlt9VtAf8obqmXnKU$X7`eqiLE&*68d%$n0QEkOS;N12g#%Dkd6_V(=GX+MMW;Ltf-> zo}pWkS5rpGIE;l{W+wCW{o7P1&kQt=%`rHi2AY5@F;To!<=}#iNpEOI7CH-CwesoO zoMT=RJRj4A*jJ7uaFJs$LkY6SEPz?LvX>)q8KcncIml-)(;3pMUij#sAjQ?>gaL*J+JWVn&)AtV7qb3$R0z@>6czHfzeUP==ptszS9Gdi8kQ0&?rK`20llKT{x5Oq0<2f1qDbi2nsE{TEiJ zU}9%sX=87}XyWkSD7mm?1*alr49!$AS7Wjtz?SqHpZpAuxg=!}4J{3F@jj3$ zV=2E!vfds}NK?M5Eqv2IdJVy7Z5Y=N z!G9^#=X9ntbXdAGq__8|IH+c^xwI1+sX{n%0A+_DIac{^rtN{eG~OsSYN{_e_=qn` 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 l4Qpv6d}!(ZT7qa_kE= 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≀AB0GQ0bu_s=iq8~b;R>eDF5av%O})a!yQ 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

ZXp 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 Date: Thu, 30 Apr 2015 16:26:09 -0400 Subject: [PATCH 0606/1169] Fix debugger registration.verify_supported platform BFF-800 --- src/certfuzz/tools/linux/minimize.py | 2 +- src/certfuzz/tools/linux/repro.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 9938d70..6867f56 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -26,7 +26,7 @@ def main(): - debuggers.verify_supported_platform() + debuggers.registration.verify_supported_platform() from optparse import OptionParser diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index 22aa845..156d8cc 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -46,7 +46,7 @@ def getiterpath(gdbfile): def main(): - debuggers.verify_supported_platform() + debuggers.registration.verify_supported_platform() from optparse import OptionParser From c1e1fe9d4a44704ea7bb9dbfbf6aab9933b24f97 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 30 Apr 2015 16:29:16 -0400 Subject: [PATCH 0607/1169] bff.cfg -> bff.yaml BFF-802 --- src/certfuzz/tools/linux/repro.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index 156d8cc..17aea9a 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -59,7 +59,7 @@ def main(): help='Enable debug messages (overrides --verbose)') parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') - parser.add_option('-c', '--config', default='conf.d/bff.cfg', + parser.add_option('-c', '--config', default='conf.d/bff.yaml', dest='config', help='path to the configuration file to use') parser.add_option('-e', '--edb', dest='use_edb', action='store_true', From 714f0c460b4dbe7e1b12538ac99bfbfb455872d7 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 5 May 2015 11:14:00 -0400 Subject: [PATCH 0608/1169] Don't use shell for app caching. BFF-801 --- src/certfuzz/campaign/campaign_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index bf58460..5ce1919 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -124,7 +124,7 @@ def _cache_app(self): # Run the program once to cache it into memory fullpathorig = self.config.full_path_original(sf.path) cmdargs = self.config.get_command_list(fullpathorig) - subp.run_with_timer(cmdargs, self.config.progtimeout * 8, self.config.killprocname, use_shell=True) + subp.run_with_timer(cmdargs, self.config.progtimeout * 8, self.config.killprocname, use_shell=False) # Give target time to die time.sleep(1) From c797b557600a1ff3c13318f8abd261616ff6622f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 3 Jun 2015 15:46:22 -0400 Subject: [PATCH 0609/1169] Various bug fixes to get BFF working on Windows --- src/certfuzz/debuggers/msec.py | 2 +- .../debuggers/output_parsers/msec_file.py | 2 +- src/certfuzz/runners/qijo.py | 140 ++++++++++ src/certfuzz/runners/winprocess.py | 250 +++++++++++++++++- .../testcase_pipeline/tc_pipeline_windows.py | 2 +- src/certfuzz/tools/windows/repro.py | 2 +- src/linux/INSTALL | 2 +- 7 files changed, 383 insertions(+), 17 deletions(-) create mode 100644 src/certfuzz/runners/qijo.py diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 3193922..36ddf4f 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -8,7 +8,7 @@ from threading import Timer import time -from certfuzz.debuggers import Debugger as DebuggerBase +from certfuzz.debuggers.debugger_base import Debugger as DebuggerBase from certfuzz.debuggers.output_parsers.msec_file import MsecFile from certfuzz.debuggers.registration import register from certfuzz.helpers import check_os_compatibility diff --git a/src/certfuzz/debuggers/output_parsers/msec_file.py b/src/certfuzz/debuggers/output_parsers/msec_file.py index a8735d0..598dd1b 100644 --- a/src/certfuzz/debuggers/output_parsers/msec_file.py +++ b/src/certfuzz/debuggers/output_parsers/msec_file.py @@ -5,7 +5,7 @@ ''' import logging -from certfuzz.debuggers.output_parsers import DebuggerFile +from certfuzz.debuggers.output_parsers.debugger_file_base import DebuggerFile logger = logging.getLogger(__name__) diff --git a/src/certfuzz/runners/qijo.py b/src/certfuzz/runners/qijo.py new file mode 100644 index 0000000..1ac8843 --- /dev/null +++ b/src/certfuzz/runners/qijo.py @@ -0,0 +1,140 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE, addressof, c_size_t, c_ulong +from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LARGE_INTEGER + +LPVOID = c_void_p +LPDWORD = POINTER(DWORD) +SIZE_T = c_size_t +ULONG_PTR = POINTER(c_ulong) + +# A ULONGLONG is a 64-bit unsigned integer. +# Thus there are 8 bytes in a ULONGLONG. +# XXX why not import c_ulonglong ? +ULONGLONG = BYTE * 8 + +class IO_COUNTERS(Structure): + # The IO_COUNTERS struct is 6 ULONGLONGs. + # TODO: Replace with non-dummy fields. + _fields_ = [('dummy', ULONGLONG * 6)] + +class JOBOBJECT_BASIC_ACCOUNTING_INFORMATION(Structure): + _fields_ = [('TotalUserTime', LARGE_INTEGER), + ('TotalKernelTime', LARGE_INTEGER), + ('ThisPeriodTotalUserTime', LARGE_INTEGER), + ('ThisPeriodTotalKernelTime', LARGE_INTEGER), + ('TotalPageFaultCount', DWORD), + ('TotalProcesses', DWORD), + ('ActiveProcesses', DWORD), + ('TotalTerminatedProcesses', DWORD)] + +class JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION(Structure): + _fields_ = [('BasicInfo', JOBOBJECT_BASIC_ACCOUNTING_INFORMATION), + ('IoInfo', IO_COUNTERS)] + +# see http://msdn.microsoft.com/en-us/library/ms684147%28VS.85%29.aspx +class JOBOBJECT_BASIC_LIMIT_INFORMATION(Structure): + _fields_ = [('PerProcessUserTimeLimit', LARGE_INTEGER), + ('PerJobUserTimeLimit', LARGE_INTEGER), + ('LimitFlags', DWORD), + ('MinimumWorkingSetSize', SIZE_T), + ('MaximumWorkingSetSize', SIZE_T), + ('ActiveProcessLimit', DWORD), + ('Affinity', ULONG_PTR), + ('PriorityClass', DWORD), + ('SchedulingClass', DWORD) + ] + +class JOBOBJECT_ASSOCIATE_COMPLETION_PORT(Structure): + _fields_ = [('CompletionKey', c_ulong), + ('CompletionPort', HANDLE)] + +# see http://msdn.microsoft.com/en-us/library/ms684156%28VS.85%29.aspx +class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(Structure): + _fields_ = [('BasicLimitInformation', JOBOBJECT_BASIC_LIMIT_INFORMATION), + ('IoInfo', IO_COUNTERS), + ('ProcessMemoryLimit', SIZE_T), + ('JobMemoryLimit', SIZE_T), + ('PeakProcessMemoryUsed', SIZE_T), + ('PeakJobMemoryUsed', SIZE_T)] + +# These numbers below come from: +# http://msdn.microsoft.com/en-us/library/ms686216%28v=vs.85%29.aspx +JobObjectAssociateCompletionPortInformation = 7 +JobObjectBasicAndIoAccountingInformation = 8 +JobObjectExtendedLimitInformation = 9 + +class JobObjectInfo(object): + mapping = { 'JobObjectBasicAndIoAccountingInformation': 8, + 'JobObjectExtendedLimitInformation': 9, + 'JobObjectAssociateCompletionPortInformation': 7 + } + structures = { + 7: JOBOBJECT_ASSOCIATE_COMPLETION_PORT, + 8: JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION, + 9: JOBOBJECT_EXTENDED_LIMIT_INFORMATION + } + def __init__(self, _class): + if isinstance(_class, basestring): + assert _class in self.mapping, 'Class should be one of %s; you gave %s' % (self.mapping, _class) + _class = self.mapping[_class] + assert _class in self.structures, 'Class should be one of %s; you gave %s' % (self.structures, _class) + self.code = _class + self.info = self.structures[_class]() + + +QueryInformationJobObjectProto = WINFUNCTYPE( + BOOL, # Return type + HANDLE, # hJob + DWORD, # JobObjectInfoClass + LPVOID, # lpJobObjectInfo + DWORD, # cbJobObjectInfoLength + LPDWORD # lpReturnLength + ) + +QueryInformationJobObjectFlags = ( + (1, 'hJob'), + (1, 'JobObjectInfoClass'), + (1, 'lpJobObjectInfo'), + (1, 'cbJobObjectInfoLength'), + (1, 'lpReturnLength', None) + ) + +_QueryInformationJobObject = QueryInformationJobObjectProto( + ('QueryInformationJobObject', windll.kernel32), + QueryInformationJobObjectFlags + ) + +class SubscriptableReadOnlyStruct(object): + def __init__(self, struct): + self._struct = struct + + def _delegate(self, name): + result = getattr(self._struct, name) + if isinstance(result, Structure): + return SubscriptableReadOnlyStruct(result) + return result + + def __getitem__(self, name): + match = [fname for fname, ftype in self._struct._fields_ + if fname == name] + if match: + return self._delegate(name) + raise KeyError(name) + + def __getattr__(self, name): + return self._delegate(name) + +def QueryInformationJobObject(hJob, JobObjectInfoClass): + jobinfo = JobObjectInfo(JobObjectInfoClass) + result = _QueryInformationJobObject( + hJob=hJob, + JobObjectInfoClass=jobinfo.code, + lpJobObjectInfo=addressof(jobinfo.info), + cbJobObjectInfoLength=sizeof(jobinfo.info) + ) + if not result: + raise WinError() + return SubscriptableReadOnlyStruct(jobinfo.info) diff --git a/src/certfuzz/runners/winprocess.py b/src/certfuzz/runners/winprocess.py index 64df9d7..6f3afc8 100644 --- a/src/certfuzz/runners/winprocess.py +++ b/src/certfuzz/runners/winprocess.py @@ -1,16 +1,48 @@ # A module to expose various thread/process/job related structures and # methods from kernel32 # +# The MIT License # +# Copyright (c) 2003-2004 by Peter Astrand # +# Additions and modifications written by Benjamin Smedberg +# are Copyright (c) 2006 by the Mozilla Foundation +# # +# More Modifications +# Copyright (c) 2006-2007 by Mike Taylor +# Copyright (c) 2007-2008 by Mikeal Rogers +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of the +# author not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE -from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD +from ctypes import c_void_p, POINTER, sizeof, Structure, Union, windll, WinError, WINFUNCTYPE, c_ulong +from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD, ULONG +from qijo import QueryInformationJobObject LPVOID = c_void_p LPBYTE = POINTER(BYTE) LPDWORD = POINTER(DWORD) +LPBOOL = POINTER(BOOL) +LPULONG = POINTER(c_ulong) def ErrCheckBool(result, func, args): """errcheck function for Windows functions that return a BOOL True @@ -19,15 +51,19 @@ def ErrCheckBool(result, func, args): raise WinError() return args -CloseHandleProto = WINFUNCTYPE(BOOL, HANDLE) -CloseHandle = CloseHandleProto(("CloseHandle", windll.kernel32)) -CloseHandle.errcheck = ErrCheckBool + +# AutoHANDLE class AutoHANDLE(HANDLE): """Subclass of HANDLE which will call CloseHandle() on deletion.""" + + CloseHandleProto = WINFUNCTYPE(BOOL, HANDLE) + CloseHandle = CloseHandleProto(("CloseHandle", windll.kernel32)) + CloseHandle.errcheck = ErrCheckBool + def Close(self): - if self.value: - CloseHandle(self) + if self.value and self.value != HANDLE(-1).value: + self.CloseHandle(self) self.value = 0 def __del__(self): @@ -42,6 +78,8 @@ def ErrCheckHandle(result, func, args): raise WinError() return AutoHANDLE(result) +# PROCESS_INFORMATION structure + class PROCESS_INFORMATION(Structure): _fields_ = [("hProcess", HANDLE), ("hThread", HANDLE), @@ -55,6 +93,8 @@ def __init__(self): LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION) +# STARTUPINFO structure + class STARTUPINFO(Structure): _fields_ = [("cb", DWORD), ("lpReserved", LPWSTR), @@ -77,6 +117,8 @@ class STARTUPINFO(Structure): ] LPSTARTUPINFO = POINTER(STARTUPINFO) +SW_HIDE = 0 + STARTF_USESHOWWINDOW = 0x01 STARTF_USESIZE = 0x02 STARTF_USEPOSITION = 0x04 @@ -87,6 +129,8 @@ class STARTUPINFO(Structure): STARTF_FORCEOFFFEEDBACK = 0x80 STARTF_USESTDHANDLES = 0x100 +# EnvironmentBlock + class EnvironmentBlock: """An object which can be passed as the lpEnv parameter of CreateProcess. It is initialized with a dictionary.""" @@ -99,7 +143,19 @@ def __init__(self, dict): for (key, value) in dict.iteritems()] values.append("") self._as_parameter_ = LPCWSTR("\0".join(values)) - + +# Error Messages we need to watch for go here +# See: http://msdn.microsoft.com/en-us/library/ms681388%28v=vs.85%29.aspx +ERROR_ABANDONED_WAIT_0 = 735 + +# GetLastError() +GetLastErrorProto = WINFUNCTYPE(DWORD # Return Type + ) +GetLastErrorFlags = () +GetLastError = GetLastErrorProto(("GetLastError", windll.kernel32), GetLastErrorFlags) + +# CreateProcess() + CreateProcessProto = WINFUNCTYPE(BOOL, # Return type LPCWSTR, # lpApplicationName LPWSTR, # lpCommandLine @@ -134,6 +190,7 @@ def ErrCheckCreateProcess(result, func, args): CreateProcessFlags) CreateProcess.errcheck = ErrCheckCreateProcess +# flags for CreateProcess CREATE_BREAKAWAY_FROM_JOB = 0x01000000 CREATE_DEFAULT_ERROR_MODE = 0x04000000 CREATE_NEW_CONSOLE = 0x00000010 @@ -141,22 +198,102 @@ def ErrCheckCreateProcess(result, func, args): CREATE_NO_WINDOW = 0x08000000 CREATE_SUSPENDED = 0x00000004 CREATE_UNICODE_ENVIRONMENT = 0x00000400 + +# Flags for IOCompletion ports (some of these would probably be defined if +# we used the win32 extensions for python, but we don't want to do that if we +# can help it. +INVALID_HANDLE_VALUE = HANDLE(-1) # From winbase.h + +# Self Defined Constants for IOPort <--> Job Object communication +COMPKEY_TERMINATE = c_ulong(0) +COMPKEY_JOBOBJECT = c_ulong(1) + +# flags for job limit information +# see http://msdn.microsoft.com/en-us/library/ms684147%28VS.85%29.aspx +JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800 +JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000 + +# Flags for Job Object Completion Port Message IDs from winnt.h +# See also: http://msdn.microsoft.com/en-us/library/ms684141%28v=vs.85%29.aspx +JOB_OBJECT_MSG_END_OF_JOB_TIME = 1 +JOB_OBJECT_MSG_END_OF_PROCESS_TIME = 2 +JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT = 3 +JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4 +JOB_OBJECT_MSG_NEW_PROCESS = 6 +JOB_OBJECT_MSG_EXIT_PROCESS = 7 +JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS = 8 +JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT = 9 +JOB_OBJECT_MSG_JOB_MEMORY_LIMIT = 10 + +# See winbase.h DEBUG_ONLY_THIS_PROCESS = 0x00000002 DEBUG_PROCESS = 0x00000001 DETACHED_PROCESS = 0x00000008 + +# GetQueuedCompletionPortStatus - http://msdn.microsoft.com/en-us/library/aa364986%28v=vs.85%29.aspx +GetQueuedCompletionStatusProto = WINFUNCTYPE(BOOL, # Return Type + HANDLE, # Completion Port + LPDWORD, # Msg ID + LPULONG, # Completion Key + LPULONG, # PID Returned from the call (may be null) + DWORD) # milliseconds to wait +GetQueuedCompletionStatusFlags = ((1, "CompletionPort", INVALID_HANDLE_VALUE), + (1, "lpNumberOfBytes", None), + (1, "lpCompletionKey", None), + (1, "lpPID", None), + (1, "dwMilliseconds", 0)) +GetQueuedCompletionStatus = GetQueuedCompletionStatusProto(("GetQueuedCompletionStatus", + windll.kernel32), + GetQueuedCompletionStatusFlags) +# CreateIOCompletionPort +# Note that the completion key is just a number, not a pointer. +CreateIoCompletionPortProto = WINFUNCTYPE(HANDLE, # Return Type + HANDLE, # File Handle + HANDLE, # Existing Completion Port + c_ulong, # Completion Key + DWORD # Number of Threads + ) +CreateIoCompletionPortFlags = ((1, "FileHandle", INVALID_HANDLE_VALUE), + (1, "ExistingCompletionPort", 0), + (1, "CompletionKey", c_ulong(0)), + (1, "NumberOfConcurrentThreads", 0)) +CreateIoCompletionPort = CreateIoCompletionPortProto(("CreateIoCompletionPort", + windll.kernel32), + CreateIoCompletionPortFlags) +CreateIoCompletionPort.errcheck = ErrCheckHandle + +# SetInformationJobObject +SetInformationJobObjectProto = WINFUNCTYPE(BOOL, # Return Type + HANDLE, # Job Handle + DWORD, # Type of Class next param is + LPVOID, # Job Object Class + DWORD # Job Object Class Length + ) +SetInformationJobObjectProtoFlags = ((1, "hJob", None), + (1, "JobObjectInfoClass", None), + (1, "lpJobObjectInfo", None), + (1, "cbJobObjectInfoLength", 0)) +SetInformationJobObject = SetInformationJobObjectProto(("SetInformationJobObject", + windll.kernel32), + SetInformationJobObjectProtoFlags) +SetInformationJobObject.errcheck = ErrCheckBool + +# CreateJobObject() CreateJobObjectProto = WINFUNCTYPE(HANDLE, # Return type LPVOID, # lpJobAttributes LPCWSTR # lpName ) CreateJobObjectFlags = ((1, "lpJobAttributes", None), - (1, "lpName", LPCWSTR("fuzzjob"))) + (1, "lpName", None)) CreateJobObject = CreateJobObjectProto(("CreateJobObjectW", windll.kernel32), CreateJobObjectFlags) CreateJobObject.errcheck = ErrCheckHandle +# AssignProcessToJobObject() + AssignProcessToJobObjectProto = WINFUNCTYPE(BOOL, # Return type HANDLE, # hJob HANDLE # hProcess @@ -168,6 +305,38 @@ def ErrCheckCreateProcess(result, func, args): AssignProcessToJobObjectFlags) AssignProcessToJobObject.errcheck = ErrCheckBool +# GetCurrentProcess() +# because os.getPid() is way too easy +GetCurrentProcessProto = WINFUNCTYPE(HANDLE # Return type + ) +GetCurrentProcessFlags = () +GetCurrentProcess = GetCurrentProcessProto( + ("GetCurrentProcess", windll.kernel32), + GetCurrentProcessFlags) +GetCurrentProcess.errcheck = ErrCheckHandle + +# IsProcessInJob() +try: + IsProcessInJobProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # Process Handle + HANDLE, # Job Handle + LPBOOL # Result + ) + IsProcessInJobFlags = ((1, "ProcessHandle"), + (1, "JobHandle", HANDLE(0)), + (2, "Result")) + IsProcessInJob = IsProcessInJobProto( + ("IsProcessInJob", windll.kernel32), + IsProcessInJobFlags) + IsProcessInJob.errcheck = ErrCheckBool +except AttributeError: + # windows 2k doesn't have this API + def IsProcessInJob(process): + return False + + +# ResumeThread() + def ErrCheckResumeThread(result, func, args): if result == -1: raise WinError() @@ -182,10 +351,12 @@ def ErrCheckResumeThread(result, func, args): ResumeThreadFlags) ResumeThread.errcheck = ErrCheckResumeThread +# TerminateProcess() + TerminateProcessProto = WINFUNCTYPE(BOOL, # Return type - HANDLE, # hProcess - UINT # uExitCode - ) + HANDLE, # hProcess + UINT # uExitCode + ) TerminateProcessFlags = ((1, "hProcess"), (1, "uExitCode", 127)) TerminateProcess = TerminateProcessProto( @@ -193,6 +364,8 @@ def ErrCheckResumeThread(result, func, args): TerminateProcessFlags) TerminateProcess.errcheck = ErrCheckBool +# TerminateJobObject() + TerminateJobObjectProto = WINFUNCTYPE(BOOL, # Return type HANDLE, # hJob UINT # uExitCode @@ -204,6 +377,8 @@ def ErrCheckResumeThread(result, func, args): TerminateJobObjectFlags) TerminateJobObject.errcheck = ErrCheckBool +# WaitForSingleObject() + WaitForSingleObjectProto = WINFUNCTYPE(DWORD, # Return type HANDLE, # hHandle DWORD, # dwMilliseconds @@ -214,11 +389,20 @@ def ErrCheckResumeThread(result, func, args): ("WaitForSingleObject", windll.kernel32), WaitForSingleObjectFlags) +# http://msdn.microsoft.com/en-us/library/ms681381%28v=vs.85%29.aspx INFINITE = -1 WAIT_TIMEOUT = 0x0102 WAIT_OBJECT_0 = 0x0 WAIT_ABANDONED = 0x0080 +# http://msdn.microsoft.com/en-us/library/ms683189%28VS.85%29.aspx +STILL_ACTIVE = 259 + +# Used when we terminate a process. +ERROR_CONTROL_C_EXIT = 0x23c + +# GetExitCodeProcess() + GetExitCodeProcessProto = WINFUNCTYPE(BOOL, # Return type HANDLE, # hProcess LPDWORD, # lpExitCode @@ -229,3 +413,45 @@ def ErrCheckResumeThread(result, func, args): ("GetExitCodeProcess", windll.kernel32), GetExitCodeProcessFlags) GetExitCodeProcess.errcheck = ErrCheckBool + +def CanCreateJobObject(): + currentProc = GetCurrentProcess() + if IsProcessInJob(currentProc): + jobinfo = QueryInformationJobObject(HANDLE(0), 'JobObjectExtendedLimitInformation') + limitflags = jobinfo['BasicLimitInformation']['LimitFlags'] + return bool(limitflags & JOB_OBJECT_LIMIT_BREAKAWAY_OK) or bool(limitflags & JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK) + else: + return True + +### testing functions + +def parent(): + print 'Starting parent' + currentProc = GetCurrentProcess() + if IsProcessInJob(currentProc): + print >> sys.stderr, "You should not be in a job object to test" + sys.exit(1) + assert CanCreateJobObject() + print 'File: %s' % __file__ + command = [sys.executable, __file__, '-child'] + print 'Running command: %s' % command + process = Popen(command) + process.kill() + code = process.returncode + print 'Child code: %s' % code + assert code == 127 + +def child(): + print 'Starting child' + currentProc = GetCurrentProcess() + injob = IsProcessInJob(currentProc) + print "Is in a job?: %s" % injob + can_create = CanCreateJobObject() + print 'Can create job?: %s' % can_create + process = Popen('c:\\windows\\notepad.exe') + assert process._job + jobinfo = QueryInformationJobObject(process._job, 'JobObjectExtendedLimitInformation') + print 'Job info: %s' % jobinfo + limitflags = jobinfo['BasicLimitInformation']['LimitFlags'] + print 'LimitFlags: %s' % limitflags + process.kill() diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 58e7638..53f64c0 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -52,7 +52,7 @@ def _minimize(self, testcase): config = self._create_minimizer_cfg() - debuggers.verify_supported_platform() + debuggers.debugger_base.verify_supported_platform() kwargs = {'cfg': config, 'crash': testcase, diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index a465907..d505d7f 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -37,7 +37,7 @@ def getiterpath(msecfile): def main(): - debuggers.verify_supported_platform() + debuggers.registration.verify_supported_platform() from optparse import OptionParser diff --git a/src/linux/INSTALL b/src/linux/INSTALL index ce14ddf..b10dbcb 100644 --- a/src/linux/INSTALL +++ b/src/linux/INSTALL @@ -152,7 +152,7 @@ To install BFF on an Ubuntu 11.10 32-bit system, for example, the following step can be performed: 1) Install dependencies present in the package system: -sudo apt-get install python-numpy python-scipy valgrind subversion automake libtool build-essential libncurses5-dev +sudo apt-get install python-numpy python-scipy python-yaml valgrind subversion automake libtool build-essential libncurses5-dev 2) Install libcaca, which is a dependency for building zzuf: svn co svn://svn.zoy.org/caca/libcaca/trunk libcaca From d1f84482ce966ee7b5e0d2288e892fd7d911d093 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 3 Jun 2015 16:15:59 -0400 Subject: [PATCH 0610/1169] Adding dummy test module for qijo, which is a new winprocess dependency --- src/certfuzz/test/runners/test_qijo.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/certfuzz/test/runners/test_qijo.py diff --git a/src/certfuzz/test/runners/test_qijo.py b/src/certfuzz/test/runners/test_qijo.py new file mode 100644 index 0000000..8e13a4e --- /dev/null +++ b/src/certfuzz/test/runners/test_qijo.py @@ -0,0 +1,21 @@ +''' +Created on Jun 3, 2012 + +@organization: cert.org +''' +import unittest + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 24244fcaaf60c032f3bb14cd66f35399c60d7d3f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 3 Jun 2015 16:44:51 -0400 Subject: [PATCH 0611/1169] Fixes to get repro.py and minimize.py working on Windows --- src/certfuzz/tools/windows/minimize.py | 5 +++-- src/certfuzz/tools/windows/repro.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index eac4fd6..671c618 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -52,7 +52,7 @@ class DummyCfg(object): def main(): - debuggers.verify_supported_platform() + debuggers.registration.verify_supported_platform() from optparse import OptionParser @@ -144,7 +144,8 @@ def main(): else: parser.error('fuzzedfile must be specified') - config = WindowsConfig(cfg_file).config + with WindowsConfig(cfg_file) as configobj: + config = configobj.config cfg = _create_minimizer_cfg(config) if options.target: diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index d505d7f..831fc12 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -83,7 +83,8 @@ def main(): else: parser.error('fuzzedfile must be specified') - config = WindowsConfig(cfg_file).config + with WindowsConfig(cfg_file) as configobj: + config = configobj.config iterationpath = '' template = string.Template(config['target']['cmdline_template']) From 822ba996eef3fa06906a89f74ba947c7584398a0 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 11:06:08 -0400 Subject: [PATCH 0612/1169] Fix windows hook. The new killableprocess seems broken as it's confusing a crash with the need to kill a process. BFF-818 --- src/certfuzz/runners/killableprocess.py | 21 ++++++++++----------- src/certfuzz/runners/winrun.py | 1 - 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/runners/killableprocess.py b/src/certfuzz/runners/killableprocess.py index 6269023..1fe02a7 100644 --- a/src/certfuzz/runners/killableprocess.py +++ b/src/certfuzz/runners/killableprocess.py @@ -108,7 +108,7 @@ class Popen(subprocess.Popen): if mswindows: def _execute_child(self, *args_tuple): # workaround for bug 958609 - if sys.hexversion < 0x02070600: # prior to 2.7.6 + if sys.hexversion < 0x02070600: # prior to 2.7.6 (args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, @@ -116,7 +116,7 @@ def _execute_child(self, *args_tuple): c2pread, c2pwrite, errread, errwrite) = args_tuple to_close = set() - else: # 2.7.6 and later + else: # 2.7.6 and later (args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, to_close, @@ -153,14 +153,14 @@ def _execute_child(self, *args_tuple): creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT if canCreateJob: # Uncomment this line below to discover very useful things about your environment - #print "++++ killableprocess: releng twistd patch not applied, we can create job objects" + # print "++++ killableprocess: releng twistd patch not applied, we can create job objects" creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB # create the process hp, ht, pid, tid = winprocess.CreateProcess( executable, args, - None, None, # No special security - 1, # Must inherit handles! + None, None, # No special security + 1, # Must inherit handles! creationflags, winprocess.EnvironmentBlock(env), cwd, startupinfo) @@ -236,7 +236,7 @@ def wait(self, timeout=None, group=True): def check(): now = datetime.datetime.now() diff = now - starttime - if (diff.seconds * 1000000 + diff.microseconds) < (timeout * 1000): # (1000*1000) + if (diff.seconds * 1000000 + diff.microseconds) < (timeout * 1000): # (1000*1000) if self._job: if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0): # Job Object is still containing active processes @@ -264,9 +264,8 @@ def check(): self.kill(group) else: - # In this case waitforsingleobject timed out. We have to - # take the process behind the woodshed and shoot it. - self.kill(group) + # The process isn't around anymore. Get the exit code + self.returncode = winprocess.GetExitCodeProcess(self._handle) else: if sys.platform in ('linux2', 'sunos5', 'solaris') \ @@ -275,14 +274,14 @@ def group_wait(timeout): try: os.waitpid(self.pid, 0) except OSError, e: - pass # If wait has already been called on this pid, bad things happen + pass # If wait has already been called on this pid, bad things happen return self.returncode elif sys.platform == 'darwin': def group_wait(timeout): try: count = 0 if timeout is None and self.kill_called: - timeout = 10 # Have to set some kind of timeout or else this could go on forever + timeout = 10 # Have to set some kind of timeout or else this could go on forever if timeout is None: while 1: os.killpg(self.pid, signal.SIG_DFL) diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 1d7e48a..f86efd5 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -63,7 +63,6 @@ def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): RunnerBase.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) logger.debug('Initialize Runner') - self.runtimeout = None self.exceptions = [] self.watchcpu = options.get('watchcpu', False) From 0388930186228afc441954361833183501228731 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 11:06:57 -0400 Subject: [PATCH 0613/1169] Fix IP detection in drillresults when there are symbols --- src/certfuzz/drillresults/common.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/drillresults/common.py b/src/certfuzz/drillresults/common.py index 40662ba..68a3947 100644 --- a/src/certfuzz/drillresults/common.py +++ b/src/certfuzz/drillresults/common.py @@ -101,7 +101,9 @@ def carve2(string): substring = carve(string, token1, token2) if len(substring): # returns the first matching substring - return substring + addressarray = substring.split('') + # Make sure we get just the address and no symbols + return addressarray[1] # if we got here, no match was found, just return empty string return "" @@ -153,7 +155,7 @@ def read_bin_file(inputfile): ''' filebytes = _read_bin_file(inputfile) - #append decommpressed zip bytes + # append decommpressed zip bytes zipbytes = _read_zip(filebytes) # _read_zip returns an empty string on failure, so we can safely From 79f3ba0961e6418fffaa6c5e4df290702f519412 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 11:07:14 -0400 Subject: [PATCH 0614/1169] Fix import of copy fuzzer --- src/certfuzz/fuzzers/copy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/fuzzers/copy.py b/src/certfuzz/fuzzers/copy.py index 337b571..aae52e8 100644 --- a/src/certfuzz/fuzzers/copy.py +++ b/src/certfuzz/fuzzers/copy.py @@ -1,4 +1,4 @@ -from certfuzz.fuzzers import Fuzzer +from certfuzz.fuzzers.fuzzer_base import Fuzzer class CopyFuzzer(Fuzzer): From 706ceffc3ad771cb3a6f9d02a9b36aa1478a7bff Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 11:07:36 -0400 Subject: [PATCH 0615/1169] Fix debugger registration. --- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 53f64c0..262913e 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -52,7 +52,7 @@ def _minimize(self, testcase): config = self._create_minimizer_cfg() - debuggers.debugger_base.verify_supported_platform() + debuggers.registration.verify_supported_platform() kwargs = {'cfg': config, 'crash': testcase, From 81daabe9386c4171f907e431eacaac35eff6edfe Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 11:08:17 -0400 Subject: [PATCH 0616/1169] Fix config object in clean_windows.py --- src/certfuzz/tools/windows/clean_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/windows/clean_windows.py b/src/certfuzz/tools/windows/clean_windows.py index 3b8276f..c23b37d 100644 --- a/src/certfuzz/tools/windows/clean_windows.py +++ b/src/certfuzz/tools/windows/clean_windows.py @@ -47,8 +47,8 @@ def main(): parser.add_option('', '--debug', dest='debug', action='store_true', default=defaults['debug']) options, _args = parser.parse_args() - cfgobj = ConfigBase(options.configfile) - c = cfgobj.config + with ConfigBase(options.configfile) as cfgobj: + c = cfgobj.config if options.debug: pprint.pprint(c) From 1907d46148a23872c98543e09e8bf711a56505f8 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 11:08:26 -0400 Subject: [PATCH 0617/1169] auto cleanups --- src/certfuzz/runners/winprocess.py | 158 ++++++++++++++--------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/src/certfuzz/runners/winprocess.py b/src/certfuzz/runners/winprocess.py index 6f3afc8..fa68d35 100644 --- a/src/certfuzz/runners/winprocess.py +++ b/src/certfuzz/runners/winprocess.py @@ -56,16 +56,16 @@ def ErrCheckBool(result, func, args): class AutoHANDLE(HANDLE): """Subclass of HANDLE which will call CloseHandle() on deletion.""" - + CloseHandleProto = WINFUNCTYPE(BOOL, HANDLE) CloseHandle = CloseHandleProto(("CloseHandle", windll.kernel32)) CloseHandle.errcheck = ErrCheckBool - + def Close(self): if self.value and self.value != HANDLE(-1).value: self.CloseHandle(self) self.value = 0 - + def __del__(self): self.Close() @@ -88,7 +88,7 @@ class PROCESS_INFORMATION(Structure): def __init__(self): Structure.__init__(self) - + self.cb = sizeof(self) LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION) @@ -117,17 +117,17 @@ class STARTUPINFO(Structure): ] LPSTARTUPINFO = POINTER(STARTUPINFO) -SW_HIDE = 0 +SW_HIDE = 0 -STARTF_USESHOWWINDOW = 0x01 -STARTF_USESIZE = 0x02 -STARTF_USEPOSITION = 0x04 -STARTF_USECOUNTCHARS = 0x08 +STARTF_USESHOWWINDOW = 0x01 +STARTF_USESIZE = 0x02 +STARTF_USEPOSITION = 0x04 +STARTF_USECOUNTCHARS = 0x08 STARTF_USEFILLATTRIBUTE = 0x10 -STARTF_RUNFULLSCREEN = 0x20 -STARTF_FORCEONFEEDBACK = 0x40 +STARTF_RUNFULLSCREEN = 0x20 +STARTF_FORCEONFEEDBACK = 0x40 STARTF_FORCEOFFFEEDBACK = 0x80 -STARTF_USESTDHANDLES = 0x100 +STARTF_USESTDHANDLES = 0x100 # EnvironmentBlock @@ -147,25 +147,25 @@ def __init__(self, dict): # Error Messages we need to watch for go here # See: http://msdn.microsoft.com/en-us/library/ms681388%28v=vs.85%29.aspx ERROR_ABANDONED_WAIT_0 = 735 - + # GetLastError() -GetLastErrorProto = WINFUNCTYPE(DWORD # Return Type +GetLastErrorProto = WINFUNCTYPE(DWORD # Return Type ) GetLastErrorFlags = () GetLastError = GetLastErrorProto(("GetLastError", windll.kernel32), GetLastErrorFlags) # CreateProcess() -CreateProcessProto = WINFUNCTYPE(BOOL, # Return type - LPCWSTR, # lpApplicationName - LPWSTR, # lpCommandLine - LPVOID, # lpProcessAttributes - LPVOID, # lpThreadAttributes - BOOL, # bInheritHandles - DWORD, # dwCreationFlags - LPVOID, # lpEnvironment - LPCWSTR, # lpCurrentDirectory - LPSTARTUPINFO, # lpStartupInfo +CreateProcessProto = WINFUNCTYPE(BOOL, # Return type + LPCWSTR, # lpApplicationName + LPWSTR, # lpCommandLine + LPVOID, # lpProcessAttributes + LPVOID, # lpThreadAttributes + BOOL, # bInheritHandles + DWORD, # dwCreationFlags + LPVOID, # lpEnvironment + LPCWSTR, # lpCurrentDirectory + LPSTARTUPINFO, # lpStartupInfo LPPROCESS_INFORMATION # lpProcessInformation ) @@ -199,10 +199,10 @@ def ErrCheckCreateProcess(result, func, args): CREATE_SUSPENDED = 0x00000004 CREATE_UNICODE_ENVIRONMENT = 0x00000400 -# Flags for IOCompletion ports (some of these would probably be defined if -# we used the win32 extensions for python, but we don't want to do that if we +# Flags for IOCompletion ports (some of these would probably be defined if +# we used the win32 extensions for python, but we don't want to do that if we # can help it. -INVALID_HANDLE_VALUE = HANDLE(-1) # From winbase.h +INVALID_HANDLE_VALUE = HANDLE(-1) # From winbase.h # Self Defined Constants for IOPort <--> Job Object communication COMPKEY_TERMINATE = c_ulong(0) @@ -215,28 +215,28 @@ def ErrCheckCreateProcess(result, func, args): # Flags for Job Object Completion Port Message IDs from winnt.h # See also: http://msdn.microsoft.com/en-us/library/ms684141%28v=vs.85%29.aspx -JOB_OBJECT_MSG_END_OF_JOB_TIME = 1 -JOB_OBJECT_MSG_END_OF_PROCESS_TIME = 2 -JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT = 3 -JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4 -JOB_OBJECT_MSG_NEW_PROCESS = 6 -JOB_OBJECT_MSG_EXIT_PROCESS = 7 -JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS = 8 -JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT = 9 -JOB_OBJECT_MSG_JOB_MEMORY_LIMIT = 10 +JOB_OBJECT_MSG_END_OF_JOB_TIME = 1 +JOB_OBJECT_MSG_END_OF_PROCESS_TIME = 2 +JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT = 3 +JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4 +JOB_OBJECT_MSG_NEW_PROCESS = 6 +JOB_OBJECT_MSG_EXIT_PROCESS = 7 +JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS = 8 +JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT = 9 +JOB_OBJECT_MSG_JOB_MEMORY_LIMIT = 10 # See winbase.h DEBUG_ONLY_THIS_PROCESS = 0x00000002 DEBUG_PROCESS = 0x00000001 DETACHED_PROCESS = 0x00000008 - + # GetQueuedCompletionPortStatus - http://msdn.microsoft.com/en-us/library/aa364986%28v=vs.85%29.aspx -GetQueuedCompletionStatusProto = WINFUNCTYPE(BOOL, # Return Type - HANDLE, # Completion Port - LPDWORD, # Msg ID - LPULONG, # Completion Key - LPULONG, # PID Returned from the call (may be null) - DWORD) # milliseconds to wait +GetQueuedCompletionStatusProto = WINFUNCTYPE(BOOL, # Return Type + HANDLE, # Completion Port + LPDWORD, # Msg ID + LPULONG, # Completion Key + LPULONG, # PID Returned from the call (may be null) + DWORD) # milliseconds to wait GetQueuedCompletionStatusFlags = ((1, "CompletionPort", INVALID_HANDLE_VALUE), (1, "lpNumberOfBytes", None), (1, "lpCompletionKey", None), @@ -248,11 +248,11 @@ def ErrCheckCreateProcess(result, func, args): # CreateIOCompletionPort # Note that the completion key is just a number, not a pointer. -CreateIoCompletionPortProto = WINFUNCTYPE(HANDLE, # Return Type - HANDLE, # File Handle - HANDLE, # Existing Completion Port - c_ulong, # Completion Key - DWORD # Number of Threads +CreateIoCompletionPortProto = WINFUNCTYPE(HANDLE, # Return Type + HANDLE, # File Handle + HANDLE, # Existing Completion Port + c_ulong, # Completion Key + DWORD # Number of Threads ) CreateIoCompletionPortFlags = ((1, "FileHandle", INVALID_HANDLE_VALUE), (1, "ExistingCompletionPort", 0), @@ -264,11 +264,11 @@ def ErrCheckCreateProcess(result, func, args): CreateIoCompletionPort.errcheck = ErrCheckHandle # SetInformationJobObject -SetInformationJobObjectProto = WINFUNCTYPE(BOOL, # Return Type - HANDLE, # Job Handle - DWORD, # Type of Class next param is - LPVOID, # Job Object Class - DWORD # Job Object Class Length +SetInformationJobObjectProto = WINFUNCTYPE(BOOL, # Return Type + HANDLE, # Job Handle + DWORD, # Type of Class next param is + LPVOID, # Job Object Class + DWORD # Job Object Class Length ) SetInformationJobObjectProtoFlags = ((1, "hJob", None), (1, "JobObjectInfoClass", None), @@ -280,13 +280,13 @@ def ErrCheckCreateProcess(result, func, args): SetInformationJobObject.errcheck = ErrCheckBool # CreateJobObject() -CreateJobObjectProto = WINFUNCTYPE(HANDLE, # Return type - LPVOID, # lpJobAttributes - LPCWSTR # lpName +CreateJobObjectProto = WINFUNCTYPE(HANDLE, # Return type + LPVOID, # lpJobAttributes + LPCWSTR # lpName ) CreateJobObjectFlags = ((1, "lpJobAttributes", None), - (1, "lpName", None)) + (1, "lpName", LPCWSTR("fuzzjob"))) CreateJobObject = CreateJobObjectProto(("CreateJobObjectW", windll.kernel32), CreateJobObjectFlags) @@ -294,9 +294,9 @@ def ErrCheckCreateProcess(result, func, args): # AssignProcessToJobObject() -AssignProcessToJobObjectProto = WINFUNCTYPE(BOOL, # Return type - HANDLE, # hJob - HANDLE # hProcess +AssignProcessToJobObjectProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hJob + HANDLE # hProcess ) AssignProcessToJobObjectFlags = ((1, "hJob"), (1, "hProcess")) @@ -307,7 +307,7 @@ def ErrCheckCreateProcess(result, func, args): # GetCurrentProcess() # because os.getPid() is way too easy -GetCurrentProcessProto = WINFUNCTYPE(HANDLE # Return type +GetCurrentProcessProto = WINFUNCTYPE(HANDLE # Return type ) GetCurrentProcessFlags = () GetCurrentProcess = GetCurrentProcessProto( @@ -317,10 +317,10 @@ def ErrCheckCreateProcess(result, func, args): # IsProcessInJob() try: - IsProcessInJobProto = WINFUNCTYPE(BOOL, # Return type - HANDLE, # Process Handle - HANDLE, # Job Handle - LPBOOL # Result + IsProcessInJobProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # Process Handle + HANDLE, # Job Handle + LPBOOL # Result ) IsProcessInJobFlags = ((1, "ProcessHandle"), (1, "JobHandle", HANDLE(0)), @@ -328,7 +328,7 @@ def ErrCheckCreateProcess(result, func, args): IsProcessInJob = IsProcessInJobProto( ("IsProcessInJob", windll.kernel32), IsProcessInJobFlags) - IsProcessInJob.errcheck = ErrCheckBool + IsProcessInJob.errcheck = ErrCheckBool except AttributeError: # windows 2k doesn't have this API def IsProcessInJob(process): @@ -343,8 +343,8 @@ def ErrCheckResumeThread(result, func, args): return args -ResumeThreadProto = WINFUNCTYPE(DWORD, # Return type - HANDLE # hThread +ResumeThreadProto = WINFUNCTYPE(DWORD, # Return type + HANDLE # hThread ) ResumeThreadFlags = ((1, "hThread"),) ResumeThread = ResumeThreadProto(("ResumeThread", windll.kernel32), @@ -353,9 +353,9 @@ def ErrCheckResumeThread(result, func, args): # TerminateProcess() -TerminateProcessProto = WINFUNCTYPE(BOOL, # Return type - HANDLE, # hProcess - UINT # uExitCode +TerminateProcessProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hProcess + UINT # uExitCode ) TerminateProcessFlags = ((1, "hProcess"), (1, "uExitCode", 127)) @@ -366,9 +366,9 @@ def ErrCheckResumeThread(result, func, args): # TerminateJobObject() -TerminateJobObjectProto = WINFUNCTYPE(BOOL, # Return type - HANDLE, # hJob - UINT # uExitCode +TerminateJobObjectProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hJob + UINT # uExitCode ) TerminateJobObjectFlags = ((1, "hJob"), (1, "uExitCode", 127)) @@ -380,7 +380,7 @@ def ErrCheckResumeThread(result, func, args): # WaitForSingleObject() WaitForSingleObjectProto = WINFUNCTYPE(DWORD, # Return type - HANDLE, # hHandle + HANDLE, # hHandle DWORD, # dwMilliseconds ) WaitForSingleObjectFlags = ((1, "hHandle"), @@ -403,9 +403,9 @@ def ErrCheckResumeThread(result, func, args): # GetExitCodeProcess() -GetExitCodeProcessProto = WINFUNCTYPE(BOOL, # Return type +GetExitCodeProcessProto = WINFUNCTYPE(BOOL, # Return type HANDLE, # hProcess - LPDWORD, # lpExitCode + LPDWORD, # lpExitCode ) GetExitCodeProcessFlags = ((1, "hProcess"), (2, "lpExitCode")) @@ -423,7 +423,7 @@ def CanCreateJobObject(): else: return True -### testing functions +# ## testing functions def parent(): print 'Starting parent' @@ -440,7 +440,7 @@ def parent(): code = process.returncode print 'Child code: %s' % code assert code == 127 - + def child(): print 'Starting child' currentProc = GetCurrentProcess() From bca2435459d42485eb62634df04be3328e0ca396 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 11:13:33 -0400 Subject: [PATCH 0618/1169] Fix typo in string splitting in drillresults --- src/certfuzz/drillresults/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/drillresults/common.py b/src/certfuzz/drillresults/common.py index 68a3947..df4fa1f 100644 --- a/src/certfuzz/drillresults/common.py +++ b/src/certfuzz/drillresults/common.py @@ -101,7 +101,7 @@ def carve2(string): substring = carve(string, token1, token2) if len(substring): # returns the first matching substring - addressarray = substring.split('') + addressarray = substring.split(' ') # Make sure we get just the address and no symbols return addressarray[1] # if we got here, no match was found, just return empty string From d1114381332cd657c4235b689fbf5aa0c4b4f269 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 11:19:06 -0400 Subject: [PATCH 0619/1169] Better error checking of silly carve2 method for drillresults. --- src/certfuzz/drillresults/common.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/drillresults/common.py b/src/certfuzz/drillresults/common.py index df4fa1f..8c67fc2 100644 --- a/src/certfuzz/drillresults/common.py +++ b/src/certfuzz/drillresults/common.py @@ -101,9 +101,12 @@ def carve2(string): substring = carve(string, token1, token2) if len(substring): # returns the first matching substring - addressarray = substring.split(' ') - # Make sure we get just the address and no symbols - return addressarray[1] + if ' ' in substring: + addressarray = substring.split(' ') + # Make sure we get just the address and no symbols + return addressarray[1] + else: + return substring # if we got here, no match was found, just return empty string return "" From 8bd9ef8d8f1ea822d5d4e452c7e873c1660caf77 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 13:18:17 -0400 Subject: [PATCH 0620/1169] Proper fix for 818. Really check exit code --- src/certfuzz/runners/killableprocess.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/runners/killableprocess.py b/src/certfuzz/runners/killableprocess.py index 1fe02a7..965fc3f 100644 --- a/src/certfuzz/runners/killableprocess.py +++ b/src/certfuzz/runners/killableprocess.py @@ -261,10 +261,13 @@ def check(): if notdone == -1: # Then check timed out, we have a hung process, attempt # last ditch kill with explosives - self.kill(group) + # WD - Or don't + self.returncode = winprocess.GetExitCodeProcess(self._handle) else: - # The process isn't around anymore. Get the exit code + # In this case waitforsingleobject timed out. We have to + # take the process behind the woodshed and shoot it. + # WD - Or don't self.returncode = winprocess.GetExitCodeProcess(self._handle) else: From 6ce4d152fe61b78198679480c12299d091f2443f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 5 Jun 2015 15:47:14 -0400 Subject: [PATCH 0621/1169] Fix drillresults on Windows BFF-821 --- src/certfuzz/drillresults/testcasebundle_base.py | 4 ++-- src/certfuzz/drillresults/testcasebundle_windows.py | 13 ++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 3a7af64..8643a69 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -160,7 +160,7 @@ def _parse_testcase(self): self.instructionpieces = instructionline.split() faultaddr = self._prefix_0x(faultaddr) - faultaddr = self.fix_efa_offset(faultaddr) + faultaddr = self.fix_efa_offset(instructionline, faultaddr) # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') @@ -369,7 +369,7 @@ def _prefix_0x(self, addr): else: return '0x{}'.format(addr) - def fix_efa_offset(self, faultaddr): + def fix_efa_offset(self, instructionline, faultaddr): try: index = self.instructionpieces.index('call') except ValueError: diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 153c2c9..8565861 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -31,14 +31,9 @@ class WindowsTestCaseBundle(TestCaseBundle): 'PrivilegedInstruction', ] - def __init__(self, dbg_outfile, testcase_file, crash_hash, re_set, + def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit): - if self.testcase_file == '': - # Old FOE version that didn't do multiple exceptions or rename msec - # file with exploitability - testcase_file, _junk = os.path.splitext(dbg_outfile) - - TestCaseBundle(self, dbg_outfile, testcase_file, crash_hash, re_set, + super(self.__class__, self).__init__(dbg_outfile, testcase_file, crash_hash, ignore_jit) self.wow64_app = False @@ -94,7 +89,7 @@ def _64bit_addr_fixup(self, faultaddr, instraddr): instraddr = ''.join([instraddr[:8], '`', instraddr[8:]]) if self.shortdesc != 'DEPViolation': faultaddr = self.fix_efa_bug(instraddr, faultaddr) - return instraddr, faultaddr + return faultaddr, instraddr @property def _64bit_target_app(self): @@ -137,7 +132,7 @@ def fix_efa_offset(self, instructionline, faultaddr): # of the call return faultaddr - return TestCaseBundle.fix_efa_offset(self, faultaddr) + return TestCaseBundle.fix_efa_offset(self, instructionline, faultaddr) def get_ex_num(self): ''' From e9b07e4cb448d37233502afa7c0d0ea2018ad0dc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Jun 2015 13:43:00 -0400 Subject: [PATCH 0622/1169] Minor tweaks to get standalone minimizer working --- src/certfuzz/crash/bff_crash.py | 1 - src/certfuzz/crash/crash_base.py | 7 +++++++ src/certfuzz/minimizer/minimizer_base.py | 3 ++- src/certfuzz/minimizer/unix_minimizer.py | 4 ++-- src/certfuzz/tools/linux/minimize.py | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index 83fe75e..0c6cc8d 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -38,7 +38,6 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, ''' Testcase.__init__(self, seedfile, fuzzedfile, debugger_timeout) self.cfg = cfg - self.workdir_base = workdir_base self.program = program self.killprocname = killprocname self.backtrace_lines = backtrace_lines diff --git a/src/certfuzz/crash/crash_base.py b/src/certfuzz/crash/crash_base.py index c73e4fb..ddf575f 100644 --- a/src/certfuzz/crash/crash_base.py +++ b/src/certfuzz/crash/crash_base.py @@ -107,6 +107,13 @@ def copy_files_to_temp(self): new_fuzzedfile = os.path.join(self.tempdir, self.fuzzedfile.basename) self.fuzzedfile = BasicFile(new_fuzzedfile) + def copy_files(self, outdir): + crash_files = os.listdir(self.tempdir) + for file in crash_files: + filepath = os.path.join(self.tempdir, file) + if os.path.isfile(filepath): + filetools.copy_file(filepath, outdir) + def debug(self, tries_remaining=None): raise NotImplementedError diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index fa2c6f9..221ac91 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -529,7 +529,8 @@ def go(self): got_hit = False while self.consecutive_misses <= self.n_misses_allowed: # clean the /tmp directory so we don't fill up the disk across tries - TmpReaper().clean_tmp() + # TODO - This cleans up files that we're working with! (standalone minimizer) + # TmpReaper().clean_tmp() if self.use_watchdog: # touch the watchdog file so we don't reboot during long minimizations diff --git a/src/certfuzz/minimizer/unix_minimizer.py b/src/certfuzz/minimizer/unix_minimizer.py index 451d507..f3eed93 100644 --- a/src/certfuzz/minimizer/unix_minimizer.py +++ b/src/certfuzz/minimizer/unix_minimizer.py @@ -14,7 +14,7 @@ def __enter__(self): # touch the watchdogfile try: open(self.cfg.watchdogfile, 'w').close() - except IOError as e: + except (OSError, IOError) as e: # it's okay if we can't, but we should note it self.logger.warning('Unable to touch watchdog file %s: %s', self.cfg.watchdogfile, e) @@ -27,7 +27,7 @@ def __enter__(self): def __exit__(self, etype, value, traceback): try: os.remove(self.cfg.watchdogfile) - except IOError as e: + except (OSError, IOError) as e: # it's okay if we can't, but we should note it self.logger.warning('Unable to remove watchdog file %s: %s', self.cfg.watchdogfile, e) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 6867f56..820f586 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -58,7 +58,7 @@ def main(): type='float') parser.add_option('-g', '--target-size-guess', dest='initial_target_size', help='A guess at the minimal value (int)', type='int') - parser.add_option('', '--config', dest='config', + parser.add_option('', '--config', dest='config', default='conf.d/bff.yaml', help='path to the configuration file to use') parser.add_option('', '--timeout', dest='timeout', metavar='N', type='int', default=0, From c5a501ce5dc65b45969737f1b543dc2de88c163c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 18 Aug 2015 15:57:53 -0400 Subject: [PATCH 0623/1169] Add zipfile support to Linux BFF --- src/certfuzz/crash/testcase_base.py | 12 +- src/certfuzz/fuzztools/filetools.py | 17 ++ src/certfuzz/fuzztools/hamming.py | 40 +++-- src/certfuzz/minimizer/minimizer_base.py | 82 +++++++++- src/certfuzz/minimizer/win_minimizer.py | 103 ------------ .../testcase_pipeline/tc_pipeline_linux.py | 4 +- .../tools/{windows => common}/zipdiff.py | 148 +++++++++--------- 7 files changed, 212 insertions(+), 194 deletions(-) rename src/certfuzz/tools/{windows => common}/zipdiff.py (96%) diff --git a/src/certfuzz/crash/testcase_base.py b/src/certfuzz/crash/testcase_base.py index 5b7f546..d731547 100644 --- a/src/certfuzz/crash/testcase_base.py +++ b/src/certfuzz/crash/testcase_base.py @@ -8,6 +8,7 @@ import tempfile from certfuzz.fuzztools import hamming +from certfuzz.fuzztools.filetools import check_zip_file from pprint import pformat @@ -30,6 +31,7 @@ def __init__(self, seedfile, fuzzedfile, workdir_base=None): self.hd_bytes = None self.signature = None self.working_dir = None + self.is_zipfile = False def __enter__(self): self._setup_workdir() @@ -53,9 +55,15 @@ def _teardown_workdir(self): self.working_dir = None def calculate_hamming_distances(self): + # If the fuzzed file is a valid zip, then we're fuzzing zip contents, not the container + self.is_zipfile = check_zip_file(self.fuzzedfile.path) try: - self.hd_bits = hamming.bitwise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) - self.hd_bytes = hamming.bytewise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + if self.is_zipfile: + self.hd_bits = hamming.bitwise_zip_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + self.hd_bytes = hamming.bytewise_zip_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + else: + self.hd_bits = hamming.bitwise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + self.hd_bytes = hamming.bytewise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) except KeyError: # one of the files wasn't defined logger.warning('Cannot find either sf_path or minimized file to calculate Hamming Distances') diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 1914b95..99f028b 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -336,6 +336,23 @@ def check_zip_file(filepath): with open(filepath, 'rb') as filehandle: return check_zip_fh(filehandle) +def get_zipcontents(filepath): + # If the file is zip-based, fuzz the contents rather than the container + logger.debug('Reading zip file: %s', filepath) + tempzip = zipfile.ZipFile(filepath, 'r') + + ''' + concatentate zip contents + ''' + unzippedbytes = '' + logger.debug('Reading files from zip...') + for i in tempzip.namelist(): + data = tempzip.read(i) + unzippedbytes += data + tempzip.close() + return unzippedbytes + + def make_writable(filename): mode = os.stat(filename).st_mode diff --git a/src/certfuzz/fuzztools/hamming.py b/src/certfuzz/fuzztools/hamming.py index f178091..45a30a5 100644 --- a/src/certfuzz/fuzztools/hamming.py +++ b/src/certfuzz/fuzztools/hamming.py @@ -9,6 +9,7 @@ import itertools import os +from certfuzz.fuzztools.filetools import get_zipcontents def vector_compare(v1, v2): ''' @@ -58,16 +59,27 @@ def bytewise_hamming_distance(file1, file2): between them. Returns the distance as an int. Throws an AssertionError unless file1 and file2 are the same size. ''' - return _file_compare(bytewise_hd, file1, file2) + return _file_compare(bytewise_hd, False, file1, file2) - -def _file_compare(distance_function, file1, file2): - assert os.path.getsize(file1) == os.path.getsize(file2) - - with open(file1, 'rb') as f1: - with open(file2, 'rb') as f2: - # find the hamming distance for each byte - distance = distance_function(f1.read(), f2.read()) +def bytewise_zip_hamming_distance(file1, file2): + ''' + Given the names of two files, compute the byte-wise Hamming Distance + between them. Returns the distance as an int. Throws an AssertionError + unless file1 and file2 are the same size. + ''' + return _file_compare(bytewise_hd, True, file1, file2) + +def _file_compare(distance_function, comparezipcontents, file1, file2): + if not comparezipcontents: + assert os.path.getsize(file1) == os.path.getsize(file2) + + with open(file1, 'rb') as f1: + with open(file2, 'rb') as f2: + # find the hamming distance for each byte + distance = distance_function(f1.read(), f2.read()) + else: + # Work with zip contents + distance = distance_function(get_zipcontents(file1), get_zipcontents(file2)) return distance @@ -97,4 +109,12 @@ def bitwise_hamming_distance(file1, file2): between them. Returns the distance as an int. Throws an AssertionError unless file1 and file2 are the same size. ''' - return _file_compare(bitwise_hd, file1, file2) + return _file_compare(bitwise_hd, False, file1, file2) + +def bitwise_zip_hamming_distance(file1, file2): + ''' + Given the names of two files, compute the bit-wise Hamming Distance + between them. Returns the distance as an int. Throws an AssertionError + unless file1 and file2 are the same size. + ''' + return _file_compare(bitwise_hd, True, file1, file2) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 221ac91..4d38eab 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -12,12 +12,15 @@ import shutil import tempfile import time +import zipfile +import collections from certfuzz.debuggers.registration import get as debugger_get from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.fuzztools import hamming, filetools, probability, text -from certfuzz.fuzztools.filetools import delete_files, write_file +from certfuzz.fuzztools.filetools import delete_files, write_file, check_zip_file +from certfuzz.fuzztools.filetools import exponential_backoff from certfuzz.minimizer.errors import MinimizerError from certfuzz.analyzers import pin_calltrace from certfuzz.analyzers.errors import AnalyzerEmptyOutputError @@ -63,6 +66,9 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, logger.setLevel(logging.INFO) + self.saved_arcinfo = None + self.is_zipfile = check_zip_file(crash.fuzzedfile.path) + if tempdir and os.path.isdir(tempdir): self.tempdir = tempfile.mkdtemp(prefix='minimizer_', dir=tempdir) else: @@ -216,6 +222,10 @@ def _read_fuzzed(self): ''' returns the contents of the fuzzed_content file ''' + # store the files in memory + if self.is_zipfile: # work with zip file contents, not the container + logger.debug('Working with a zip file') + return self._readzip(self.crash.fuzzedfile.path) return self.crash.fuzzedfile.read() def _read_seed(self): @@ -224,12 +234,75 @@ def _read_seed(self): ''' # we're either going to minimize to the seedfile, the metasploit pattern, or a string of 'x's if self.seedfile_as_target: - return self.crash.seedfile.read() + if self.is_zipfile and self.seedfile_as_target: + return self._readzip(self.crash.seedfile.path) + else: + return self.crash.seedfile.read() elif self.preferx: return self.minchar * len(self.fuzzed_content) else: return text.metasploit_pattern_orig(len(self.fuzzed_content)) + def _readzip(self, filepath): + # If the seed is zip-based, fuzz the contents rather than the container + logger.debug('Reading zip file: %s', filepath) + tempzip = zipfile.ZipFile(filepath, 'r') + + ''' + get info on all the archived files and concatentate their contents + into self.input + ''' + self.saved_arcinfo = collections.OrderedDict() + unzippedbytes = '' + logger.debug('Reading files from zip...') + for i in tempzip.namelist(): + data = tempzip.read(i) + + # save split indices and compression type for archival + # reconstruction. Keeping the same compression types is + # probably unnecessary since it's the content that matters + + self.saved_arcinfo[i] = (len(unzippedbytes), len(data), + tempzip.getinfo(i).compress_type) + unzippedbytes += data + tempzip.close() + return unzippedbytes + + @exponential_backoff + def _safe_createzip(self, filepath): + tempzip = zipfile.ZipFile(filepath, 'w') + return tempzip + + def _writezip(self): + '''rebuild the zip file and put it in self.fuzzed + Note: We assume that the fuzzer has not changes the lengths + of the archived files, otherwise we won't be able to properly + split self.fuzzed + ''' + if self.saved_arcinfo is None: + raise WindowsMinimizerError('_readzip was not called') + + filedata = ''.join(self.newfuzzed) + filepath = self.tempfile + + logger.debug('Creating zip with mutated contents.') + tempzip = self._safe_createzip(filepath) + + ''' + reconstruct archived files, using the same compression scheme as + the source + ''' + for name, info in self.saved_arcinfo.iteritems(): + # write out fuzzed file + if info[2] == 0 or info[2] == 8: + # Python zipfile only supports compression types 0 and 8 + compressiontype = info[2] + else: + logger.warning('Compression type %s is not supported. Overriding', info[2]) + compressiontype = 8 + tempzip.writestr(name, str(filedata[info[0]:info[0] + info[1]]), compress_type=compressiontype) + tempzip.close() + def _logger_setup(self): dirname = os.path.dirname(self.minimizer_logfile) @@ -502,7 +575,10 @@ def _time_exceeded(self): return(self.use_timer and (elapsed_time > self.max_time)) def _write_file(self): - write_file(''.join(self.newfuzzed), self.tempfile) + if self.is_zipfile: + self._writezip() + else: + write_file(''.join(self.newfuzzed), self.tempfile) def go(self): # start by copying the fuzzed_content file since as of now it's our best fit diff --git a/src/certfuzz/minimizer/win_minimizer.py b/src/certfuzz/minimizer/win_minimizer.py index 93f5e6e..9178673 100755 --- a/src/certfuzz/minimizer/win_minimizer.py +++ b/src/certfuzz/minimizer/win_minimizer.py @@ -1,9 +1,6 @@ -import collections import logging -import zipfile from certfuzz.fuzztools.filetools import check_zip_file, write_file -from certfuzz.fuzztools.filetools import exponential_backoff from certfuzz.minimizer import Minimizer as MinimizerBase from certfuzz.minimizer.errors import WindowsMinimizerError @@ -14,19 +11,6 @@ class WindowsMinimizer(MinimizerBase): use_watchdog = False - def __init__(self, cfg=None, crash=None, crash_dst_dir=None, - seedfile_as_target=False, bitwise=False, confidence=0.999, - logfile=None, tempdir=None, maxtime=3600, preferx=False, - keep_uniq_faddr=False, watchcpu=False): - - self.saved_arcinfo = None - self.is_zipfile = check_zip_file(crash.fuzzedfile.path) - - MinimizerBase.__init__(self, cfg, crash, crash_dst_dir, - seedfile_as_target, bitwise, confidence, - logfile, tempdir, maxtime, preferx, - keep_uniq_faddr, watchcpu) - def get_signature(self, dbg, backtracelevels): # get the basic signature crash_hash = MinimizerBase.get_signature(self, dbg, backtracelevels) @@ -39,90 +23,3 @@ def get_signature(self, dbg, backtracelevels): self.signature = '.'.join(crash_id_parts) return self.signature - def _read_fuzzed(self): - ''' - returns the contents of the fuzzed file - ''' - # store the files in memory - if self.is_zipfile: # work with zip file contents, not the container - logger.debug('Working with a zip file') - return self._readzip(self.crash.fuzzedfile.path) - # otherwise just call the parent class method - return MinimizerBase._read_fuzzed(self) - - def _read_seed(self): - ''' - returns the contents of the seed file - ''' - # we're either going to minimize to the seedfile, the metasploit - # pattern, or a string of 'x's - if self.is_zipfile and self.seedfile_as_target: - return self._readzip(self.crash.seedfile.path) - # otherwise just call the parent class method - return MinimizerBase._read_seed(self) - - def _readzip(self, filepath): - # If the seed is zip-based, fuzz the contents rather than the container - logger.debug('Reading zip file: %s', filepath) - tempzip = zipfile.ZipFile(filepath, 'r') - - ''' - get info on all the archived files and concatentate their contents - into self.input - ''' - self.saved_arcinfo = collections.OrderedDict() - unzippedbytes = '' - logger.debug('Reading files from zip...') - for i in tempzip.namelist(): - data = tempzip.read(i) - - # save split indices and compression type for archival - # reconstruction. Keeping the same compression types is - # probably unnecessary since it's the content that matters - - self.saved_arcinfo[i] = (len(unzippedbytes), len(data), - tempzip.getinfo(i).compress_type) - unzippedbytes += data - tempzip.close() - return unzippedbytes - - @exponential_backoff - def _safe_createzip(self, filepath): - tempzip = zipfile.ZipFile(filepath, 'w') - return tempzip - - def _writezip(self): - '''rebuild the zip file and put it in self.fuzzed - Note: We assume that the fuzzer has not changes the lengths - of the archived files, otherwise we won't be able to properly - split self.fuzzed - ''' - if self.saved_arcinfo is None: - raise WindowsMinimizerError('_readzip was not called') - - filedata = ''.join(self.newfuzzed) - filepath = self.tempfile - - logger.debug('Creating zip with mutated contents.') - tempzip = self._safe_createzip(filepath, 'w') - - ''' - reconstruct archived files, using the same compression scheme as - the source - ''' - for name, info in self.saved_arcinfo.iteritems(): - # write out fuzzed file - if info[2] == 0 or info[2] == 8: - # Python zipfile only supports compression types 0 and 8 - compressiontype = info[2] - else: - logger.warning('Compression type %s is not supported. Overriding', info[2]) - compressiontype = 8 - tempzip.writestr(name, str(filedata[info[0]:info[0] + info[1]]), compress_type=compressiontype) - tempzip.close() - - def _write_file(self): - if self.is_zipfile: - self._writezip() - else: - write_file(''.join(self.newfuzzed), self.tempfile) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 79c787e..7519fc2 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -85,7 +85,7 @@ def _post_verify(self, testcase): def _minimize(self, testcase): if self.options.get('minimize_crashers'): touch_watchdog_file() - self._mininimize_to_seedfile(testcase) + self._minimize_to_seedfile(testcase) if self.options.get('minimize_to_string'): touch_watchdog_file() self._minimize_to_string(testcase) @@ -153,7 +153,7 @@ def _post_report(self, testcase): # clean up testcase.delete_files() - def _mininimize_to_seedfile(self, testcase): + def _minimize_to_seedfile(self, testcase): self._minimize_generic(testcase, sftarget=True, confidence=0.999) # calculate the hamming distances for this crash # between the original seedfile and the minimized fuzzed file diff --git a/src/certfuzz/tools/windows/zipdiff.py b/src/certfuzz/tools/common/zipdiff.py similarity index 96% rename from src/certfuzz/tools/windows/zipdiff.py rename to src/certfuzz/tools/common/zipdiff.py index 345c13c..cfa636c 100644 --- a/src/certfuzz/tools/windows/zipdiff.py +++ b/src/certfuzz/tools/common/zipdiff.py @@ -1,74 +1,74 @@ -''' -Created on Jul 10, 2013 - -@organization: cert.org -''' - -import os -import collections -import zipfile -from optparse import OptionParser - -saved_arcinfo = collections.OrderedDict() - - -def readzip(filepath): - global savedarcinfo - # If the seed is zip-based, fuzz the contents rather than the container - tempzip = zipfile.ZipFile(filepath, 'r') - - ''' - get info on all the archived files and concatentate their contents - into self.input - ''' - unzippedbytes = '' - for i in tempzip.namelist(): - data = tempzip.read(i) - - # save split indices and compression type for archival reconstruction - - saved_arcinfo[i] = (len(unzippedbytes), len(data)) - unzippedbytes += data - tempzip.close() - return unzippedbytes - - -def main(): - global saved_arcinfo - usage = 'usage: %prog zip1 zip2' - parser = OptionParser(usage=usage) - (options, args) = parser.parse_args() - - if len(args) != 2: - parser.error('Incorrect number of arguments') - return - - changedbytes = [] - changedfiles = [] - - zip1 = args[0] - zip2 = args[1] - zip1bytes = readzip(zip1) - zip2bytes = readzip(zip2) - zip1len = len(zip1bytes) - - if zip1len != len(zip2bytes): - print 'Zip contents are not the same size. Aborting.' - - for i in range(0, zip1len): - if zip1bytes[i] != zip2bytes[i]: -# print 'Zip contents differ at offset %s' % i - changedbytes.append(i) - - for changedbyte in changedbytes: - for name, info in saved_arcinfo.iteritems(): - startaddr = info[0] - endaddr = info[0] + info[1] - if startaddr <= changedbyte <= endaddr and name not in changedfiles: - print '%s modified' % name - changedfiles.append(name) - #print '%s: %s-%s' %(name, info[0], info[0]+info[1]) - - -if __name__ == '__main__': - main() +''' +Created on Jul 10, 2013 + +@organization: cert.org +''' + +import os +import collections +import zipfile +from optparse import OptionParser + +saved_arcinfo = collections.OrderedDict() + + +def readzip(filepath): + global savedarcinfo + # If the seed is zip-based, fuzz the contents rather than the container + tempzip = zipfile.ZipFile(filepath, 'r') + + ''' + get info on all the archived files and concatentate their contents + into self.input + ''' + unzippedbytes = '' + for i in tempzip.namelist(): + data = tempzip.read(i) + + # save split indices and compression type for archival reconstruction + + saved_arcinfo[i] = (len(unzippedbytes), len(data)) + unzippedbytes += data + tempzip.close() + return unzippedbytes + + +def main(): + global saved_arcinfo + usage = 'usage: %prog zip1 zip2' + parser = OptionParser(usage=usage) + (options, args) = parser.parse_args() + + if len(args) != 2: + parser.error('Incorrect number of arguments') + return + + changedbytes = [] + changedfiles = [] + + zip1 = args[0] + zip2 = args[1] + zip1bytes = readzip(zip1) + zip2bytes = readzip(zip2) + zip1len = len(zip1bytes) + + if zip1len != len(zip2bytes): + print 'Zip contents are not the same size. Aborting.' + + for i in range(0, zip1len): + if zip1bytes[i] != zip2bytes[i]: +# print 'Zip contents differ at offset %s' % i + changedbytes.append(i) + + for changedbyte in changedbytes: + for name, info in saved_arcinfo.iteritems(): + startaddr = info[0] + endaddr = info[0] + info[1] + if startaddr <= changedbyte <= endaddr and name not in changedfiles: + print '%s modified' % name + changedfiles.append(name) + #print '%s: %s-%s' %(name, info[0], info[0]+info[1]) + + +if __name__ == '__main__': + main() From 801fc25168d28269b38052ecd589087b1abc6ee5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 19 Aug 2015 11:25:07 -0400 Subject: [PATCH 0624/1169] Fix drillresults on Windows BFF-821 --- src/certfuzz/drillresults/result_driller_windows.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index 1f9dd1e..f038c66 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -23,7 +23,6 @@ def _platform_find_testcases(self, crash_hash, files, root): # Create dictionary for hashes in results dictionary hash_dict = {} hash_dict['hash'] = crash_hash - self.results[crash_hash] = hash_dict crasherfile = '' # Check each of the files in the hash directory for current_file in files: From 8304544286ad3f894598d1f2d7e59abed9911fbd Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 20 Aug 2015 11:59:48 -0400 Subject: [PATCH 0625/1169] Don't let minimizer failures kill the campaign. BFF-834 --- src/certfuzz/iteration/iteration_base3.py | 7 +++- src/certfuzz/iteration/iteration_windows.py | 4 +++ src/certfuzz/minimizer/minimizer_base.py | 35 +++++++++++-------- .../testcase_pipeline/tc_pipeline_windows.py | 18 ++++++---- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 6df75b2..edf3c6f 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -81,7 +81,12 @@ def __exit__(self, etype, value, traceback): return handled # clean up - rm_rf(self.working_dir) + try: + rm_rf(self.working_dir) + except: + # TODO: Minimizer may have a log file handle open + # If we get here, we've left files behind + pass return handled def _pre_fuzz(self): diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 90df43f..81dd1f0 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -16,6 +16,7 @@ from certfuzz.fuzzers.errors import FuzzerError from certfuzz.fuzzers.errors import FuzzerExhaustedError from certfuzz.fuzzers.errors import FuzzerInputMatchesOutputError +from certfuzz.minimizer.errors import MinimizerError from certfuzz.fuzztools.filetools import delete_files_or_dirs from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.runners.errors import RunnerRegistryError @@ -83,6 +84,9 @@ def __exit__(self, etype, value, traceback): elif etype is FuzzerError: logger.warning('Failed to fuzz, Skipping seed %d.', self.seednum) handled = True + elif etype is MinimizerError: + logger.warning('Failed to minimize %d, Continuing.', self.seednum) + handled = True elif etype is DebuggerFileError: logger.warning('Failed to debug, Skipping seed %d', self.seednum) handled = True diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 4d38eab..008d44d 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -41,9 +41,9 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, logfile=None, tempdir=None, maxtime=3600, preferx=False, keep_uniq_faddr=False, watchcpu=False): if not cfg: - raise MinimizerError('Config must be specified') + self._raise('Config must be specified') if not crash: - raise MinimizerError('Crasher must be specified') + self._raise('Crasher must be specified') self.cfg = cfg self.crash = crash @@ -88,7 +88,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, minf = '%s-minimized%s' % (self.crash.fuzzedfile.root, self.crash.fuzzedfile.ext) minlog = os.path.join(self.crash.fuzzedfile.dirname, 'minimizer_log.txt') if not os.path.exists(self.crash.seedfile.path): - raise MinimizerError('Seedfile not found at %s' % + self._raise('Seedfile not found at %s' % self.crash.seedfile.path) elif self.preferx: minf = '%s-min-%s%s' % (self.crash.fuzzedfile.root, self.minchar, self.crash.fuzzedfile.ext) @@ -110,9 +110,9 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, self.crash_dst = self.crash.fuzzedfile.dirname if not os.path.exists(self.crash_dst): - raise MinimizerError("%s does not exist" % self.crash_dst) + self._raise("%s does not exist" % self.crash_dst) if not os.path.isdir(self.crash_dst): - raise MinimizerError("%s is not a directory" % self.crash_dst) + self._raise("%s is not a directory" % self.crash_dst) self.debugger = debugger_get() @@ -120,7 +120,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, self.logger.info("Minimizer initializing for %s", self.crash.fuzzedfile.path) if not os.path.exists(self.crash.fuzzedfile.path): - raise MinimizerError("%s does not exist" % self.crash.fuzzedfile.path) + self._raise("%s does not exist" % self.crash.fuzzedfile.path) self.crash.set_debugger_template('bt_only') @@ -153,7 +153,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, # none of this will work if the files are of different size if len(self.seed) != len(self.fuzzed_content): - raise MinimizerError('Minimizer requires seed and fuzzed_content to have the same length. %d != %d' % (len(self.seed), len(self.fuzzed_content))) + self._raise('Minimizer requires seed and fuzzed_content to have the same length. %d != %d' % (len(self.seed), len(self.fuzzed_content))) # initialize the hamming distance self.start_distance = self.hd_func(self.seed, self.fuzzed_content) @@ -192,15 +192,15 @@ def __enter__(self): if not self._is_crash_to_minimize(): msg = 'Unable to minimize: No crash' self.logger.info(msg) - raise MinimizerError(msg) + self._raise(msg) if self._is_already_minimized(): msg = 'Unable to minimize: Already minimized' self.logger.info(msg) - raise MinimizerError(msg) + self._raise(msg) if self.crash.debugger_missed_stack_corruption: msg = 'Unable to minimize: Stack corruption crash, which the debugger missed.' self.logger.info(msg) - raise MinimizerError(msg) + self._raise(msg) # start the timer self.start_time = time.time() return self @@ -218,6 +218,13 @@ def __exit__(self, etype, value, traceback): # # it's okay if we can't # pass + def _raise(description): + # Minimizer has separate logging. Close up handles before + # raising any exception + self.log_file_hdlr.close() + self.logger.removeHandler(self.log_file_hdlr) + raise MinimizerError(description) + def _read_fuzzed(self): ''' returns the contents of the fuzzed_content file @@ -280,7 +287,7 @@ def _writezip(self): split self.fuzzed ''' if self.saved_arcinfo is None: - raise WindowsMinimizerError('_readzip was not called') + self._raise('_readzip was not called') filedata = ''.join(self.newfuzzed) filepath = self.tempfile @@ -307,9 +314,9 @@ def _logger_setup(self): dirname = os.path.dirname(self.minimizer_logfile) if not os.path.exists(dirname): - raise MinimizerError('Directory should already exist: %s' % dirname) + self._raise('Directory should already exist: %s' % dirname) if os.path.exists(self.minimizer_logfile): - raise MinimizerError('Log file must not already exist: %s' % self.minimizer_logfile) + self._raise('Log file must not already exist: %s' % self.minimizer_logfile) self.logger = logging.getLogger(__name__) self.log_file_hdlr = logging.FileHandler(self.minimizer_logfile) self.logger.addHandler(self.log_file_hdlr) @@ -410,7 +417,7 @@ def _crash_builder(self): outfile = f if os.path.exists(outfile): - raise MinimizerError('Outfile should not already exist: %s' % outfile) + self._raise('Outfile should not already exist: %s' % outfile) self.logger.debug('\tCopying %s to %s', self.tempfile, outfile) filetools.copy_file(self.tempfile, outfile) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 262913e..1f3784c 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -63,13 +63,17 @@ def _minimize(self, testcase): 'maxtime': self.cfg['runoptions']['minimizer_timeout'] } - with Minimizer(**kwargs) as minimizer: - minimizer.go() - - # minimzer found other crashes, so we should add them - # to our list for subsequent processing - for tc in minimizer.other_crashes.values(): - self.tc_candidate_q.put(tc) + try: + with Minimizer(**kwargs) as minimizer: + minimizer.go() + + # minimzer found other crashes, so we should add them + # to our list for subsequent processing + for tc in minimizer.other_crashes.values(): + self.tc_candidate_q.put(tc) + except: + # Minimizer failed for some reason. Don't stop campaign! + pass def _analyze(self, testcase): pass From 676da00f7cfa83997f38cc818c8b45d256e0deea Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 24 Aug 2015 15:52:44 -0400 Subject: [PATCH 0626/1169] Don't complain when debugger output doesn't have what you're looking for. BFF-835 --- src/certfuzz/drillresults/testcasebundle_base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 8643a69..cdd0df0 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -143,10 +143,12 @@ def _parse_testcase(self): # No faulting address means no crash. if not faultaddr: - raise TestCaseBundleError('No faulting address means no crash') + # raise TestCaseBundleError('No faulting address means no crash') + return if not instraddr: - raise TestCaseBundleError('No instraddr address means no crash') + #raise TestCaseBundleError('No instraddr address means no crash') + return faultaddr, instraddr = self._64bit_addr_fixup(faultaddr, instraddr) From fe0bd943150afd6b0d102d04d27a685a4a039bee Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 24 Aug 2015 17:10:10 -0400 Subject: [PATCH 0627/1169] Fix tools/minimizer_plot.py BFF-836 --- src/certfuzz/tools/linux/minimizer_plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/linux/minimizer_plot.py b/src/certfuzz/tools/linux/minimizer_plot.py index 925ffeb..064ae66 100755 --- a/src/certfuzz/tools/linux/minimizer_plot.py +++ b/src/certfuzz/tools/linux/minimizer_plot.py @@ -142,7 +142,7 @@ def main(): if options.cfgfile: cfg_file = options.cfgfile else: - cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') + cfg_file = os.path.join('conf.d', 'bff.yaml') if options.dir: result_dir = options.dir @@ -152,7 +152,7 @@ def main(): with cfg: pass - result_dir = cfg.crashers_dir + result_dir = os.path.join(cfg.output_dir, cfg.campaign_id, 'crashers') logger.info('Reading results from %s', result_dir) log = None From 08c4e3fd176488c186b53105b9c652299732a6eb Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 24 Aug 2015 17:17:55 -0400 Subject: [PATCH 0628/1169] Remove create_crasher_script.py I can't imagine that it even applies to any recent BFF. --- .../tools/linux/create_crasher_script.py | 125 ------------------ src/linux/tools/create_crasher_script.py | 20 --- 2 files changed, 145 deletions(-) delete mode 100755 src/certfuzz/tools/linux/create_crasher_script.py delete mode 100755 src/linux/tools/create_crasher_script.py diff --git a/src/certfuzz/tools/linux/create_crasher_script.py b/src/certfuzz/tools/linux/create_crasher_script.py deleted file mode 100755 index b7e9ab3..0000000 --- a/src/certfuzz/tools/linux/create_crasher_script.py +++ /dev/null @@ -1,125 +0,0 @@ -''' -Created on Jan 13, 2011 - -@organization: cert.org - -''' - -import logging -from optparse import OptionParser -import os -import re -import sys -from certfuzz.config.config_linux import LinuxConfig - - -parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, parent_path) - -logger = logging.getLogger(__name__) -# set default logging level (override with command line options) -logger.setLevel(logging.INFO) -stdout_hdlr = logging.StreamHandler(sys.stdout) -stderr_hdlr = logging.StreamHandler(sys.stderr) -stderr_hdlr.setLevel(logging.WARNING) - -logger.addHandler(stdout_hdlr) -logger.addHandler(stderr_hdlr) - - -def parse_options(): - usage = "usage: %prog [options] " - - parser = OptionParser(usage) - parser.add_option("-d", "--debug", dest="debug", help="Turn on debugging output", action='store_true', default=False) - parser.add_option("-F", "--config", dest="cfgfile", help="read config data from CFGFILE") - parser.add_option("-o", '--outfile', dest='outfile', help="write script to OUTFILE instead of stdout") - parser.add_option("", "--force", dest="force", help="Force overwrite of existing OUTFILE", action='store_true', default=False) - parser.add_option('', '--destination', dest='dest', help="Replace output location in script with DEST") - - options, args = parser.parse_args() - if not len(args): - parser.print_help() - parser.error("Please specify a crash_id") - return options, args - - -def main(): - options, args = parse_options() - - if options.debug: - logger.setLevel(logging.DEBUG) - - file_logger = False - if options.outfile: - file_logger = logging.getLogger('outfile') - file_logger.setLevel(logging.INFO) - if os.path.exists(options.outfile) and not options.force: - logging.warning('%s exists, use --force to overwrite', options.outfile) - sys.exit() - hdlr = logging.FileHandler(options.outfile, 'w') - fmt = logging.Formatter('%(message)s') - hdlr.setFormatter(fmt) - hdlr.setLevel(logging.INFO) - file_logger.addHandler(hdlr) - # since we're logging to a file, we can suppress output to stdout - # but we still want to keep warnings - if not options.debug: - logger.removeHandler(stdout_hdlr) - - if options.cfgfile: - cfg_file = options.cfgfile - else: - cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') - - logger.debug('Using config file: %s', cfg_file) - - cfg = LinuxConfig(cfg_file) - with cfg: - pass - - result_dir = cfg.crashers_dir - logger.debug('Reading results from %s', result_dir) - - for crash_id in args: - logger.debug('Crash_id=%s', crash_id) - crash_dir = os.path.join(result_dir, crash_id) - if not os.path.isdir(crash_dir): - logger.debug('%s is not a dir', crash_dir) - continue - - logger.debug('Looking for crash log in %s', crash_dir) - log = os.path.join(crash_dir, crash_id + '.log') - if not os.path.exists(log): - logger.warning('No log found at %s', log) - continue - - logger.debug('Found log at %s', log) - - f = open(log, 'r') - try: - for l in f.readlines(): - m = re.match('^Command:\s+(.+)$', l) - if not m: - continue - - cmdline = m.group(1) - - if options.dest: - (cmd, dst) = [x.strip() for x in cmdline.split('>')] - filename = os.path.basename(dst) - logger.debug(filename) - new_dst = os.path.join(options.dest, filename) - logger.debug(new_dst) - cmdline = ' > '.join((cmd, new_dst)) - - if file_logger: - file_logger.info(cmdline) - else: - logger.info(cmdline) - finally: - f.close() - - -if __name__ == '__main__': - main() diff --git a/src/linux/tools/create_crasher_script.py b/src/linux/tools/create_crasher_script.py deleted file mode 100755 index cdfe062..0000000 --- a/src/linux/tools/create_crasher_script.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -''' -Created on Jan 13, 2011 - -@organization: cert.org - -''' -import os -import sys -try: - from certfuzz.tools.linux.create_crasher_script import main -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.tools.linux.create_crasher_script import main - -if __name__ == '__main__': - main() From 0ce41ffa8b7730d3545d9035a7396fd412bf476a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 24 Aug 2015 17:18:24 -0400 Subject: [PATCH 0629/1169] Make other ./tools scripts work as well --- src/certfuzz/tools/linux/bff_stats.py | 4 ++-- src/linux/tools/callsim.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/tools/linux/bff_stats.py b/src/certfuzz/tools/linux/bff_stats.py index 4a0c363..5be2658 100755 --- a/src/certfuzz/tools/linux/bff_stats.py +++ b/src/certfuzz/tools/linux/bff_stats.py @@ -98,14 +98,14 @@ def main(): if options.cfgfile: cfg_file = options.cfgfile else: - cfg_file = os.path.join(parent_path, 'conf.d', 'bff.cfg') + cfg_file = os.path.join('conf.d', 'bff.yaml') logger.debug('Using config file: %s', cfg_file) cfg = LinuxConfig(cfg_file) with cfg: pass - result_dir = cfg.crashers_dir + result_dir = os.path.join(cfg.output_dir, cfg.campaign_id, 'crashers') logger.debug('Reading results from %s', result_dir) counters = {} diff --git a/src/linux/tools/callsim.py b/src/linux/tools/callsim.py index 14ac497..76fd77d 100755 --- a/src/linux/tools/callsim.py +++ b/src/linux/tools/callsim.py @@ -7,13 +7,13 @@ import os import sys try: - from from certfuzz.tools.linux.callsim import main + from certfuzz.tools.linux.callsim import main except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) - from from certfuzz.tools.linux.callsim import main + from certfuzz.tools.linux.callsim import main if __name__ == '__main__': main() From 4941845a3075db2c3677259855ae1b1908b70818 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 24 Aug 2015 17:19:23 -0400 Subject: [PATCH 0630/1169] Try to close handle on raise, but continue --- src/certfuzz/minimizer/minimizer_base.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 008d44d..3d7975d 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -218,11 +218,14 @@ def __exit__(self, etype, value, traceback): # # it's okay if we can't # pass - def _raise(description): + def _raise(self, description): # Minimizer has separate logging. Close up handles before # raising any exception - self.log_file_hdlr.close() - self.logger.removeHandler(self.log_file_hdlr) + try: + self.log_file_hdlr.close() + self.logger.removeHandler(self.log_file_hdlr) + except: + pass raise MinimizerError(description) def _read_fuzzed(self): From ae9232a16905d2c60f01c2902ec86f7baff6d71c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 26 Aug 2015 17:31:53 -0400 Subject: [PATCH 0631/1169] Can also get EOFError on loading the pickle --- src/certfuzz/drillresults/result_driller_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/drillresults/result_driller_base.py b/src/certfuzz/drillresults/result_driller_base.py index 7201897..29f5621 100644 --- a/src/certfuzz/drillresults/result_driller_base.py +++ b/src/certfuzz/drillresults/result_driller_base.py @@ -87,7 +87,7 @@ def load_cached(self): try: with open(self.pickle_file, 'rb') as pkl_file: self.cached_testcases = pickle.load(pkl_file) - except IOError: + except (IOError, EOFError): # No cached results pass From 0b35ee2d6aa2446c4b84db3d235545ee1c27ec6b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 26 Aug 2015 17:32:57 -0400 Subject: [PATCH 0632/1169] Prevent runaway memory usage by drillresults. BFF-837 --- src/certfuzz/drillresults/testcasebundle_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index cdd0df0..3a09485 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -92,6 +92,8 @@ def __enter__(self): return self def __exit__(self, etype, value, traceback): + # Explicitly remove crasherdata to prevent runaway memory usage + self.crasherdata = '' pass def _find_testcase_file(self): From 7171e204f7f9502fd244b02dbcd0156a399f4fb1 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 28 Aug 2015 15:50:16 -0400 Subject: [PATCH 0633/1169] Add feature to keep other crashers BFF-838 --- src/certfuzz/tools/linux/minimize.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 820f586..54570d6 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -63,6 +63,9 @@ def main(): parser.add_option('', '--timeout', dest='timeout', metavar='N', type='int', default=0, help='Stop minimizing after N seconds (default is 0, never time out).') + parser.add_option('-k', '--keepothers', dest='keep_other_crashes', + action='store_true', + help='Keep other crash hashes encountered during minimization') (options, args) = parser.parse_args() @@ -143,20 +146,23 @@ def main(): cfg.debugger_timeout, cfg.killprocname, cfg.backtracelevels, crashers_dir, options.keep_uniq_faddr) as crash: + crash.tempdir = outdir filetools.make_directories(crash.tempdir) logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) filetools.copy_file(fuzzed_file.path, crash.tempdir) + minlog = os.path.join(outdir, 'min_log.txt') + with Minimizer(cfg=cfg, crash=crash, crash_dst_dir=outdir, seedfile_as_target=min2seed, bitwise=options.bitwise, confidence=confidence, - logfile='./min_log.txt', - tempdir=crash.tempdir, + logfile=minlog, + tempdir=outdir, maxtime=options.timeout, preferx=options.prefer_x_target, keep_uniq_faddr=options.keep_uniq_faddr) as minimize: - minimize.save_others = False + minimize.save_others = options.keep_other_crashes minimize.target_size_guess = int(options.initial_target_size) minimize.go() @@ -181,9 +187,12 @@ def main(): with open(metasploit_file, 'wb') as f: f.writelines(targetstring) - - crash.copy_files(outdir) - crash.clean_tmpdir() + raw_input('attach debugger') + for othercrash in minimize.other_crashes: + othercrashdir = os.path.join(outdir, minimize.other_crashes[othercrash].tempdir) + outcrashdir = os.path.join(outdir, os.path.basename(othercrashdir)) + filetools.mkdir_p(outcrashdir) + minimize.other_crashes[othercrash].copy_files(outcrashdir) if __name__ == '__main__': main() From 22efd03d5ec297f83a69fed3fca3aafab6c83dbc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 28 Aug 2015 16:03:35 -0400 Subject: [PATCH 0634/1169] Reap tmp on testcase pipline exit --- src/certfuzz/file_handlers/tmp_reaper.py | 1 + src/certfuzz/minimizer/minimizer_base.py | 4 ---- src/certfuzz/testcase_pipeline/tc_pipeline_base.py | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/file_handlers/tmp_reaper.py b/src/certfuzz/file_handlers/tmp_reaper.py index 6f5599f..83c475e 100644 --- a/src/certfuzz/file_handlers/tmp_reaper.py +++ b/src/certfuzz/file_handlers/tmp_reaper.py @@ -23,6 +23,7 @@ def __init__(self): ''' Constructor ''' + logger.debug('Reaping tmp...') self.tmp_dir = tempfile.gettempdir() if platform.system() == 'Windows': self.clean_tmp = self.clean_tmp_windows diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 3d7975d..e3b8ab5 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -17,7 +17,6 @@ from certfuzz.debuggers.registration import get as debugger_get from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.fuzztools import hamming, filetools, probability, text from certfuzz.fuzztools.filetools import delete_files, write_file, check_zip_file from certfuzz.fuzztools.filetools import exponential_backoff @@ -614,9 +613,6 @@ def go(self): got_hit = False while self.consecutive_misses <= self.n_misses_allowed: - # clean the /tmp directory so we don't fill up the disk across tries - # TODO - This cleans up files that we're working with! (standalone minimizer) - # TmpReaper().clean_tmp() if self.use_watchdog: # touch the watchdog file so we don't reboot during long minimizations diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 12cf42d..64fab85 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -14,6 +14,7 @@ from certfuzz.fuzztools import filetools from certfuzz.helpers.coroutine import coroutine from certfuzz.testcase_pipeline.errors import TestCasePipelineError +from certfuzz.file_handlers.tmp_reaper import TmpReaper logger = logging.getLogger(__name__) @@ -57,7 +58,7 @@ def __enter__(self): return self.go def __exit__(self, etype, value, traceback): - pass + TmpReaper().clean_tmp() @abc.abstractmethod def _setup_analyzers(self): From fd02cad34f4313113a0a9b00f243a48c98d84a81 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 31 Aug 2015 16:40:18 -0400 Subject: [PATCH 0635/1169] Don't change directory into target. Standalone minimizer uses relative dirs. BFF-311 --- src/certfuzz/debuggers/msec.py | 2 +- src/certfuzz/tools/windows/minimize.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 36ddf4f..203339a 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -94,7 +94,7 @@ def run_with_timer(self): args = self._get_cmdline(self.outfile) p = Popen(args, stdout=open(os.devnull), stderr=open(os.devnull), - cwd=targetdir, universal_newlines=True) + universal_newlines=True) if self.watchcpu == True: wmiInterface = wmi.WMI() diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 671c618..e4c324f 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -134,7 +134,7 @@ def main(): if options.outdir: outdir = options.outdir else: - outdir = './minimizer_out' + outdir = 'minimizer_out' filetools.make_directories(outdir) From 2a1c5732d6604c9010c1751cc9de5068a1d94763 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 2 Sep 2015 13:37:16 -0400 Subject: [PATCH 0636/1169] Fix touching the watchdog file. BFF-829 --- src/certfuzz/campaign/campaign_linux.py | 1 - src/certfuzz/config/config_linux.py | 3 +-- src/certfuzz/file_handlers/watchdog_file.py | 3 +-- src/certfuzz/test/config/test_config_linux.py | 1 - src/linux/conf.d/bff.yaml | 1 - 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 5ce1919..63be510 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -132,7 +132,6 @@ def _cache_app(self): def _setup_watchdog(self): logger.debug('setup watchdog') # setup our watchdog file toucher - TWDF.remote_d = self.config.remote_dir TWDF.wdf = self.config.watchdogfile TWDF.enable() touch_watchdog_file() diff --git a/src/certfuzz/config/config_linux.py b/src/certfuzz/config/config_linux.py index 1130cf1..1f32b7b 100644 --- a/src/certfuzz/config/config_linux.py +++ b/src/certfuzz/config/config_linux.py @@ -38,8 +38,7 @@ 'savefailedasserts': bool, 'recycle_crashers': bool, }, - 'directories': {'remote_dir': str, - 'seedfile_origin_dir': str, + 'directories': {'seedfile_origin_dir': str, 'debugger_template_dir': str, 'local_dir': str, 'output_dir': str, diff --git a/src/certfuzz/file_handlers/watchdog_file.py b/src/certfuzz/file_handlers/watchdog_file.py index 3683905..0eb3c81 100644 --- a/src/certfuzz/file_handlers/watchdog_file.py +++ b/src/certfuzz/file_handlers/watchdog_file.py @@ -23,8 +23,7 @@ def _noop(self, *_args, **_kwargs): pass def _twdf(self): - if os.access(self.remote_d, os.W_OK): - open(self.wdf, 'w').close() + open(self.wdf, 'w').close() TWDF = Twdf() diff --git a/src/certfuzz/test/config/test_config_linux.py b/src/certfuzz/test/config/test_config_linux.py index 2251369..91f370d 100644 --- a/src/certfuzz/test/config/test_config_linux.py +++ b/src/certfuzz/test/config/test_config_linux.py @@ -23,7 +23,6 @@ cmdline: ~/convert $SEEDFILE /dev/null killprocname: convert directories: - remote_dir: ~/bff &remote_dir seedfile_origin_dir: ~/bff/seedfiles/examples debugger_template_dir: ~/bff/certfuzz/debuggers/templates output_dir: ~/results diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index c3c1d0e..be38a5c 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -52,7 +52,6 @@ target: # if fuzzer is still running directories: - remote_dir: ~/bff &remote_dir seedfile_origin_dir: ~/bff/seedfiles/examples debugger_template_dir: ~/bff/certfuzz/debuggers/templates output_dir: ~/results From 88376e71ce226b52039dc3b71525a98cc77a89e7 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 2 Sep 2015 13:47:29 -0400 Subject: [PATCH 0637/1169] Don't explicitly increment tries count. It's done in the base class __exit__. BFF-841 --- src/certfuzz/iteration/iteration_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 38adbe7..de7bf5f 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -84,7 +84,7 @@ def _run(self): self.runner.run() def _post_run(self): - self.record_tries() + #self.record_tries() if not self.runner.saw_crash: logger.debug('No crash seen') From cb61bd87e63b0d805ffa904771b9bb0e8a2894e1 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 2 Sep 2015 13:49:06 -0400 Subject: [PATCH 0638/1169] Only add minimizer to pipeline for minimizable fuzzers. BFF-840 --- src/certfuzz/iteration/iteration_base3.py | 3 ++- src/certfuzz/testcase_pipeline/tc_pipeline_base.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index edf3c6f..db144dc 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -150,7 +150,8 @@ def process_testcases(self): cfg=self.cfg, options=self.pipeline_options, outdir=self.outdir, - workdirbase=self.working_dir) as pipeline: + workdirbase=self.working_dir, + minimizable=self.fuzzer.is_minimizable) as pipeline: pipeline() def go(self): diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 64fab85..b37f64e 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -28,7 +28,7 @@ class TestCasePipelineBase(object): pipes = ['verify', 'minimize', 'analyze', 'report'] def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, - outdir=None, workdirbase=None): + outdir=None, workdirbase=None, minimizable=None): ''' Constructor ''' @@ -39,6 +39,7 @@ def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, self.tc_dir = os.path.join(self.outdir, 'crashers') self.working_dir = workdirbase + self.minimizable = minimizable self.tc_candidate_q = Queue.Queue() @@ -70,6 +71,8 @@ def _setup_analysis_pipeline(self): logger.debug('Construct analysis pipeline') setup_order = list(self.pipes) + if not self.minimizable: + setup_order.remove('minimize') setup_order.reverse() pipeline = None From f404afbac2d5798e340cc2bb6a6795365f46c41c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 2 Sep 2015 13:50:22 -0400 Subject: [PATCH 0639/1169] Fix drop and other similar mutators. BFF-839 --- src/certfuzz/file_handlers/seedfile.py | 2 ++ src/certfuzz/iteration/iteration_base3.py | 14 ++++++++++++-- src/certfuzz/iteration/iteration_linux.py | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/file_handlers/seedfile.py b/src/certfuzz/file_handlers/seedfile.py index c99bbe0..e2a812a 100644 --- a/src/certfuzz/file_handlers/seedfile.py +++ b/src/certfuzz/file_handlers/seedfile.py @@ -46,6 +46,8 @@ def __init__(self, output_base_dir, path): self.range_min = 0 self.range_max = 1 + self.tries = 0 + self.rangefinder = RangeFinder(self.range_min, self.range_max) def __getstate__(self): diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index db144dc..f6c7f32 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -130,14 +130,24 @@ def run(self): def record_success(self): self.sf_set.record_success(key=self.seedfile.md5) - self.rf.record_success(key=self.r.id) + if self.r is not None: + # Fuzzer uses rangefinder + self.rf.record_success(key=self.r.id) + else: + # Fuzzer without rangefinder + self.seedfile.tries += 1 def record_failure(self): self.record_tries() def record_tries(self): self.sf_set.record_tries(key=self.seedfile.md5, tries=1) - self.rf.record_tries(key=self.r.id, tries=1) + if self.r is not None: + # Fuzzer uses rangefinder + self.rf.record_tries(key=self.r.id, tries=1) + else: + # Fuzzer without rangefinder + self.seedfile.tries += 1 def process_testcases(self): if not len(self.testcases): diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index de7bf5f..9682eec 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -13,7 +13,7 @@ from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.testcase_pipeline.tc_pipeline_linux import LinuxTestCasePipeline from certfuzz.runners.zzufrun import ZzufRunner -from certfuzz.fuzzers.bytemut import ByteMutFuzzer +from certfuzz.fuzzers.bytemut import ByteMutFuzzer as Fuzzer logger = logging.getLogger(__name__) @@ -56,7 +56,7 @@ def __enter__(self): def _pre_fuzz(self): fuzz_opts = self.cfg.config['fuzzer'] - self.fuzzer = ByteMutFuzzer(self.seedfile, + self.fuzzer = Fuzzer(self.seedfile, self.working_dir, self.seednum, fuzz_opts) From 0e084eb093d0a0e468040ec811e3f64ffdca2c8d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 2 Sep 2015 16:56:31 -0400 Subject: [PATCH 0640/1169] Don't attempt to annotate callgrind if valgrind is disabled. Conditional logging based on whether we know HD and ranges. BFF-843 --- .../testcase_pipeline/tc_pipeline_linux.py | 32 +++++++++++++------ src/certfuzz/tools/linux/minimize.py | 2 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 7519fc2..f483753 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -126,18 +126,32 @@ def _analyze(self, testcase): TestCasePipelineBase._analyze(self, testcase) def _post_analyze(self, testcase): - logger.info('Annotating callgrind output') - try: - annotate_callgrind(testcase) - annotate_callgrind_tree(testcase) - except CallgrindAnnotateEmptyOutputFileError: - logger.warning('Unexpected empty output from annotate_callgrind. Continuing') - except CallgrindAnnotateMissingInputFileError: - logger.warning('Missing callgrind output. Continuing') + if self.options.get('use_valgrind'): + logger.info('Annotating callgrind output') + try: + annotate_callgrind(testcase) + annotate_callgrind_tree(testcase) + except CallgrindAnnotateEmptyOutputFileError: + logger.warning('Unexpected empty output from annotate_callgrind. Continuing') + except CallgrindAnnotateMissingInputFileError: + logger.warning('Missing callgrind output. Continuing') def _pre_report(self, testcase): uniqlogger = get_uniq_logger(self.options.get('uniq_log')) - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) + if testcase.hd_bits is not None: + # We know HD info, since we minimized + if testcase.range is not None: + # Fuzzer specifies a range + uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) + else: + uniqlogger.info('%s crash_id=%s seed=%d bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.hd_bits, testcase.hd_bytes) + else: + # We don't know the HD info + if testcase.range is not None: + # We have a fuzzer that uses a range + uniqlogger.info('%s crash_id=%s seed=%d range=%s', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range) + else: + uniqlogger.info('%s crash_id=%s seed=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum) logger.info('%s first seen at %d', testcase.signature, testcase.seednum) def _report(self, testcase): diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 54570d6..8720faf 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -187,7 +187,7 @@ def main(): with open(metasploit_file, 'wb') as f: f.writelines(targetstring) - raw_input('attach debugger') + for othercrash in minimize.other_crashes: othercrashdir = os.path.join(outdir, minimize.other_crashes[othercrash].tempdir) outcrashdir = os.path.join(outdir, os.path.basename(othercrashdir)) From 14d179eb2530268e94ed1a0aa4d5982f2f73b018 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 2 Sep 2015 17:42:19 -0400 Subject: [PATCH 0641/1169] Remove seedfiles from the set as the fuzzer exhausts them. BFF-845 --- src/certfuzz/campaign/campaign_base.py | 23 +++++++++++++++++++--- src/certfuzz/file_handlers/seedfile_set.py | 4 ++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index bad6704..b5fbaf0 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -20,6 +20,8 @@ from certfuzz.fuzztools import filetools from certfuzz.runners.errors import RunnerArchitectureError, \ RunnerPlatformVersionError +from certfuzz.fuzzers.errors import FuzzerExhaustedError +from certfuzz.file_handlers.errors import SeedfileSetError from certfuzz.version import __version__ from certfuzz.file_handlers.tmp_reaper import TmpReaper import gc @@ -96,6 +98,7 @@ def __init__(self, config_file, result_dir=None, debug=False): self.outdir_base = None self.outdir = None self.sf_set_out = None + self.stopfuzzing = False if result_dir: self.outdir_base = os.path.abspath(result_dir) @@ -365,7 +368,10 @@ def _keep_going(self): ''' Returns True if a campaign should proceed. False otherwise. ''' - return True + if self.stopfuzzing: + return False + else: + return True def _do_interval(self): ''' @@ -375,7 +381,13 @@ def _do_interval(self): TmpReaper().clean_tmp() # choose seedfile - sf = self.seedfile_set.next_item() + try: + sf = self.seedfile_set.next_item() + except SeedfileSetError: + logger.info('Seedfile set is empty. Terminating') + self.stopfuzzing = True + return + logger.info('Selected seedfile: %s', sf.basename) # TODO: restore this @@ -394,7 +406,12 @@ def _do_interval(self): # note that range does not include interval_limit logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) for seednum in xrange(self.current_seed, interval_limit): - self._do_iteration(sf, r, qf, seednum) + try: + self._do_iteration(sf, r, qf, seednum) + except FuzzerExhaustedError: + logger.info('Fuzzer exhausted for seedfile %s' % sf.basename) + self.seedfile_set.remove_file(sf) + break del sf # manually collect garbage diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index e4e3b73..331141c 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -84,6 +84,10 @@ def add_file(self, *files): logger.info('Adding file to set: %s', seedfile.path) self.add_item(seedfile.md5, seedfile) + def remove_file(self, seedfile): + logger.info('Removing file from set: %s', seedfile.md5) + self.del_item(seedfile.md5) + def copy_file_from_origin(self, f): if (os.path.basename(f.path) == '.DS_Store'): return 0 From aa7ea4f680d1821f1ddae1385f9d13bdfdd12053 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 2 Sep 2015 17:52:40 -0400 Subject: [PATCH 0642/1169] Use seedfile basename instead of md5. --- src/certfuzz/file_handlers/seedfile_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 331141c..7a90de8 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -85,7 +85,7 @@ def add_file(self, *files): self.add_item(seedfile.md5, seedfile) def remove_file(self, seedfile): - logger.info('Removing file from set: %s', seedfile.md5) + logger.info('Removing file from set: %s', seedfile.basename) self.del_item(seedfile.md5) def copy_file_from_origin(self, f): From 4d47518351ccb7b23beb6b4a82ff1e55aacaf2f2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 3 Sep 2015 14:57:08 -0400 Subject: [PATCH 0643/1169] Don't wait forever for debugger. BFF-846 --- src/certfuzz/bff/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/bff/common.py b/src/certfuzz/bff/common.py index bc23571..0bb33f4 100644 --- a/src/certfuzz/bff/common.py +++ b/src/certfuzz/bff/common.py @@ -28,7 +28,7 @@ def setup_debugging(): logger.debug('Instantiating embedded rpdb2 debugger with password "bff"...') try: import rpdb2 - rpdb2.start_embedded_debugger("bff", timeout=0.0) + rpdb2.start_embedded_debugger("bff", timeout=5.0) except ImportError: logger.debug('Skipping import of rpdb2. Is Winpdb installed?') @@ -60,7 +60,7 @@ def __enter__(self): setup_debugging() return self.go - + def __exit__(self, etype, value, traceback): pass From 58cb36c60a9646576c3891e043fa64695c716143 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 3 Sep 2015 16:43:09 -0400 Subject: [PATCH 0644/1169] Fall back to fuzzing ZIP containers on bad ZIP files. BFF-826 --- src/certfuzz/fuzzers/fuzzer_base.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/fuzzers/fuzzer_base.py b/src/certfuzz/fuzzers/fuzzer_base.py index a4e1cae..51002e6 100644 --- a/src/certfuzz/fuzzers/fuzzer_base.py +++ b/src/certfuzz/fuzzers/fuzzer_base.py @@ -178,7 +178,7 @@ def _prefuzz(self): try: tempzip = zipfile.ZipFile(inmemseed, 'r') except: - logger.warning('Bad zip file. Aborting.') + logger.warning('Bad zip file. Falling back to mutating container.') self.sf.is_zip = False inmemseed.close() return @@ -187,14 +187,14 @@ def _prefuzz(self): get info on all the archived files and concatentate their contents into self.input ''' - self.input = bytearray() + self.zipinput = bytearray() logger.debug('Reading files from zip...') for i in tempzip.namelist(): try: data = tempzip.read(i) except: # BadZipfile or encrypted - logger.warning('Bad zip file. Aborting.') + logger.warning('Bad zip file. Falling back to mutating container.') self.sf.is_zip = False tempzip.close() inmemseed.close() @@ -204,11 +204,13 @@ def _prefuzz(self): # reconstruction # save compress type - self.saved_arcinfo[i] = (len(self.input), len(data), + self.saved_arcinfo[i] = (len(self.zipinput), len(data), tempzip.getinfo(i).compress_type) self.input += data tempzip.close() inmemseed.close() + # Zip processing went fine, so use the zip contents as self.input to fuzzer + self.input = self.zipinput def _postfuzz(self): if self.options.get('fuzz_zip_container') or not self.sf.is_zip: From ccced185c737e4993e96b3d42e414d11745be632 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 3 Sep 2015 16:44:16 -0400 Subject: [PATCH 0645/1169] Kill killproc.sh when campaign finishes. BFF-847 --- src/certfuzz/campaign/campaign_base.py | 5 +++++ src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/fuzztools/process_killer.py | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index b5fbaf0..73afd7a 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -13,6 +13,7 @@ import tempfile import traceback import cPickle as pickle +import signal from certfuzz.campaign.errors import CampaignError from certfuzz.debuggers import registration @@ -99,6 +100,7 @@ def __init__(self, config_file, result_dir=None, debug=False): self.outdir = None self.sf_set_out = None self.stopfuzzing = False + self.pk_pid = None if result_dir: self.outdir_base = os.path.abspath(result_dir) @@ -210,6 +212,9 @@ def _pre_exit(self): Implements methods to be completed prior to handling errors in the __exit__ method. No return value. ''' + # Kill off process killer + if self.pk_pid: + os.kill(self.pk_pid, signal.SIGTERM) def __exit__(self, etype, value, mytraceback): ''' diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 63be510..528eded 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -115,7 +115,7 @@ def _set_unbuffered_stdout(self): def _start_process_killer(self): logger.debug('start process killer') with ProcessKiller(self.config.killprocname, self.config.killproctimeout) as pk: - pk.go() + self.pk_pid = pk.go() def _cache_app(self): logger.debug('cache program') diff --git a/src/certfuzz/fuzztools/process_killer.py b/src/certfuzz/fuzztools/process_killer.py index f059c43..d4b27b1 100644 --- a/src/certfuzz/fuzztools/process_killer.py +++ b/src/certfuzz/fuzztools/process_killer.py @@ -48,4 +48,5 @@ def go(self): Spawns a separate process to kill out of control processes. ''' logger.debug('Running [%s]', self.cmdline) - subprocess.check_call(self.cmdline, shell=True) + pk_pid = subprocess.Popen(self.cmdline, shell=True).pid + return pk_pid From 9c2076b21671c47a326850dcc1fd2f52ec149d7b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 8 Sep 2015 10:22:25 -0400 Subject: [PATCH 0646/1169] New inetc that is supposed to check SSL certificates. BFF-848 --- build/distmods/windows/nsis/inetc.dll | Bin 22016 -> 25088 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/build/distmods/windows/nsis/inetc.dll b/build/distmods/windows/nsis/inetc.dll index cca1550d58a25b25dafeae38cdec508103bdef29..07a8b24bafd97218dd75c125515d261b2263d8d8 100644 GIT binary patch literal 25088 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 literal 22016 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 From e53c77c7ac5467107d025802b8034ee9c1c59d6f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 8 Sep 2015 11:10:16 -0400 Subject: [PATCH 0647/1169] Dummy test file for zipdiff.py --- src/certfuzz/test/tools/linux/test_zipdiff.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/certfuzz/test/tools/linux/test_zipdiff.py diff --git a/src/certfuzz/test/tools/linux/test_zipdiff.py b/src/certfuzz/test/tools/linux/test_zipdiff.py new file mode 100644 index 0000000..fc47475 --- /dev/null +++ b/src/certfuzz/test/tools/linux/test_zipdiff.py @@ -0,0 +1,27 @@ +''' +Created on Apr 10, 2012 + +@organization: cert.org +''' + +import unittest + + +class Test(unittest.TestCase): + + + def setUp(self): + pass + + + def tearDown(self): + pass + + + def testName(self): + pass + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 26875aee84d9f36be9ebfd2deea11077b498f0d0 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 8 Sep 2015 11:14:23 -0400 Subject: [PATCH 0648/1169] zipdiff is in common. Not platform-specific --- .../tools/{linux => common}/test_zipdiff.py | 0 .../test/tools/windows/test_zipdiff.py | 22 ------------------- 2 files changed, 22 deletions(-) rename src/certfuzz/test/tools/{linux => common}/test_zipdiff.py (100%) delete mode 100644 src/certfuzz/test/tools/windows/test_zipdiff.py diff --git a/src/certfuzz/test/tools/linux/test_zipdiff.py b/src/certfuzz/test/tools/common/test_zipdiff.py similarity index 100% rename from src/certfuzz/test/tools/linux/test_zipdiff.py rename to src/certfuzz/test/tools/common/test_zipdiff.py diff --git a/src/certfuzz/test/tools/windows/test_zipdiff.py b/src/certfuzz/test/tools/windows/test_zipdiff.py deleted file mode 100644 index 1efab39..0000000 --- a/src/certfuzz/test/tools/windows/test_zipdiff.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Jan 23, 2014 - -@organization: cert.org -''' -import unittest - - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From 0997fd58de81129f530b26d6ed8816f266dddac7 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 10 Sep 2015 12:41:33 -0400 Subject: [PATCH 0649/1169] Fix import of AnalyzerEmptyOutputError. BFF-849 --- src/certfuzz/crash/bff_crash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index 0c6cc8d..af458cf 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -13,7 +13,7 @@ try: from certfuzz.analyzers import pin_calltrace - from certfuzz.analyzers import AnalyzerEmptyOutputError + from certfuzz.analyzers.errors import AnalyzerEmptyOutputError from certfuzz.debuggers.output_parsers.calltracefile import Calltracefile except ImportError: pass From 19da53b45cedc64e17308f8bd0d84452b70f63a5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 11 Sep 2015 14:35:35 -0400 Subject: [PATCH 0650/1169] Derive faulting address from backtrace on ReturnAV on Linux. BFF-851 --- .../drillresults/testcasebundle_base.py | 22 ++++++++++++++++++- .../drillresults/testcasebundle_linux.py | 8 ++++++- .../drillresults/testcasebundle_windows.py | 3 +++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 3a09485..ba1d8d4 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -163,8 +163,10 @@ def _parse_testcase(self): if instructionline: self.instructionpieces = instructionline.split() faultaddr = self._prefix_0x(faultaddr) - faultaddr = self.fix_efa_offset(instructionline, faultaddr) + if self.shortdesc == 'ReturnAv': + faultaddr = self.fix_return_efa(faultaddr) + # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') @@ -373,6 +375,24 @@ def _prefix_0x(self, addr): else: return '0x{}'.format(addr) + def fix_return_efa(self, faultaddr): + ''' + The faulting address on Linux on a ReturnAV is reported as null + We can figure out what it actually is based on the backtrace + ''' + if int(faultaddr, base=16) == 0: + derived_faultaddr = self.get_return_addr() + logger.debug('New faulting address derived from backtrace: %s' % derived_faultaddr) + if derived_faultaddr is not None: + faultaddr = derived_faultaddr + return faultaddr + + @abc.abstractmethod + def get_return_addr(self): + ''' + Get return address based on backtrace + ''' + def fix_efa_offset(self, instructionline, faultaddr): try: index = self.instructionpieces.index('call') diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/drillresults/testcasebundle_linux.py index 3f81561..31fb327 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/drillresults/testcasebundle_linux.py @@ -18,7 +18,7 @@ RE_FRAME_0 = re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+') RE_MAPPED_FRAME = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)') RE_VDSO = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(\[vdso\])') - +RE_RETURN_ADDR = re.compile(r'^#1\s.(0x[0-9a-fA-F]+)\s') class LinuxTestCaseBundle(TestCaseBundle): really_exploitable = [ @@ -77,6 +77,12 @@ def get_instr(self, instraddr): return self._match_rgx(rgx, rvfunc) + def get_return_addr(self): + rvfunc = lambda x, l: x.group(1) + rgx = RE_RETURN_ADDR + + return self._match_rgx(rgx, rvfunc) + def get_instr_addr(self): ''' Find the address for the current (crashing) instruction diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/drillresults/testcasebundle_windows.py index 8565861..a05d04b 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/drillresults/testcasebundle_windows.py @@ -160,6 +160,9 @@ def get_instr(self, instraddr): return self._match_rgx(rgx, rvfunc) + def get_return_addr(self): + return None + def fix_efa_bug(self, instraddr, faultaddr): ''' !exploitable often reports an incorrect EFA for 64-bit targets. From 85964559ca6d55cccd33f5331d6da8ace62b3981 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 14 Sep 2015 10:20:22 -0400 Subject: [PATCH 0651/1169] Don't use .gdbinit when fuzzing. BFF-853 --- src/certfuzz/debuggers/gdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index c5abe0d..07ad5a0 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -36,7 +36,7 @@ def _get_cmdline(self): if not os.path.exists(self.input_file): raise DebuggerError('Input file does not exist: %s', self.input_file) - args = [self.debugger_app(), '-batch', '-command', self.input_file] + args = [self.debugger_app(), '-n', '-batch', '-command', self.input_file] logger.log(5, "GDB command: [%s]", ' '.join(args)) return args From a22a9a2545325dad94f2aaa1b72cea530f5bc3c0 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 22 Sep 2015 10:19:24 -0400 Subject: [PATCH 0652/1169] msec runner should obey the "hideoutput" option just like the winrunner (hook) does. BFF-855 --- src/certfuzz/crash/crash_windows.py | 5 +++-- src/certfuzz/debuggers/msec.py | 12 +++++++++--- src/certfuzz/iteration/iteration_windows.py | 4 +++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/crash/crash_windows.py index c83e01a..e5a7db2 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/crash/crash_windows.py @@ -43,7 +43,7 @@ class WindowsCrash(Testcase): # TODO: do we still need fuzzer as an arg? def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, dbg_class, dbg_opts, workingdir_base, keep_faddr, program, - heisenbug_retries=4, copy_fuzzedfile=True): + heisenbug_retries=4, copy_fuzzedfile=True, hideoutput=False): dbg_timeout = dbg_opts['runtimeout'] @@ -52,6 +52,7 @@ def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, self.dbg_class = dbg_class self.dbg_opts = dbg_opts self.copy_fuzzedfile = copy_fuzzedfile + self.hideoutput = hideoutput self.cmdargs = cmdlist self.workdir_base = workingdir_base @@ -120,7 +121,7 @@ def debug_once(self): killprocname=None, exception_depth=self.exception_depth, workingdir=self.tempdir, - watchcpu=self.watchcpu) + watchcpu=self.watchcpu, hideoutput=self.hideoutput) self.parsed_outputs.append(debugger.go()) self.reached_secondchance = self.parsed_outputs[self.exception_depth].secondchance diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 203339a..e384766 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -30,10 +30,11 @@ class MsecDebugger(DebuggerBase): _key = 'msec' _ext = 'msec' - def __init__(self, program, cmd_args, outfile_base, timeout, killprocname, watchcpu, exception_depth=0, **options): + def __init__(self, program, cmd_args, outfile_base, timeout, killprocname, watchcpu, exception_depth=0, hideoutput=False, **options): DebuggerBase.__init__(self, program, cmd_args, outfile_base, timeout, killprocname, **options) self.exception_depth = exception_depth self.watchcpu = watchcpu + self.hideoutput = hideoutput def kill(self, pid, returncode): """kill function for Win32""" @@ -93,8 +94,13 @@ def run_with_timer(self): foundpid = False args = self._get_cmdline(self.outfile) - p = Popen(args, stdout=open(os.devnull), stderr=open(os.devnull), - universal_newlines=True) + if self.hideoutput: + # We're hiding output. Note that this can affect some target + # programs, such as python + p = Popen(args, stdout=open(os.devnull), stderr=open(os.devnull), + universal_newlines=True) + else: + p = Popen(args, universal_newlines=True) if self.watchcpu == True: wmiInterface = wmi.WMI() diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 81dd1f0..a758103 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -46,6 +46,7 @@ def __init__(self, seedfile, seednum, config, fuzzer_cls, self.debug = debug # TODO: do we use keep_uniq_faddr at all? self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] + self.hideoutput = config['runner']['hideoutput'] self.cmd_template = string.Template(cmd_template) @@ -179,7 +180,8 @@ def _construct_testcase(self): self.working_dir, self.cfg['runoptions']['keep_unique_faddr'], self.cfg['target']['program'], heisenbug_retries=self.retries, - copy_fuzzedfile=self.fuzzer.fuzzed_changes_input) as testcase: + copy_fuzzedfile=self.fuzzer.fuzzed_changes_input, + hideoutput=self.hideoutput) as testcase: # put it on the list for the analysis pipeline self.testcases.append(testcase) From aa42cf0dafc94fb931f5abfbdd243b692c5f22a7 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 22 Sep 2015 16:05:40 -0400 Subject: [PATCH 0653/1169] Change devnull stdout to specify 'w' mode. BFF-855 --- src/certfuzz/debuggers/msec.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index e384766..388fcad 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -94,13 +94,8 @@ def run_with_timer(self): foundpid = False args = self._get_cmdline(self.outfile) - if self.hideoutput: - # We're hiding output. Note that this can affect some target - # programs, such as python - p = Popen(args, stdout=open(os.devnull), stderr=open(os.devnull), + p = Popen(args, stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'), universal_newlines=True) - else: - p = Popen(args, universal_newlines=True) if self.watchcpu == True: wmiInterface = wmi.WMI() From ac760c9e9ac190d7f0150569ef4fec7982e877f2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 22 Sep 2015 16:08:44 -0400 Subject: [PATCH 0654/1169] Undo proposed hideoutput fix. BFF-855 --- src/certfuzz/crash/crash_windows.py | 5 ++--- src/certfuzz/debuggers/msec.py | 3 +-- src/certfuzz/iteration/iteration_windows.py | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/crash/crash_windows.py index e5a7db2..c83e01a 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/crash/crash_windows.py @@ -43,7 +43,7 @@ class WindowsCrash(Testcase): # TODO: do we still need fuzzer as an arg? def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, dbg_class, dbg_opts, workingdir_base, keep_faddr, program, - heisenbug_retries=4, copy_fuzzedfile=True, hideoutput=False): + heisenbug_retries=4, copy_fuzzedfile=True): dbg_timeout = dbg_opts['runtimeout'] @@ -52,7 +52,6 @@ def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, self.dbg_class = dbg_class self.dbg_opts = dbg_opts self.copy_fuzzedfile = copy_fuzzedfile - self.hideoutput = hideoutput self.cmdargs = cmdlist self.workdir_base = workingdir_base @@ -121,7 +120,7 @@ def debug_once(self): killprocname=None, exception_depth=self.exception_depth, workingdir=self.tempdir, - watchcpu=self.watchcpu, hideoutput=self.hideoutput) + watchcpu=self.watchcpu) self.parsed_outputs.append(debugger.go()) self.reached_secondchance = self.parsed_outputs[self.exception_depth].secondchance diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 388fcad..71196fe 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -30,11 +30,10 @@ class MsecDebugger(DebuggerBase): _key = 'msec' _ext = 'msec' - def __init__(self, program, cmd_args, outfile_base, timeout, killprocname, watchcpu, exception_depth=0, hideoutput=False, **options): + def __init__(self, program, cmd_args, outfile_base, timeout, killprocname, watchcpu, exception_depth=0, **options): DebuggerBase.__init__(self, program, cmd_args, outfile_base, timeout, killprocname, **options) self.exception_depth = exception_depth self.watchcpu = watchcpu - self.hideoutput = hideoutput def kill(self, pid, returncode): """kill function for Win32""" diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index a758103..81dd1f0 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -46,7 +46,6 @@ def __init__(self, seedfile, seednum, config, fuzzer_cls, self.debug = debug # TODO: do we use keep_uniq_faddr at all? self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] - self.hideoutput = config['runner']['hideoutput'] self.cmd_template = string.Template(cmd_template) @@ -180,8 +179,7 @@ def _construct_testcase(self): self.working_dir, self.cfg['runoptions']['keep_unique_faddr'], self.cfg['target']['program'], heisenbug_retries=self.retries, - copy_fuzzedfile=self.fuzzer.fuzzed_changes_input, - hideoutput=self.hideoutput) as testcase: + copy_fuzzedfile=self.fuzzer.fuzzed_changes_input) as testcase: # put it on the list for the analysis pipeline self.testcases.append(testcase) From 16230dc18d2378e473fe517f29c867655e3bb130 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 22 Oct 2015 13:48:17 -0400 Subject: [PATCH 0655/1169] fix unit test --- src/certfuzz/test/debuggers/test_gdb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/test/debuggers/test_gdb.py b/src/certfuzz/test/debuggers/test_gdb.py index 5c86627..91e26ca 100644 --- a/src/certfuzz/test/debuggers/test_gdb.py +++ b/src/certfuzz/test/debuggers/test_gdb.py @@ -31,7 +31,7 @@ def tearDown(self): def test_get_gdb_cmdline(self): self.gdb._create_input_file() - expected = ['gdb', '-batch', '-command', self.gdb.input_file] + expected = ['gdb', '-n', '-batch', '-command', self.gdb.input_file] self.assertEqual(self.gdb._get_cmdline(), expected) def test_get_gdb(self): @@ -39,5 +39,5 @@ def test_get_gdb(self): pass if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 2366578e641868d9b76c6bc46e8906e4e81a597f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 22 Oct 2015 13:48:32 -0400 Subject: [PATCH 0656/1169] fix unit test --- src/certfuzz/test/drillresults/test_testcasebundle_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/test/drillresults/test_testcasebundle_base.py b/src/certfuzz/test/drillresults/test_testcasebundle_base.py index 964c0ca..2b87d65 100644 --- a/src/certfuzz/test/drillresults/test_testcasebundle_base.py +++ b/src/certfuzz/test/drillresults/test_testcasebundle_base.py @@ -39,6 +39,9 @@ def get_instr(self): def get_instr_addr(self): return '0xdeadbeef' + def get_return_addr(self): + testcasebundle_base.TestCaseBundle.get_return_addr(self) + class Test(unittest.TestCase): def setUp(self): @@ -226,5 +229,5 @@ def test_score_less_interesting(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From d89de7f9a75b0578ccf90c61362c7aa818652bf4 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 12 Nov 2015 16:02:44 -0500 Subject: [PATCH 0657/1169] Only minimize when told to. BFF-856 --- .../testcase_pipeline/tc_pipeline_windows.py | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 1f3784c..701ca3f 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -47,33 +47,34 @@ def _verify(self, testcase): self.success = True def _minimize(self, testcase): - logger.info('Minimizing testcase %s', testcase.signature) - logger.debug('config = %s', self.cfg) - - config = self._create_minimizer_cfg() - - debuggers.registration.verify_supported_platform() - - kwargs = {'cfg': config, - 'crash': testcase, - 'seedfile_as_target': True, - 'bitwise': False, - 'confidence': 0.999, - 'tempdir': self.working_dir, - 'maxtime': self.cfg['runoptions']['minimizer_timeout'] - } - - try: - with Minimizer(**kwargs) as minimizer: - minimizer.go() - - # minimzer found other crashes, so we should add them - # to our list for subsequent processing - for tc in minimizer.other_crashes.values(): - self.tc_candidate_q.put(tc) - except: - # Minimizer failed for some reason. Don't stop campaign! - pass + if self.cfg['runoptions']['minimize'] is True: + logger.info('Minimizing testcase %s', testcase.signature) + logger.debug('config = %s', self.cfg) + + config = self._create_minimizer_cfg() + + debuggers.registration.verify_supported_platform() + + kwargs = {'cfg': config, + 'crash': testcase, + 'seedfile_as_target': True, + 'bitwise': False, + 'confidence': 0.999, + 'tempdir': self.working_dir, + 'maxtime': self.cfg['runoptions']['minimizer_timeout'] + } + + try: + with Minimizer(**kwargs) as minimizer: + minimizer.go() + + # minimzer found other crashes, so we should add them + # to our list for subsequent processing + for tc in minimizer.other_crashes.values(): + self.tc_candidate_q.put(tc) + except: + # Minimizer failed for some reason. Don't stop campaign! + pass def _analyze(self, testcase): pass From ad7336183d16729b1a36add6f99eb60a441e43ee Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 17 Dec 2015 14:14:38 -0500 Subject: [PATCH 0658/1169] Explicitly specify gdb output file, rather than collect stdout. BFF-858 --- src/certfuzz/debuggers/gdb.py | 5 +++-- src/certfuzz/debuggers/templates/gdb_bt_only_template.txt | 2 ++ .../debuggers/templates/gdb_complete_nofunction_template.txt | 2 ++ src/certfuzz/debuggers/templates/gdb_complete_template.txt | 2 ++ .../debuggers/templates/gdb_exploitable_template.txt | 2 ++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index 07ad5a0..8460f2c 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -57,7 +57,8 @@ def _create_input_file(self): s = Template(template) cmdargs = ' '.join(self.cmd_args) - new_script = s.safe_substitute(PROGRAM=self.program, CMD_ARGS=cmdargs) + new_script = s.safe_substitute(PROGRAM=self.program, CMD_ARGS=cmdargs, + OUTFILE=self.outfile) (fd, f) = tempfile.mkstemp(text=True) try: @@ -105,7 +106,7 @@ def go(self): # build the command line in a separate function so we can unit test # it without actually running the command cmdline = self._get_cmdline() - subp.run_with_timer(cmdline, self.timeout, self.killprocname, stdout=self.outfile) + subp.run_with_timer(cmdline, self.timeout, self.killprocname, stdout=os.devnull) self._remove_temp_file() return GDBfile(self.outfile, self.exclude_unmapped_frames, self.keep_uniq_faddr) diff --git a/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt b/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt index 4bd61df..7f540b0 100644 --- a/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt @@ -1,4 +1,6 @@ set disassembly-flavor intel +set logging file $OUTFILE +set logging on file $PROGRAM run $CMD_ARGS info proc map diff --git a/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt index 4daab40..229e67b 100644 --- a/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt @@ -1,4 +1,6 @@ set disassembly-flavor intel +set logging file $OUTFILE +set logging on file $PROGRAM run $CMD_ARGS echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_complete_template.txt index cda7788..d484529 100644 --- a/src/certfuzz/debuggers/templates/gdb_complete_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_complete_template.txt @@ -1,4 +1,6 @@ set disassembly-flavor intel +set logging file $OUTFILE +set logging on file $PROGRAM run $CMD_ARGS echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_exploitable_template.txt b/src/certfuzz/debuggers/templates/gdb_exploitable_template.txt index 9f879c2..3db9440 100644 --- a/src/certfuzz/debuggers/templates/gdb_exploitable_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_exploitable_template.txt @@ -1,4 +1,6 @@ set disassembly-flavor intel +set logging file $OUTFILE +set logging on file $PROGRAM run $CMD_ARGS source ~/bff/CERT_triage_tools/exploitable/exploitable.py From 66143a79941ec4f24adda8b85d75035fe0f5a3ab Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 5 Jan 2016 09:55:07 -0500 Subject: [PATCH 0659/1169] Re-enable crash recycling. BFF-860 --- src/certfuzz/iteration/iteration_base3.py | 3 ++- .../testcase_pipeline/tc_pipeline_base.py | 3 ++- .../testcase_pipeline/tc_pipeline_linux.py | 16 +++++++--------- .../testcase_pipeline/tc_pipeline_windows.py | 9 +++++++++ 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index f6c7f32..bad6c39 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -161,7 +161,8 @@ def process_testcases(self): options=self.pipeline_options, outdir=self.outdir, workdirbase=self.working_dir, - minimizable=self.fuzzer.is_minimizable) as pipeline: + minimizable=self.fuzzer.is_minimizable, + sf_set=self.sf_set) as pipeline: pipeline() def go(self): diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index b37f64e..13a839b 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -28,7 +28,7 @@ class TestCasePipelineBase(object): pipes = ['verify', 'minimize', 'analyze', 'report'] def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, - outdir=None, workdirbase=None, minimizable=None): + outdir=None, workdirbase=None, minimizable=None, sf_set=None): ''' Constructor ''' @@ -40,6 +40,7 @@ def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, self.working_dir = workdirbase self.minimizable = minimizable + self.sf_set = sf_set self.tc_candidate_q = Queue.Queue() diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index f483753..64b9cf7 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -91,15 +91,13 @@ def _minimize(self, testcase): self._minimize_to_string(testcase) def _post_minimize(self, testcase): - pass - # TODO -# if self.cfg.recycle_crashers: -# logger.debug('Recycling crash as seedfile') -# iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] -# crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext -# crasherseed_path = os.path.join(self.cfg.seedfile_origin_dir, crasherseedname) -# filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) -# seedfile_set.add_file(crasherseed_path) + if self.cfg.config['verifier']['recycle_crashers']: + logger.debug('Recycling crash as seedfile') + iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext + crasherseed_path = os.path.join(self.cfg.config['directories']['seedfile_origin_dir'], crasherseedname) + filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) + self.sf_set.add_file(crasherseed_path) def _pre_analyze(self, testcase): # get one last debugger output for the newly minimized file diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 701ca3f..27c8cc8 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -76,6 +76,15 @@ def _minimize(self, testcase): # Minimizer failed for some reason. Don't stop campaign! pass + def _post_minimize(self, testcase): + if self.cfg['runoptions']['recycle_crashers']: + logger.debug('Recycling crash as seedfile') + iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext + crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) + filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) + self.sf_set.add_file(crasherseed_path) + def _analyze(self, testcase): pass From 378bfe0f09cb578b9845f0dfa7935c02706d1bc7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 9 Jan 2015 07:52:21 -0500 Subject: [PATCH 0660/1169] remove dead code --- src/certfuzz/config/config_linux.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/certfuzz/config/config_linux.py b/src/certfuzz/config/config_linux.py index 1f32b7b..d912c62 100644 --- a/src/certfuzz/config/config_linux.py +++ b/src/certfuzz/config/config_linux.py @@ -108,15 +108,6 @@ def _set_derived_options(self): self.crashexitcodesfile = os.path.join(self.local_dir, CRASH_EXIT_CODE_FILE) self.zzuf_log_file = os.path.join(self.local_dir, ZZUF_LOG_FILE) - # derived cached paths -# self.cached_config_file = os.path.join(self.local_dir, CACHED_CONFIG_OBJECT_FILE) -# self.cached_seedrange_file = os.path.join(self.local_dir, CACHED_SEEDRANGE_OBJECT_FILE) -# self.cached_rangefinder_file = os.path.join(self.local_dir, CACHED_RANGEFINDER_OBJECT_FILE) -# self.cached_seedfile_set = os.path.join(self.local_dir, CACHED_SEEDFILESET_OBJECT_FILE) - - -# self.tmpdir = None - def get_command(self, filepath): return ' '.join(self.get_command_list(filepath)) From 50fd92ca6efe9e52168c1ff97f583d55ea9f1f13 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 10:10:27 -0500 Subject: [PATCH 0661/1169] Revert "Only minimize when told to. BFF-856" This reverts commit d89de7f9a75b0578ccf90c61362c7aa818652bf4. --- .../testcase_pipeline/tc_pipeline_windows.py | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 27c8cc8..8c92a83 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -47,34 +47,33 @@ def _verify(self, testcase): self.success = True def _minimize(self, testcase): - if self.cfg['runoptions']['minimize'] is True: - logger.info('Minimizing testcase %s', testcase.signature) - logger.debug('config = %s', self.cfg) - - config = self._create_minimizer_cfg() - - debuggers.registration.verify_supported_platform() - - kwargs = {'cfg': config, - 'crash': testcase, - 'seedfile_as_target': True, - 'bitwise': False, - 'confidence': 0.999, - 'tempdir': self.working_dir, - 'maxtime': self.cfg['runoptions']['minimizer_timeout'] - } - - try: - with Minimizer(**kwargs) as minimizer: - minimizer.go() - - # minimzer found other crashes, so we should add them - # to our list for subsequent processing - for tc in minimizer.other_crashes.values(): - self.tc_candidate_q.put(tc) - except: - # Minimizer failed for some reason. Don't stop campaign! - pass + logger.info('Minimizing testcase %s', testcase.signature) + logger.debug('config = %s', self.cfg) + + config = self._create_minimizer_cfg() + + debuggers.registration.verify_supported_platform() + + kwargs = {'cfg': config, + 'crash': testcase, + 'seedfile_as_target': True, + 'bitwise': False, + 'confidence': 0.999, + 'tempdir': self.working_dir, + 'maxtime': self.cfg['runoptions']['minimizer_timeout'] + } + + try: + with Minimizer(**kwargs) as minimizer: + minimizer.go() + + # minimzer found other crashes, so we should add them + # to our list for subsequent processing + for tc in minimizer.other_crashes.values(): + self.tc_candidate_q.put(tc) + except: + # Minimizer failed for some reason. Don't stop campaign! + pass def _post_minimize(self, testcase): if self.cfg['runoptions']['recycle_crashers']: From 165f973ae9593c8d2e18764f01b9670f8bef19db Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 14 Jan 2015 09:08:04 -0500 Subject: [PATCH 0662/1169] refactor config class into an attribute --- src/certfuzz/campaign/campaign_linux.py | 3 ++- src/certfuzz/campaign/campaign_windows.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 528eded..9d56973 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -55,6 +55,7 @@ class LinuxCampaign(CampaignBase): ''' Extends CampaignBase to add linux-specific features. ''' + _config_cls = LinuxConfig def __init__(self, config_file=None, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) @@ -79,7 +80,7 @@ def __init__(self, config_file=None, result_dir=None, debug=False): def _read_config_file(self): CampaignBase._read_config_file(self) - with LinuxConfig(self.config_file) as cfgobj: + with self._config_cls(self.config_file) as cfgobj: self.config = cfgobj self.configdate = cfgobj.configdate diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 25b883f..f4b5fad 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -24,6 +24,8 @@ class WindowsCampaign(CampaignBase): ''' Extends CampaignBase to add windows-specific features like ButtonClicker ''' + _config_cls = WindowsConfig + def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) @@ -66,7 +68,7 @@ def _read_config_file(self): CampaignBase._read_config_file(self) # read configs - with WindowsConfig(self.config_file) as cfgobj: + with self._config_cls(self.config_file) as cfgobj: self.config = cfgobj.config self.configdate = cfgobj.configdate From 194caf915eceb02dfa5d93de735b914d35e53e32 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 14 Jan 2015 09:13:18 -0500 Subject: [PATCH 0663/1169] rename runner to runner_cls where appropriate --- src/certfuzz/campaign/campaign_base.py | 4 ++-- src/certfuzz/campaign/campaign_windows.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 73afd7a..1839669 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -81,7 +81,7 @@ def __init__(self, config_file, result_dir=None, debug=False): self.runner_module_name = None self.runner_module = None - self.runner = None + self.runner_cls = None self.seedfile_set = None self.working_dir = None @@ -258,7 +258,7 @@ def _set_fuzzer(self): def _set_runner(self): if self.runner_module_name: self.runner_module = import_module_by_name(self.runner_module_name) - self.runner = self.runner_module._runner_class + self.runner_cls = self.runner_module._runner_class def _set_debugger(self): # this will import the module which registers the debugger diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index f4b5fad..a7b97e0 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -84,7 +84,7 @@ def __getstate__(self): # we can safely delete them as they will be # reconstituted when we __enter__ a context for key in ['fuzzer_module', 'fuzzer_cls', - 'runner_module', 'runner', + 'runner_module', 'runner_cls', 'debugger_module', 'dbg_class' ]: if key in state: @@ -191,7 +191,7 @@ def _do_iteration(self, sf, range_obj, quiet_flag, seednum): # the __enter__ and __exit__ methods of the # newly created WindowsIteration() with WindowsIteration(sf, seednum, self.config, self.fuzzer_cls, - self.runner, self.debugger_module, self.dbg_class, + self.runner_cls, self.debugger_module, self.dbg_class, self.keep_heisenbugs, self.keep_duplicates, self.cmd_template, self._crash_is_unique, self.working_dir, self.outdir, self.debug, self.seedfile_set, From 1bbadb9f8e583905a775cf414150354e8ec28fa9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 14 Jan 2015 09:26:05 -0500 Subject: [PATCH 0664/1169] reorder linux iteration args to match windows iteration --- src/certfuzz/campaign/campaign_linux.py | 12 ++++++------ src/certfuzz/iteration/iteration_linux.py | 17 +++++++++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 9d56973..401c975 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -192,14 +192,14 @@ def _save_state(self): def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() - with LinuxIteration(cfg=self.config, + with LinuxIteration(seedfile=seedfile, seednum=seednum, - seedfile=seedfile, - r=range_obj, workdirbase=self.working_dir, - quiet=quiet_flag, - uniq_func=self._crash_is_unique, + outdir=self.outdir, sf_set=self.seedfile_set, rf=seedfile.rangefinder, - outdir=self.outdir) as iteration: + uniq_func=self._crash_is_unique, + cfg=self.config, + r=range_obj, + quiet=quiet_flag) as iteration: iteration() diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 9682eec..c6514f6 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -22,10 +22,19 @@ class LinuxIteration(IterationBase3): tcpipeline_cls = LinuxTestCasePipeline - def __init__(self, cfg=None, seednum=None, seedfile=None, r=None, workdirbase=None, quiet=True, uniq_func=None, - sf_set=None, rf=None, outdir=None): - IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, - sf_set, rf, uniq_func, cfg, r) + def __init__(self, seedfile=None, seednum=None, workdirbase=None, outdir=None, + sf_set=None, rf=None, uniq_func=None, cfg=None, r=None, quiet=True,): + + IterationBase3.__init__(self, + seedfile, + seednum, + workdirbase, + outdir, + sf_set, + rf, + uniq_func, + cfg, + r) self.quiet_flag = quiet From e6e1d8aa86fed9c8bf3fbb00baa2bc7edba1b74b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 14 Jan 2015 09:26:58 -0500 Subject: [PATCH 0665/1169] use runner_cls and fuzzer_cls --- src/certfuzz/iteration/iteration_linux.py | 19 +++++----- src/certfuzz/iteration/iteration_windows.py | 41 +++++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index c6514f6..ea55697 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -13,7 +13,7 @@ from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.testcase_pipeline.tc_pipeline_linux import LinuxTestCasePipeline from certfuzz.runners.zzufrun import ZzufRunner -from certfuzz.fuzzers.bytemut import ByteMutFuzzer as Fuzzer +from certfuzz.fuzzers.bytemut import ByteMutFuzzer logger = logging.getLogger(__name__) @@ -36,6 +36,8 @@ def __init__(self, seedfile=None, seednum=None, workdirbase=None, outdir=None, cfg, r) + self.fuzzer_cls = ByteMutFuzzer + self.runner_cls = ZzufRunner self.quiet_flag = quiet self.testcase_base_dir = os.path.join(self.outdir, 'crashers') @@ -44,8 +46,8 @@ def __init__(self, seedfile=None, seednum=None, workdirbase=None, outdir=None, self._zzuf_line = None # analysis is required in two cases: - # 1) runner is not defined (self.runner == None) - # 2) runner is defined, and detects crash (runner.saw_crash == True) + # 1) runner_cls is not defined (self.runner_cls == None) + # 2) runner_cls is defined, and detects crash (runner_cls.saw_crash == True) # this takes care of case 1 by default self._analysis_needed = True @@ -65,10 +67,7 @@ def __enter__(self): def _pre_fuzz(self): fuzz_opts = self.cfg.config['fuzzer'] - self.fuzzer = Fuzzer(self.seedfile, - self.working_dir, - self.seednum, - fuzz_opts) + self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, fuzz_opts) def _post_fuzz(self): self.r = self.fuzzer.range @@ -78,7 +77,7 @@ def _post_fuzz(self): # decide if we can minimize this case later # do this here (and not sooner) because the fuzzer could # decide at runtime whether it is or is not minimizable - self.minimizable = self.fuzzer.is_minimizable and self.cfg.config['runoptions']['minimize'] + self.pipeline_options['minimizable'] = self.fuzzer.is_minimizable and self.cfg.config['runoptions']['minimize'] def _pre_run(self): options = self.cfg.config['runner'] @@ -86,7 +85,7 @@ def _pre_run(self): fuzzed_file = self.fuzzer.output_file_path workingdir_base = self.working_dir - self.runner = ZzufRunner(options, cmd_template, fuzzed_file, workingdir_base) + self.runner = self.runner_cls(options, cmd_template, fuzzed_file, workingdir_base) def _run(self): with self.runner: @@ -110,7 +109,7 @@ def _post_run(self): # Don't generate cases for killed process or out-of-memory # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will # report the exit code in its output log. The exit code is 128 + the signal number. - self._analysis_needed = zzuf_log.crash_logged(self.cfg.copymode) + self._analysis_needed = zzuf_log.crash_logged(self.cfg.config['zzuf']['copymode']) if not self._analysis_needed: return diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 81dd1f0..b6e6c6c 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -40,7 +40,7 @@ def __init__(self, seedfile, seednum, config, fuzzer_cls, IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, sf_set, rf, uniq_func, config, None) self.fuzzer_cls = fuzzer_cls - self.runner = runner + self.runner_cls = runner self.debugger_module = debugger self.debugger_class = dbg_class self.debug = debug @@ -49,11 +49,11 @@ def __init__(self, seedfile, seednum, config, fuzzer_cls, self.cmd_template = string.Template(cmd_template) - if self.runner is None: - # null runner case + if self.runner_cls is None: + # null runner_cls case self.retries = 0 else: - # runner is not null + # runner_cls is not null self.retries = 4 self.pipeline_options = { @@ -61,7 +61,7 @@ def __init__(self, seedfile, seednum, config, fuzzer_cls, 'keep_heisenbugs': keep_heisenbugs, 'minimizable': False, 'cmd_template': self.cmd_template, - 'used_runner': self.runner is not None, + 'used_runner': self.runner_cls is not None, } def __exit__(self, etype, value, traceback): @@ -129,12 +129,8 @@ def _tidy(self): TmpReaper().clean_tmp() def _pre_fuzz(self): - # generated test case (fuzzed input) - logger.info('...fuzzing') fuzz_opts = self.cfg['fuzzer'] - self.fuzzer = self.fuzzer_cls(self.seedfile, - self.working_dir, - self.seednum, fuzz_opts) + self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, fuzz_opts) def _post_fuzz(self): self.r = self.fuzzer.range @@ -146,21 +142,26 @@ def _post_fuzz(self): # decide at runtime whether it is or is not minimizable self.pipeline_options['minimizable'] = self.fuzzer.is_minimizable and self.cfg['runoptions']['minimize'] + def _pre_run(self): + options = self.cfg['runner'] + cmd_template = self.cmd_template + fuzzed_file = self.fuzzer.output_file_path + workingdir_base = self.working_dir + + self.runner = self.runner_cls(options, cmd_template, fuzzed_file, workingdir_base) + def _run(self): # analysis is required in two cases: - # 1) runner is not defined (self.runner == None) - # 2) runner is defined, and detects crash (runner.saw_crash == True) + # 1) runner_cls is not defined (self.runner_cls == None) + # 2) runner_cls is defined, and detects crash (runner_cls.saw_crash == True) # this takes care of case 1 by default + # TODO: does case 1 ever happen? analysis_needed = True - if self.runner: - logger.info('...running %s', self.runner.__name__) - with self.runner(self.cfg['runner'], - self.cmd_template, - self.fuzzer.output_file_path, - self.working_dir) as runner: - runner.run() + if self.runner_cls: + with self.runner: + self.runner.run() # this takes care of case 2 - analysis_needed = runner.saw_crash + analysis_needed = self.runner.saw_crash # is further analysis needed? if not analysis_needed: From 04477844ebdcbe725757821b3ebc73de6c623fba Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 14 Jan 2015 09:27:20 -0500 Subject: [PATCH 0666/1169] pull success status back from pipeline --- src/certfuzz/iteration/iteration_base3.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index bad6c39..f8d24ce 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -165,6 +165,8 @@ def process_testcases(self): sf_set=self.sf_set) as pipeline: pipeline() + self.success = pipeline.success + def go(self): logger.debug('go') self.fuzz() From 994bfb4576ed91b071df2ce364ba566ff89cfc35 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 14 Jan 2015 09:27:43 -0500 Subject: [PATCH 0667/1169] fix imports --- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 8c92a83..85cd058 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -5,15 +5,11 @@ ''' import logging import os -import shutil -from certfuzz import debuggers from certfuzz.config.config_windows import get_command_args_list -from certfuzz.fuzztools import filetools -from certfuzz.iteration.errors import IterationError from certfuzz.minimizer import WindowsMinimizer as Minimizer -from certfuzz.testcase_pipeline.errors import TestCasePipelineError from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase +from certfuzz.debuggers.registration import verify_supported_platform logger = logging.getLogger(__name__) @@ -52,7 +48,11 @@ def _minimize(self, testcase): config = self._create_minimizer_cfg() +<<<<<<< HEAD debuggers.registration.verify_supported_platform() +======= + verify_supported_platform() +>>>>>>> fix imports kwargs = {'cfg': config, 'crash': testcase, From e3dd4591d1e53b375fdcbe06fa160a7898eac90e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 14 Jan 2015 11:03:40 -0500 Subject: [PATCH 0668/1169] eliminate debuggers.registration --- src/certfuzz/campaign/campaign_base.py | 10 -- src/certfuzz/campaign/campaign_linux.py | 3 - src/certfuzz/campaign/campaign_windows.py | 4 +- src/certfuzz/crash/bff_crash.py | 18 ++-- src/certfuzz/crash/crash_base.py | 1 + src/certfuzz/crash/crash_windows.py | 7 +- src/certfuzz/debuggers/crashwrangler.py | 3 - src/certfuzz/debuggers/debugger_base.py | 8 +- src/certfuzz/debuggers/gdb.py | 3 - src/certfuzz/debuggers/msec.py | 4 - src/certfuzz/debuggers/registration.py | 94 ------------------- src/certfuzz/iteration/iteration_windows.py | 2 +- .../testcase_pipeline/tc_pipeline_windows.py | 23 ++--- src/certfuzz/tools/linux/minimize.py | 23 ++--- src/certfuzz/tools/linux/repro.py | 4 +- 15 files changed, 34 insertions(+), 173 deletions(-) delete mode 100644 src/certfuzz/debuggers/registration.py diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 1839669..f7cc855 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -16,7 +16,6 @@ import signal from certfuzz.campaign.errors import CampaignError -from certfuzz.debuggers import registration from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.fuzztools import filetools from certfuzz.runners.errors import RunnerArchitectureError, \ @@ -159,7 +158,6 @@ def __enter__(self): self._setup_workdir() self._set_fuzzer() self._set_runner() - self._set_debugger() self._setup_output() self._create_seedfile_set() @@ -260,14 +258,6 @@ def _set_runner(self): self.runner_module = import_module_by_name(self.runner_module_name) self.runner_cls = self.runner_module._runner_class - def _set_debugger(self): - # this will import the module which registers the debugger - self.debugger_module = import_module_by_name(self.debugger_module_name) - # confirm that the registered debugger is compatible - registration.verify_supported_platform() - # now we have some class - self.dbg_class = registration.debug_class - @property def _version_file(self): return os.path.join(self.outdir, 'version.txt') diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 401c975..11f564a 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -15,7 +15,6 @@ from certfuzz.config.config_linux import LinuxConfig from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport -from certfuzz.debuggers.registration import verify_supported_platform from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools import subprocess_helper as subp from certfuzz.fuzztools.ppid_observer import check_ppid @@ -85,8 +84,6 @@ def _read_config_file(self): self.configdate = cfgobj.configdate def _pre_enter(self): - # give up if we don't have a debugger - verify_supported_platform() # give up if prog is a script self._check_for_script() diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index a7b97e0..625d640 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -85,7 +85,7 @@ def __getstate__(self): # reconstituted when we __enter__ a context for key in ['fuzzer_module', 'fuzzer_cls', 'runner_module', 'runner_cls', - 'debugger_module', 'dbg_class' + 'debugger_module' ]: if key in state: del state[key] @@ -191,7 +191,7 @@ def _do_iteration(self, sf, range_obj, quiet_flag, seednum): # the __enter__ and __exit__ methods of the # newly created WindowsIteration() with WindowsIteration(sf, seednum, self.config, self.fuzzer_cls, - self.runner_cls, self.debugger_module, self.dbg_class, + self.runner_cls, self.debugger_module, self.keep_heisenbugs, self.keep_duplicates, self.cmd_template, self._crash_is_unique, self.working_dir, self.outdir, self.debug, self.seedfile_set, diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index af458cf..ca3f513 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -7,10 +7,8 @@ import os from certfuzz.crash.crash_base import Testcase, CrashError -from certfuzz.debuggers import registration from certfuzz.fuzztools import hostinfo, filetools - try: from certfuzz.analyzers import pin_calltrace from certfuzz.analyzers.errors import AnalyzerEmptyOutputError @@ -20,15 +18,20 @@ logger = logging.getLogger(__name__) -debugger = None host_info = hostinfo.HostInfo() +if host_info.is_linux(): + from certfuzz.debuggers.gdb import GDB as debugger_cls +elif host_info.is_osx(): + from certfuzz.debuggers.crashwrangler import CrashWrangler as debugger_cls + class BffCrash(Testcase): ''' classdocs ''' tmpdir_pfx = 'bff-crash-' + _debugger_cls = debugger_cls def __init__(self, cfg, seedfile, fuzzedfile, program, debugger_timeout, killprocname, backtrace_lines, @@ -62,7 +65,7 @@ def __exit__(self, etype, value, traceback): def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): - dbg_template_name = '%s_%s_template.txt' % (registration.debugger, option) + dbg_template_name = '%s_%s_template.txt' % (self._debugger_cls._key, option) self.debugger_template = os.path.join(self.cfg.debugger_template_dir, dbg_template_name) logger.debug('Debugger template set to %s', self.debugger_template) if not os.path.exists(self.debugger_template): @@ -94,15 +97,10 @@ def update_crash_details(self): return self.is_crash def get_debug_output(self, outfile_base): - # FIXME: does this need to be a global? - global debugger - if not debugger: - debugger = registration.get() - logger.debug('Got debugger %s', debugger) # get debugger output logger.debug('Debugger template: %s outfile_base: %s', self.debugger_template, outfile_base) - debugger_obj = debugger(self.program, + debugger_obj = self._debugger_cls(self.program, self.cmdargs, outfile_base, self.debugger_timeout, diff --git a/src/certfuzz/crash/crash_base.py b/src/certfuzz/crash/crash_base.py index ddf575f..6996a3e 100644 --- a/src/certfuzz/crash/crash_base.py +++ b/src/certfuzz/crash/crash_base.py @@ -18,6 +18,7 @@ class Testcase(TestCaseBase): tmpdir_pfx = 'crash-' + _debugger_cls = None def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): logger.debug('Inititalize Testcase') diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/crash/crash_windows.py index c83e01a..c92afa1 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/crash/crash_windows.py @@ -12,6 +12,7 @@ from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import best_effort_move from certfuzz.helpers import random_str +from certfuzz.debuggers.msec import MsecDebugger logger = logging.getLogger(__name__) @@ -39,17 +40,17 @@ def logerror(func, path, excinfo): class WindowsCrash(Testcase): tmpdir_pfx = 'bff-crash-' + _debugger_cls = MsecDebugger # TODO: do we still need fuzzer as an arg? def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, - dbg_class, dbg_opts, workingdir_base, keep_faddr, program, + dbg_opts, workingdir_base, keep_faddr, program, heisenbug_retries=4, copy_fuzzedfile=True): dbg_timeout = dbg_opts['runtimeout'] Testcase.__init__(self, seedfile, fuzzedfile, dbg_timeout) - self.dbg_class = dbg_class self.dbg_opts = dbg_opts self.copy_fuzzedfile = copy_fuzzedfile @@ -113,7 +114,7 @@ def set_debugger_template(self, *args): def debug_once(self): outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) - debugger = self.dbg_class(program=self.program, + debugger = self._debugger_cls(program=self.program, cmd_args=self.cmdargs, outfile_base=outfile_base, timeout=self.debugger_timeout, diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index 411cd82..cdccb4f 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -11,7 +11,6 @@ import re from certfuzz.debuggers.debugger_base import Debugger -from certfuzz.debuggers.registration import register from certfuzz.debuggers.output_parsers.cwfile import CWfile from certfuzz.fuzztools import subprocess_helper as subp @@ -88,5 +87,3 @@ def go(self): open(self.outfile, 'w').close() return CWfile(self.outfile) - -register(CrashWrangler) diff --git a/src/certfuzz/debuggers/debugger_base.py b/src/certfuzz/debuggers/debugger_base.py index ab5750a..2d620ab 100644 --- a/src/certfuzz/debuggers/debugger_base.py +++ b/src/certfuzz/debuggers/debugger_base.py @@ -6,12 +6,14 @@ import logging from certfuzz.debuggers.errors import DebuggerError -from certfuzz.debuggers.registration import get_debug_file, result_fields, \ - allowed_exploitability_values logger = logging.getLogger(__name__) +result_fields = 'debug_crash crash_hash exp faddr output dbg_type'.split() +allowed_exploitability_values = ['UNKNOWN', 'PROBABLY_NOT_EXPLOITABLE', + 'PROBABLY_EXPLOITABLE', 'EXPLOITABLE'] + class Debugger(object): ''' @@ -28,7 +30,7 @@ def __init__(self, program=None, cmd_args=None, outfile_base=None, timeout=None, logger.debug('Initialize Debugger') self.program = program self.cmd_args = cmd_args - self.outfile = get_debug_file(outfile_base, self._ext) + self.outfile = '.'.join((outfile_base, self._ext)) self.timeout = timeout self.killprocname = killprocname self.input_file = '' diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index 8460f2c..7fcea44 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -13,7 +13,6 @@ from certfuzz.debuggers.debugger_base import Debugger from certfuzz.debuggers.errors import DebuggerError from certfuzz.debuggers.output_parsers.gdbfile import GDBfile -from certfuzz.debuggers.registration import register from certfuzz.fuzztools import subprocess_helper as subp @@ -110,5 +109,3 @@ def go(self): self._remove_temp_file() return GDBfile(self.outfile, self.exclude_unmapped_frames, self.keep_uniq_faddr) - -register(GDB) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 71196fe..7bfaf27 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -10,7 +10,6 @@ from certfuzz.debuggers.debugger_base import Debugger as DebuggerBase from certfuzz.debuggers.output_parsers.msec_file import MsecFile -from certfuzz.debuggers.registration import register from certfuzz.helpers import check_os_compatibility import wmi @@ -160,6 +159,3 @@ def go(self): logger.debug('parsed: %s', l) return parsed # END MsecDebugger - -# register this class as a debugger -register(MsecDebugger) diff --git a/src/certfuzz/debuggers/registration.py b/src/certfuzz/debuggers/registration.py deleted file mode 100644 index 81eb5ea..0000000 --- a/src/certfuzz/debuggers/registration.py +++ /dev/null @@ -1,94 +0,0 @@ -''' -Created on Oct 23, 2012 - -@organization: cert.org -''' -import logging -import os -import platform -import subprocess - -from certfuzz.debuggers.errors import UndefinedDebuggerError, \ - DebuggerNotFoundError - - -logger = logging.getLogger(__name__) - -result_fields = 'debug_crash crash_hash exp faddr output dbg_type'.split() -allowed_exploitability_values = ['UNKNOWN', 'PROBABLY_NOT_EXPLOITABLE', - 'PROBABLY_EXPLOITABLE', 'EXPLOITABLE'] - -# remember the system platform (we'll use it a lot) -system = platform.system() - -# the keys for debugger_for should match strings returned by platform.system() -debugger_for = { # platform: key - # 'Linux': 'gdb', - # 'Darwin': 'crashwrangler', -# 'Windows': 'msec', - } - -debugger_class_for = { - # key: class -# 'gdb': GDB, -# 'crashwrangler': CrashWrangler, -# 'msec': MsecDebugger, - } - -debugger_ext = { - # key: ext -# 'gdb': 'gdb', -# 'crashwrangler': 'cw', -# 'msec': 'msec', - } - -debugger = None -debug_class = None.__class__ -debug_ext = None - - -def register(cls=None): -# logger.debug('Registering debugger for %s: key=%s class=%s ext=%s', -# cls._platform, cls._key, cls.__name__, cls._ext) - debugger_for[cls._platform] = cls._key - debugger_class_for[cls._key] = cls - debugger_ext[cls._key] = cls._ext - - -def verify_supported_platform(): - global debugger - global debug_class - global debug_ext - # make sure that we're running on a supported platform - try: - debugger = debugger_for[system] - debug_class = debugger_class_for[debugger] - debug_ext = debugger_ext[debugger] - except KeyError: - raise UndefinedDebuggerError(system) - - if not system in debugger_for.keys(): - raise UndefinedDebuggerError(system) - - try: - dbg = debug_class(None, None, None, None, None) - with open(os.devnull, 'w') as devnull: - subprocess.call(dbg.debugger_test(), stderr=devnull, - stdout=devnull) - except OSError: - raise DebuggerNotFoundError(debugger) - except TypeError: - logger.warning('Skipping debugger test for debugger %s', debugger) - - -def get_debug_file(basename, ext=debug_ext): - return "%s.%s" % (basename, ext) - - -def get(): - ''' - Returns a debugger class to be instantiated - @param system: a string specifying which system you're on - (output of platform.system()) - ''' - return debug_class diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index b6e6c6c..86e00af 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -176,7 +176,7 @@ def _construct_testcase(self): logger.debug('Building testcase object') with WindowsCrash(self.cmd_template, self.seedfile, fuzzed_file, cmdlist, - self.fuzzer, self.debugger_class, dbg_opts, + self.fuzzer, dbg_opts, self.working_dir, self.cfg['runoptions']['keep_unique_faddr'], self.cfg['target']['program'], heisenbug_retries=self.retries, diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 85cd058..4654745 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -9,7 +9,6 @@ from certfuzz.config.config_windows import get_command_args_list from certfuzz.minimizer import WindowsMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase -from certfuzz.debuggers.registration import verify_supported_platform logger = logging.getLogger(__name__) @@ -48,12 +47,6 @@ def _minimize(self, testcase): config = self._create_minimizer_cfg() -<<<<<<< HEAD - debuggers.registration.verify_supported_platform() -======= - verify_supported_platform() ->>>>>>> fix imports - kwargs = {'cfg': config, 'crash': testcase, 'seedfile_as_target': True, @@ -63,17 +56,13 @@ def _minimize(self, testcase): 'maxtime': self.cfg['runoptions']['minimizer_timeout'] } - try: - with Minimizer(**kwargs) as minimizer: - minimizer.go() + with Minimizer(**kwargs) as minimizer: + minimizer.go() - # minimzer found other crashes, so we should add them - # to our list for subsequent processing - for tc in minimizer.other_crashes.values(): - self.tc_candidate_q.put(tc) - except: - # Minimizer failed for some reason. Don't stop campaign! - pass + # minimzer found other crashes, so we should add them + # to our list for subsequent processing + for tc in minimizer.other_crashes.values(): + self.tc_candidate_q.put(tc) def _post_minimize(self, testcase): if self.cfg['runoptions']['recycle_crashers']: diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 8720faf..f249710 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -26,8 +26,6 @@ def main(): - debuggers.registration.verify_supported_platform() - from optparse import OptionParser hdlr = logging.StreamHandler() @@ -58,14 +56,11 @@ def main(): type='float') parser.add_option('-g', '--target-size-guess', dest='initial_target_size', help='A guess at the minimal value (int)', type='int') - parser.add_option('', '--config', dest='config', default='conf.d/bff.yaml', + parser.add_option('', '--config', dest='config', help='path to the configuration file to use') parser.add_option('', '--timeout', dest='timeout', metavar='N', type='int', default=0, help='Stop minimizing after N seconds (default is 0, never time out).') - parser.add_option('-k', '--keepothers', dest='keep_other_crashes', - action='store_true', - help='Keep other crash hashes encountered during minimization') (options, args) = parser.parse_args() @@ -146,23 +141,20 @@ def main(): cfg.debugger_timeout, cfg.killprocname, cfg.backtracelevels, crashers_dir, options.keep_uniq_faddr) as crash: - crash.tempdir = outdir filetools.make_directories(crash.tempdir) logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) filetools.copy_file(fuzzed_file.path, crash.tempdir) - minlog = os.path.join(outdir, 'min_log.txt') - with Minimizer(cfg=cfg, crash=crash, crash_dst_dir=outdir, seedfile_as_target=min2seed, bitwise=options.bitwise, confidence=confidence, - logfile=minlog, - tempdir=outdir, + logfile='./min_log.txt', + tempdir=crash.tempdir, maxtime=options.timeout, preferx=options.prefer_x_target, keep_uniq_faddr=options.keep_uniq_faddr) as minimize: - minimize.save_others = options.keep_other_crashes + minimize.save_others = False minimize.target_size_guess = int(options.initial_target_size) minimize.go() @@ -188,11 +180,8 @@ def main(): with open(metasploit_file, 'wb') as f: f.writelines(targetstring) - for othercrash in minimize.other_crashes: - othercrashdir = os.path.join(outdir, minimize.other_crashes[othercrash].tempdir) - outcrashdir = os.path.join(outdir, os.path.basename(othercrashdir)) - filetools.mkdir_p(outcrashdir) - minimize.other_crashes[othercrash].copy_files(outcrashdir) + crash.copy_files(outdir) + crash.clean_tmpdir() if __name__ == '__main__': main() diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index 17aea9a..21d084a 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -46,8 +46,6 @@ def getiterpath(gdbfile): def main(): - debuggers.registration.verify_supported_platform() - from optparse import OptionParser hdlr = logging.StreamHandler() @@ -59,7 +57,7 @@ def main(): help='Enable debug messages (overrides --verbose)') parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') - parser.add_option('-c', '--config', default='conf.d/bff.yaml', + parser.add_option('-c', '--config', default='conf.d/bff.cfg', dest='config', help='path to the configuration file to use') parser.add_option('-e', '--edb', dest='use_edb', action='store_true', From 67ceec56f75b1822d295f38d9f9c269cab3cf468 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 15 Jan 2015 09:30:50 -0500 Subject: [PATCH 0669/1169] eliminate check_os_compatibility for the most part we don't need it as the newly integrated architecture helps with this --- src/certfuzz/debuggers/msec.py | 7 ++++--- src/certfuzz/helpers/__init__.py | 1 - src/certfuzz/helpers/misc.py | 4 ---- src/certfuzz/minimizer/__init__.py | 5 ----- src/certfuzz/runners/winrun.py | 2 -- src/certfuzz/test/helpers/test_helpers_pkg.py | 2 -- src/certfuzz/test/helpers/test_misc.py | 9 --------- 7 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 7bfaf27..c651309 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -10,14 +10,15 @@ from certfuzz.debuggers.debugger_base import Debugger as DebuggerBase from certfuzz.debuggers.output_parsers.msec_file import MsecFile -from certfuzz.helpers import check_os_compatibility -import wmi +import sys + +if sys.platform.startswith('win'): + import wmi logger = logging.getLogger(__name__) -check_os_compatibility('Windows', __name__) def factory(options): diff --git a/src/certfuzz/helpers/__init__.py b/src/certfuzz/helpers/__init__.py index 68a6612..f12f92b 100644 --- a/src/certfuzz/helpers/__init__.py +++ b/src/certfuzz/helpers/__init__.py @@ -1,6 +1,5 @@ from certfuzz.helpers.misc import quoted from certfuzz.helpers.misc import print_dict -from certfuzz.helpers.misc import check_os_compatibility from certfuzz.helpers.misc import random_str from certfuzz.helpers.misc import bitswap from certfuzz.helpers.misc import log_object diff --git a/src/certfuzz/helpers/misc.py b/src/certfuzz/helpers/misc.py index 1e94c07..07dc5b9 100644 --- a/src/certfuzz/helpers/misc.py +++ b/src/certfuzz/helpers/misc.py @@ -21,10 +21,6 @@ def print_dict(d): pprint(d) -def check_os_compatibility(expected_os, module_name=__name__): - if not my_os == expected_os: - template = 'Module %s is incompatible with %s (%s expected)' - raise ImportError(template % (module_name, my_os, expected_os)) def random_str(length=1): diff --git a/src/certfuzz/minimizer/__init__.py b/src/certfuzz/minimizer/__init__.py index 81029d0..e69de29 100644 --- a/src/certfuzz/minimizer/__init__.py +++ b/src/certfuzz/minimizer/__init__.py @@ -1,5 +0,0 @@ -# convenience imports to expose objects for use in other packages -from certfuzz.minimizer.minimizer_base import Minimizer -from certfuzz.minimizer.unix_minimizer import UnixMinimizer -from certfuzz.minimizer.win_minimizer import WindowsMinimizer -from certfuzz.minimizer.errors import MinimizerError, WindowsMinimizerError diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index f86efd5..89ed364 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -1,6 +1,4 @@ -from certfuzz.helpers import check_os_compatibility -check_os_compatibility('Windows') import platform from .errors import RunnerPlatformVersionError diff --git a/src/certfuzz/test/helpers/test_helpers_pkg.py b/src/certfuzz/test/helpers/test_helpers_pkg.py index 9bbf5d3..a4b1228 100644 --- a/src/certfuzz/test/helpers/test_helpers_pkg.py +++ b/src/certfuzz/test/helpers/test_helpers_pkg.py @@ -19,7 +19,6 @@ def test_api(self): module = certfuzz.helpers api_list = ['quoted', 'print_dict', - 'check_os_compatibility', 'random_str', 'bitswap', 'log_object', @@ -29,5 +28,4 @@ def test_api(self): if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/certfuzz/test/helpers/test_misc.py b/src/certfuzz/test/helpers/test_misc.py index 824edf6..2982309 100644 --- a/src/certfuzz/test/helpers/test_misc.py +++ b/src/certfuzz/test/helpers/test_misc.py @@ -23,15 +23,6 @@ def test_quoted(self): self.assertTrue(s in helpers.quoted(s)) self.assertEqual('"' + s + '"', helpers.quoted(s)) - def test_check_os_compatibility(self): - for expected in list('qwertyuiopasdfghjklzxcvbnm'): - self.assertRaises(ImportError, helpers.check_os_compatibility, expected) - - my_sys = platform.system() - try: - helpers.check_os_compatibility(my_sys) - except: - self.fail('Unexpected exception on %s' % my_sys) def test_random_str(self): self.assertEqual(1, len(helpers.random_str())) From b82e7954b7f846403b36610e32390fcebdb6a002 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 15 Jan 2015 09:32:38 -0500 Subject: [PATCH 0670/1169] fix how pipeline is invoked --- src/certfuzz/iteration/iteration_base3.py | 23 ++++--------------- src/certfuzz/iteration/iteration_windows.py | 15 +++++------- .../testcase_pipeline/tc_pipeline_base.py | 4 +++- .../testcase_pipeline/tc_pipeline_linux.py | 3 ++- .../testcase_pipeline/tc_pipeline_windows.py | 2 +- 5 files changed, 16 insertions(+), 31 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index f8d24ce..c853789 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -81,12 +81,7 @@ def __exit__(self, etype, value, traceback): return handled # clean up - try: - rm_rf(self.working_dir) - except: - # TODO: Minimizer may have a log file handle open - # If we get here, we've left files behind - pass + rm_rf(self.working_dir) return handled def _pre_fuzz(self): @@ -130,24 +125,14 @@ def run(self): def record_success(self): self.sf_set.record_success(key=self.seedfile.md5) - if self.r is not None: - # Fuzzer uses rangefinder - self.rf.record_success(key=self.r.id) - else: - # Fuzzer without rangefinder - self.seedfile.tries += 1 + self.rf.record_success(key=self.r.id) def record_failure(self): self.record_tries() def record_tries(self): self.sf_set.record_tries(key=self.seedfile.md5, tries=1) - if self.r is not None: - # Fuzzer uses rangefinder - self.rf.record_tries(key=self.r.id, tries=1) - else: - # Fuzzer without rangefinder - self.seedfile.tries += 1 + self.rf.record_tries(key=self.r.id, tries=1) def process_testcases(self): if not len(self.testcases): @@ -163,7 +148,7 @@ def process_testcases(self): workdirbase=self.working_dir, minimizable=self.fuzzer.is_minimizable, sf_set=self.sf_set) as pipeline: - pipeline() + pipeline.go() self.success = pipeline.success diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 86e00af..da7dd3e 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -33,16 +33,14 @@ class WindowsIteration(IterationBase3): tcpipeline_cls = WindowsTestCasePipeline - def __init__(self, seedfile, seednum, config, fuzzer_cls, - runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, - cmd_template, uniq_func, workdirbase, outdir, debug, - sf_set, rf): + def __init__(self, seedfile, seednum, workdirbase, outdir, sf_set, rf, uniq_func, config, + fuzzer_cls, runner_cls, keep_heisenbugs, keep_duplicates, + cmd_template, debug, + ): IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, sf_set, rf, uniq_func, config, None) self.fuzzer_cls = fuzzer_cls - self.runner_cls = runner - self.debugger_module = debugger - self.debugger_class = dbg_class + self.runner_cls = runner_cls self.debug = debug # TODO: do we use keep_uniq_faddr at all? self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] @@ -56,8 +54,7 @@ def __init__(self, seedfile, seednum, config, fuzzer_cls, # runner_cls is not null self.retries = 4 - self.pipeline_options = { - 'keep_duplicates': keep_duplicates, + self.pipeline_options = {'keep_duplicates': keep_duplicates, 'keep_heisenbugs': keep_heisenbugs, 'minimizable': False, 'cmd_template': self.cmd_template, diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 13a839b..80133a9 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -47,6 +47,8 @@ def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, self.analyzer_classes = [] self._setup_analyzers() + self.success = False + # this gets set up in __enter__ self.analysis_pipeline = None @@ -57,7 +59,7 @@ def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, def __enter__(self): self._setup_analysis_pipeline() filetools.mkdir_p(self.tc_dir) - return self.go + return self def __exit__(self, etype, value, traceback): TmpReaper().clean_tmp() diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 64b9cf7..8004ed3 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -17,7 +17,8 @@ from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file from certfuzz.fuzztools import filetools -from certfuzz.minimizer import MinimizerError, UnixMinimizer as Minimizer +from certfuzz.minimizer.errors import MinimizerError +from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.testcase_pipeline.errors import TestCasePipelineError import shutil diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 4654745..b13b1b4 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -7,7 +7,7 @@ import os from certfuzz.config.config_windows import get_command_args_list -from certfuzz.minimizer import WindowsMinimizer as Minimizer +from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase From d47fffa75769cb5a8ca23cd7cb3ec85dc43cbd39 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 15 Jan 2015 09:33:17 -0500 Subject: [PATCH 0671/1169] fix unit tests --- src/certfuzz/minimizer/minimizer_base.py | 6 +- src/certfuzz/minimizer/unix_minimizer.py | 9 +- src/certfuzz/minimizer/win_minimizer.py | 107 +++++++++++++++++- .../test/iteration/test_iteration_windows.py | 4 +- .../test/minimizer/test_minimizer_base.py | 7 +- .../test/minimizer/test_minimizer_pkg.py | 31 ----- 6 files changed, 119 insertions(+), 45 deletions(-) delete mode 100644 src/certfuzz/test/minimizer/test_minimizer_pkg.py diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index e3b8ab5..61dc909 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -15,7 +15,6 @@ import zipfile import collections -from certfuzz.debuggers.registration import get as debugger_get from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools import hamming, filetools, probability, text from certfuzz.fuzztools.filetools import delete_files, write_file, check_zip_file @@ -34,6 +33,7 @@ class Minimizer(object): use_watchdog = False + _debugger_cls = None def __init__(self, cfg=None, crash=None, crash_dst_dir=None, seedfile_as_target=False, bitwise=False, confidence=0.999, @@ -113,8 +113,6 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, if not os.path.isdir(self.crash_dst): self._raise("%s is not a directory" % self.crash_dst) - self.debugger = debugger_get() - self._logger_setup() self.logger.info("Minimizer initializing for %s", self.crash.fuzzedfile.path) @@ -383,7 +381,7 @@ def run_debugger(self, infile, outfile): self.debugger_runs += 1 cmd_args = self.cfg.get_command_args_list(infile) - dbg = self.debugger(self.cfg.program, + dbg = self._debugger_cls(self.cfg.program, cmd_args, outfile, self.debugger_timeout, diff --git a/src/certfuzz/minimizer/unix_minimizer.py b/src/certfuzz/minimizer/unix_minimizer.py index f3eed93..3ddd033 100644 --- a/src/certfuzz/minimizer/unix_minimizer.py +++ b/src/certfuzz/minimizer/unix_minimizer.py @@ -3,12 +3,19 @@ @organization: cert.org ''' -from certfuzz.minimizer import Minimizer as MinimizerBase +from certfuzz.minimizer.minimizer_base import Minimizer as MinimizerBase import os +from certfuzz.fuzztools.hostinfo import HostInfo + +if HostInfo().is_osx(): + from certfuzz.debuggers.crashwrangler import CrashWrangler as debugger_cls +else: + from certfuzz.debuggers.gdb import GDB as debugger_cls class UnixMinimizer(MinimizerBase): use_watchdog = True + _debugger_cls = debugger_cls def __enter__(self): # touch the watchdogfile diff --git a/src/certfuzz/minimizer/win_minimizer.py b/src/certfuzz/minimizer/win_minimizer.py index 9178673..0396d83 100755 --- a/src/certfuzz/minimizer/win_minimizer.py +++ b/src/certfuzz/minimizer/win_minimizer.py @@ -1,8 +1,12 @@ +import collections import logging +import zipfile from certfuzz.fuzztools.filetools import check_zip_file, write_file -from certfuzz.minimizer import Minimizer as MinimizerBase +from certfuzz.fuzztools.filetools import exponential_backoff +from certfuzz.minimizer.minimizer_base import Minimizer as MinimizerBase from certfuzz.minimizer.errors import WindowsMinimizerError +from certfuzz.debuggers.msec import MsecDebugger logger = logging.getLogger(__name__) @@ -10,6 +14,20 @@ class WindowsMinimizer(MinimizerBase): use_watchdog = False + _debugger_cls = MsecDebugger + + def __init__(self, cfg=None, crash=None, crash_dst_dir=None, + seedfile_as_target=False, bitwise=False, confidence=0.999, + logfile=None, tempdir=None, maxtime=3600, preferx=False, + keep_uniq_faddr=False, watchcpu=False): + + self.saved_arcinfo = None + self.is_zipfile = check_zip_file(crash.fuzzedfile.path) + + MinimizerBase.__init__(self, cfg, crash, crash_dst_dir, + seedfile_as_target, bitwise, confidence, + logfile, tempdir, maxtime, preferx, + keep_uniq_faddr, watchcpu) def get_signature(self, dbg, backtracelevels): # get the basic signature @@ -23,3 +41,90 @@ def get_signature(self, dbg, backtracelevels): self.signature = '.'.join(crash_id_parts) return self.signature + def _read_fuzzed(self): + ''' + returns the contents of the fuzzed file + ''' + # store the files in memory + if self.is_zipfile: # work with zip file contents, not the container + logger.debug('Working with a zip file') + return self._readzip(self.crash.fuzzedfile.path) + # otherwise just call the parent class method + return MinimizerBase._read_fuzzed(self) + + def _read_seed(self): + ''' + returns the contents of the seed file + ''' + # we're either going to minimize to the seedfile, the metasploit + # pattern, or a string of 'x's + if self.is_zipfile and self.seedfile_as_target: + return self._readzip(self.crash.seedfile.path) + # otherwise just call the parent class method + return MinimizerBase._read_seed(self) + + def _readzip(self, filepath): + # If the seed is zip-based, fuzz the contents rather than the container + logger.debug('Reading zip file: %s', filepath) + tempzip = zipfile.ZipFile(filepath, 'r') + + ''' + get info on all the archived files and concatentate their contents + into self.input + ''' + self.saved_arcinfo = collections.OrderedDict() + unzippedbytes = '' + logger.debug('Reading files from zip...') + for i in tempzip.namelist(): + data = tempzip.read(i) + + # save split indices and compression type for archival + # reconstruction. Keeping the same compression types is + # probably unnecessary since it's the content that matters + + self.saved_arcinfo[i] = (len(unzippedbytes), len(data), + tempzip.getinfo(i).compress_type) + unzippedbytes += data + tempzip.close() + return unzippedbytes + + @exponential_backoff + def _safe_createzip(self, filepath): + tempzip = zipfile.ZipFile(filepath, 'w') + return tempzip + + def _writezip(self): + '''rebuild the zip file and put it in self.fuzzed + Note: We assume that the fuzzer has not changes the lengths + of the archived files, otherwise we won't be able to properly + split self.fuzzed + ''' + if self.saved_arcinfo is None: + raise WindowsMinimizerError('_readzip was not called') + + filedata = ''.join(self.newfuzzed) + filepath = self.tempfile + + logger.debug('Creating zip with mutated contents.') + tempzip = self._safe_createzip(filepath, 'w') + + ''' + reconstruct archived files, using the same compression scheme as + the source + ''' + for name, info in self.saved_arcinfo.iteritems(): + # write out fuzzed file + if info[2] == 0 or info[2] == 8: + # Python zipfile only supports compression types 0 and 8 + compressiontype = info[2] + else: + logger.warning('Compression type %s is not supported. Overriding', info[2]) + compressiontype = 8 + tempzip.writestr(name, str(filedata[info[0]:info[0] + info[1]]), compress_type=compressiontype) + tempzip.close() + + def _write_file(self): + if self.is_zipfile: + self._writezip() + else: + write_file(''.join(self.newfuzzed), self.tempfile) diff --git a/src/certfuzz/test/iteration/test_iteration_windows.py b/src/certfuzz/test/iteration/test_iteration_windows.py index a353fc2..c32b00d 100644 --- a/src/certfuzz/test/iteration/test_iteration_windows.py +++ b/src/certfuzz/test/iteration/test_iteration_windows.py @@ -16,8 +16,8 @@ def setUp(self): # runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, # cmd_template, uniq_func, workdirbase, outdir, debug, # sf_set, rf - args = list('0123456789ABCDEF') - args[2] = {'runoptions': {'keep_unique_faddr': False}} + args = list('0123456789ABCD') + args[7] = {'runoptions': {'keep_unique_faddr': False}} self.iteration = WindowsIteration(*args) def tearDown(self): diff --git a/src/certfuzz/test/minimizer/test_minimizer_base.py b/src/certfuzz/test/minimizer/test_minimizer_base.py index 3d9bcb9..dff3b04 100644 --- a/src/certfuzz/test/minimizer/test_minimizer_base.py +++ b/src/certfuzz/test/minimizer/test_minimizer_base.py @@ -50,10 +50,6 @@ def get(self): def go(self): return MockDbgOut() -def _mock_dbg_get(): - return MockDebugger - - class Test(unittest.TestCase): def delete_file(self, f): os.remove(f) @@ -63,7 +59,7 @@ def setUp(self): self.cfg = MockCfg() self.crash = MockCrasher() - certfuzz.minimizer.minimizer_base.debugger_get = _mock_dbg_get + certfuzz.minimizer.minimizer_base.Minimizer._debugger_cls = MockDebugger self.tempdir = tempfile.mkdtemp(prefix='minimizer_test_') self.crash_dst_dir = tempfile.mkdtemp(prefix='crash_', dir=self.tempdir) @@ -71,7 +67,6 @@ def setUp(self): os.close(fd) os.remove(self.logfile) self.assertFalse(os.path.exists(self.logfile)) - certfuzz.minimizer.minimizer_base.debuggers = MockDebugger() self.m = Minimizer(cfg=self.cfg, crash=self.crash, crash_dst_dir=self.crash_dst_dir, diff --git a/src/certfuzz/test/minimizer/test_minimizer_pkg.py b/src/certfuzz/test/minimizer/test_minimizer_pkg.py deleted file mode 100644 index 78a23e7..0000000 --- a/src/certfuzz/test/minimizer/test_minimizer_pkg.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from certfuzz.test import misc -import certfuzz.minimizer - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.minimizer - api_list = ['Minimizer', - 'MinimizerError', - 'UnixMinimizer', - 'WindowsMinimizer', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From ad92afa6129fe99647475c5b14300bc5c95ccc72 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 15 Jan 2015 10:57:36 -0500 Subject: [PATCH 0672/1169] use an iteration counter to decide if quiet flag is set --- src/certfuzz/campaign/campaign_base.py | 29 +++-------------------- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/iteration/iteration_base3.py | 5 ++++ src/certfuzz/iteration/iteration_linux.py | 1 + 4 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index f7cc855..29b7e30 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -13,15 +13,12 @@ import tempfile import traceback import cPickle as pickle -import signal from certfuzz.campaign.errors import CampaignError from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.fuzztools import filetools from certfuzz.runners.errors import RunnerArchitectureError, \ RunnerPlatformVersionError -from certfuzz.fuzzers.errors import FuzzerExhaustedError -from certfuzz.file_handlers.errors import SeedfileSetError from certfuzz.version import __version__ from certfuzz.file_handlers.tmp_reaper import TmpReaper import gc @@ -98,8 +95,6 @@ def __init__(self, config_file, result_dir=None, debug=False): self.outdir_base = None self.outdir = None self.sf_set_out = None - self.stopfuzzing = False - self.pk_pid = None if result_dir: self.outdir_base = os.path.abspath(result_dir) @@ -210,9 +205,6 @@ def _pre_exit(self): Implements methods to be completed prior to handling errors in the __exit__ method. No return value. ''' - # Kill off process killer - if self.pk_pid: - os.kill(self.pk_pid, signal.SIGTERM) def __exit__(self, etype, value, mytraceback): ''' @@ -363,10 +355,7 @@ def _keep_going(self): ''' Returns True if a campaign should proceed. False otherwise. ''' - if self.stopfuzzing: - return False - else: - return True + return True def _do_interval(self): ''' @@ -376,13 +365,7 @@ def _do_interval(self): TmpReaper().clean_tmp() # choose seedfile - try: - sf = self.seedfile_set.next_item() - except SeedfileSetError: - logger.info('Seedfile set is empty. Terminating') - self.stopfuzzing = True - return - + sf = self.seedfile_set.next_item() logger.info('Selected seedfile: %s', sf.basename) # TODO: restore this @@ -391,7 +374,6 @@ def _do_interval(self): # self._save_state() r = sf.rangefinder.next_item() - qf = not self.first_chunk # rng_seed = int(sf.md5, 16) @@ -401,12 +383,7 @@ def _do_interval(self): # note that range does not include interval_limit logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) for seednum in xrange(self.current_seed, interval_limit): - try: - self._do_iteration(sf, r, qf, seednum) - except FuzzerExhaustedError: - logger.info('Fuzzer exhausted for seedfile %s' % sf.basename) - self.seedfile_set.remove_file(sf) - break + self._do_iteration(sf, r, seednum) del sf # manually collect garbage diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 11f564a..4314934 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -186,7 +186,7 @@ def _save_state(self): ''' pass - def _do_iteration(self, seedfile, range_obj, quiet_flag, seednum): + def _do_iteration(self, seedfile, range_obj, seednum): # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() with LinuxIteration(seedfile=seedfile, diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index c853789..f24f7a5 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -14,6 +14,7 @@ class IterationBase3(object): __metaclass__ = abc.ABCMeta _tmpdir_pfx = 'iteration_' + _iteration_counter = 0 def __init__(self, seedfile=None, @@ -63,11 +64,15 @@ def __enter__(self): dir=self.workdirbase) logger.debug('workdir=%s', self.working_dir) # self._setup_analysis_pipeline() + return self.go def __exit__(self, etype, value, traceback): handled = False + # increment the iteration counter + IterationBase3._iteration_counter += 1 + if self.success: # score it so we can learn self.record_success() diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index ea55697..9cc4a2b 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -39,6 +39,7 @@ def __init__(self, seedfile=None, seednum=None, workdirbase=None, outdir=None, self.fuzzer_cls = ByteMutFuzzer self.runner_cls = ZzufRunner self.quiet_flag = quiet + self.quiet_flag = self._iteration_counter < 2 self.testcase_base_dir = os.path.join(self.outdir, 'crashers') From 0256ab0b19021cfb7c169fe6a2e52be7f86ebf19 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 15 Jan 2015 14:15:44 -0500 Subject: [PATCH 0673/1169] refactor iteration args --- src/certfuzz/campaign/campaign_linux.py | 8 +++- src/certfuzz/campaign/campaign_windows.py | 23 +++++++---- src/certfuzz/iteration/iteration_base3.py | 13 +++++- src/certfuzz/iteration/iteration_linux.py | 32 +++++++------- src/certfuzz/iteration/iteration_windows.py | 46 +++++++++++++-------- 5 files changed, 78 insertions(+), 44 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 4314934..9f11675 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -21,6 +21,9 @@ from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.iteration.iteration_linux import LinuxIteration +from certfuzz.fuzzers.zzuf import ZzufFuzzer +from certfuzz.fuzzers.bytemut import ByteMutFuzzer +from certfuzz.runners.zzufrun import ZzufRunner logger = logging.getLogger(__name__) @@ -197,6 +200,7 @@ def _do_iteration(self, seedfile, range_obj, seednum): rf=seedfile.rangefinder, uniq_func=self._crash_is_unique, cfg=self.config, - r=range_obj, - quiet=quiet_flag) as iteration: + fuzzer_cls=ByteMutFuzzer, + runner_cls=ZzufRunner, + ) as iteration: iteration() diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 625d640..75fa8e5 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -186,16 +186,25 @@ def _stop_buttonclicker(self): if self.use_buttonclicker: os.system('taskkill /im buttonclicker.exe') - def _do_iteration(self, sf, range_obj, quiet_flag, seednum): + def _do_iteration(self, sf, range_obj, seednum): # use a with...as to ensure we always hit # the __enter__ and __exit__ methods of the # newly created WindowsIteration() - with WindowsIteration(sf, seednum, self.config, self.fuzzer_cls, - self.runner_cls, self.debugger_module, - self.keep_heisenbugs, self.keep_duplicates, - self.cmd_template, self._crash_is_unique, - self.working_dir, self.outdir, self.debug, self.seedfile_set, - sf.rangefinder) as iteration: + with WindowsIteration(seedfile=sf, + seednum=seednum, + workdirbase=self.working_dir, + outdir=self.outdir, + sf_set=self.seedfile_set, + rf=sf.rangefinder, + uniq_func=self._crash_is_unique, + config=self.config, + fuzzer_cls=self.fuzzer_cls, + runner_cls=self.runner_cls, + keep_heisenbugs=self.keep_heisenbugs, + keep_duplicates=self.keep_duplicates, + cmd_template=self.cmd_template, + debug=self.debug, + ) as iteration: try: iteration() except FuzzerExhaustedError: diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index f24f7a5..790615f 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -25,7 +25,9 @@ def __init__(self, rf=None, uniq_func=None, config=None, - r=None): + fuzzer_cls=None, + runner_cls=None, + ): logger.debug('init') self.seedfile = seedfile @@ -35,7 +37,10 @@ def __init__(self, self.sf_set = sf_set self.rf = rf self.cfg = config - self.r = r + self.fuzzer_cls = fuzzer_cls + self.runner_cls = runner_cls + + self.r = None self.pipeline_options = {} @@ -96,6 +101,10 @@ def _fuzz(self): with self.fuzzer: self.fuzzer.fuzz() + self.r = self.fuzzer.range + if self.r is not None: + logger.debug('Selected r: %s', self.r) + def _post_fuzz(self): pass diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 9cc4a2b..6e8abea 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -22,8 +22,18 @@ class LinuxIteration(IterationBase3): tcpipeline_cls = LinuxTestCasePipeline - def __init__(self, seedfile=None, seednum=None, workdirbase=None, outdir=None, - sf_set=None, rf=None, uniq_func=None, cfg=None, r=None, quiet=True,): + def __init__(self, + seedfile=None, + seednum=None, + workdirbase=None, + outdir=None, + sf_set=None, + rf=None, + uniq_func=None, + cfg=None, + fuzzer_cls=None, + runner_cls=None, + ): IterationBase3.__init__(self, seedfile, @@ -34,11 +44,10 @@ def __init__(self, seedfile=None, seednum=None, workdirbase=None, outdir=None, rf, uniq_func, cfg, - r) + fuzzer_cls, + runner_cls, + ) - self.fuzzer_cls = ByteMutFuzzer - self.runner_cls = ZzufRunner - self.quiet_flag = quiet self.quiet_flag = self._iteration_counter < 2 self.testcase_base_dir = os.path.join(self.outdir, 'crashers') @@ -59,6 +68,7 @@ def __init__(self, seedfile=None, seednum=None, workdirbase=None, outdir=None, 'uniq_log': self.cfg.uniq_log, 'local_dir': self.cfg.local_dir, 'minimizertimeout': self.cfg.minimizertimeout, + 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg.config['runoptions']['minimize'], } def __enter__(self): @@ -70,16 +80,6 @@ def _pre_fuzz(self): fuzz_opts = self.cfg.config['fuzzer'] self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, fuzz_opts) - def _post_fuzz(self): - self.r = self.fuzzer.range - if self.r: - logger.debug('Selected r: %s', self.r) - - # decide if we can minimize this case later - # do this here (and not sooner) because the fuzzer could - # decide at runtime whether it is or is not minimizable - self.pipeline_options['minimizable'] = self.fuzzer.is_minimizable and self.cfg.config['runoptions']['minimize'] - def _pre_run(self): options = self.cfg.config['runner'] cmd_template = self.cfg.config['target']['cmdline'] diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index da7dd3e..473f97b 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -33,14 +33,35 @@ class WindowsIteration(IterationBase3): tcpipeline_cls = WindowsTestCasePipeline - def __init__(self, seedfile, seednum, workdirbase, outdir, sf_set, rf, uniq_func, config, - fuzzer_cls, runner_cls, keep_heisenbugs, keep_duplicates, - cmd_template, debug, + def __init__(self, + seedfile=None, + seednum=None, + workdirbase=None, + outdir=None, + sf_set=None, + rf=None, + uniq_func=None, + config=None, + fuzzer_cls=None, + runner_cls=None, + keep_heisenbugs=None, + keep_duplicates=None, + cmd_template=None, + debug=False, ): - IterationBase3.__init__(self, seedfile, seednum, workdirbase, outdir, - sf_set, rf, uniq_func, config, None) - self.fuzzer_cls = fuzzer_cls - self.runner_cls = runner_cls + IterationBase3.__init__(self, + seedfile, + seednum, + workdirbase, + outdir, + sf_set, + rf, + uniq_func, + config, + fuzzer_cls, + runner_cls, + ) + self.debug = debug # TODO: do we use keep_uniq_faddr at all? self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] @@ -59,6 +80,7 @@ def __init__(self, seedfile, seednum, workdirbase, outdir, sf_set, rf, uniq_func 'minimizable': False, 'cmd_template': self.cmd_template, 'used_runner': self.runner_cls is not None, + 'minimizable': self.fuzzer.is_minimizable and self.cfg['runoptions']['minimize'] } def __exit__(self, etype, value, traceback): @@ -129,16 +151,6 @@ def _pre_fuzz(self): fuzz_opts = self.cfg['fuzzer'] self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, fuzz_opts) - def _post_fuzz(self): - self.r = self.fuzzer.range - if self.r: - logger.info('Selected r: %s', self.r) - - # decide if we can minimize this case later - # do this here (and not sooner) because the fuzzer_cls could - # decide at runtime whether it is or is not minimizable - self.pipeline_options['minimizable'] = self.fuzzer.is_minimizable and self.cfg['runoptions']['minimize'] - def _pre_run(self): options = self.cfg['runner'] cmd_template = self.cmd_template From ad2ecf88bd00590628fae479936978c3298d2d5a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 15 Jan 2015 14:16:17 -0500 Subject: [PATCH 0674/1169] we don't need to pass rangefinders around separately from the seed file they are attached to --- src/certfuzz/campaign/campaign_linux.py | 1 - src/certfuzz/campaign/campaign_windows.py | 1 - src/certfuzz/iteration/iteration_base3.py | 6 ++---- src/certfuzz/iteration/iteration_linux.py | 2 -- src/certfuzz/iteration/iteration_windows.py | 2 -- 5 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 9f11675..84a7f6b 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -197,7 +197,6 @@ def _do_iteration(self, seedfile, range_obj, seednum): workdirbase=self.working_dir, outdir=self.outdir, sf_set=self.seedfile_set, - rf=seedfile.rangefinder, uniq_func=self._crash_is_unique, cfg=self.config, fuzzer_cls=ByteMutFuzzer, diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 75fa8e5..1950b95 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -195,7 +195,6 @@ def _do_iteration(self, sf, range_obj, seednum): workdirbase=self.working_dir, outdir=self.outdir, sf_set=self.seedfile_set, - rf=sf.rangefinder, uniq_func=self._crash_is_unique, config=self.config, fuzzer_cls=self.fuzzer_cls, diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 790615f..a3e9a9f 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -22,7 +22,6 @@ def __init__(self, workdirbase=None, outdir=None, sf_set=None, - rf=None, uniq_func=None, config=None, fuzzer_cls=None, @@ -35,7 +34,6 @@ def __init__(self, self.workdirbase = workdirbase self.outdir = outdir self.sf_set = sf_set - self.rf = rf self.cfg = config self.fuzzer_cls = fuzzer_cls self.runner_cls = runner_cls @@ -139,14 +137,14 @@ def run(self): def record_success(self): self.sf_set.record_success(key=self.seedfile.md5) - self.rf.record_success(key=self.r.id) + self.seedfile.rangefinder.record_success(key=self.r.id) def record_failure(self): self.record_tries() def record_tries(self): self.sf_set.record_tries(key=self.seedfile.md5, tries=1) - self.rf.record_tries(key=self.r.id, tries=1) + self.seedfile.rangefinder.record_tries(key=self.r.id, tries=1) def process_testcases(self): if not len(self.testcases): diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 6e8abea..a3895ff 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -28,7 +28,6 @@ def __init__(self, workdirbase=None, outdir=None, sf_set=None, - rf=None, uniq_func=None, cfg=None, fuzzer_cls=None, @@ -41,7 +40,6 @@ def __init__(self, workdirbase, outdir, sf_set, - rf, uniq_func, cfg, fuzzer_cls, diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 473f97b..a06be01 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -39,7 +39,6 @@ def __init__(self, workdirbase=None, outdir=None, sf_set=None, - rf=None, uniq_func=None, config=None, fuzzer_cls=None, @@ -55,7 +54,6 @@ def __init__(self, workdirbase, outdir, sf_set, - rf, uniq_func, config, fuzzer_cls, From 2efdb9a776b93a7f00cb7f1f3b73d7047c197ee1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 15 Jan 2015 14:43:09 -0500 Subject: [PATCH 0675/1169] refactor runner compatibility check --- src/certfuzz/campaign/campaign_windows.py | 26 +++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 1950b95..7883d87 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -107,13 +107,25 @@ def __setstate__(self, state): self.__dict__.update(state) def _pre_enter(self): - if sys.platform == 'win32': - winver = sys.getwindowsversion().major - machine = platform.machine() - hook_incompat = winver > 5 or machine == 'AMD64' - if hook_incompat and self.runner_module_name == 'certfuzz.runners.winrun': - logger.debug('winrun is not compatible with Windows %s %s. Overriding.', winver, machine) - self.runner_module_name = None + # check to see if the platform supports winrun + # set runner module to none otherwise + + if sys.platform != 'win32': + return + + if not self.runner_module_name == 'certfuzz.runners.winrun': + return + + # if we got here, we're on win32, and trying to use winrun + winver = sys.getwindowsversion().major + machine = platform.machine() + hook_incompatible = winver > 5 or machine == 'AMD64' + + if not hook_incompatible: + return + + logger.debug('winrun is not compatible with Windows %s %s. Overriding.', winver, machine) + self.runner_module_name = None def _post_enter(self): self._start_buttonclicker() From e4456d6a8bfdcdf6c8bc7adb460da3dc54521262 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 15 May 2015 14:41:55 -0400 Subject: [PATCH 0676/1169] tweak campaign id semantics --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/config/config_linux.py | 2 +- src/certfuzz/config/config_windows.py | 1 - src/certfuzz/file_handlers/seedfile_set.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 84a7f6b..1297d85 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -63,7 +63,7 @@ def __init__(self, config_file=None, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) # pull stuff out of configs - self.campaign_id = self.config.campaign_id + self.campaign_id = self.config.config['campaign']['id'] self.current_seed = self.config.start_seed self.seed_interval = self.config.seed_interval self.seed_dir_in = self.config.seedfile_origin_dir diff --git a/src/certfuzz/config/config_linux.py b/src/certfuzz/config/config_linux.py index d912c62..86e2800 100644 --- a/src/certfuzz/config/config_linux.py +++ b/src/certfuzz/config/config_linux.py @@ -72,7 +72,7 @@ def _set_derived_options(self): # [campaign] campaign_id = re.sub('\s+', '_', self.config['campaign']['id']) - self.campaign_id = campaign_id + self.config['campaign']['id'] = campaign_id # unroll ~ & relative paths dir_dict = self.config['directories'] diff --git a/src/certfuzz/config/config_windows.py b/src/certfuzz/config/config_windows.py index b329bd3..cc2c364 100644 --- a/src/certfuzz/config/config_windows.py +++ b/src/certfuzz/config/config_windows.py @@ -41,7 +41,6 @@ def _set_derived_options(self): # interpolate program name # add quotes around $SEEDFILE t = Template(self.config['target']['cmdline_template']) -# self.config['target']['cmdline_template'] = t.safe_substitute(PROGRAM=self.config['target']['program']) self.config['target']['cmdline_template'] = t.safe_substitute(PROGRAM=quoted(self.config['target']['program']), SEEDFILE=quoted('$SEEDFILE')) diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 7a90de8..2006d28 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -27,7 +27,7 @@ def __init__(self, campaign_id=None, originpath=None, localpath=None, Constructor ''' MultiArmedBandit.__init__(self) - self.campaign_id = campaign_id +# self.campaign_id = campaign_id self.seedfile_output_base_dir = outputpath self.originpath = originpath From 8af55dacd7cdb8866f396499a1cb110ff5deae0f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 10:30:28 -0500 Subject: [PATCH 0677/1169] better fix for BFF-856 --- src/certfuzz/iteration/iteration_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index a06be01..157e15e 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -78,7 +78,7 @@ def __init__(self, 'minimizable': False, 'cmd_template': self.cmd_template, 'used_runner': self.runner_cls is not None, - 'minimizable': self.fuzzer.is_minimizable and self.cfg['runoptions']['minimize'] + 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions']['minimize'] } def __exit__(self, etype, value, traceback): From 023481806e37007ed9d76057d111278ef6409c4a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 10:30:56 -0500 Subject: [PATCH 0678/1169] fix unit tests --- .../test/iteration/test_iteration_windows.py | 24 ++++++++++++++----- src/certfuzz/test/mocks.py | 3 +++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/test/iteration/test_iteration_windows.py b/src/certfuzz/test/iteration/test_iteration_windows.py index c32b00d..4769173 100644 --- a/src/certfuzz/test/iteration/test_iteration_windows.py +++ b/src/certfuzz/test/iteration/test_iteration_windows.py @@ -6,18 +6,30 @@ import unittest from certfuzz.iteration.iteration_windows import WindowsIteration +from certfuzz.test.mocks import MockFuzzer class Test(unittest.TestCase): def setUp(self): # args: -# seedfile, seednum, config, fuzzer_cls, -# runner, debugger, dbg_class, keep_heisenbugs, keep_duplicates, -# cmd_template, uniq_func, workdirbase, outdir, debug, -# sf_set, rf - args = list('0123456789ABCD') - args[7] = {'runoptions': {'keep_unique_faddr': False}} +# seedfile=None, +# seednum=None, +# workdirbase=None, +# outdir=None, +# sf_set=None, +# uniq_func=None, +# config=None, +# fuzzer_cls=None, +# runner_cls=None, +# keep_heisenbugs=None, +# keep_duplicates=None, +# cmd_template=None, +# debug=False, + + args = list('ABCDEFGHIJKLM') + args[6] = {'runoptions': {'keep_unique_faddr': False}} + args[7] = MockFuzzer self.iteration = WindowsIteration(*args) def tearDown(self): diff --git a/src/certfuzz/test/mocks.py b/src/certfuzz/test/mocks.py index 67fdc53..1a9b70c 100644 --- a/src/certfuzz/test/mocks.py +++ b/src/certfuzz/test/mocks.py @@ -29,3 +29,6 @@ def __init__(self, sz=1000): def read(self): return self.value + +class MockFuzzer(Mock): + is_minimizable = False From 94e92476d9d792e52420769426ee08e7868a8efb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 11:08:00 -0500 Subject: [PATCH 0679/1169] fix standalone minimizer import in windows --- src/certfuzz/tools/windows/minimize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index e4c324f..38f7bb6 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -14,7 +14,7 @@ from certfuzz import debuggers from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.minimizer import WindowsMinimizer as Minimizer + from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer from certfuzz.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport @@ -27,7 +27,7 @@ from certfuzz import debuggers from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.minimizer import WindowsMinimizer as Minimizer + from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer from certfuzz.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport From 0d064166a7d558acf61ac349b3d086439dd482c8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 12:25:48 -0500 Subject: [PATCH 0680/1169] fix import --- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index b13b1b4..2f62386 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -9,6 +9,7 @@ from certfuzz.config.config_windows import get_command_args_list from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase +from certfuzz.fuzztools import filetools logger = logging.getLogger(__name__) From 7d211a476aa0b5b7d7436a537a7d47f0552d57bb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 12:26:39 -0500 Subject: [PATCH 0681/1169] improve fix for BFF-834 --- .../testcase_pipeline/tc_pipeline_windows.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 2f62386..097a9e8 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -10,6 +10,7 @@ from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.fuzztools import filetools +from certfuzz.minimizer.errors import MinimizerError logger = logging.getLogger(__name__) @@ -57,13 +58,16 @@ def _minimize(self, testcase): 'maxtime': self.cfg['runoptions']['minimizer_timeout'] } - with Minimizer(**kwargs) as minimizer: - minimizer.go() + try: + with Minimizer(**kwargs) as minimizer: + minimizer.go() - # minimzer found other crashes, so we should add them - # to our list for subsequent processing - for tc in minimizer.other_crashes.values(): - self.tc_candidate_q.put(tc) + # minimizer found other crashes, so we should add them + # to our list for subsequent processing + for tc in minimizer.other_crashes.values(): + self.tc_candidate_q.put(tc) + except MinimizerError as e: + logger.error('Caught MinimizerError: {}'.format(e)) def _post_minimize(self, testcase): if self.cfg['runoptions']['recycle_crashers']: From f675afaed46186c503d7bb90592fd95a58823752 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 11:00:00 -0500 Subject: [PATCH 0682/1169] nothing was using _zzuf_range and _zzuf_line anymore? --- src/certfuzz/iteration/iteration_linux.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index a3895ff..d32bc27 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -50,8 +50,8 @@ def __init__(self, self.testcase_base_dir = os.path.join(self.outdir, 'crashers') - self._zzuf_range = None - self._zzuf_line = None +# self._zzuf_range = None +# self._zzuf_line = None # analysis is required in two cases: # 1) runner_cls is not defined (self.runner_cls == None) @@ -91,7 +91,7 @@ def _run(self): self.runner.run() def _post_run(self): - #self.record_tries() + # self.record_tries() if not self.runner.saw_crash: logger.debug('No crash seen') @@ -114,8 +114,8 @@ def _post_run(self): return # store a few things for use downstream - self._zzuf_range = zzuf_log.range - self._zzuf_line = zzuf_log.line +# self._zzuf_range = zzuf_log.range +# self._zzuf_line = zzuf_log.line self._construct_testcase() def _construct_testcase(self): From 0a3d52a744a7f6dc0fd9d2e0acf6dce9c30360da Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 11:14:16 -0500 Subject: [PATCH 0683/1169] restore quiet_flag after first two iterations --- src/certfuzz/iteration/iteration_linux.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index d32bc27..6ebca2d 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -80,6 +80,9 @@ def _pre_fuzz(self): def _pre_run(self): options = self.cfg.config['runner'] + + if self.quiet_flag: + options['hideoutput'] = True cmd_template = self.cfg.config['target']['cmdline'] fuzzed_file = self.fuzzer.output_file_path workingdir_base = self.working_dir From b349fae980061c71dc7c394219c18c03e1a6d452 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 11:19:39 -0500 Subject: [PATCH 0684/1169] refactor windows crash invocation --- src/certfuzz/iteration/iteration_windows.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 157e15e..2835eda 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -177,15 +177,16 @@ def _run(self): self._construct_testcase() def _construct_testcase(self): - cmdlist = get_command_args_list(self.cmd_template, self.fuzzer.output_file_path)[1] - dbg_opts = self.cfg['debugger'] - fuzzed_file = BasicFile(self.fuzzer.output_file_path) - logger.debug('Building testcase object') - with WindowsCrash(self.cmd_template, self.seedfile, fuzzed_file, cmdlist, - self.fuzzer, dbg_opts, - self.working_dir, self.cfg['runoptions']['keep_unique_faddr'], - self.cfg['target']['program'], + with WindowsCrash(cmd_template=self.cmd_template, + seedfile=self.seedfile, + fuzzedfile=BasicFile(self.fuzzer.output_file_path), + cmdlist=get_command_args_list(self.cmd_template, self.fuzzer.output_file_path)[1], + fuzzer=self.fuzzer, + dbg_opts=self.cfg['debugger'], + workingdir_base=self.working_dir, + keep_faddr=self.cfg['runoptions']['keep_unique_faddr'], + program=self.cfg['target']['program'], heisenbug_retries=self.retries, copy_fuzzedfile=self.fuzzer.fuzzed_changes_input) as testcase: From 714972d622046dee6888cc03d237c7a5b1699702 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 11:38:32 -0500 Subject: [PATCH 0685/1169] refactor linux crash invocation --- src/certfuzz/iteration/iteration_linux.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 6ebca2d..c666cef 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -122,12 +122,10 @@ def _post_run(self): self._construct_testcase() def _construct_testcase(self): - fuzzed_file = BasicFile(self.fuzzer.output_file_path) - logger.info('Building testcase object') with BffCrash(cfg=self.cfg, seedfile=self.seedfile, - fuzzedfile=fuzzed_file, + fuzzedfile=BasicFile(self.fuzzer.output_file_path), program=self.cfg.program, debugger_timeout=self.cfg.debugger_timeout, killprocname=self.cfg.killprocname, From 343aeeb92ad13233c1122ef47dd032a4323af138 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 11:39:13 -0500 Subject: [PATCH 0686/1169] refactor common _pre_fuzz code into base class --- src/certfuzz/iteration/iteration_base3.py | 2 +- src/certfuzz/iteration/iteration_linux.py | 4 ++-- src/certfuzz/iteration/iteration_windows.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index a3e9a9f..f63aa2e 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -93,7 +93,7 @@ def __exit__(self, etype, value, traceback): return handled def _pre_fuzz(self): - pass + self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, self._fuzz_opts) def _fuzz(self): with self.fuzzer: diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index c666cef..290a656 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -75,8 +75,8 @@ def __enter__(self): return self.go def _pre_fuzz(self): - fuzz_opts = self.cfg.config['fuzzer'] - self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, fuzz_opts) + self._fuzz_opts = self.cfg.config['fuzzer'] + IterationBase3._pre_fuzz(self) def _pre_run(self): options = self.cfg.config['runner'] diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 2835eda..9385c18 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -146,8 +146,8 @@ def _tidy(self): TmpReaper().clean_tmp() def _pre_fuzz(self): - fuzz_opts = self.cfg['fuzzer'] - self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, fuzz_opts) + self._fuzz_opts = self.cfg['fuzzer'] + IterationBase3._pre_fuzz(self) def _pre_run(self): options = self.cfg['runner'] From 7fa616122314b26e26f8d1652f378c86a8efe228 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 11:39:49 -0500 Subject: [PATCH 0687/1169] refactor common runner code into base class --- src/certfuzz/iteration/iteration_base3.py | 8 ++++-- src/certfuzz/iteration/iteration_linux.py | 32 +++++++++------------ src/certfuzz/iteration/iteration_windows.py | 15 ++++------ 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index f63aa2e..20e4bae 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -107,11 +107,15 @@ def _post_fuzz(self): pass def _pre_run(self): + fuzzed_file = self.fuzzer.output_file_path + workingdir_base = self.working_dir + self.runner = self.runner_cls(self._runner_options, self._runner_cmd_template, fuzzed_file, workingdir_base) + pass - @abc.abstractmethod def _run(self): - pass + with self.runner: + self.runner.run() def _post_run(self): pass diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 290a656..eded140 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -53,11 +53,6 @@ def __init__(self, # self._zzuf_range = None # self._zzuf_line = None - # analysis is required in two cases: - # 1) runner_cls is not defined (self.runner_cls == None) - # 2) runner_cls is defined, and detects crash (runner_cls.saw_crash == True) - # this takes care of case 1 by default - self._analysis_needed = True self.pipeline_options = {'use_valgrind': self.cfg.use_valgrind, 'use_pin_calltrace': self.cfg.use_pin_calltrace, @@ -79,23 +74,15 @@ def _pre_fuzz(self): IterationBase3._pre_fuzz(self) def _pre_run(self): - options = self.cfg.config['runner'] + self._runner_options = self.cfg.config['runner'] if self.quiet_flag: - options['hideoutput'] = True - cmd_template = self.cfg.config['target']['cmdline'] - fuzzed_file = self.fuzzer.output_file_path - workingdir_base = self.working_dir - - self.runner = self.runner_cls(options, cmd_template, fuzzed_file, workingdir_base) + self._runner_options['hideoutput'] = True + self._runner_cmd_template = self.cfg.config['target']['cmdline'] - def _run(self): - with self.runner: - self.runner.run() + IterationBase3._pre_run(self) def _post_run(self): - # self.record_tries() - if not self.runner.saw_crash: logger.debug('No crash seen') return @@ -108,12 +95,19 @@ def _post_run(self): for line in pformat(zzuf_log.__dict__).splitlines(): logger.debug(line) + # analysis is required in two cases: + # 1) runner_cls is not defined (self.runner_cls == None) + # 2) runner_cls is defined, and detects crash (runner_cls.saw_crash == True) + # this takes care of case 1 by default + # TODO: does case 1 ever happen? + analysis_needed = True + # Don't generate cases for killed process or out-of-memory # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will # report the exit code in its output log. The exit code is 128 + the signal number. - self._analysis_needed = zzuf_log.crash_logged(self.cfg.config['zzuf']['copymode']) + analysis_needed = zzuf_log.crash_logged(self.cfg.config['zzuf']['copymode']) - if not self._analysis_needed: + if not analysis_needed: return # store a few things for use downstream diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 9385c18..81edbe4 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -150,27 +150,22 @@ def _pre_fuzz(self): IterationBase3._pre_fuzz(self) def _pre_run(self): - options = self.cfg['runner'] - cmd_template = self.cmd_template - fuzzed_file = self.fuzzer.output_file_path - workingdir_base = self.working_dir + self._runner_options = self.cfg['runner'] + self._runner_cmd_template = self.cmd_template - self.runner = self.runner_cls(options, cmd_template, fuzzed_file, workingdir_base) + IterationBase3._pre_run(self) - def _run(self): + def _post_run(self): # analysis is required in two cases: # 1) runner_cls is not defined (self.runner_cls == None) # 2) runner_cls is defined, and detects crash (runner_cls.saw_crash == True) # this takes care of case 1 by default # TODO: does case 1 ever happen? analysis_needed = True + if self.runner_cls: - with self.runner: - self.runner.run() - # this takes care of case 2 analysis_needed = self.runner.saw_crash - # is further analysis needed? if not analysis_needed: return From ae41052616bada1567c9e13273befa73121113db Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 11:41:23 -0500 Subject: [PATCH 0688/1169] remove dead code --- src/certfuzz/iteration/iteration_linux.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index eded140..f92711e 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -50,10 +50,6 @@ def __init__(self, self.testcase_base_dir = os.path.join(self.outdir, 'crashers') -# self._zzuf_range = None -# self._zzuf_line = None - - self.pipeline_options = {'use_valgrind': self.cfg.use_valgrind, 'use_pin_calltrace': self.cfg.use_pin_calltrace, 'minimize_crashers': self.cfg.minimizecrashers, @@ -110,9 +106,6 @@ def _post_run(self): if not analysis_needed: return - # store a few things for use downstream -# self._zzuf_range = zzuf_log.range -# self._zzuf_line = zzuf_log.line self._construct_testcase() def _construct_testcase(self): From 5b93f844f7da82553aeae1a0d16348be7e5bc205 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 14:08:25 -0500 Subject: [PATCH 0689/1169] move certfuzz/test test_certfuzz --- .../test => test_certfuzz}/__init__.py | 0 .../analyzers/__init__.py | 0 .../analyzers/callgrind/__init__.py | 0 .../analyzers/callgrind/test_annotate.py | 0 .../callgrind/test_annotation_file.py | 0 .../analyzers/callgrind/test_callgrind.py | 0 .../analyzers/callgrind/test_calltree_file.py | 0 .../analyzers/test_analyzer_base.py | 0 .../analyzers/test_cw_gmalloc.py | 0 .../analyzers/test_pin_calltrace.py | 0 .../analyzers/test_stderr.py | 0 .../analyzers/test_valgrind.py | 0 .../test => test_certfuzz}/bff/__init__.py | 0 .../test => test_certfuzz}/bff/test_common.py | 0 .../test => test_certfuzz}/bff/test_linux.py | 0 .../bff/test_windows.py | 0 .../campaign/__init__.py | 0 .../campaign/test_campaign_base.py | 0 .../campaign/test_campaign_linux.py | 0 .../campaign/test_campaign_windows.py | 0 .../test => test_certfuzz}/config/__init__.py | 0 .../config/test_config_base.py | 0 .../config/test_config_linux.py | 0 .../config/test_config_windows.py | 0 .../test => test_certfuzz}/crash/__init__.py | 0 .../crash/test_bff_crash.py | 0 .../crash/test_crash_base.py | 0 .../crash/test_crash_windows.py | 0 .../crash/test_testcase_base.py | 0 .../debuggers/__init__.py | 0 .../debuggers/output_parsers/__init__.py | 0 .../backtraces/_abrt-kcrash.txt | 0 .../_abrt-manual_bt_and_thread_bt.txt | 0 .../backtraces/_abrt-manual_gdb_disass.txt | 0 .../backtraces/_abrt-manual_gdb_nautilus.txt | 0 .../output_parsers/backtraces/abrt-1.txt | 0 .../output_parsers/backtraces/abrt-2.txt | 0 .../output_parsers/backtraces/abrt-3.txt | 0 .../backtraces/abrt-av_on_eip.txt | 0 .../backtraces/abrt-cannot_access_memory.txt | 0 .../backtraces/abrt-corrupt.txt | 0 .../abrt-debuginfo_warn_extra_procmapcol.txt | 0 .../abrt-gdu-notification-daemon.txt | 0 .../abrt-glibc_detected_inkskape.txt | 0 .../output_parsers/backtraces/abrt-hdparm.txt | 0 .../backtraces/abrt-multithread.txt | 0 .../backtraces/abrt-multithread2.txt | 0 .../backtraces/abrt-multithread_firefox.txt | 0 .../abrt-multithread_firefox_dupe1.txt | 0 .../abrt-multithread_firefox_dupe2.txt | 0 .../abrt-multithread_firefox_dupe3.txt | 0 .../backtraces/abrt-no_shared_libraries.txt | 0 .../backtraces/abrt-onethread.txt | 0 .../abrt-onethread_procmap_regs_disass.txt | 0 .../backtraces/abrt-openoffice.txt | 0 .../backtraces/abrt-should_not_pop.txt | 0 .../bff-ffmpeg_total_stack_corruption.gdb | 0 ..._total_stack_corruption_failed_disass._gdb | 0 .../backtraces/bff-flash_no_proc_map_jit.gdb | 0 .../backtraces/bff-glibc_detected_in_bt.gdb | 0 .../backtraces/bff-no_proc_map-openoffice.gdb | 0 ...f-outside-in-detected_stack_corruption.gdb | 0 ...outside-in-undetected_stack_corruption.gdb | 0 ...utside-in-undetected_stack_corruption2.gdb | 0 .../backtraces/bff-outside_in.gdb | 0 .../output_parsers/backtraces/bff-xpdf.gdb | 0 .../backtraces/bff-xpdf_libjpeg_btfirst.gdb | 0 .../backtraces/konqi-k9copy.txt | 214 ++--- .../konqi-kdesvn-backtrace_only.txt | 0 .../backtraces/konqi-openoffice.txt | 754 +++++++++--------- .../backtraces/konqi-reconq.txt | 150 ++-- .../backtraces/konqi-tellico.txt | 214 ++--- .../debuggers/output_parsers/test_abrtfile.py | 0 .../output_parsers/test_calltracefile.py | 0 .../debuggers/output_parsers/test_cwfile.py | 0 .../output_parsers/test_debugger_file_base.py | 0 .../debuggers/output_parsers/test_gdbfile.py | 0 .../output_parsers/test_konqifile.py | 0 .../output_parsers/test_msec_file.py | 0 .../debuggers/test_crashwrangler.py | 0 .../debuggers/test_debugger_base.py | 0 .../debuggers/test_gdb.py | 0 .../debuggers/test_jit.py | 0 .../debuggers/test_mr_crash_hash.py | 0 .../debuggers/test_msec.py | 0 .../debuggers/test_nulldebugger.py | 0 .../debuggers/test_registration.py | 0 .../drillresults/__init__.py | 0 .../drillresults/test_common.py | 0 .../drillresults/test_result_driller_base.py | 0 .../drillresults/test_result_driller_linux.py | 0 .../test_result_driller_windows.py | 0 .../drillresults/test_testcasebundle_base.py | 0 .../drillresults/test_testcasebundle_linux.py | 0 .../test_testcasebundle_windows.py | 0 .../file_handlers/__init__.py | 0 .../file_handlers/test_basicfile.py | 0 .../file_handlers/test_directory.py | 0 .../file_handlers/test_file_handlers_pkg.py | 0 .../file_handlers/test_fuzzedfile.py | 0 .../file_handlers/test_seedfile.py | 0 .../file_handlers/test_seedfile_set.py | 0 .../file_handlers/test_tempdir.py | 0 .../file_handlers/test_tmp_reaper.py | 0 .../file_handlers/test_watchdog_file.py | 0 .../fuzzers/__init__.py | 0 .../fuzzers/test_bitmut.py | 0 .../fuzzers/test_bytemut.py | 0 .../fuzzers/test_copy.py | 0 .../fuzzers/test_crlfmut.py | 0 .../fuzzers/test_crmut.py | 0 .../fuzzers/test_drop.py | 0 .../fuzzers/test_fuzzer_base.py | 0 .../fuzzers/test_insert.py | 0 .../fuzzers/test_nullmut.py | 0 .../fuzzers/test_swap.py | 0 .../fuzzers/test_truncate.py | 0 .../fuzzers/test_verify.py | 0 .../fuzzers/test_wave.py | 0 .../fuzzers/test_zzuf.py | 0 .../fuzztools/__init__.py | 0 .../fuzztools/test_bff_helper.py | 0 .../fuzztools/test_command_line_callable.py | 0 .../fuzztools/test_distance_matrix.py | 0 .../fuzztools/test_filetools.py | 0 .../fuzztools/test_hamming.py | 0 .../fuzztools/test_hostinfo.py | 0 .../fuzztools/test_object_caching.py | 0 .../fuzztools/test_performance.py | 0 .../fuzztools/test_ppid_observer.py | 0 .../fuzztools/test_probability.py | 0 .../fuzztools/test_process_killer.py | 0 .../fuzztools/test_range.py | 0 .../fuzztools/test_rangefinder.py | 0 .../fuzztools/test_seedrange.py | 0 .../fuzztools/test_similarity_matrix.py | 0 .../fuzztools/test_state_timer.py | 0 .../fuzztools/test_subprocess_helper.py | 0 .../fuzztools/test_text.py | 0 .../fuzztools/test_vectors.py | 0 .../fuzztools/test_watchdog.py | 0 .../fuzztools/test_zzuflog.py | 0 .../helpers/__init__.py | 0 .../helpers/test_coroutine.py | 0 .../helpers/test_helpers_pkg.py | 0 .../helpers/test_misc.py | 0 .../iteration/__init__.py | 0 .../iteration/test_iteration_base.py | 0 .../iteration/test_iteration_base3.py | 0 .../iteration/test_iteration_linux.py | 0 .../iteration/test_iteration_windows.py | 0 .../minimizer/__init__.py | 0 .../minimizer/test_minimizer_base.py | 0 .../minimizer/test_unix_minimizer.py | 0 .../minimizer/test_win_minimizer.py | 0 src/{certfuzz/test => test_certfuzz}/misc.py | 0 src/{certfuzz/test => test_certfuzz}/mocks.py | 0 .../runners/__init__.py | 0 .../runners/test_killableprocess.py | 0 .../runners/test_qijo.py | 0 .../runners/test_runner_base.py | 0 .../runners/test_winprocess.py | 0 .../runners/test_winrun.py | 0 .../runners/test_zzufrun.py | 0 .../scoring/__init__.py | 0 .../scoring/multiarmed_bandit/__init__.py | 0 .../multiarmed_bandit/arms/__init__.py | 0 .../multiarmed_bandit/arms/test_base.py | 0 .../arms/test_bayes_laplace.py | 0 .../multiarmed_bandit/test_bayesian_bandit.py | 0 .../multiarmed_bandit/test_e_greedy_bandit.py | 0 .../scoring/multiarmed_bandit/test_errors.py | 0 .../test_multiarmed_bandit_base.py | 0 .../multiarmed_bandit/test_random_bandit.py | 0 .../test_round_robin_bandit.py | 0 .../test => test_certfuzz}/test_meta.py | 0 .../test => test_certfuzz}/test_version.py | 0 .../testcase_pipeline/__init__.py | 0 .../test_tc_pipeline_base.py | 0 .../test_tc_pipeline_linux.py | 0 .../test_tc_pipeline_windows.py | 0 .../test => test_certfuzz}/tools/__init__.py | 0 .../tools/common/__init__.py | 0 .../tools/common/test_mtsp_enum.py | 0 .../tools/common/test_zipdiff.py | 0 .../tools/linux/__init__.py | 0 .../tools/linux/test_bff_stats.py | 0 .../tools/linux/test_callsim.py | 0 .../tools/linux/test_create_crasher_script.py | 0 .../tools/linux/test_debuggerfile.py | 0 .../tools/linux/test_minimize.py | 0 .../tools/linux/test_minimizer_plot.py | 0 .../tools/linux/test_repro.py | 0 .../tools/windows/__init__.py | 0 .../tools/windows/test_clean_windows.py | 0 .../tools/windows/test_copycrashers.py | 0 .../tools/windows/test_minimize.py | 0 .../tools/windows/test_quickstats.py | 0 .../tools/windows/test_repro.py | 0 199 files changed, 666 insertions(+), 666 deletions(-) rename src/{certfuzz/test => test_certfuzz}/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/callgrind/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/callgrind/test_annotate.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/callgrind/test_annotation_file.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/callgrind/test_callgrind.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/callgrind/test_calltree_file.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/test_analyzer_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/test_cw_gmalloc.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/test_pin_calltrace.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/test_stderr.py (100%) rename src/{certfuzz/test => test_certfuzz}/analyzers/test_valgrind.py (100%) rename src/{certfuzz/test => test_certfuzz}/bff/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/bff/test_common.py (100%) rename src/{certfuzz/test => test_certfuzz}/bff/test_linux.py (100%) rename src/{certfuzz/test => test_certfuzz}/bff/test_windows.py (100%) rename src/{certfuzz/test => test_certfuzz}/campaign/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/campaign/test_campaign_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/campaign/test_campaign_linux.py (100%) rename src/{certfuzz/test => test_certfuzz}/campaign/test_campaign_windows.py (100%) rename src/{certfuzz/test => test_certfuzz}/config/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/config/test_config_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/config/test_config_linux.py (100%) rename src/{certfuzz/test => test_certfuzz}/config/test_config_windows.py (100%) rename src/{certfuzz/test => test_certfuzz}/crash/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/crash/test_bff_crash.py (100%) rename src/{certfuzz/test => test_certfuzz}/crash/test_crash_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/crash/test_crash_windows.py (100%) rename src/{certfuzz/test => test_certfuzz}/crash/test_testcase_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/_abrt-kcrash.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/_abrt-manual_bt_and_thread_bt.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/_abrt-manual_gdb_disass.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/_abrt-manual_gdb_nautilus.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-1.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-2.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-3.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-av_on_eip.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-cannot_access_memory.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-corrupt.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-debuginfo_warn_extra_procmapcol.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-gdu-notification-daemon.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-glibc_detected_inkskape.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-hdparm.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-multithread.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-multithread2.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-multithread_firefox.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe1.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe2.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe3.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-no_shared_libraries.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-onethread.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-onethread_procmap_regs_disass.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-openoffice.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/abrt-should_not_pop.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption_failed_disass._gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-flash_no_proc_map_jit.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-glibc_detected_in_bt.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-no_proc_map-openoffice.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-outside-in-detected_stack_corruption.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption2.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-outside_in.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-xpdf.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/bff-xpdf_libjpeg_btfirst.gdb (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/konqi-k9copy.txt (98%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/konqi-kdesvn-backtrace_only.txt (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/konqi-openoffice.txt (98%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/konqi-reconq.txt (98%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/backtraces/konqi-tellico.txt (97%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/test_abrtfile.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/test_calltracefile.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/test_cwfile.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/test_debugger_file_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/test_gdbfile.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/test_konqifile.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/output_parsers/test_msec_file.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/test_crashwrangler.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/test_debugger_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/test_gdb.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/test_jit.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/test_mr_crash_hash.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/test_msec.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/test_nulldebugger.py (100%) rename src/{certfuzz/test => test_certfuzz}/debuggers/test_registration.py (100%) rename src/{certfuzz/test => test_certfuzz}/drillresults/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/drillresults/test_common.py (100%) rename src/{certfuzz/test => test_certfuzz}/drillresults/test_result_driller_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/drillresults/test_result_driller_linux.py (100%) rename src/{certfuzz/test => test_certfuzz}/drillresults/test_result_driller_windows.py (100%) rename src/{certfuzz/test => test_certfuzz}/drillresults/test_testcasebundle_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/drillresults/test_testcasebundle_linux.py (100%) rename src/{certfuzz/test => test_certfuzz}/drillresults/test_testcasebundle_windows.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/test_basicfile.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/test_directory.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/test_file_handlers_pkg.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/test_fuzzedfile.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/test_seedfile.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/test_seedfile_set.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/test_tempdir.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/test_tmp_reaper.py (100%) rename src/{certfuzz/test => test_certfuzz}/file_handlers/test_watchdog_file.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_bitmut.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_bytemut.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_copy.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_crlfmut.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_crmut.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_drop.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_fuzzer_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_insert.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_nullmut.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_swap.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_truncate.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_verify.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_wave.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzzers/test_zzuf.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_bff_helper.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_command_line_callable.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_distance_matrix.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_filetools.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_hamming.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_hostinfo.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_object_caching.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_performance.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_ppid_observer.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_probability.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_process_killer.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_range.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_rangefinder.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_seedrange.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_similarity_matrix.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_state_timer.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_subprocess_helper.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_text.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_vectors.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_watchdog.py (100%) rename src/{certfuzz/test => test_certfuzz}/fuzztools/test_zzuflog.py (100%) rename src/{certfuzz/test => test_certfuzz}/helpers/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/helpers/test_coroutine.py (100%) rename src/{certfuzz/test => test_certfuzz}/helpers/test_helpers_pkg.py (100%) rename src/{certfuzz/test => test_certfuzz}/helpers/test_misc.py (100%) rename src/{certfuzz/test => test_certfuzz}/iteration/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/iteration/test_iteration_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/iteration/test_iteration_base3.py (100%) rename src/{certfuzz/test => test_certfuzz}/iteration/test_iteration_linux.py (100%) rename src/{certfuzz/test => test_certfuzz}/iteration/test_iteration_windows.py (100%) rename src/{certfuzz/test => test_certfuzz}/minimizer/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/minimizer/test_minimizer_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/minimizer/test_unix_minimizer.py (100%) rename src/{certfuzz/test => test_certfuzz}/minimizer/test_win_minimizer.py (100%) rename src/{certfuzz/test => test_certfuzz}/misc.py (100%) rename src/{certfuzz/test => test_certfuzz}/mocks.py (100%) rename src/{certfuzz/test => test_certfuzz}/runners/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/runners/test_killableprocess.py (100%) rename src/{certfuzz/test => test_certfuzz}/runners/test_qijo.py (100%) rename src/{certfuzz/test => test_certfuzz}/runners/test_runner_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/runners/test_winprocess.py (100%) rename src/{certfuzz/test => test_certfuzz}/runners/test_winrun.py (100%) rename src/{certfuzz/test => test_certfuzz}/runners/test_zzufrun.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/arms/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/arms/test_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/arms/test_bayes_laplace.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/test_bayesian_bandit.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/test_e_greedy_bandit.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/test_errors.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/test_random_bandit.py (100%) rename src/{certfuzz/test => test_certfuzz}/scoring/multiarmed_bandit/test_round_robin_bandit.py (100%) rename src/{certfuzz/test => test_certfuzz}/test_meta.py (100%) rename src/{certfuzz/test => test_certfuzz}/test_version.py (100%) rename src/{certfuzz/test => test_certfuzz}/testcase_pipeline/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/testcase_pipeline/test_tc_pipeline_base.py (100%) rename src/{certfuzz/test => test_certfuzz}/testcase_pipeline/test_tc_pipeline_linux.py (100%) rename src/{certfuzz/test => test_certfuzz}/testcase_pipeline/test_tc_pipeline_windows.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/common/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/common/test_mtsp_enum.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/common/test_zipdiff.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/linux/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/linux/test_bff_stats.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/linux/test_callsim.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/linux/test_create_crasher_script.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/linux/test_debuggerfile.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/linux/test_minimize.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/linux/test_minimizer_plot.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/linux/test_repro.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/windows/__init__.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/windows/test_clean_windows.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/windows/test_copycrashers.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/windows/test_minimize.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/windows/test_quickstats.py (100%) rename src/{certfuzz/test => test_certfuzz}/tools/windows/test_repro.py (100%) diff --git a/src/certfuzz/test/__init__.py b/src/test_certfuzz/__init__.py similarity index 100% rename from src/certfuzz/test/__init__.py rename to src/test_certfuzz/__init__.py diff --git a/src/certfuzz/test/analyzers/__init__.py b/src/test_certfuzz/analyzers/__init__.py similarity index 100% rename from src/certfuzz/test/analyzers/__init__.py rename to src/test_certfuzz/analyzers/__init__.py diff --git a/src/certfuzz/test/analyzers/callgrind/__init__.py b/src/test_certfuzz/analyzers/callgrind/__init__.py similarity index 100% rename from src/certfuzz/test/analyzers/callgrind/__init__.py rename to src/test_certfuzz/analyzers/callgrind/__init__.py diff --git a/src/certfuzz/test/analyzers/callgrind/test_annotate.py b/src/test_certfuzz/analyzers/callgrind/test_annotate.py similarity index 100% rename from src/certfuzz/test/analyzers/callgrind/test_annotate.py rename to src/test_certfuzz/analyzers/callgrind/test_annotate.py diff --git a/src/certfuzz/test/analyzers/callgrind/test_annotation_file.py b/src/test_certfuzz/analyzers/callgrind/test_annotation_file.py similarity index 100% rename from src/certfuzz/test/analyzers/callgrind/test_annotation_file.py rename to src/test_certfuzz/analyzers/callgrind/test_annotation_file.py diff --git a/src/certfuzz/test/analyzers/callgrind/test_callgrind.py b/src/test_certfuzz/analyzers/callgrind/test_callgrind.py similarity index 100% rename from src/certfuzz/test/analyzers/callgrind/test_callgrind.py rename to src/test_certfuzz/analyzers/callgrind/test_callgrind.py diff --git a/src/certfuzz/test/analyzers/callgrind/test_calltree_file.py b/src/test_certfuzz/analyzers/callgrind/test_calltree_file.py similarity index 100% rename from src/certfuzz/test/analyzers/callgrind/test_calltree_file.py rename to src/test_certfuzz/analyzers/callgrind/test_calltree_file.py diff --git a/src/certfuzz/test/analyzers/test_analyzer_base.py b/src/test_certfuzz/analyzers/test_analyzer_base.py similarity index 100% rename from src/certfuzz/test/analyzers/test_analyzer_base.py rename to src/test_certfuzz/analyzers/test_analyzer_base.py diff --git a/src/certfuzz/test/analyzers/test_cw_gmalloc.py b/src/test_certfuzz/analyzers/test_cw_gmalloc.py similarity index 100% rename from src/certfuzz/test/analyzers/test_cw_gmalloc.py rename to src/test_certfuzz/analyzers/test_cw_gmalloc.py diff --git a/src/certfuzz/test/analyzers/test_pin_calltrace.py b/src/test_certfuzz/analyzers/test_pin_calltrace.py similarity index 100% rename from src/certfuzz/test/analyzers/test_pin_calltrace.py rename to src/test_certfuzz/analyzers/test_pin_calltrace.py diff --git a/src/certfuzz/test/analyzers/test_stderr.py b/src/test_certfuzz/analyzers/test_stderr.py similarity index 100% rename from src/certfuzz/test/analyzers/test_stderr.py rename to src/test_certfuzz/analyzers/test_stderr.py diff --git a/src/certfuzz/test/analyzers/test_valgrind.py b/src/test_certfuzz/analyzers/test_valgrind.py similarity index 100% rename from src/certfuzz/test/analyzers/test_valgrind.py rename to src/test_certfuzz/analyzers/test_valgrind.py diff --git a/src/certfuzz/test/bff/__init__.py b/src/test_certfuzz/bff/__init__.py similarity index 100% rename from src/certfuzz/test/bff/__init__.py rename to src/test_certfuzz/bff/__init__.py diff --git a/src/certfuzz/test/bff/test_common.py b/src/test_certfuzz/bff/test_common.py similarity index 100% rename from src/certfuzz/test/bff/test_common.py rename to src/test_certfuzz/bff/test_common.py diff --git a/src/certfuzz/test/bff/test_linux.py b/src/test_certfuzz/bff/test_linux.py similarity index 100% rename from src/certfuzz/test/bff/test_linux.py rename to src/test_certfuzz/bff/test_linux.py diff --git a/src/certfuzz/test/bff/test_windows.py b/src/test_certfuzz/bff/test_windows.py similarity index 100% rename from src/certfuzz/test/bff/test_windows.py rename to src/test_certfuzz/bff/test_windows.py diff --git a/src/certfuzz/test/campaign/__init__.py b/src/test_certfuzz/campaign/__init__.py similarity index 100% rename from src/certfuzz/test/campaign/__init__.py rename to src/test_certfuzz/campaign/__init__.py diff --git a/src/certfuzz/test/campaign/test_campaign_base.py b/src/test_certfuzz/campaign/test_campaign_base.py similarity index 100% rename from src/certfuzz/test/campaign/test_campaign_base.py rename to src/test_certfuzz/campaign/test_campaign_base.py diff --git a/src/certfuzz/test/campaign/test_campaign_linux.py b/src/test_certfuzz/campaign/test_campaign_linux.py similarity index 100% rename from src/certfuzz/test/campaign/test_campaign_linux.py rename to src/test_certfuzz/campaign/test_campaign_linux.py diff --git a/src/certfuzz/test/campaign/test_campaign_windows.py b/src/test_certfuzz/campaign/test_campaign_windows.py similarity index 100% rename from src/certfuzz/test/campaign/test_campaign_windows.py rename to src/test_certfuzz/campaign/test_campaign_windows.py diff --git a/src/certfuzz/test/config/__init__.py b/src/test_certfuzz/config/__init__.py similarity index 100% rename from src/certfuzz/test/config/__init__.py rename to src/test_certfuzz/config/__init__.py diff --git a/src/certfuzz/test/config/test_config_base.py b/src/test_certfuzz/config/test_config_base.py similarity index 100% rename from src/certfuzz/test/config/test_config_base.py rename to src/test_certfuzz/config/test_config_base.py diff --git a/src/certfuzz/test/config/test_config_linux.py b/src/test_certfuzz/config/test_config_linux.py similarity index 100% rename from src/certfuzz/test/config/test_config_linux.py rename to src/test_certfuzz/config/test_config_linux.py diff --git a/src/certfuzz/test/config/test_config_windows.py b/src/test_certfuzz/config/test_config_windows.py similarity index 100% rename from src/certfuzz/test/config/test_config_windows.py rename to src/test_certfuzz/config/test_config_windows.py diff --git a/src/certfuzz/test/crash/__init__.py b/src/test_certfuzz/crash/__init__.py similarity index 100% rename from src/certfuzz/test/crash/__init__.py rename to src/test_certfuzz/crash/__init__.py diff --git a/src/certfuzz/test/crash/test_bff_crash.py b/src/test_certfuzz/crash/test_bff_crash.py similarity index 100% rename from src/certfuzz/test/crash/test_bff_crash.py rename to src/test_certfuzz/crash/test_bff_crash.py diff --git a/src/certfuzz/test/crash/test_crash_base.py b/src/test_certfuzz/crash/test_crash_base.py similarity index 100% rename from src/certfuzz/test/crash/test_crash_base.py rename to src/test_certfuzz/crash/test_crash_base.py diff --git a/src/certfuzz/test/crash/test_crash_windows.py b/src/test_certfuzz/crash/test_crash_windows.py similarity index 100% rename from src/certfuzz/test/crash/test_crash_windows.py rename to src/test_certfuzz/crash/test_crash_windows.py diff --git a/src/certfuzz/test/crash/test_testcase_base.py b/src/test_certfuzz/crash/test_testcase_base.py similarity index 100% rename from src/certfuzz/test/crash/test_testcase_base.py rename to src/test_certfuzz/crash/test_testcase_base.py diff --git a/src/certfuzz/test/debuggers/__init__.py b/src/test_certfuzz/debuggers/__init__.py similarity index 100% rename from src/certfuzz/test/debuggers/__init__.py rename to src/test_certfuzz/debuggers/__init__.py diff --git a/src/certfuzz/test/debuggers/output_parsers/__init__.py b/src/test_certfuzz/debuggers/output_parsers/__init__.py similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/__init__.py rename to src/test_certfuzz/debuggers/output_parsers/__init__.py diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/_abrt-kcrash.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/_abrt-kcrash.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/_abrt-kcrash.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/_abrt-kcrash.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/_abrt-manual_bt_and_thread_bt.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/_abrt-manual_bt_and_thread_bt.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/_abrt-manual_bt_and_thread_bt.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/_abrt-manual_bt_and_thread_bt.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/_abrt-manual_gdb_disass.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/_abrt-manual_gdb_disass.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/_abrt-manual_gdb_disass.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/_abrt-manual_gdb_disass.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/_abrt-manual_gdb_nautilus.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/_abrt-manual_gdb_nautilus.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/_abrt-manual_gdb_nautilus.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/_abrt-manual_gdb_nautilus.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-1.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-1.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-1.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-1.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-2.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-2.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-2.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-2.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-3.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-3.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-3.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-3.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-av_on_eip.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-av_on_eip.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-av_on_eip.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-av_on_eip.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-cannot_access_memory.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-cannot_access_memory.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-cannot_access_memory.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-cannot_access_memory.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-corrupt.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-corrupt.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-corrupt.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-corrupt.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-debuginfo_warn_extra_procmapcol.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-debuginfo_warn_extra_procmapcol.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-debuginfo_warn_extra_procmapcol.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-debuginfo_warn_extra_procmapcol.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-gdu-notification-daemon.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-gdu-notification-daemon.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-gdu-notification-daemon.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-gdu-notification-daemon.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-glibc_detected_inkskape.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-glibc_detected_inkskape.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-glibc_detected_inkskape.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-glibc_detected_inkskape.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-hdparm.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-hdparm.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-hdparm.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-hdparm.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread2.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread2.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread2.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread2.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread_firefox.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread_firefox.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread_firefox.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread_firefox.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe1.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe1.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe1.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe1.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe2.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe2.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe2.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe2.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe3.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe3.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe3.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-multithread_firefox_dupe3.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-no_shared_libraries.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-no_shared_libraries.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-no_shared_libraries.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-no_shared_libraries.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-onethread.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-onethread.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-onethread.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-onethread.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-onethread_procmap_regs_disass.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-onethread_procmap_regs_disass.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-onethread_procmap_regs_disass.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-onethread_procmap_regs_disass.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-openoffice.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-openoffice.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-openoffice.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-openoffice.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-should_not_pop.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-should_not_pop.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/abrt-should_not_pop.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/abrt-should_not_pop.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption_failed_disass._gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption_failed_disass._gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption_failed_disass._gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-ffmpeg_total_stack_corruption_failed_disass._gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-flash_no_proc_map_jit.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-flash_no_proc_map_jit.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-flash_no_proc_map_jit.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-flash_no_proc_map_jit.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-glibc_detected_in_bt.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-glibc_detected_in_bt.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-glibc_detected_in_bt.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-glibc_detected_in_bt.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-no_proc_map-openoffice.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-no_proc_map-openoffice.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-no_proc_map-openoffice.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-no_proc_map-openoffice.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-outside-in-detected_stack_corruption.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-outside-in-detected_stack_corruption.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-outside-in-detected_stack_corruption.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-outside-in-detected_stack_corruption.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption2.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption2.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption2.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-outside-in-undetected_stack_corruption2.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-outside_in.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-outside_in.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-outside_in.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-outside_in.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-xpdf.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-xpdf.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-xpdf.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-xpdf.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/bff-xpdf_libjpeg_btfirst.gdb b/src/test_certfuzz/debuggers/output_parsers/backtraces/bff-xpdf_libjpeg_btfirst.gdb similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/bff-xpdf_libjpeg_btfirst.gdb rename to src/test_certfuzz/debuggers/output_parsers/backtraces/bff-xpdf_libjpeg_btfirst.gdb diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-k9copy.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-k9copy.txt similarity index 98% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-k9copy.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-k9copy.txt index cbed0fb..5544402 100644 --- a/src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-k9copy.txt +++ b/src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-k9copy.txt @@ -1,108 +1,108 @@ -Application: k9copy (2.3.7) -KDE Platform Version: 4.6.00 (4.6.0) "release 6" -Qt Version: 4.7.1 -Operating System: Linux 2.6.37.6-0.5-desktop i686 -Distribution: "openSUSE 11.4 (i586)" - --- Information about the crash: - - -The crash can be reproduced every time. - --- Backtrace: -Application: k9copy (k9copy), signal: Segmentation fault -[Current thread is 1 (Thread 0xb4cc5710 (LWP 9828))] - -Thread 4 (Thread 0xb2856b70 (LWP 9829)): -#0 0xffffe424 in __kernel_vsyscall () -#1 0xb5d6603e in poll () from /lib/libc.so.6 -#2 0xb55b10bb in g_poll () from /lib/libglib-2.0.so.0 -#3 0xb55a0c46 in ?? () from /lib/libglib-2.0.so.0 -#4 0xb55a0fce in g_main_context_iteration () from /lib/libglib-2.0.so.0 -#5 0xb6c4d7b7 in QEventDispatcherGlib::processEvents (this=0x895e8e0, -flags=...) at kernel/qeventdispatcher_glib.cpp:424 -#6 0xb6c1e2bd in QEventLoop::processEvents (this=0xb28562b0, flags=...) at -kernel/qeventloop.cpp:149 -#7 0xb6c1e4e9 in QEventLoop::exec (this=0xb28562b0, flags=...) at -kernel/qeventloop.cpp:201 -#8 0xb6b1f7b9 in QThread::exec (this=0x88f9ee8) at thread/qthread.cpp:490 -#9 0xb6bfe23d in QInotifyFileSystemWatcherEngine::run (this=0x88f9ee8) at -io/qfilesystemwatcher_inotify.cpp:248 -#10 0xb6b223aa in QThreadPrivate::start (arg=0x88f9ee8) at -thread/qthread_unix.cpp:285 -#11 0xb6aa7b05 in start_thread () from /lib/libpthread.so.0 -#12 0xb5d70d5e in clone () from /lib/libc.so.6 - -Thread 3 (Thread 0xae4f9b70 (LWP 9845)): -#0 0xffffe424 in __kernel_vsyscall () -#1 0xb6aac105 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/libpthread.so.0 -#2 0xb6b22ab0 in wait (this=0xafeff17c, mutex=0xae4f915c, time=4294967295) at -thread/qwaitcondition_unix.cpp:88 -#3 QWaitCondition::wait (this=0xafeff17c, mutex=0xae4f915c, time=4294967295) -at thread/qwaitcondition_unix.cpp:160 -#4 0x0813e480 in ?? () -#5 0x0813f142 in ?? () -#6 0x081403ac in ?? () -#7 0x081406e0 in ?? () -#8 0x081409f5 in ?? () -#9 0xb6b223aa in QThreadPrivate::start (arg=0xafaff008) at -thread/qthread_unix.cpp:285 -#10 0xb6aa7b05 in start_thread () from /lib/libpthread.so.0 -#11 0xb5d70d5e in clone () from /lib/libc.so.6 - -Thread 2 (Thread 0xad8f7b70 (LWP 9848)): -[KCrash Handler] -#7 0xb5f66458 in sse2_idct (block=0x8ccf1c0, dest=0x8d534c0 "", stride=720) at -idct_mmx.c:950 -#8 mpeg2_idct_copy_sse2 (block=0x8ccf1c0, dest=0x8d534c0 "", stride=720) at -idct_mmx.c:1228 -#9 0xb5f5fcdb in slice_intra_DCT (decoder=0x8ccf0c0, code=1, buffer=0xacfcc040 -"\v\370})H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\210") -at slice.c:954 -#10 mpeg2_slice (decoder=0x8ccf0c0, code=1, buffer=0xacfcc040 -"\v\370})H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\210") -at slice.c:1844 -#11 0xb5f53eb5 in mpeg2_parse (mpeg2dec=0x8ccf0c0) at decode.c:188 -#12 0x08106103 in ?? () -#13 0x0810693b in ?? () -#14 0x08106ac6 in ?? () -#15 0x0810530b in ?? () -#16 0xb6b223aa in QThreadPrivate::start (arg=0x8b8b49c) at -thread/qthread_unix.cpp:285 -#17 0xb6aa7b05 in start_thread () from /lib/libpthread.so.0 -#18 0xb5d70d5e in clone () from /lib/libc.so.6 - -Thread 1 (Thread 0xb4cc5710 (LWP 9828)): -#0 0xffffe424 in __kernel_vsyscall () -#1 0xb5d6603e in poll () from /lib/libc.so.6 -#2 0xb5388480 in ?? () from /usr/lib/libxcb.so.1 -#3 0xb5388aa4 in ?? () from /usr/lib/libxcb.so.1 -#4 0xb5388b51 in xcb_writev () from /usr/lib/libxcb.so.1 -#5 0xb5b798c8 in _XSend () from /usr/lib/libX11.so.6 -#6 0xb5b79cc0 in _XEventsQueued () from /usr/lib/libX11.so.6 -#7 0xb5b6a3e8 in XEventsQueued () from /usr/lib/libX11.so.6 -#8 0xb6166413 in ?? () from /usr/lib/libQtGui.so.4 -#9 0xb559fa70 in g_main_context_prepare () from /lib/libglib-2.0.so.0 -#10 0xb55a08e2 in ?? () from /lib/libglib-2.0.so.0 -#11 0xb55a0fce in g_main_context_iteration () from /lib/libglib-2.0.so.0 -#12 0xb6c4d76b in QEventDispatcherGlib::processEvents (this=0x88365f8, -flags=...) at kernel/qeventdispatcher_glib.cpp:422 -#13 0xb616655a in ?? () from /usr/lib/libQtGui.so.4 -#14 0xb6c230ac in QCoreApplication::processEvents (flags=..., maxtime=1) at -kernel/qcoreapplication.cpp:952 -#15 0x080eb0fa in ?? () -#16 0x08118ad9 in ?? () -#17 0x08118eb2 in ?? () -#18 0x0811a263 in ?? () -#19 0x0811a67f in ?? () -#20 0x08130715 in ?? () -#21 0x080d3c88 in ?? () -#22 0xb65f2c7c in QDialog::qt_metacall(QMetaObject::Call, int, void**) () from -/usr/lib/libQtGui.so.4 -#23 0xb76c09a2 in KDialog::qt_metacall(QMetaObject::Call, int, void**) () from -/usr/lib/libkdeui.so.5 -#24 0xb76c0e12 in KPageDialog::qt_metacall(QMetaObject::Call, int, void**) () -from /usr/lib/libkdeui.so.5 -#25 0xb76c104e in KAssistantDialog::qt_metacall(QMetaObject::Call, int, void**) -() from /usr/lib/libkdeui.so.5 +Application: k9copy (2.3.7) +KDE Platform Version: 4.6.00 (4.6.0) "release 6" +Qt Version: 4.7.1 +Operating System: Linux 2.6.37.6-0.5-desktop i686 +Distribution: "openSUSE 11.4 (i586)" + +-- Information about the crash: + + +The crash can be reproduced every time. + +-- Backtrace: +Application: k9copy (k9copy), signal: Segmentation fault +[Current thread is 1 (Thread 0xb4cc5710 (LWP 9828))] + +Thread 4 (Thread 0xb2856b70 (LWP 9829)): +#0 0xffffe424 in __kernel_vsyscall () +#1 0xb5d6603e in poll () from /lib/libc.so.6 +#2 0xb55b10bb in g_poll () from /lib/libglib-2.0.so.0 +#3 0xb55a0c46 in ?? () from /lib/libglib-2.0.so.0 +#4 0xb55a0fce in g_main_context_iteration () from /lib/libglib-2.0.so.0 +#5 0xb6c4d7b7 in QEventDispatcherGlib::processEvents (this=0x895e8e0, +flags=...) at kernel/qeventdispatcher_glib.cpp:424 +#6 0xb6c1e2bd in QEventLoop::processEvents (this=0xb28562b0, flags=...) at +kernel/qeventloop.cpp:149 +#7 0xb6c1e4e9 in QEventLoop::exec (this=0xb28562b0, flags=...) at +kernel/qeventloop.cpp:201 +#8 0xb6b1f7b9 in QThread::exec (this=0x88f9ee8) at thread/qthread.cpp:490 +#9 0xb6bfe23d in QInotifyFileSystemWatcherEngine::run (this=0x88f9ee8) at +io/qfilesystemwatcher_inotify.cpp:248 +#10 0xb6b223aa in QThreadPrivate::start (arg=0x88f9ee8) at +thread/qthread_unix.cpp:285 +#11 0xb6aa7b05 in start_thread () from /lib/libpthread.so.0 +#12 0xb5d70d5e in clone () from /lib/libc.so.6 + +Thread 3 (Thread 0xae4f9b70 (LWP 9845)): +#0 0xffffe424 in __kernel_vsyscall () +#1 0xb6aac105 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/libpthread.so.0 +#2 0xb6b22ab0 in wait (this=0xafeff17c, mutex=0xae4f915c, time=4294967295) at +thread/qwaitcondition_unix.cpp:88 +#3 QWaitCondition::wait (this=0xafeff17c, mutex=0xae4f915c, time=4294967295) +at thread/qwaitcondition_unix.cpp:160 +#4 0x0813e480 in ?? () +#5 0x0813f142 in ?? () +#6 0x081403ac in ?? () +#7 0x081406e0 in ?? () +#8 0x081409f5 in ?? () +#9 0xb6b223aa in QThreadPrivate::start (arg=0xafaff008) at +thread/qthread_unix.cpp:285 +#10 0xb6aa7b05 in start_thread () from /lib/libpthread.so.0 +#11 0xb5d70d5e in clone () from /lib/libc.so.6 + +Thread 2 (Thread 0xad8f7b70 (LWP 9848)): +[KCrash Handler] +#7 0xb5f66458 in sse2_idct (block=0x8ccf1c0, dest=0x8d534c0 "", stride=720) at +idct_mmx.c:950 +#8 mpeg2_idct_copy_sse2 (block=0x8ccf1c0, dest=0x8d534c0 "", stride=720) at +idct_mmx.c:1228 +#9 0xb5f5fcdb in slice_intra_DCT (decoder=0x8ccf0c0, code=1, buffer=0xacfcc040 +"\v\370})H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\210") +at slice.c:954 +#10 mpeg2_slice (decoder=0x8ccf0c0, code=1, buffer=0xacfcc040 +"\v\370})H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\213\224\245\".R\224\210\271JR\"\345)H\210") +at slice.c:1844 +#11 0xb5f53eb5 in mpeg2_parse (mpeg2dec=0x8ccf0c0) at decode.c:188 +#12 0x08106103 in ?? () +#13 0x0810693b in ?? () +#14 0x08106ac6 in ?? () +#15 0x0810530b in ?? () +#16 0xb6b223aa in QThreadPrivate::start (arg=0x8b8b49c) at +thread/qthread_unix.cpp:285 +#17 0xb6aa7b05 in start_thread () from /lib/libpthread.so.0 +#18 0xb5d70d5e in clone () from /lib/libc.so.6 + +Thread 1 (Thread 0xb4cc5710 (LWP 9828)): +#0 0xffffe424 in __kernel_vsyscall () +#1 0xb5d6603e in poll () from /lib/libc.so.6 +#2 0xb5388480 in ?? () from /usr/lib/libxcb.so.1 +#3 0xb5388aa4 in ?? () from /usr/lib/libxcb.so.1 +#4 0xb5388b51 in xcb_writev () from /usr/lib/libxcb.so.1 +#5 0xb5b798c8 in _XSend () from /usr/lib/libX11.so.6 +#6 0xb5b79cc0 in _XEventsQueued () from /usr/lib/libX11.so.6 +#7 0xb5b6a3e8 in XEventsQueued () from /usr/lib/libX11.so.6 +#8 0xb6166413 in ?? () from /usr/lib/libQtGui.so.4 +#9 0xb559fa70 in g_main_context_prepare () from /lib/libglib-2.0.so.0 +#10 0xb55a08e2 in ?? () from /lib/libglib-2.0.so.0 +#11 0xb55a0fce in g_main_context_iteration () from /lib/libglib-2.0.so.0 +#12 0xb6c4d76b in QEventDispatcherGlib::processEvents (this=0x88365f8, +flags=...) at kernel/qeventdispatcher_glib.cpp:422 +#13 0xb616655a in ?? () from /usr/lib/libQtGui.so.4 +#14 0xb6c230ac in QCoreApplication::processEvents (flags=..., maxtime=1) at +kernel/qcoreapplication.cpp:952 +#15 0x080eb0fa in ?? () +#16 0x08118ad9 in ?? () +#17 0x08118eb2 in ?? () +#18 0x0811a263 in ?? () +#19 0x0811a67f in ?? () +#20 0x08130715 in ?? () +#21 0x080d3c88 in ?? () +#22 0xb65f2c7c in QDialog::qt_metacall(QMetaObject::Call, int, void**) () from +/usr/lib/libQtGui.so.4 +#23 0xb76c09a2 in KDialog::qt_metacall(QMetaObject::Call, int, void**) () from +/usr/lib/libkdeui.so.5 +#24 0xb76c0e12 in KPageDialog::qt_metacall(QMetaObject::Call, int, void**) () +from /usr/lib/libkdeui.so.5 +#25 0xb76c104e in KAssistantDialog::qt_metacall(QMetaObject::Call, int, void**) +() from /usr/lib/libkdeui.so.5 #26 0x0806854f in _start () \ No newline at end of file diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-kdesvn-backtrace_only.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-kdesvn-backtrace_only.txt similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-kdesvn-backtrace_only.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-kdesvn-backtrace_only.txt diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-openoffice.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-openoffice.txt similarity index 98% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-openoffice.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-openoffice.txt index cc550e2..566956d 100644 --- a/src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-openoffice.txt +++ b/src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-openoffice.txt @@ -1,378 +1,378 @@ -Application: soffice (3.0.0) -KDE Platform Version: 4.4.4 (KDE 4.4.4) "release 2" -Qt Version: 4.6.3 -Operating System: Linux 2.6.34-12-desktop x86_64 -Distribution: "openSUSE 11.3 (x86_64)" - --- Information about the crash: - - -The crash can be reproduced every time. - - -- Backtrace: -Application: OpenOffice.org (soffice.bin), signal: Aborted -[Current thread is 1 (Thread 0x7fb815831720 (LWP 8556))] - -Thread 14 (Thread 0x7fb80d147710 (LWP 8557)): -#0 0x00007fb814039709 in pthread_cond_timedwait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb81541e2e6 in rtl_cache_wsupdate_wait (arg=) -at alloc_cache.c:1398 -#2 rtl_cache_wsupdate_all (arg=) at alloc_cache.c:1542 -#3 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#4 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#5 0x0000000000000000 in ?? () - -Thread 13 (Thread 0x7fb800c75710 (LWP 8558)): -#0 0x00007fb814039709 in pthread_cond_timedwait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb8153f4166 in osl_waitCondition (Condition=0x7b7d90, -pTimeout=0x7fb800c74d60) at conditn.c:250 -#2 0x00007fb810f4f82d in vos::OTimerManager::run() () from -/usr/lib64/ooo3/program/../basis-link/program/libvos3gcc3.so -#3 0x00007fb810f4de1a in vos::threadWorkerFunction_impl(void*) () from -/usr/lib64/ooo3/program/../basis-link/program/libvos3gcc3.so -#4 0x00007fb8153f743c in osl_thread_start_Impl (pData=0x7acc20) at -thread.c:266 -#5 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#6 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#7 0x0000000000000000 in ?? () - -Thread 12 (Thread 0x7fb7bfb9b710 (LWP 8568)): -#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7ed9912de in Monitor::wait(bool, long, bool) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed7a4f6a in GCTaskManager::get_task(unsigned int) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7ed7a6140 in GCTaskThread::run() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#6 0x00007fb7ed9b29e2 in java_start(Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#7 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#8 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#9 0x0000000000000000 in ?? () - -Thread 11 (Thread 0x7fb7bfa9a710 (LWP 8569)): -#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7ed9912de in Monitor::wait(bool, long, bool) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed7a4f6a in GCTaskManager::get_task(unsigned int) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7ed7a6140 in GCTaskThread::run() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#6 0x00007fb7ed9b29e2 in java_start(Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#7 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#8 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#9 0x0000000000000000 in ?? () - -Thread 10 (Thread 0x7fb7bf7fc710 (LWP 8570)): -#0 0x00007fb814039709 in pthread_cond_timedwait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b6685 in os::PlatformEvent::park(long) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7ed990ddf in Monitor::IWait(Thread*, long) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7ed9912de in Monitor::wait(bool, long, bool) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7edae0bc3 in VMThread::loop() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7edae0de2 in VMThread::run() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#6 0x00007fb7ed9b29e2 in java_start(Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#7 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#8 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#9 0x0000000000000000 in ?? () - -Thread 9 (Thread 0x7fb7bf6fb710 (LWP 8571)): -#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7eda6ea9c in ObjectMonitor::wait(long, bool, Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7eda6ec0b in ObjectSynchronizer::wait(Handle, long, Thread*) () -from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed85eaa9 in JVM_MonitorWait () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7e9c17f50 in ?? () -#6 0x0000000000000000 in ?? () - -Thread 8 (Thread 0x7fb7bf5fa710 (LWP 8572)): -#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7eda6ea9c in ObjectMonitor::wait(long, bool, Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7eda6ec0b in ObjectSynchronizer::wait(Handle, long, Thread*) () -from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed85eaa9 in JVM_MonitorWait () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7e9c17f50 in ?? () -#6 0x00007fb7bf5f98c8 in ?? () -#7 0x00007fb7bfd86b68 in ?? () -#8 0x00007fb7bfd86020 in ?? () -#9 0x00007fb7bfd86b60 in ?? () -#10 0x00007fb7bf5f9840 in ?? () -#11 0x0000000000000000 in ?? () - -Thread 7 (Thread 0x7fb7bf4f9710 (LWP 8573)): -#0 0x00007fb81403b600 in sem_wait () from /lib64/libpthread.so.0 -#1 0x00007fb7ed9af94a in check_pending_signals(bool) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7ed9ad7a3 in signal_thread_entry(JavaThread*, Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7edaa0f97 in JavaThread::thread_main_inner() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed9b29e2 in java_start(Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#6 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#7 0x0000000000000000 in ?? () - -Thread 6 (Thread 0x7fb7bf3f8710 (LWP 8574)): -#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7ed991375 in Monitor::wait(bool, long, bool) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed6f5a17 in CompileQueue::get() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7ed6f8850 in CompileBroker::compiler_thread_loop() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#6 0x00007fb7edaa0f97 in JavaThread::thread_main_inner() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#7 0x00007fb7ed9b29e2 in java_start(Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#8 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#9 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#10 0x0000000000000000 in ?? () - -Thread 5 (Thread 0x7fb7bf2f7710 (LWP 8575)): -#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7ed991375 in Monitor::wait(bool, long, bool) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed6f5a17 in CompileQueue::get() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7ed6f8850 in CompileBroker::compiler_thread_loop() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#6 0x00007fb7edaa0f97 in JavaThread::thread_main_inner() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#7 0x00007fb7ed9b29e2 in java_start(Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#8 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#9 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#10 0x0000000000000000 in ?? () - -Thread 4 (Thread 0x7fb7bf1f6710 (LWP 8576)): -#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7ed9912de in Monitor::wait(bool, long, bool) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed9505b2 in -LowMemoryDetector::low_memory_detector_thread_entry(JavaThread*, Thread*) () -from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7edaa0f97 in JavaThread::thread_main_inner() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#6 0x00007fb7ed9b29e2 in java_start(Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#7 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#8 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#9 0x0000000000000000 in ?? () - -Thread 3 (Thread 0x7fb7bf0f5710 (LWP 8577)): -#0 0x00007fb814039709 in pthread_cond_timedwait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b6685 in os::PlatformEvent::park(long) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7ed9b682b in os::sleep(Thread*, long, bool) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7eda9bed0 in WatcherThread::run() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed9b29e2 in java_start(Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 -#6 0x00007fb81475a82d in clone () from /lib64/libc.so.6 -#7 0x0000000000000000 in ?? () - -Thread 2 (Thread 0x7fb7ee0b1710 (LWP 8578)): -#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7eda6ea9c in ObjectMonitor::wait(long, bool, Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7eda6ec0b in ObjectSynchronizer::wait(Handle, long, Thread*) () -from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed85eaa9 in JVM_MonitorWait () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7e9c17f50 in ?? () -#6 0x0000000000000000 in ?? () - -Thread 1 (Thread 0x7fb815831720 (LWP 8556)): -[KCrash Handler] -#5 0x00007fb8146bc9e5 in raise () from /lib64/libc.so.6 -#6 0x00007fb8146bdee6 in abort () from /lib64/libc.so.6 -#7 0x00007fb8146f7c53 in __libc_message () from /lib64/libc.so.6 -#8 0x00007fb8146fd226 in malloc_printerr () from /lib64/libc.so.6 -#9 0x00007fb814701fcc in free () from /lib64/libc.so.6 -#10 0x00007fb8137bef8c in ~OUString (pMapping=) at -/usr/src/debug/ooo320-m19-ure/solver/unxlngx6.pro/inc/rtl/ustring.hxx:232 -#11 ~MappingEntry (pMapping=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbmap.cxx:115 -#12 uno_revokeMapping (pMapping=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbmap.cxx:667 -#13 0x00007fb7bc9693e2 in ~Mapping (this=, -__in_chrg=) at ../../../inc/uno/mapping.hxx:249 -#14 Proxy::~Proxy (this=, __in_chrg=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/helper/purpenv/helper_purpenv_Proxy.cxx:264 -#15 0x00007fb7bc969491 in Proxy_free (pProxy=0x7fb7f00ded50) at -/usr/src/debug/ooo320-m19-ure/cppu/source/helper/purpenv/helper_purpenv_Proxy.cxx:190 -#16 0x00007fb8137c51ba in (anonymous -namespace)::s_stub_defenv_revokeInterface (pParam=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbenv.cxx:397 -#17 0x00007fb8137c925b in s_environment_invoke_v (pCurrEnv=0x7fb7f00de5f0, -pTargetEnv=, pCallee=0x7fb8137c4f0a <(anonymous -namespace)::s_stub_defenv_revokeInterface(va_list*)>, - pParam=0x7fff7301f0a0) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:292 -#18 0x00007fb8137c92f4 in uno_Environment_invoke_v -(pTargetEnv=0x7fb7f00de5f0, pCallee=0x7fb8137c4f0a <(anonymous -namespace)::s_stub_defenv_revokeInterface(va_list*)>, pParam=0x7fff7301f0a0) - at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:311 -#19 0x00007fb8137c938f in uno_Environment_invoke (pEnv=, -pCallee=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:320 -#20 0x00007fb8137c295d in (anonymous namespace)::defenv_revokeInterface -(pEnv=, pInterface=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbenv.cxx:453 -#21 0x00007fb8137c925b in s_environment_invoke_v (pCurrEnv=0x7fb7f00de5f0, -pTargetEnv=, pCallee=0x7fb7bc967980 -, pParam=0x7fff7301f1f0) - at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:292 -#22 0x00007fb8137c92f4 in uno_Environment_invoke_v -(pTargetEnv=0x7fb7f00de5f0, pCallee=0x7fb7bc967980 -, pParam=0x7fff7301f1f0) - at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:311 -#23 0x00007fb8137c938f in uno_Environment_invoke (pEnv=, -pCallee=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:320 -#24 0x00007fb7bc9696b1 in Proxy::release (this=0x7fb7f00ded50) at -/usr/src/debug/ooo320-m19-ure/cppu/source/helper/purpenv/helper_purpenv_Proxy.cxx:360 -#25 0x00007fb80327ba62 in bridges::cpp_uno::shared::freeCppInterfaceProxy -(pEnv=0x7fb7f00de4e0, pInterface=0x7fb7f00e2328) - at -/usr/src/debug/ooo320-m19-ure/bridges/source/cpp_uno/shared/cppinterfaceproxy.cxx:107 -#26 0x00007fb8137c51ba in (anonymous -namespace)::s_stub_defenv_revokeInterface (pParam=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbenv.cxx:397 -#27 0x00007fb8137c925b in s_environment_invoke_v (pCurrEnv=0x7fb7f00de5f0, -pTargetEnv=, pCallee=0x7fb8137c4f0a <(anonymous -namespace)::s_stub_defenv_revokeInterface(va_list*)>, - pParam=0x7fff7301f670) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:292 -#28 0x00007fb8137c9430 in s_environment_invoke_vv (pParam=) at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:275 -#29 0x00007fb8137c860c in s_pull (pParam=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:225 -#30 0x00007fb7bcb6d494 in dge::v_callInto_v (this=, -pCallee=0x7fb8137c85c0 , pParam=0x7fff7301f450) - at -/usr/src/debug/ooo320-m19-ure/cppu/source/UnsafeBridge/UnsafeBridge.cxx:89 -#31 0x00007fb8137c967c in callInto_v (this=, -pCallee=) at ../../inc/cppu/Enterable.hxx:61 -#32 cppu::Enterable::callInto (this=, pCallee=) at ../../inc/cppu/Enterable.hxx:98 -#33 0x00007fb8137c8c58 in s_callInto_v (pEnv=, -pCallee=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:232 -#34 s_callInto (pEnv=, pCallee=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:243 -#35 0x00007fb8137c9291 in s_environment_invoke_v (pCurrEnv=0x0, -pTargetEnv=0x7fb7f00de4e0, pCallee=0x7fb8137c4f0a <(anonymous -namespace)::s_stub_defenv_revokeInterface(va_list*)>, - pParam=0x7fff7301f670) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:299 -#36 0x00007fb8137c92f4 in uno_Environment_invoke_v -(pTargetEnv=0x7fb7f00de4e0, pCallee=0x7fb8137c4f0a <(anonymous -namespace)::s_stub_defenv_revokeInterface(va_list*)>, pParam=0x7fff7301f670) - at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:311 -#37 0x00007fb8137c938f in uno_Environment_invoke (pEnv=, -pCallee=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:320 -#38 0x00007fb8137c295d in (anonymous namespace)::defenv_revokeInterface -(pEnv=, pInterface=) at -/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbenv.cxx:453 -#39 0x00007fb80327bd1a in -bridges::cpp_uno::shared::CppInterfaceProxy::releaseProxy -(this=0x7fb7f00e2300) at -/usr/src/debug/ooo320-m19-ure/bridges/source/cpp_uno/shared/cppinterfaceproxy.cxx:162 -#40 0x00007fb803279478 in cpp_vtable_call (nFunctionIndex=2, -nVtableOffset=, gpreg=0x7fff7301f890, -fpreg=0x7fff7301f8c0, ovrflw=0x7fff7301f910, pRegisterReturn=0x7fff7301f870) - at -/usr/src/debug/ooo320-m19-ure/bridges/source/cpp_uno/gcc3_linux_x86-64/cpp2uno.cxx:344 -#41 0x00007fb80327ab82 in privateSnippetExecutor () from -/usr/lib64/ooo3/ure/lib/libgcc3_uno.so -#42 0x00007fb7bd1f0486 in voikko::GrammarChecker::~GrammarChecker() () from -/usr/lib64/ooo3/program/../share/uno_packages/cache/uno_packages/vvpSpu_/voikko-3.1.oxt/voikko.so -#43 0x00007fb813a1e1e8 in ~Reference (this=0x20bc250) at -/usr/src/debug/ooo320-m19-ure/solver/unxlngx6.pro/inc/com/sun/star/uno/Reference.hxx:118 -#44 ~EventObject (this=0x20bc250) at -../unxlngx6.pro/inc/cppuhelper/com/sun/star/lang/EventObject.hdl:18 -#45 cppu::WeakComponentImplHelperBase::dispose (this=0x20bc250) at -/usr/src/debug/ooo320-m19-ure/cppuhelper/source/implbase.cxx:276 -#46 0x00007fb7bd1eff79 in -cppu::WeakComponentImplHelper4::dispose() () from -/usr/lib64/ooo3/program/../share/uno_packages/cache/uno_packages/vvpSpu_/voikko-3.1.oxt/voikko.so -#47 0x00007fb813a18ff7 in cppu::WeakComponentImplHelperBase::release -(this=0x20bc250) at -/usr/src/debug/ooo320-m19-ure/cppuhelper/source/implbase.cxx:251 -#48 0x00007fb8146bf4e1 in __run_exit_handlers () from /lib64/libc.so.6 -#49 0x00007fb8146bf535 in exit () from /lib64/libc.so.6 -#50 0x00007fb8146a8b84 in __libc_start_main () from /lib64/libc.so.6 -#51 0x0000000000400e19 in _start () - - -Thread 0 (Thread 0x7fb7ee0b1710 (LWP 8578)): -#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib64/libpthread.so.0 -#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#2 0x00007fb7eda6ea9c in ObjectMonitor::wait(long, bool, Thread*) () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#3 0x00007fb7eda6ec0b in ObjectSynchronizer::wait(Handle, long, Thread*) () -from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#4 0x00007fb7ed85eaa9 in JVM_MonitorWait () from -/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so -#5 0x00007fb7e9c17f50 in ?? () +Application: soffice (3.0.0) +KDE Platform Version: 4.4.4 (KDE 4.4.4) "release 2" +Qt Version: 4.6.3 +Operating System: Linux 2.6.34-12-desktop x86_64 +Distribution: "openSUSE 11.3 (x86_64)" + +-- Information about the crash: + + +The crash can be reproduced every time. + + -- Backtrace: +Application: OpenOffice.org (soffice.bin), signal: Aborted +[Current thread is 1 (Thread 0x7fb815831720 (LWP 8556))] + +Thread 14 (Thread 0x7fb80d147710 (LWP 8557)): +#0 0x00007fb814039709 in pthread_cond_timedwait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb81541e2e6 in rtl_cache_wsupdate_wait (arg=) +at alloc_cache.c:1398 +#2 rtl_cache_wsupdate_all (arg=) at alloc_cache.c:1542 +#3 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#4 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#5 0x0000000000000000 in ?? () + +Thread 13 (Thread 0x7fb800c75710 (LWP 8558)): +#0 0x00007fb814039709 in pthread_cond_timedwait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb8153f4166 in osl_waitCondition (Condition=0x7b7d90, +pTimeout=0x7fb800c74d60) at conditn.c:250 +#2 0x00007fb810f4f82d in vos::OTimerManager::run() () from +/usr/lib64/ooo3/program/../basis-link/program/libvos3gcc3.so +#3 0x00007fb810f4de1a in vos::threadWorkerFunction_impl(void*) () from +/usr/lib64/ooo3/program/../basis-link/program/libvos3gcc3.so +#4 0x00007fb8153f743c in osl_thread_start_Impl (pData=0x7acc20) at +thread.c:266 +#5 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#6 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#7 0x0000000000000000 in ?? () + +Thread 12 (Thread 0x7fb7bfb9b710 (LWP 8568)): +#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7ed9912de in Monitor::wait(bool, long, bool) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed7a4f6a in GCTaskManager::get_task(unsigned int) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7ed7a6140 in GCTaskThread::run() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#6 0x00007fb7ed9b29e2 in java_start(Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#7 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#8 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#9 0x0000000000000000 in ?? () + +Thread 11 (Thread 0x7fb7bfa9a710 (LWP 8569)): +#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7ed9912de in Monitor::wait(bool, long, bool) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed7a4f6a in GCTaskManager::get_task(unsigned int) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7ed7a6140 in GCTaskThread::run() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#6 0x00007fb7ed9b29e2 in java_start(Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#7 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#8 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#9 0x0000000000000000 in ?? () + +Thread 10 (Thread 0x7fb7bf7fc710 (LWP 8570)): +#0 0x00007fb814039709 in pthread_cond_timedwait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b6685 in os::PlatformEvent::park(long) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7ed990ddf in Monitor::IWait(Thread*, long) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7ed9912de in Monitor::wait(bool, long, bool) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7edae0bc3 in VMThread::loop() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7edae0de2 in VMThread::run() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#6 0x00007fb7ed9b29e2 in java_start(Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#7 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#8 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#9 0x0000000000000000 in ?? () + +Thread 9 (Thread 0x7fb7bf6fb710 (LWP 8571)): +#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7eda6ea9c in ObjectMonitor::wait(long, bool, Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7eda6ec0b in ObjectSynchronizer::wait(Handle, long, Thread*) () +from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed85eaa9 in JVM_MonitorWait () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7e9c17f50 in ?? () +#6 0x0000000000000000 in ?? () + +Thread 8 (Thread 0x7fb7bf5fa710 (LWP 8572)): +#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7eda6ea9c in ObjectMonitor::wait(long, bool, Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7eda6ec0b in ObjectSynchronizer::wait(Handle, long, Thread*) () +from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed85eaa9 in JVM_MonitorWait () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7e9c17f50 in ?? () +#6 0x00007fb7bf5f98c8 in ?? () +#7 0x00007fb7bfd86b68 in ?? () +#8 0x00007fb7bfd86020 in ?? () +#9 0x00007fb7bfd86b60 in ?? () +#10 0x00007fb7bf5f9840 in ?? () +#11 0x0000000000000000 in ?? () + +Thread 7 (Thread 0x7fb7bf4f9710 (LWP 8573)): +#0 0x00007fb81403b600 in sem_wait () from /lib64/libpthread.so.0 +#1 0x00007fb7ed9af94a in check_pending_signals(bool) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7ed9ad7a3 in signal_thread_entry(JavaThread*, Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7edaa0f97 in JavaThread::thread_main_inner() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed9b29e2 in java_start(Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#6 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#7 0x0000000000000000 in ?? () + +Thread 6 (Thread 0x7fb7bf3f8710 (LWP 8574)): +#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7ed991375 in Monitor::wait(bool, long, bool) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed6f5a17 in CompileQueue::get() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7ed6f8850 in CompileBroker::compiler_thread_loop() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#6 0x00007fb7edaa0f97 in JavaThread::thread_main_inner() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#7 0x00007fb7ed9b29e2 in java_start(Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#8 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#9 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#10 0x0000000000000000 in ?? () + +Thread 5 (Thread 0x7fb7bf2f7710 (LWP 8575)): +#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7ed991375 in Monitor::wait(bool, long, bool) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed6f5a17 in CompileQueue::get() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7ed6f8850 in CompileBroker::compiler_thread_loop() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#6 0x00007fb7edaa0f97 in JavaThread::thread_main_inner() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#7 0x00007fb7ed9b29e2 in java_start(Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#8 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#9 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#10 0x0000000000000000 in ?? () + +Thread 4 (Thread 0x7fb7bf1f6710 (LWP 8576)): +#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7ed990c7b in Monitor::IWait(Thread*, long) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7ed9912de in Monitor::wait(bool, long, bool) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed9505b2 in +LowMemoryDetector::low_memory_detector_thread_entry(JavaThread*, Thread*) () +from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7edaa0f97 in JavaThread::thread_main_inner() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#6 0x00007fb7ed9b29e2 in java_start(Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#7 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#8 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#9 0x0000000000000000 in ?? () + +Thread 3 (Thread 0x7fb7bf0f5710 (LWP 8577)): +#0 0x00007fb814039709 in pthread_cond_timedwait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b6685 in os::PlatformEvent::park(long) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7ed9b682b in os::sleep(Thread*, long, bool) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7eda9bed0 in WatcherThread::run() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed9b29e2 in java_start(Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb814034a4f in start_thread () from /lib64/libpthread.so.0 +#6 0x00007fb81475a82d in clone () from /lib64/libc.so.6 +#7 0x0000000000000000 in ?? () + +Thread 2 (Thread 0x7fb7ee0b1710 (LWP 8578)): +#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7eda6ea9c in ObjectMonitor::wait(long, bool, Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7eda6ec0b in ObjectSynchronizer::wait(Handle, long, Thread*) () +from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed85eaa9 in JVM_MonitorWait () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7e9c17f50 in ?? () +#6 0x0000000000000000 in ?? () + +Thread 1 (Thread 0x7fb815831720 (LWP 8556)): +[KCrash Handler] +#5 0x00007fb8146bc9e5 in raise () from /lib64/libc.so.6 +#6 0x00007fb8146bdee6 in abort () from /lib64/libc.so.6 +#7 0x00007fb8146f7c53 in __libc_message () from /lib64/libc.so.6 +#8 0x00007fb8146fd226 in malloc_printerr () from /lib64/libc.so.6 +#9 0x00007fb814701fcc in free () from /lib64/libc.so.6 +#10 0x00007fb8137bef8c in ~OUString (pMapping=) at +/usr/src/debug/ooo320-m19-ure/solver/unxlngx6.pro/inc/rtl/ustring.hxx:232 +#11 ~MappingEntry (pMapping=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbmap.cxx:115 +#12 uno_revokeMapping (pMapping=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbmap.cxx:667 +#13 0x00007fb7bc9693e2 in ~Mapping (this=, +__in_chrg=) at ../../../inc/uno/mapping.hxx:249 +#14 Proxy::~Proxy (this=, __in_chrg=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/helper/purpenv/helper_purpenv_Proxy.cxx:264 +#15 0x00007fb7bc969491 in Proxy_free (pProxy=0x7fb7f00ded50) at +/usr/src/debug/ooo320-m19-ure/cppu/source/helper/purpenv/helper_purpenv_Proxy.cxx:190 +#16 0x00007fb8137c51ba in (anonymous +namespace)::s_stub_defenv_revokeInterface (pParam=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbenv.cxx:397 +#17 0x00007fb8137c925b in s_environment_invoke_v (pCurrEnv=0x7fb7f00de5f0, +pTargetEnv=, pCallee=0x7fb8137c4f0a <(anonymous +namespace)::s_stub_defenv_revokeInterface(va_list*)>, + pParam=0x7fff7301f0a0) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:292 +#18 0x00007fb8137c92f4 in uno_Environment_invoke_v +(pTargetEnv=0x7fb7f00de5f0, pCallee=0x7fb8137c4f0a <(anonymous +namespace)::s_stub_defenv_revokeInterface(va_list*)>, pParam=0x7fff7301f0a0) + at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:311 +#19 0x00007fb8137c938f in uno_Environment_invoke (pEnv=, +pCallee=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:320 +#20 0x00007fb8137c295d in (anonymous namespace)::defenv_revokeInterface +(pEnv=, pInterface=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbenv.cxx:453 +#21 0x00007fb8137c925b in s_environment_invoke_v (pCurrEnv=0x7fb7f00de5f0, +pTargetEnv=, pCallee=0x7fb7bc967980 +, pParam=0x7fff7301f1f0) + at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:292 +#22 0x00007fb8137c92f4 in uno_Environment_invoke_v +(pTargetEnv=0x7fb7f00de5f0, pCallee=0x7fb7bc967980 +, pParam=0x7fff7301f1f0) + at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:311 +#23 0x00007fb8137c938f in uno_Environment_invoke (pEnv=, +pCallee=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:320 +#24 0x00007fb7bc9696b1 in Proxy::release (this=0x7fb7f00ded50) at +/usr/src/debug/ooo320-m19-ure/cppu/source/helper/purpenv/helper_purpenv_Proxy.cxx:360 +#25 0x00007fb80327ba62 in bridges::cpp_uno::shared::freeCppInterfaceProxy +(pEnv=0x7fb7f00de4e0, pInterface=0x7fb7f00e2328) + at +/usr/src/debug/ooo320-m19-ure/bridges/source/cpp_uno/shared/cppinterfaceproxy.cxx:107 +#26 0x00007fb8137c51ba in (anonymous +namespace)::s_stub_defenv_revokeInterface (pParam=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbenv.cxx:397 +#27 0x00007fb8137c925b in s_environment_invoke_v (pCurrEnv=0x7fb7f00de5f0, +pTargetEnv=, pCallee=0x7fb8137c4f0a <(anonymous +namespace)::s_stub_defenv_revokeInterface(va_list*)>, + pParam=0x7fff7301f670) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:292 +#28 0x00007fb8137c9430 in s_environment_invoke_vv (pParam=) at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:275 +#29 0x00007fb8137c860c in s_pull (pParam=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:225 +#30 0x00007fb7bcb6d494 in dge::v_callInto_v (this=, +pCallee=0x7fb8137c85c0 , pParam=0x7fff7301f450) + at +/usr/src/debug/ooo320-m19-ure/cppu/source/UnsafeBridge/UnsafeBridge.cxx:89 +#31 0x00007fb8137c967c in callInto_v (this=, +pCallee=) at ../../inc/cppu/Enterable.hxx:61 +#32 cppu::Enterable::callInto (this=, pCallee=) at ../../inc/cppu/Enterable.hxx:98 +#33 0x00007fb8137c8c58 in s_callInto_v (pEnv=, +pCallee=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:232 +#34 s_callInto (pEnv=, pCallee=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:243 +#35 0x00007fb8137c9291 in s_environment_invoke_v (pCurrEnv=0x0, +pTargetEnv=0x7fb7f00de4e0, pCallee=0x7fb8137c4f0a <(anonymous +namespace)::s_stub_defenv_revokeInterface(va_list*)>, + pParam=0x7fff7301f670) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:299 +#36 0x00007fb8137c92f4 in uno_Environment_invoke_v +(pTargetEnv=0x7fb7f00de4e0, pCallee=0x7fb8137c4f0a <(anonymous +namespace)::s_stub_defenv_revokeInterface(va_list*)>, pParam=0x7fff7301f670) + at /usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:311 +#37 0x00007fb8137c938f in uno_Environment_invoke (pEnv=, +pCallee=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/EnvStack.cxx:320 +#38 0x00007fb8137c295d in (anonymous namespace)::defenv_revokeInterface +(pEnv=, pInterface=) at +/usr/src/debug/ooo320-m19-ure/cppu/source/uno/lbenv.cxx:453 +#39 0x00007fb80327bd1a in +bridges::cpp_uno::shared::CppInterfaceProxy::releaseProxy +(this=0x7fb7f00e2300) at +/usr/src/debug/ooo320-m19-ure/bridges/source/cpp_uno/shared/cppinterfaceproxy.cxx:162 +#40 0x00007fb803279478 in cpp_vtable_call (nFunctionIndex=2, +nVtableOffset=, gpreg=0x7fff7301f890, +fpreg=0x7fff7301f8c0, ovrflw=0x7fff7301f910, pRegisterReturn=0x7fff7301f870) + at +/usr/src/debug/ooo320-m19-ure/bridges/source/cpp_uno/gcc3_linux_x86-64/cpp2uno.cxx:344 +#41 0x00007fb80327ab82 in privateSnippetExecutor () from +/usr/lib64/ooo3/ure/lib/libgcc3_uno.so +#42 0x00007fb7bd1f0486 in voikko::GrammarChecker::~GrammarChecker() () from +/usr/lib64/ooo3/program/../share/uno_packages/cache/uno_packages/vvpSpu_/voikko-3.1.oxt/voikko.so +#43 0x00007fb813a1e1e8 in ~Reference (this=0x20bc250) at +/usr/src/debug/ooo320-m19-ure/solver/unxlngx6.pro/inc/com/sun/star/uno/Reference.hxx:118 +#44 ~EventObject (this=0x20bc250) at +../unxlngx6.pro/inc/cppuhelper/com/sun/star/lang/EventObject.hdl:18 +#45 cppu::WeakComponentImplHelperBase::dispose (this=0x20bc250) at +/usr/src/debug/ooo320-m19-ure/cppuhelper/source/implbase.cxx:276 +#46 0x00007fb7bd1eff79 in +cppu::WeakComponentImplHelper4::dispose() () from +/usr/lib64/ooo3/program/../share/uno_packages/cache/uno_packages/vvpSpu_/voikko-3.1.oxt/voikko.so +#47 0x00007fb813a18ff7 in cppu::WeakComponentImplHelperBase::release +(this=0x20bc250) at +/usr/src/debug/ooo320-m19-ure/cppuhelper/source/implbase.cxx:251 +#48 0x00007fb8146bf4e1 in __run_exit_handlers () from /lib64/libc.so.6 +#49 0x00007fb8146bf535 in exit () from /lib64/libc.so.6 +#50 0x00007fb8146a8b84 in __libc_start_main () from /lib64/libc.so.6 +#51 0x0000000000400e19 in _start () + + +Thread 0 (Thread 0x7fb7ee0b1710 (LWP 8578)): +#0 0x00007fb81403939c in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib64/libpthread.so.0 +#1 0x00007fb7ed9b64bb in os::PlatformEvent::park() () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#2 0x00007fb7eda6ea9c in ObjectMonitor::wait(long, bool, Thread*) () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#3 0x00007fb7eda6ec0b in ObjectSynchronizer::wait(Handle, long, Thread*) () +from /usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#4 0x00007fb7ed85eaa9 in JVM_MonitorWait () from +/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/amd64/server/libjvm.so +#5 0x00007fb7e9c17f50 in ?? () #6 0x0000000000000000 in ?? () \ No newline at end of file diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-reconq.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-reconq.txt similarity index 98% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-reconq.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-reconq.txt index 001aa9e..bb4a532 100644 --- a/src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-reconq.txt +++ b/src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-reconq.txt @@ -1,76 +1,76 @@ -Application that crashed: rekonq -Version of the application: 0.1.9 -KDE Version: 4.2.96 (KDE 4.2.96 (KDE 4.3 RC2)) -Qt Version: 4.5.2 -Operating System: Linux 2.6.30-8-generic i686 -Distribution: Ubuntu karmic (development branch) - -What I was doing when the application crashed: - - - -- Backtrace: -Application: rekonq (rekonq), signal: Segmentation fault -[Current thread is 1 (Thread 0xb80bd700 (LWP 9322))] - -Thread 4 (Thread 0xb6955b90 (LWP 9323)): -#0 0x00778422 in __kernel_vsyscall () -#1 0x007bc0a5 in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib/tls/i686/cmov/libpthread.so.0 -#2 0x064c9c7d in pthread_cond_wait () from /lib/tls/i686/cmov/libc.so.6 -#3 0x0567fe37 in QWaitCondition::wait(QMutex*, unsigned long) () from -/usr/lib/libQtCore.so.4 -#4 0x00bb3c10 in ctiVMThrowTrampoline () from /usr/lib/libQtWebKit.so.4 -#5 0x010bbe34 in ?? () from /usr/lib/libQtWebKit.so.4 -#6 0x010bbf1b in ?? () from /usr/lib/libQtWebKit.so.4 -#7 0x00bb3b34 in ctiVMThrowTrampoline () from /usr/lib/libQtWebKit.so.4 -#8 0x00bb3b63 in ctiVMThrowTrampoline () from /usr/lib/libQtWebKit.so.4 -#9 0x0567ee02 in ?? () from /usr/lib/libQtCore.so.4 -#10 0x007b84df in start_thread () from /lib/tls/i686/cmov/libpthread.so.0 -#11 0x064baf3e in clone () from /lib/tls/i686/cmov/libc.so.6 - -Thread 3 (Thread 0xb6154b90 (LWP 9802)): -#0 0x00778422 in __kernel_vsyscall () -#1 0x007bc0a5 in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib/tls/i686/cmov/libpthread.so.0 -#2 0x064c9c7d in pthread_cond_wait () from /lib/tls/i686/cmov/libc.so.6 -#3 0xb4c0569f in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so -#4 0xb4d378ef in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so -#5 0xb4c05b4d in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so -#6 0x007b84df in start_thread () from /lib/tls/i686/cmov/libpthread.so.0 -#7 0x064baf3e in clone () from /lib/tls/i686/cmov/libc.so.6 - -Thread 2 (Thread 0xaff9cb90 (LWP 9803)): -#0 0x00778422 in __kernel_vsyscall () -#1 0x007bc0a5 in pthread_cond_wait@@GLIBC_2.3.2 () from -/lib/tls/i686/cmov/libpthread.so.0 -#2 0x064c9c7d in pthread_cond_wait () from /lib/tls/i686/cmov/libc.so.6 -#3 0xb4c0569f in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so -#4 0xb4d378ef in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so -#5 0xb4c05b4d in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so -#6 0x007b84df in start_thread () from /lib/tls/i686/cmov/libpthread.so.0 -#7 0x064baf3e in clone () from /lib/tls/i686/cmov/libc.so.6 - -Thread 1 (Thread 0xb80bd700 (LWP 9322)): -[KCrash Handler] -#6 0x0576f7ce in QCoreApplication::notifyInternal(QObject*, QEvent*) () from -/usr/lib/libQtCore.so.4 -#7 0x0480778c in QApplicationPrivate::dispatchEnterLeave(QWidget*, QWidget*) -() from /usr/lib/libQtGui.so.4 -#8 0x04874a2c in QApplication::x11ProcessEvent(_XEvent*) () from -/usr/lib/libQtGui.so.4 -#9 0x048a2102 in ?? () from /usr/lib/libQtGui.so.4 -#10 0x018e5c48 in g_main_context_dispatch () from /usr/lib/libglib-2.0.so.0 -#11 0x018e94f0 in ?? () from /usr/lib/libglib-2.0.so.0 -#12 0x018e9623 in g_main_context_iteration () from /usr/lib/libglib-2.0.so.0 -#13 0x0579a14c in -QEventDispatcherGlib::processEvents(QFlags) () -from /usr/lib/libQtCore.so.4 -#14 0x048a17e5 in ?? () from /usr/lib/libQtGui.so.4 -#15 0x0576dd99 in -QEventLoop::processEvents(QFlags) () from -/usr/lib/libQtCore.so.4 -#16 0x0576e1ea in QEventLoop::exec(QFlags) () -from /usr/lib/libQtCore.so.4 -#17 0x0577065f in QCoreApplication::exec() () from /usr/lib/libQtCore.so.4 -#18 0x04800af7 in QApplication::exec() () from /usr/lib/libQtGui.so.4 +Application that crashed: rekonq +Version of the application: 0.1.9 +KDE Version: 4.2.96 (KDE 4.2.96 (KDE 4.3 RC2)) +Qt Version: 4.5.2 +Operating System: Linux 2.6.30-8-generic i686 +Distribution: Ubuntu karmic (development branch) + +What I was doing when the application crashed: + + + -- Backtrace: +Application: rekonq (rekonq), signal: Segmentation fault +[Current thread is 1 (Thread 0xb80bd700 (LWP 9322))] + +Thread 4 (Thread 0xb6955b90 (LWP 9323)): +#0 0x00778422 in __kernel_vsyscall () +#1 0x007bc0a5 in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib/tls/i686/cmov/libpthread.so.0 +#2 0x064c9c7d in pthread_cond_wait () from /lib/tls/i686/cmov/libc.so.6 +#3 0x0567fe37 in QWaitCondition::wait(QMutex*, unsigned long) () from +/usr/lib/libQtCore.so.4 +#4 0x00bb3c10 in ctiVMThrowTrampoline () from /usr/lib/libQtWebKit.so.4 +#5 0x010bbe34 in ?? () from /usr/lib/libQtWebKit.so.4 +#6 0x010bbf1b in ?? () from /usr/lib/libQtWebKit.so.4 +#7 0x00bb3b34 in ctiVMThrowTrampoline () from /usr/lib/libQtWebKit.so.4 +#8 0x00bb3b63 in ctiVMThrowTrampoline () from /usr/lib/libQtWebKit.so.4 +#9 0x0567ee02 in ?? () from /usr/lib/libQtCore.so.4 +#10 0x007b84df in start_thread () from /lib/tls/i686/cmov/libpthread.so.0 +#11 0x064baf3e in clone () from /lib/tls/i686/cmov/libc.so.6 + +Thread 3 (Thread 0xb6154b90 (LWP 9802)): +#0 0x00778422 in __kernel_vsyscall () +#1 0x007bc0a5 in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib/tls/i686/cmov/libpthread.so.0 +#2 0x064c9c7d in pthread_cond_wait () from /lib/tls/i686/cmov/libc.so.6 +#3 0xb4c0569f in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so +#4 0xb4d378ef in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so +#5 0xb4c05b4d in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so +#6 0x007b84df in start_thread () from /lib/tls/i686/cmov/libpthread.so.0 +#7 0x064baf3e in clone () from /lib/tls/i686/cmov/libc.so.6 + +Thread 2 (Thread 0xaff9cb90 (LWP 9803)): +#0 0x00778422 in __kernel_vsyscall () +#1 0x007bc0a5 in pthread_cond_wait@@GLIBC_2.3.2 () from +/lib/tls/i686/cmov/libpthread.so.0 +#2 0x064c9c7d in pthread_cond_wait () from /lib/tls/i686/cmov/libc.so.6 +#3 0xb4c0569f in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so +#4 0xb4d378ef in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so +#5 0xb4c05b4d in ?? () from /usr/lib/adobe-flashplugin/libflashplayer.so +#6 0x007b84df in start_thread () from /lib/tls/i686/cmov/libpthread.so.0 +#7 0x064baf3e in clone () from /lib/tls/i686/cmov/libc.so.6 + +Thread 1 (Thread 0xb80bd700 (LWP 9322)): +[KCrash Handler] +#6 0x0576f7ce in QCoreApplication::notifyInternal(QObject*, QEvent*) () from +/usr/lib/libQtCore.so.4 +#7 0x0480778c in QApplicationPrivate::dispatchEnterLeave(QWidget*, QWidget*) +() from /usr/lib/libQtGui.so.4 +#8 0x04874a2c in QApplication::x11ProcessEvent(_XEvent*) () from +/usr/lib/libQtGui.so.4 +#9 0x048a2102 in ?? () from /usr/lib/libQtGui.so.4 +#10 0x018e5c48 in g_main_context_dispatch () from /usr/lib/libglib-2.0.so.0 +#11 0x018e94f0 in ?? () from /usr/lib/libglib-2.0.so.0 +#12 0x018e9623 in g_main_context_iteration () from /usr/lib/libglib-2.0.so.0 +#13 0x0579a14c in +QEventDispatcherGlib::processEvents(QFlags) () +from /usr/lib/libQtCore.so.4 +#14 0x048a17e5 in ?? () from /usr/lib/libQtGui.so.4 +#15 0x0576dd99 in +QEventLoop::processEvents(QFlags) () from +/usr/lib/libQtCore.so.4 +#16 0x0576e1ea in QEventLoop::exec(QFlags) () +from /usr/lib/libQtCore.so.4 +#17 0x0577065f in QCoreApplication::exec() () from /usr/lib/libQtCore.so.4 +#18 0x04800af7 in QApplication::exec() () from /usr/lib/libQtGui.so.4 #19 0x080ae823 in _start () \ No newline at end of file diff --git a/src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-tellico.txt b/src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-tellico.txt similarity index 97% rename from src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-tellico.txt rename to src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-tellico.txt index 125b027..fb57d96 100644 --- a/src/certfuzz/test/debuggers/output_parsers/backtraces/konqi-tellico.txt +++ b/src/test_certfuzz/debuggers/output_parsers/backtraces/konqi-tellico.txt @@ -1,108 +1,108 @@ -Application that crashed: tellico -Version of the application: 2.0 -KDE Version: 4.3.2 (KDE 4.3.2) -Qt Version: 4.5.2 -Operating System: Linux 2.6.30.9-90.fc11.i586 i686 -Distribution: "Fedora release 11 (Leonidas)" - -What I was doing when the application crashed: - - - -- Backtrace: -Application: Tellico (tellico), signal: Segmentation fault -[KCrash Handler] -#6 Tellico::Export::HTMLExporter::readOptions -(this=0xb839a08, config_={d = 0xa2f8488}) at -/usr/src/debug/tellico-2.0/src/translators/htmlexporter.cpp:521 -#7 0x080cdf68 in Tellico::ExportDialog::exporter -(format_=Tellico::Export::HTML) at -/usr/src/debug/tellico-2.0/src/exportdialog.cpp:225 -#8 0x080ce983 in -Tellico::ExportDialog::ExportDialog(Tellico::Export::Format, -Tellico::Data::CollPtr, struct QWidget *) (this=0xbfb12484, -format_=Tellico::Export::HTML, coll_={d = 0xbfb124f4}, - parent_=0xa402db8) at -/usr/src/debug/tellico-2.0/src/exportdialog.cpp:61 -#9 0x0810bddb in Tellico::MainWindow::slotFileExport -(this=0xa402db8, format_=4) at -/usr/src/debug/tellico-2.0/src/mainwindow.cpp:1758 -#10 0x0810c472 in Tellico::MainWindow::qt_metacall -(this=0xa402db8, _c=QMetaObject::InvokeMetaMethod, -_id=30, _a=0xbfb12628) at /usr/src/debug/tellico-2.0/i586- -redhat-linux-gnu/src/mainwindow.moc:197 -#11 0x050fa853 in QMetaObject::activate(QObject*, int, int, -void**) () from /usr/lib/libQtCore.so.4 -#12 0x050fb4b2 in QMetaObject::activate(QObject*, -QMetaObject const*, int, void**) () from -/usr/lib/libQtCore.so.4 -#13 0x050fd443 in QSignalMapper::mapped(int) () from -/usr/lib/libQtCore.so.4 -#14 0x050fdcdd in QSignalMapper::map(QObject*) () from -/usr/lib/libQtCore.so.4 -#15 0x050fdebe in QSignalMapper::map() () from -/usr/lib/libQtCore.so.4 -#16 0x050fe6fb in -QSignalMapper::qt_metacall(QMetaObject::Call, int, void**) -() from /usr/lib/libQtCore.so.4 -#17 0x050fa853 in QMetaObject::activate(QObject*, int, int, -void**) () from /usr/lib/libQtCore.so.4 -#18 0x050facc8 in QMetaObject::activate(QObject*, -QMetaObject const*, int, int, void**) () from -/usr/lib/libQtCore.so.4 -#19 0x05618331 in QAction::triggered(bool) () from -/usr/lib/libQtGui.so.4 -#20 0x056198c6 in QAction::activate(QAction::ActionEvent) -() from /usr/lib/libQtGui.so.4 -#21 0x05a6583c in ?? () from /usr/lib/libQtGui.so.4 -#22 0x05a6be8b in ?? () from /usr/lib/libQtGui.so.4 -#23 0x05a6ca4f in -QMenu::mouseReleaseEvent(QMouseEvent*) () from -/usr/lib/libQtGui.so.4 -#24 0x06404f15 in -KMenu::mouseReleaseEvent(QMouseEvent*) () from -/usr/lib/libkdeui.so.5 -#25 0x05673b9a in QWidget::event(QEvent*) () from -/usr/lib/libQtGui.so.4 -#26 0x05a6eca4 in QMenu::event(QEvent*) () from -/usr/lib/libQtGui.so.4 -#27 0x0561e974 in -QApplicationPrivate::notify_helper(QObject*, QEvent*) () -from /usr/lib/libQtGui.so.4 -#28 0x05626a9b in QApplication::notify(QObject*, QEvent*) -() from /usr/lib/libQtGui.so.4 -#29 0x0631ac3a in KApplication::notify(QObject*, QEvent*) -() from /usr/lib/libkdeui.so.5 -#30 0x050e4b0b in -QCoreApplication::notifyInternal(QObject*, QEvent*) () from -/usr/lib/libQtCore.so.4 -#31 0x056259de in -QApplicationPrivate::sendMouseEvent(QWidget*, -QMouseEvent*, QWidget*, QWidget*, QWidget**, -QPointer&) () from /usr/lib/libQtGui.so.4 -#32 0x0569454f in ?? () from /usr/lib/libQtGui.so.4 -#33 0x05693523 in -QApplication::x11ProcessEvent(_XEvent*) () from -/usr/lib/libQtGui.so.4 -#34 0x056be5d2 in ?? () from /usr/lib/libQtGui.so.4 -#35 0x0458a308 in g_main_context_dispatch () from -/lib/libglib-2.0.so.0 -#36 0x0458d9e0 in ?? () from /lib/libglib-2.0.so.0 -#37 0x0458db13 in g_main_context_iteration () from -/lib/libglib-2.0.so.0 -#38 0x0510f5ec in -QEventDispatcherGlib::processEvents(QFlags) -() from /usr/lib/libQtCore.so.4 -#39 0x056bdcb5 in ?? () from /usr/lib/libQtGui.so.4 -#40 0x050e30d9 in -QEventLoop::processEvents(QFlags) -() from /usr/lib/libQtCore.so.4 -#41 0x050e352a in -QEventLoop::exec(QFlags) -() from /usr/lib/libQtCore.so.4 -#42 0x050e59af in QCoreApplication::exec() () from -/usr/lib/libQtCore.so.4 -#43 0x0561e7f7 in QApplication::exec() () from -/usr/lib/libQtGui.so.4 -#44 0x080f6ce8 in main (argc=1, argv=0xbfb14b64) at +Application that crashed: tellico +Version of the application: 2.0 +KDE Version: 4.3.2 (KDE 4.3.2) +Qt Version: 4.5.2 +Operating System: Linux 2.6.30.9-90.fc11.i586 i686 +Distribution: "Fedora release 11 (Leonidas)" + +What I was doing when the application crashed: + + + -- Backtrace: +Application: Tellico (tellico), signal: Segmentation fault +[KCrash Handler] +#6 Tellico::Export::HTMLExporter::readOptions +(this=0xb839a08, config_={d = 0xa2f8488}) at +/usr/src/debug/tellico-2.0/src/translators/htmlexporter.cpp:521 +#7 0x080cdf68 in Tellico::ExportDialog::exporter +(format_=Tellico::Export::HTML) at +/usr/src/debug/tellico-2.0/src/exportdialog.cpp:225 +#8 0x080ce983 in +Tellico::ExportDialog::ExportDialog(Tellico::Export::Format, +Tellico::Data::CollPtr, struct QWidget *) (this=0xbfb12484, +format_=Tellico::Export::HTML, coll_={d = 0xbfb124f4}, + parent_=0xa402db8) at +/usr/src/debug/tellico-2.0/src/exportdialog.cpp:61 +#9 0x0810bddb in Tellico::MainWindow::slotFileExport +(this=0xa402db8, format_=4) at +/usr/src/debug/tellico-2.0/src/mainwindow.cpp:1758 +#10 0x0810c472 in Tellico::MainWindow::qt_metacall +(this=0xa402db8, _c=QMetaObject::InvokeMetaMethod, +_id=30, _a=0xbfb12628) at /usr/src/debug/tellico-2.0/i586- +redhat-linux-gnu/src/mainwindow.moc:197 +#11 0x050fa853 in QMetaObject::activate(QObject*, int, int, +void**) () from /usr/lib/libQtCore.so.4 +#12 0x050fb4b2 in QMetaObject::activate(QObject*, +QMetaObject const*, int, void**) () from +/usr/lib/libQtCore.so.4 +#13 0x050fd443 in QSignalMapper::mapped(int) () from +/usr/lib/libQtCore.so.4 +#14 0x050fdcdd in QSignalMapper::map(QObject*) () from +/usr/lib/libQtCore.so.4 +#15 0x050fdebe in QSignalMapper::map() () from +/usr/lib/libQtCore.so.4 +#16 0x050fe6fb in +QSignalMapper::qt_metacall(QMetaObject::Call, int, void**) +() from /usr/lib/libQtCore.so.4 +#17 0x050fa853 in QMetaObject::activate(QObject*, int, int, +void**) () from /usr/lib/libQtCore.so.4 +#18 0x050facc8 in QMetaObject::activate(QObject*, +QMetaObject const*, int, int, void**) () from +/usr/lib/libQtCore.so.4 +#19 0x05618331 in QAction::triggered(bool) () from +/usr/lib/libQtGui.so.4 +#20 0x056198c6 in QAction::activate(QAction::ActionEvent) +() from /usr/lib/libQtGui.so.4 +#21 0x05a6583c in ?? () from /usr/lib/libQtGui.so.4 +#22 0x05a6be8b in ?? () from /usr/lib/libQtGui.so.4 +#23 0x05a6ca4f in +QMenu::mouseReleaseEvent(QMouseEvent*) () from +/usr/lib/libQtGui.so.4 +#24 0x06404f15 in +KMenu::mouseReleaseEvent(QMouseEvent*) () from +/usr/lib/libkdeui.so.5 +#25 0x05673b9a in QWidget::event(QEvent*) () from +/usr/lib/libQtGui.so.4 +#26 0x05a6eca4 in QMenu::event(QEvent*) () from +/usr/lib/libQtGui.so.4 +#27 0x0561e974 in +QApplicationPrivate::notify_helper(QObject*, QEvent*) () +from /usr/lib/libQtGui.so.4 +#28 0x05626a9b in QApplication::notify(QObject*, QEvent*) +() from /usr/lib/libQtGui.so.4 +#29 0x0631ac3a in KApplication::notify(QObject*, QEvent*) +() from /usr/lib/libkdeui.so.5 +#30 0x050e4b0b in +QCoreApplication::notifyInternal(QObject*, QEvent*) () from +/usr/lib/libQtCore.so.4 +#31 0x056259de in +QApplicationPrivate::sendMouseEvent(QWidget*, +QMouseEvent*, QWidget*, QWidget*, QWidget**, +QPointer&) () from /usr/lib/libQtGui.so.4 +#32 0x0569454f in ?? () from /usr/lib/libQtGui.so.4 +#33 0x05693523 in +QApplication::x11ProcessEvent(_XEvent*) () from +/usr/lib/libQtGui.so.4 +#34 0x056be5d2 in ?? () from /usr/lib/libQtGui.so.4 +#35 0x0458a308 in g_main_context_dispatch () from +/lib/libglib-2.0.so.0 +#36 0x0458d9e0 in ?? () from /lib/libglib-2.0.so.0 +#37 0x0458db13 in g_main_context_iteration () from +/lib/libglib-2.0.so.0 +#38 0x0510f5ec in +QEventDispatcherGlib::processEvents(QFlags) +() from /usr/lib/libQtCore.so.4 +#39 0x056bdcb5 in ?? () from /usr/lib/libQtGui.so.4 +#40 0x050e30d9 in +QEventLoop::processEvents(QFlags) +() from /usr/lib/libQtCore.so.4 +#41 0x050e352a in +QEventLoop::exec(QFlags) +() from /usr/lib/libQtCore.so.4 +#42 0x050e59af in QCoreApplication::exec() () from +/usr/lib/libQtCore.so.4 +#43 0x0561e7f7 in QApplication::exec() () from +/usr/lib/libQtGui.so.4 +#44 0x080f6ce8 in main (argc=1, argv=0xbfb14b64) at /usr/src/debug/tellico-2.0/src/main.cpp:95 \ No newline at end of file diff --git a/src/certfuzz/test/debuggers/output_parsers/test_abrtfile.py b/src/test_certfuzz/debuggers/output_parsers/test_abrtfile.py similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/test_abrtfile.py rename to src/test_certfuzz/debuggers/output_parsers/test_abrtfile.py diff --git a/src/certfuzz/test/debuggers/output_parsers/test_calltracefile.py b/src/test_certfuzz/debuggers/output_parsers/test_calltracefile.py similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/test_calltracefile.py rename to src/test_certfuzz/debuggers/output_parsers/test_calltracefile.py diff --git a/src/certfuzz/test/debuggers/output_parsers/test_cwfile.py b/src/test_certfuzz/debuggers/output_parsers/test_cwfile.py similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/test_cwfile.py rename to src/test_certfuzz/debuggers/output_parsers/test_cwfile.py diff --git a/src/certfuzz/test/debuggers/output_parsers/test_debugger_file_base.py b/src/test_certfuzz/debuggers/output_parsers/test_debugger_file_base.py similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/test_debugger_file_base.py rename to src/test_certfuzz/debuggers/output_parsers/test_debugger_file_base.py diff --git a/src/certfuzz/test/debuggers/output_parsers/test_gdbfile.py b/src/test_certfuzz/debuggers/output_parsers/test_gdbfile.py similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/test_gdbfile.py rename to src/test_certfuzz/debuggers/output_parsers/test_gdbfile.py diff --git a/src/certfuzz/test/debuggers/output_parsers/test_konqifile.py b/src/test_certfuzz/debuggers/output_parsers/test_konqifile.py similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/test_konqifile.py rename to src/test_certfuzz/debuggers/output_parsers/test_konqifile.py diff --git a/src/certfuzz/test/debuggers/output_parsers/test_msec_file.py b/src/test_certfuzz/debuggers/output_parsers/test_msec_file.py similarity index 100% rename from src/certfuzz/test/debuggers/output_parsers/test_msec_file.py rename to src/test_certfuzz/debuggers/output_parsers/test_msec_file.py diff --git a/src/certfuzz/test/debuggers/test_crashwrangler.py b/src/test_certfuzz/debuggers/test_crashwrangler.py similarity index 100% rename from src/certfuzz/test/debuggers/test_crashwrangler.py rename to src/test_certfuzz/debuggers/test_crashwrangler.py diff --git a/src/certfuzz/test/debuggers/test_debugger_base.py b/src/test_certfuzz/debuggers/test_debugger_base.py similarity index 100% rename from src/certfuzz/test/debuggers/test_debugger_base.py rename to src/test_certfuzz/debuggers/test_debugger_base.py diff --git a/src/certfuzz/test/debuggers/test_gdb.py b/src/test_certfuzz/debuggers/test_gdb.py similarity index 100% rename from src/certfuzz/test/debuggers/test_gdb.py rename to src/test_certfuzz/debuggers/test_gdb.py diff --git a/src/certfuzz/test/debuggers/test_jit.py b/src/test_certfuzz/debuggers/test_jit.py similarity index 100% rename from src/certfuzz/test/debuggers/test_jit.py rename to src/test_certfuzz/debuggers/test_jit.py diff --git a/src/certfuzz/test/debuggers/test_mr_crash_hash.py b/src/test_certfuzz/debuggers/test_mr_crash_hash.py similarity index 100% rename from src/certfuzz/test/debuggers/test_mr_crash_hash.py rename to src/test_certfuzz/debuggers/test_mr_crash_hash.py diff --git a/src/certfuzz/test/debuggers/test_msec.py b/src/test_certfuzz/debuggers/test_msec.py similarity index 100% rename from src/certfuzz/test/debuggers/test_msec.py rename to src/test_certfuzz/debuggers/test_msec.py diff --git a/src/certfuzz/test/debuggers/test_nulldebugger.py b/src/test_certfuzz/debuggers/test_nulldebugger.py similarity index 100% rename from src/certfuzz/test/debuggers/test_nulldebugger.py rename to src/test_certfuzz/debuggers/test_nulldebugger.py diff --git a/src/certfuzz/test/debuggers/test_registration.py b/src/test_certfuzz/debuggers/test_registration.py similarity index 100% rename from src/certfuzz/test/debuggers/test_registration.py rename to src/test_certfuzz/debuggers/test_registration.py diff --git a/src/certfuzz/test/drillresults/__init__.py b/src/test_certfuzz/drillresults/__init__.py similarity index 100% rename from src/certfuzz/test/drillresults/__init__.py rename to src/test_certfuzz/drillresults/__init__.py diff --git a/src/certfuzz/test/drillresults/test_common.py b/src/test_certfuzz/drillresults/test_common.py similarity index 100% rename from src/certfuzz/test/drillresults/test_common.py rename to src/test_certfuzz/drillresults/test_common.py diff --git a/src/certfuzz/test/drillresults/test_result_driller_base.py b/src/test_certfuzz/drillresults/test_result_driller_base.py similarity index 100% rename from src/certfuzz/test/drillresults/test_result_driller_base.py rename to src/test_certfuzz/drillresults/test_result_driller_base.py diff --git a/src/certfuzz/test/drillresults/test_result_driller_linux.py b/src/test_certfuzz/drillresults/test_result_driller_linux.py similarity index 100% rename from src/certfuzz/test/drillresults/test_result_driller_linux.py rename to src/test_certfuzz/drillresults/test_result_driller_linux.py diff --git a/src/certfuzz/test/drillresults/test_result_driller_windows.py b/src/test_certfuzz/drillresults/test_result_driller_windows.py similarity index 100% rename from src/certfuzz/test/drillresults/test_result_driller_windows.py rename to src/test_certfuzz/drillresults/test_result_driller_windows.py diff --git a/src/certfuzz/test/drillresults/test_testcasebundle_base.py b/src/test_certfuzz/drillresults/test_testcasebundle_base.py similarity index 100% rename from src/certfuzz/test/drillresults/test_testcasebundle_base.py rename to src/test_certfuzz/drillresults/test_testcasebundle_base.py diff --git a/src/certfuzz/test/drillresults/test_testcasebundle_linux.py b/src/test_certfuzz/drillresults/test_testcasebundle_linux.py similarity index 100% rename from src/certfuzz/test/drillresults/test_testcasebundle_linux.py rename to src/test_certfuzz/drillresults/test_testcasebundle_linux.py diff --git a/src/certfuzz/test/drillresults/test_testcasebundle_windows.py b/src/test_certfuzz/drillresults/test_testcasebundle_windows.py similarity index 100% rename from src/certfuzz/test/drillresults/test_testcasebundle_windows.py rename to src/test_certfuzz/drillresults/test_testcasebundle_windows.py diff --git a/src/certfuzz/test/file_handlers/__init__.py b/src/test_certfuzz/file_handlers/__init__.py similarity index 100% rename from src/certfuzz/test/file_handlers/__init__.py rename to src/test_certfuzz/file_handlers/__init__.py diff --git a/src/certfuzz/test/file_handlers/test_basicfile.py b/src/test_certfuzz/file_handlers/test_basicfile.py similarity index 100% rename from src/certfuzz/test/file_handlers/test_basicfile.py rename to src/test_certfuzz/file_handlers/test_basicfile.py diff --git a/src/certfuzz/test/file_handlers/test_directory.py b/src/test_certfuzz/file_handlers/test_directory.py similarity index 100% rename from src/certfuzz/test/file_handlers/test_directory.py rename to src/test_certfuzz/file_handlers/test_directory.py diff --git a/src/certfuzz/test/file_handlers/test_file_handlers_pkg.py b/src/test_certfuzz/file_handlers/test_file_handlers_pkg.py similarity index 100% rename from src/certfuzz/test/file_handlers/test_file_handlers_pkg.py rename to src/test_certfuzz/file_handlers/test_file_handlers_pkg.py diff --git a/src/certfuzz/test/file_handlers/test_fuzzedfile.py b/src/test_certfuzz/file_handlers/test_fuzzedfile.py similarity index 100% rename from src/certfuzz/test/file_handlers/test_fuzzedfile.py rename to src/test_certfuzz/file_handlers/test_fuzzedfile.py diff --git a/src/certfuzz/test/file_handlers/test_seedfile.py b/src/test_certfuzz/file_handlers/test_seedfile.py similarity index 100% rename from src/certfuzz/test/file_handlers/test_seedfile.py rename to src/test_certfuzz/file_handlers/test_seedfile.py diff --git a/src/certfuzz/test/file_handlers/test_seedfile_set.py b/src/test_certfuzz/file_handlers/test_seedfile_set.py similarity index 100% rename from src/certfuzz/test/file_handlers/test_seedfile_set.py rename to src/test_certfuzz/file_handlers/test_seedfile_set.py diff --git a/src/certfuzz/test/file_handlers/test_tempdir.py b/src/test_certfuzz/file_handlers/test_tempdir.py similarity index 100% rename from src/certfuzz/test/file_handlers/test_tempdir.py rename to src/test_certfuzz/file_handlers/test_tempdir.py diff --git a/src/certfuzz/test/file_handlers/test_tmp_reaper.py b/src/test_certfuzz/file_handlers/test_tmp_reaper.py similarity index 100% rename from src/certfuzz/test/file_handlers/test_tmp_reaper.py rename to src/test_certfuzz/file_handlers/test_tmp_reaper.py diff --git a/src/certfuzz/test/file_handlers/test_watchdog_file.py b/src/test_certfuzz/file_handlers/test_watchdog_file.py similarity index 100% rename from src/certfuzz/test/file_handlers/test_watchdog_file.py rename to src/test_certfuzz/file_handlers/test_watchdog_file.py diff --git a/src/certfuzz/test/fuzzers/__init__.py b/src/test_certfuzz/fuzzers/__init__.py similarity index 100% rename from src/certfuzz/test/fuzzers/__init__.py rename to src/test_certfuzz/fuzzers/__init__.py diff --git a/src/certfuzz/test/fuzzers/test_bitmut.py b/src/test_certfuzz/fuzzers/test_bitmut.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_bitmut.py rename to src/test_certfuzz/fuzzers/test_bitmut.py diff --git a/src/certfuzz/test/fuzzers/test_bytemut.py b/src/test_certfuzz/fuzzers/test_bytemut.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_bytemut.py rename to src/test_certfuzz/fuzzers/test_bytemut.py diff --git a/src/certfuzz/test/fuzzers/test_copy.py b/src/test_certfuzz/fuzzers/test_copy.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_copy.py rename to src/test_certfuzz/fuzzers/test_copy.py diff --git a/src/certfuzz/test/fuzzers/test_crlfmut.py b/src/test_certfuzz/fuzzers/test_crlfmut.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_crlfmut.py rename to src/test_certfuzz/fuzzers/test_crlfmut.py diff --git a/src/certfuzz/test/fuzzers/test_crmut.py b/src/test_certfuzz/fuzzers/test_crmut.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_crmut.py rename to src/test_certfuzz/fuzzers/test_crmut.py diff --git a/src/certfuzz/test/fuzzers/test_drop.py b/src/test_certfuzz/fuzzers/test_drop.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_drop.py rename to src/test_certfuzz/fuzzers/test_drop.py diff --git a/src/certfuzz/test/fuzzers/test_fuzzer_base.py b/src/test_certfuzz/fuzzers/test_fuzzer_base.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_fuzzer_base.py rename to src/test_certfuzz/fuzzers/test_fuzzer_base.py diff --git a/src/certfuzz/test/fuzzers/test_insert.py b/src/test_certfuzz/fuzzers/test_insert.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_insert.py rename to src/test_certfuzz/fuzzers/test_insert.py diff --git a/src/certfuzz/test/fuzzers/test_nullmut.py b/src/test_certfuzz/fuzzers/test_nullmut.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_nullmut.py rename to src/test_certfuzz/fuzzers/test_nullmut.py diff --git a/src/certfuzz/test/fuzzers/test_swap.py b/src/test_certfuzz/fuzzers/test_swap.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_swap.py rename to src/test_certfuzz/fuzzers/test_swap.py diff --git a/src/certfuzz/test/fuzzers/test_truncate.py b/src/test_certfuzz/fuzzers/test_truncate.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_truncate.py rename to src/test_certfuzz/fuzzers/test_truncate.py diff --git a/src/certfuzz/test/fuzzers/test_verify.py b/src/test_certfuzz/fuzzers/test_verify.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_verify.py rename to src/test_certfuzz/fuzzers/test_verify.py diff --git a/src/certfuzz/test/fuzzers/test_wave.py b/src/test_certfuzz/fuzzers/test_wave.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_wave.py rename to src/test_certfuzz/fuzzers/test_wave.py diff --git a/src/certfuzz/test/fuzzers/test_zzuf.py b/src/test_certfuzz/fuzzers/test_zzuf.py similarity index 100% rename from src/certfuzz/test/fuzzers/test_zzuf.py rename to src/test_certfuzz/fuzzers/test_zzuf.py diff --git a/src/certfuzz/test/fuzztools/__init__.py b/src/test_certfuzz/fuzztools/__init__.py similarity index 100% rename from src/certfuzz/test/fuzztools/__init__.py rename to src/test_certfuzz/fuzztools/__init__.py diff --git a/src/certfuzz/test/fuzztools/test_bff_helper.py b/src/test_certfuzz/fuzztools/test_bff_helper.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_bff_helper.py rename to src/test_certfuzz/fuzztools/test_bff_helper.py diff --git a/src/certfuzz/test/fuzztools/test_command_line_callable.py b/src/test_certfuzz/fuzztools/test_command_line_callable.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_command_line_callable.py rename to src/test_certfuzz/fuzztools/test_command_line_callable.py diff --git a/src/certfuzz/test/fuzztools/test_distance_matrix.py b/src/test_certfuzz/fuzztools/test_distance_matrix.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_distance_matrix.py rename to src/test_certfuzz/fuzztools/test_distance_matrix.py diff --git a/src/certfuzz/test/fuzztools/test_filetools.py b/src/test_certfuzz/fuzztools/test_filetools.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_filetools.py rename to src/test_certfuzz/fuzztools/test_filetools.py diff --git a/src/certfuzz/test/fuzztools/test_hamming.py b/src/test_certfuzz/fuzztools/test_hamming.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_hamming.py rename to src/test_certfuzz/fuzztools/test_hamming.py diff --git a/src/certfuzz/test/fuzztools/test_hostinfo.py b/src/test_certfuzz/fuzztools/test_hostinfo.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_hostinfo.py rename to src/test_certfuzz/fuzztools/test_hostinfo.py diff --git a/src/certfuzz/test/fuzztools/test_object_caching.py b/src/test_certfuzz/fuzztools/test_object_caching.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_object_caching.py rename to src/test_certfuzz/fuzztools/test_object_caching.py diff --git a/src/certfuzz/test/fuzztools/test_performance.py b/src/test_certfuzz/fuzztools/test_performance.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_performance.py rename to src/test_certfuzz/fuzztools/test_performance.py diff --git a/src/certfuzz/test/fuzztools/test_ppid_observer.py b/src/test_certfuzz/fuzztools/test_ppid_observer.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_ppid_observer.py rename to src/test_certfuzz/fuzztools/test_ppid_observer.py diff --git a/src/certfuzz/test/fuzztools/test_probability.py b/src/test_certfuzz/fuzztools/test_probability.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_probability.py rename to src/test_certfuzz/fuzztools/test_probability.py diff --git a/src/certfuzz/test/fuzztools/test_process_killer.py b/src/test_certfuzz/fuzztools/test_process_killer.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_process_killer.py rename to src/test_certfuzz/fuzztools/test_process_killer.py diff --git a/src/certfuzz/test/fuzztools/test_range.py b/src/test_certfuzz/fuzztools/test_range.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_range.py rename to src/test_certfuzz/fuzztools/test_range.py diff --git a/src/certfuzz/test/fuzztools/test_rangefinder.py b/src/test_certfuzz/fuzztools/test_rangefinder.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_rangefinder.py rename to src/test_certfuzz/fuzztools/test_rangefinder.py diff --git a/src/certfuzz/test/fuzztools/test_seedrange.py b/src/test_certfuzz/fuzztools/test_seedrange.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_seedrange.py rename to src/test_certfuzz/fuzztools/test_seedrange.py diff --git a/src/certfuzz/test/fuzztools/test_similarity_matrix.py b/src/test_certfuzz/fuzztools/test_similarity_matrix.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_similarity_matrix.py rename to src/test_certfuzz/fuzztools/test_similarity_matrix.py diff --git a/src/certfuzz/test/fuzztools/test_state_timer.py b/src/test_certfuzz/fuzztools/test_state_timer.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_state_timer.py rename to src/test_certfuzz/fuzztools/test_state_timer.py diff --git a/src/certfuzz/test/fuzztools/test_subprocess_helper.py b/src/test_certfuzz/fuzztools/test_subprocess_helper.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_subprocess_helper.py rename to src/test_certfuzz/fuzztools/test_subprocess_helper.py diff --git a/src/certfuzz/test/fuzztools/test_text.py b/src/test_certfuzz/fuzztools/test_text.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_text.py rename to src/test_certfuzz/fuzztools/test_text.py diff --git a/src/certfuzz/test/fuzztools/test_vectors.py b/src/test_certfuzz/fuzztools/test_vectors.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_vectors.py rename to src/test_certfuzz/fuzztools/test_vectors.py diff --git a/src/certfuzz/test/fuzztools/test_watchdog.py b/src/test_certfuzz/fuzztools/test_watchdog.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_watchdog.py rename to src/test_certfuzz/fuzztools/test_watchdog.py diff --git a/src/certfuzz/test/fuzztools/test_zzuflog.py b/src/test_certfuzz/fuzztools/test_zzuflog.py similarity index 100% rename from src/certfuzz/test/fuzztools/test_zzuflog.py rename to src/test_certfuzz/fuzztools/test_zzuflog.py diff --git a/src/certfuzz/test/helpers/__init__.py b/src/test_certfuzz/helpers/__init__.py similarity index 100% rename from src/certfuzz/test/helpers/__init__.py rename to src/test_certfuzz/helpers/__init__.py diff --git a/src/certfuzz/test/helpers/test_coroutine.py b/src/test_certfuzz/helpers/test_coroutine.py similarity index 100% rename from src/certfuzz/test/helpers/test_coroutine.py rename to src/test_certfuzz/helpers/test_coroutine.py diff --git a/src/certfuzz/test/helpers/test_helpers_pkg.py b/src/test_certfuzz/helpers/test_helpers_pkg.py similarity index 100% rename from src/certfuzz/test/helpers/test_helpers_pkg.py rename to src/test_certfuzz/helpers/test_helpers_pkg.py diff --git a/src/certfuzz/test/helpers/test_misc.py b/src/test_certfuzz/helpers/test_misc.py similarity index 100% rename from src/certfuzz/test/helpers/test_misc.py rename to src/test_certfuzz/helpers/test_misc.py diff --git a/src/certfuzz/test/iteration/__init__.py b/src/test_certfuzz/iteration/__init__.py similarity index 100% rename from src/certfuzz/test/iteration/__init__.py rename to src/test_certfuzz/iteration/__init__.py diff --git a/src/certfuzz/test/iteration/test_iteration_base.py b/src/test_certfuzz/iteration/test_iteration_base.py similarity index 100% rename from src/certfuzz/test/iteration/test_iteration_base.py rename to src/test_certfuzz/iteration/test_iteration_base.py diff --git a/src/certfuzz/test/iteration/test_iteration_base3.py b/src/test_certfuzz/iteration/test_iteration_base3.py similarity index 100% rename from src/certfuzz/test/iteration/test_iteration_base3.py rename to src/test_certfuzz/iteration/test_iteration_base3.py diff --git a/src/certfuzz/test/iteration/test_iteration_linux.py b/src/test_certfuzz/iteration/test_iteration_linux.py similarity index 100% rename from src/certfuzz/test/iteration/test_iteration_linux.py rename to src/test_certfuzz/iteration/test_iteration_linux.py diff --git a/src/certfuzz/test/iteration/test_iteration_windows.py b/src/test_certfuzz/iteration/test_iteration_windows.py similarity index 100% rename from src/certfuzz/test/iteration/test_iteration_windows.py rename to src/test_certfuzz/iteration/test_iteration_windows.py diff --git a/src/certfuzz/test/minimizer/__init__.py b/src/test_certfuzz/minimizer/__init__.py similarity index 100% rename from src/certfuzz/test/minimizer/__init__.py rename to src/test_certfuzz/minimizer/__init__.py diff --git a/src/certfuzz/test/minimizer/test_minimizer_base.py b/src/test_certfuzz/minimizer/test_minimizer_base.py similarity index 100% rename from src/certfuzz/test/minimizer/test_minimizer_base.py rename to src/test_certfuzz/minimizer/test_minimizer_base.py diff --git a/src/certfuzz/test/minimizer/test_unix_minimizer.py b/src/test_certfuzz/minimizer/test_unix_minimizer.py similarity index 100% rename from src/certfuzz/test/minimizer/test_unix_minimizer.py rename to src/test_certfuzz/minimizer/test_unix_minimizer.py diff --git a/src/certfuzz/test/minimizer/test_win_minimizer.py b/src/test_certfuzz/minimizer/test_win_minimizer.py similarity index 100% rename from src/certfuzz/test/minimizer/test_win_minimizer.py rename to src/test_certfuzz/minimizer/test_win_minimizer.py diff --git a/src/certfuzz/test/misc.py b/src/test_certfuzz/misc.py similarity index 100% rename from src/certfuzz/test/misc.py rename to src/test_certfuzz/misc.py diff --git a/src/certfuzz/test/mocks.py b/src/test_certfuzz/mocks.py similarity index 100% rename from src/certfuzz/test/mocks.py rename to src/test_certfuzz/mocks.py diff --git a/src/certfuzz/test/runners/__init__.py b/src/test_certfuzz/runners/__init__.py similarity index 100% rename from src/certfuzz/test/runners/__init__.py rename to src/test_certfuzz/runners/__init__.py diff --git a/src/certfuzz/test/runners/test_killableprocess.py b/src/test_certfuzz/runners/test_killableprocess.py similarity index 100% rename from src/certfuzz/test/runners/test_killableprocess.py rename to src/test_certfuzz/runners/test_killableprocess.py diff --git a/src/certfuzz/test/runners/test_qijo.py b/src/test_certfuzz/runners/test_qijo.py similarity index 100% rename from src/certfuzz/test/runners/test_qijo.py rename to src/test_certfuzz/runners/test_qijo.py diff --git a/src/certfuzz/test/runners/test_runner_base.py b/src/test_certfuzz/runners/test_runner_base.py similarity index 100% rename from src/certfuzz/test/runners/test_runner_base.py rename to src/test_certfuzz/runners/test_runner_base.py diff --git a/src/certfuzz/test/runners/test_winprocess.py b/src/test_certfuzz/runners/test_winprocess.py similarity index 100% rename from src/certfuzz/test/runners/test_winprocess.py rename to src/test_certfuzz/runners/test_winprocess.py diff --git a/src/certfuzz/test/runners/test_winrun.py b/src/test_certfuzz/runners/test_winrun.py similarity index 100% rename from src/certfuzz/test/runners/test_winrun.py rename to src/test_certfuzz/runners/test_winrun.py diff --git a/src/certfuzz/test/runners/test_zzufrun.py b/src/test_certfuzz/runners/test_zzufrun.py similarity index 100% rename from src/certfuzz/test/runners/test_zzufrun.py rename to src/test_certfuzz/runners/test_zzufrun.py diff --git a/src/certfuzz/test/scoring/__init__.py b/src/test_certfuzz/scoring/__init__.py similarity index 100% rename from src/certfuzz/test/scoring/__init__.py rename to src/test_certfuzz/scoring/__init__.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/__init__.py b/src/test_certfuzz/scoring/multiarmed_bandit/__init__.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/__init__.py rename to src/test_certfuzz/scoring/multiarmed_bandit/__init__.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/arms/__init__.py b/src/test_certfuzz/scoring/multiarmed_bandit/arms/__init__.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/arms/__init__.py rename to src/test_certfuzz/scoring/multiarmed_bandit/arms/__init__.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_base.py b/src/test_certfuzz/scoring/multiarmed_bandit/arms/test_base.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/arms/test_base.py rename to src/test_certfuzz/scoring/multiarmed_bandit/arms/test_base.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py b/src/test_certfuzz/scoring/multiarmed_bandit/arms/test_bayes_laplace.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/arms/test_bayes_laplace.py rename to src/test_certfuzz/scoring/multiarmed_bandit/arms/test_bayes_laplace.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py b/src/test_certfuzz/scoring/multiarmed_bandit/test_bayesian_bandit.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/test_bayesian_bandit.py rename to src/test_certfuzz/scoring/multiarmed_bandit/test_bayesian_bandit.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py b/src/test_certfuzz/scoring/multiarmed_bandit/test_e_greedy_bandit.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/test_e_greedy_bandit.py rename to src/test_certfuzz/scoring/multiarmed_bandit/test_e_greedy_bandit.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_errors.py b/src/test_certfuzz/scoring/multiarmed_bandit/test_errors.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/test_errors.py rename to src/test_certfuzz/scoring/multiarmed_bandit/test_errors.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py b/src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py rename to src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py b/src/test_certfuzz/scoring/multiarmed_bandit/test_random_bandit.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/test_random_bandit.py rename to src/test_certfuzz/scoring/multiarmed_bandit/test_random_bandit.py diff --git a/src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py b/src/test_certfuzz/scoring/multiarmed_bandit/test_round_robin_bandit.py similarity index 100% rename from src/certfuzz/test/scoring/multiarmed_bandit/test_round_robin_bandit.py rename to src/test_certfuzz/scoring/multiarmed_bandit/test_round_robin_bandit.py diff --git a/src/certfuzz/test/test_meta.py b/src/test_certfuzz/test_meta.py similarity index 100% rename from src/certfuzz/test/test_meta.py rename to src/test_certfuzz/test_meta.py diff --git a/src/certfuzz/test/test_version.py b/src/test_certfuzz/test_version.py similarity index 100% rename from src/certfuzz/test/test_version.py rename to src/test_certfuzz/test_version.py diff --git a/src/certfuzz/test/testcase_pipeline/__init__.py b/src/test_certfuzz/testcase_pipeline/__init__.py similarity index 100% rename from src/certfuzz/test/testcase_pipeline/__init__.py rename to src/test_certfuzz/testcase_pipeline/__init__.py diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py similarity index 100% rename from src/certfuzz/test/testcase_pipeline/test_tc_pipeline_base.py rename to src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_linux.py b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_linux.py similarity index 100% rename from src/certfuzz/test/testcase_pipeline/test_tc_pipeline_linux.py rename to src/test_certfuzz/testcase_pipeline/test_tc_pipeline_linux.py diff --git a/src/certfuzz/test/testcase_pipeline/test_tc_pipeline_windows.py b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_windows.py similarity index 100% rename from src/certfuzz/test/testcase_pipeline/test_tc_pipeline_windows.py rename to src/test_certfuzz/testcase_pipeline/test_tc_pipeline_windows.py diff --git a/src/certfuzz/test/tools/__init__.py b/src/test_certfuzz/tools/__init__.py similarity index 100% rename from src/certfuzz/test/tools/__init__.py rename to src/test_certfuzz/tools/__init__.py diff --git a/src/certfuzz/test/tools/common/__init__.py b/src/test_certfuzz/tools/common/__init__.py similarity index 100% rename from src/certfuzz/test/tools/common/__init__.py rename to src/test_certfuzz/tools/common/__init__.py diff --git a/src/certfuzz/test/tools/common/test_mtsp_enum.py b/src/test_certfuzz/tools/common/test_mtsp_enum.py similarity index 100% rename from src/certfuzz/test/tools/common/test_mtsp_enum.py rename to src/test_certfuzz/tools/common/test_mtsp_enum.py diff --git a/src/certfuzz/test/tools/common/test_zipdiff.py b/src/test_certfuzz/tools/common/test_zipdiff.py similarity index 100% rename from src/certfuzz/test/tools/common/test_zipdiff.py rename to src/test_certfuzz/tools/common/test_zipdiff.py diff --git a/src/certfuzz/test/tools/linux/__init__.py b/src/test_certfuzz/tools/linux/__init__.py similarity index 100% rename from src/certfuzz/test/tools/linux/__init__.py rename to src/test_certfuzz/tools/linux/__init__.py diff --git a/src/certfuzz/test/tools/linux/test_bff_stats.py b/src/test_certfuzz/tools/linux/test_bff_stats.py similarity index 100% rename from src/certfuzz/test/tools/linux/test_bff_stats.py rename to src/test_certfuzz/tools/linux/test_bff_stats.py diff --git a/src/certfuzz/test/tools/linux/test_callsim.py b/src/test_certfuzz/tools/linux/test_callsim.py similarity index 100% rename from src/certfuzz/test/tools/linux/test_callsim.py rename to src/test_certfuzz/tools/linux/test_callsim.py diff --git a/src/certfuzz/test/tools/linux/test_create_crasher_script.py b/src/test_certfuzz/tools/linux/test_create_crasher_script.py similarity index 100% rename from src/certfuzz/test/tools/linux/test_create_crasher_script.py rename to src/test_certfuzz/tools/linux/test_create_crasher_script.py diff --git a/src/certfuzz/test/tools/linux/test_debuggerfile.py b/src/test_certfuzz/tools/linux/test_debuggerfile.py similarity index 100% rename from src/certfuzz/test/tools/linux/test_debuggerfile.py rename to src/test_certfuzz/tools/linux/test_debuggerfile.py diff --git a/src/certfuzz/test/tools/linux/test_minimize.py b/src/test_certfuzz/tools/linux/test_minimize.py similarity index 100% rename from src/certfuzz/test/tools/linux/test_minimize.py rename to src/test_certfuzz/tools/linux/test_minimize.py diff --git a/src/certfuzz/test/tools/linux/test_minimizer_plot.py b/src/test_certfuzz/tools/linux/test_minimizer_plot.py similarity index 100% rename from src/certfuzz/test/tools/linux/test_minimizer_plot.py rename to src/test_certfuzz/tools/linux/test_minimizer_plot.py diff --git a/src/certfuzz/test/tools/linux/test_repro.py b/src/test_certfuzz/tools/linux/test_repro.py similarity index 100% rename from src/certfuzz/test/tools/linux/test_repro.py rename to src/test_certfuzz/tools/linux/test_repro.py diff --git a/src/certfuzz/test/tools/windows/__init__.py b/src/test_certfuzz/tools/windows/__init__.py similarity index 100% rename from src/certfuzz/test/tools/windows/__init__.py rename to src/test_certfuzz/tools/windows/__init__.py diff --git a/src/certfuzz/test/tools/windows/test_clean_windows.py b/src/test_certfuzz/tools/windows/test_clean_windows.py similarity index 100% rename from src/certfuzz/test/tools/windows/test_clean_windows.py rename to src/test_certfuzz/tools/windows/test_clean_windows.py diff --git a/src/certfuzz/test/tools/windows/test_copycrashers.py b/src/test_certfuzz/tools/windows/test_copycrashers.py similarity index 100% rename from src/certfuzz/test/tools/windows/test_copycrashers.py rename to src/test_certfuzz/tools/windows/test_copycrashers.py diff --git a/src/certfuzz/test/tools/windows/test_minimize.py b/src/test_certfuzz/tools/windows/test_minimize.py similarity index 100% rename from src/certfuzz/test/tools/windows/test_minimize.py rename to src/test_certfuzz/tools/windows/test_minimize.py diff --git a/src/certfuzz/test/tools/windows/test_quickstats.py b/src/test_certfuzz/tools/windows/test_quickstats.py similarity index 100% rename from src/certfuzz/test/tools/windows/test_quickstats.py rename to src/test_certfuzz/tools/windows/test_quickstats.py diff --git a/src/certfuzz/test/tools/windows/test_repro.py b/src/test_certfuzz/tools/windows/test_repro.py similarity index 100% rename from src/certfuzz/test/tools/windows/test_repro.py rename to src/test_certfuzz/tools/windows/test_repro.py From 8e383a20abc8d01183663510332e58da3bc531d6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 14:15:02 -0500 Subject: [PATCH 0690/1169] fix imports --- src/test_certfuzz/crash/test_bff_crash.py | 2 +- src/test_certfuzz/fuzzers/test_bitmut.py | 2 +- src/test_certfuzz/fuzzers/test_bytemut.py | 2 +- src/test_certfuzz/fuzzers/test_crlfmut.py | 2 +- src/test_certfuzz/fuzzers/test_crmut.py | 2 +- src/test_certfuzz/fuzzers/test_drop.py | 2 +- src/test_certfuzz/fuzzers/test_fuzzer_base.py | 2 +- src/test_certfuzz/fuzzers/test_insert.py | 2 +- src/test_certfuzz/fuzzers/test_nullmut.py | 2 +- src/test_certfuzz/fuzzers/test_swap.py | 2 +- src/test_certfuzz/fuzzers/test_truncate.py | 2 +- src/test_certfuzz/fuzzers/test_wave.py | 2 +- src/test_certfuzz/fuzzers/test_zzuf.py | 2 +- src/test_certfuzz/helpers/test_helpers_pkg.py | 2 +- src/test_certfuzz/iteration/test_iteration_windows.py | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/test_certfuzz/crash/test_bff_crash.py b/src/test_certfuzz/crash/test_bff_crash.py index da8cb5c..77fb5f4 100644 --- a/src/test_certfuzz/crash/test_bff_crash.py +++ b/src/test_certfuzz/crash/test_bff_crash.py @@ -6,7 +6,7 @@ import unittest # from certfuzz.crash.bff_crash import Crash -from certfuzz.test.mocks import Mock +from test_certfuzz.mocks import Mock import tempfile import os import shutil diff --git a/src/test_certfuzz/fuzzers/test_bitmut.py b/src/test_certfuzz/fuzzers/test_bitmut.py index 42da10f..5738c3e 100644 --- a/src/test_certfuzz/fuzzers/test_bitmut.py +++ b/src/test_certfuzz/fuzzers/test_bitmut.py @@ -6,7 +6,7 @@ import unittest import tempfile import os -from certfuzz.test.mocks import MockSeedfile +from test_certfuzz.mocks import MockSeedfile import shutil from certfuzz.fuzzers.bitmut import BitMutFuzzer diff --git a/src/test_certfuzz/fuzzers/test_bytemut.py b/src/test_certfuzz/fuzzers/test_bytemut.py index e6c1c4c..72e961e 100644 --- a/src/test_certfuzz/fuzzers/test_bytemut.py +++ b/src/test_certfuzz/fuzzers/test_bytemut.py @@ -9,7 +9,7 @@ import shutil from certfuzz.fuzzers.bytemut import fuzz, _fuzzable from certfuzz.fuzzers.bytemut import ByteMutFuzzer -from certfuzz.test.mocks import MockSeedfile, MockRange +from test_certfuzz.mocks import MockSeedfile, MockRange import tempfile from certfuzz.fuzztools.hamming import bytewise_hd diff --git a/src/test_certfuzz/fuzzers/test_crlfmut.py b/src/test_certfuzz/fuzzers/test_crlfmut.py index ea986a6..838b126 100644 --- a/src/test_certfuzz/fuzzers/test_crlfmut.py +++ b/src/test_certfuzz/fuzzers/test_crlfmut.py @@ -9,7 +9,7 @@ import shutil from certfuzz.fuzzers.bytemut import fuzz from certfuzz.fuzzers.crlfmut import CRLFMutFuzzer -from certfuzz.test.mocks import MockSeedfile, MockRange +from test_certfuzz.mocks import MockSeedfile, MockRange import tempfile from certfuzz.fuzztools.hamming import bytewise_hd import copy diff --git a/src/test_certfuzz/fuzzers/test_crmut.py b/src/test_certfuzz/fuzzers/test_crmut.py index 7f246e0..c2f2146 100644 --- a/src/test_certfuzz/fuzzers/test_crmut.py +++ b/src/test_certfuzz/fuzzers/test_crmut.py @@ -9,7 +9,7 @@ import shutil from certfuzz.fuzzers.bytemut import fuzz from certfuzz.fuzzers.crmut import CRMutFuzzer -from certfuzz.test.mocks import MockSeedfile, MockRange +from test_certfuzz.mocks import MockSeedfile, MockRange import tempfile from certfuzz.fuzztools.hamming import bytewise_hd import copy diff --git a/src/test_certfuzz/fuzzers/test_drop.py b/src/test_certfuzz/fuzzers/test_drop.py index d243c41..2de049d 100644 --- a/src/test_certfuzz/fuzzers/test_drop.py +++ b/src/test_certfuzz/fuzzers/test_drop.py @@ -10,7 +10,7 @@ import shutil from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging -from certfuzz.test.mocks import MockSeedfile +from test_certfuzz.mocks import MockSeedfile import tempfile certfuzz.fuzzers.drop.logger.setLevel(logging.WARNING) diff --git a/src/test_certfuzz/fuzzers/test_fuzzer_base.py b/src/test_certfuzz/fuzzers/test_fuzzer_base.py index a02aff8..2ce25d3 100644 --- a/src/test_certfuzz/fuzzers/test_fuzzer_base.py +++ b/src/test_certfuzz/fuzzers/test_fuzzer_base.py @@ -7,7 +7,7 @@ import unittest import os from certfuzz.fuzzers.fuzzer_base import Fuzzer -from certfuzz.test.mocks import MockSeedfile +from test_certfuzz.mocks import MockSeedfile import shutil from certfuzz.fuzzers.fuzzer_base import MinimizableFuzzer import tempfile diff --git a/src/test_certfuzz/fuzzers/test_insert.py b/src/test_certfuzz/fuzzers/test_insert.py index 9b1c6df..a5814ff 100644 --- a/src/test_certfuzz/fuzzers/test_insert.py +++ b/src/test_certfuzz/fuzzers/test_insert.py @@ -10,7 +10,7 @@ import shutil from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging -from certfuzz.test.mocks import MockSeedfile +from test_certfuzz.mocks import MockSeedfile import tempfile certfuzz.fuzzers.insert.logger.setLevel(logging.WARNING) diff --git a/src/test_certfuzz/fuzzers/test_nullmut.py b/src/test_certfuzz/fuzzers/test_nullmut.py index 6ef244d..133f6ea 100644 --- a/src/test_certfuzz/fuzzers/test_nullmut.py +++ b/src/test_certfuzz/fuzzers/test_nullmut.py @@ -8,7 +8,7 @@ import os import shutil from certfuzz.fuzzers.nullmut import NullMutFuzzer -from certfuzz.test.mocks import MockSeedfile, MockRange +from test_certfuzz.mocks import MockSeedfile, MockRange import tempfile from certfuzz.fuzztools.hamming import bytewise_hd diff --git a/src/test_certfuzz/fuzzers/test_swap.py b/src/test_certfuzz/fuzzers/test_swap.py index 88acecc..92501b1 100644 --- a/src/test_certfuzz/fuzzers/test_swap.py +++ b/src/test_certfuzz/fuzzers/test_swap.py @@ -5,7 +5,7 @@ ''' import unittest from certfuzz.fuzzers.swap import SwapFuzzer -from certfuzz.test.mocks import MockSeedfile +from test_certfuzz.mocks import MockSeedfile from certfuzz.fuzzers.errors import FuzzerExhaustedError import shutil import tempfile diff --git a/src/test_certfuzz/fuzzers/test_truncate.py b/src/test_certfuzz/fuzzers/test_truncate.py index ccb4d58..8b7b192 100644 --- a/src/test_certfuzz/fuzzers/test_truncate.py +++ b/src/test_certfuzz/fuzzers/test_truncate.py @@ -10,7 +10,7 @@ import shutil from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging -from certfuzz.test.mocks import MockSeedfile +from test_certfuzz.mocks import MockSeedfile import tempfile certfuzz.fuzzers.drop.logger.setLevel(logging.WARNING) diff --git a/src/test_certfuzz/fuzzers/test_wave.py b/src/test_certfuzz/fuzzers/test_wave.py index c3a0070..8e25be1 100644 --- a/src/test_certfuzz/fuzzers/test_wave.py +++ b/src/test_certfuzz/fuzzers/test_wave.py @@ -10,7 +10,7 @@ import shutil from certfuzz.fuzzers.errors import FuzzerExhaustedError import logging -from certfuzz.test.mocks import MockSeedfile +from test_certfuzz.mocks import MockSeedfile import tempfile certfuzz.fuzzers.wave.logger.setLevel(logging.WARNING) diff --git a/src/test_certfuzz/fuzzers/test_zzuf.py b/src/test_certfuzz/fuzzers/test_zzuf.py index 9fef2d2..e7dced4 100644 --- a/src/test_certfuzz/fuzzers/test_zzuf.py +++ b/src/test_certfuzz/fuzzers/test_zzuf.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz.test.mocks import MockSeedfile +from test_certfuzz.mocks import MockSeedfile import tempfile import shutil from certfuzz.fuzzers.zzuf import ZzufFuzzer diff --git a/src/test_certfuzz/helpers/test_helpers_pkg.py b/src/test_certfuzz/helpers/test_helpers_pkg.py index a4b1228..c69be1d 100644 --- a/src/test_certfuzz/helpers/test_helpers_pkg.py +++ b/src/test_certfuzz/helpers/test_helpers_pkg.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz.test import misc +from test_certfuzz import misc import certfuzz.helpers class Test(unittest.TestCase): diff --git a/src/test_certfuzz/iteration/test_iteration_windows.py b/src/test_certfuzz/iteration/test_iteration_windows.py index 4769173..79fa402 100644 --- a/src/test_certfuzz/iteration/test_iteration_windows.py +++ b/src/test_certfuzz/iteration/test_iteration_windows.py @@ -6,7 +6,7 @@ import unittest from certfuzz.iteration.iteration_windows import WindowsIteration -from certfuzz.test.mocks import MockFuzzer +from test_certfuzz.mocks import MockFuzzer class Test(unittest.TestCase): From 52c0685ad6c1bf438458d805b9e5c2d81498f4f1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 11 Jan 2016 14:33:19 -0500 Subject: [PATCH 0691/1169] fix meta-test --- src/test_certfuzz/test_meta.py | 37 +++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/test_certfuzz/test_meta.py b/src/test_certfuzz/test_meta.py index 9dd91c5..4a455d5 100644 --- a/src/test_certfuzz/test_meta.py +++ b/src/test_certfuzz/test_meta.py @@ -8,7 +8,10 @@ import os from certfuzz.fuzztools import filetools -basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'certfuzz')) +test_basedir = os.path.dirname(__file__) +src_basedir = os.path.abspath(os.path.join(test_basedir, '..')) +certfuzz_basedir = os.path.join(src_basedir, 'certfuzz') + ignorelist = ['obsolete', 'dist'] @@ -48,28 +51,30 @@ def find_modules(d): class Test(unittest.TestCase): def setUp(self): - self.basedir = basedir + pass def tearDown(self): pass def test_each_package_has_a_test_package(self): - package_list = find_packages(self.basedir) + package_list = [os.path.relpath(x, certfuzz_basedir) for x in find_packages(certfuzz_basedir)] missing_pkgs = [] non_pkgs = [] - for pkg in non_tst_packages(self.basedir): - relpath = os.path.relpath(pkg, basedir) - test_pkg = os.path.join(basedir, 'test', relpath) + for pkg in non_tst_packages(certfuzz_basedir): + relpath = os.path.relpath(pkg, certfuzz_basedir) + test_pkg = os.path.join(test_basedir, relpath) if not os.path.exists(test_pkg): missing_pkgs.append(test_pkg) - if not test_pkg in package_list: + + pkgrelpath = os.path.relpath(test_pkg, test_basedir) + if pkgrelpath not in package_list: non_pkgs.append(test_pkg) self.assertFalse(missing_pkgs, 'Missing test packages:\n %s' % '\n '.join(missing_pkgs)) self.assertFalse(non_pkgs, 'Not a package:\n %s' % '\n '.join(non_pkgs)) def test_each_module_has_a_test_module(self): - module_list = find_modules(self.basedir) + module_list = find_modules(certfuzz_basedir) missing_modules = [] _ignored_modules = set(['errors.py']) for m in module_list: @@ -79,17 +84,17 @@ def test_each_module_has_a_test_module(self): continue test_b = 'test_%s' % b - relpath = os.path.relpath(d, basedir) - test_path = os.path.join(basedir, 'test', relpath, test_b) + relpath = os.path.relpath(d, certfuzz_basedir) + test_path = os.path.join(test_basedir, relpath, test_b) if not os.path.exists(test_path): - missing_modules.append((os.path.relpath(m, basedir), os.path.relpath(test_path, basedir))) + missing_modules.append((os.path.relpath(m, certfuzz_basedir), os.path.relpath(test_path, certfuzz_basedir))) fail_lines = ['Module %s has no corresponding test module %s' % mm for mm in missing_modules] fail_string = '\n '.join(fail_lines) self.assertFalse(missing_modules, fail_string) def test_each_nonempty_init_module_has_a_test_module(self): - module_list = filetools.all_files(self.basedir, '__init__.py') + module_list = filetools.all_files(certfuzz_basedir, '__init__.py') nonempty_mods = [x for x in module_list if os.path.getsize(x)] missing_mods = [] @@ -102,15 +107,15 @@ def test_each_nonempty_init_module_has_a_test_module(self): test_base = os.path.basename(d) test_b = 'test_%s_pkg.py' % test_base - relpath = os.path.relpath(d, basedir) + relpath = os.path.relpath(d, certfuzz_basedir) - test_path = os.path.join(basedir, 'test', relpath, test_b) + test_path = os.path.join(test_basedir, relpath, test_b) if not os.path.exists(test_path): - missing_mods.append((os.path.relpath(d, basedir), os.path.relpath(test_path, basedir))) + missing_mods.append((os.path.relpath(d, certfuzz_basedir), os.path.relpath(test_path, certfuzz_basedir))) fail_lines = ['Package %s has no corresponding test module %s' % mm for mm in missing_mods] fail_string = '\n'.join(fail_lines) self.assertFalse(missing_mods, fail_string) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 3cbbdc2277feb7d4d3dee73091f411f3a3eccffe Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 12 Jan 2016 08:47:33 -0500 Subject: [PATCH 0692/1169] update code metrics script --- build/scripts/code_metrics.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/scripts/code_metrics.sh b/build/scripts/code_metrics.sh index 00938a4..2635f0c 100755 --- a/build/scripts/code_metrics.sh +++ b/build/scripts/code_metrics.sh @@ -13,5 +13,5 @@ export PYTHONPATH=./src:$PYTHONPATH /usr/local/bin/nosetests-2.7 --with-xunit --with-xcoverage \ --cover-inclusive --cover-package=src --cover-erase \ --cover-branches -e test_probability \ - src/certfuzz/test src/linux/test src/windows/test + src/test_certfuzz src/linux/test src/windows/test From 852c9eee3b25260af73b84bac5cce005e3208d16 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 12 Jan 2016 10:18:44 -0500 Subject: [PATCH 0693/1169] debuggers.registration was removed in e3dd4591d1e53b375fdcbe06fa160a7898eac90e -- missed removing it from windows standalone minimizer --- src/certfuzz/tools/windows/minimize.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 38f7bb6..fe2f3fd 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -52,8 +52,6 @@ class DummyCfg(object): def main(): - debuggers.registration.verify_supported_platform() - from optparse import OptionParser hdlr = logging.StreamHandler() From 6a07a3e2d47bec20d6e8eb54e315664e6e8eec7c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 12 Jan 2016 14:52:38 -0500 Subject: [PATCH 0694/1169] remove unused imports --- src/certfuzz/campaign/campaign_linux.py | 1 - src/certfuzz/iteration/iteration_linux.py | 2 -- src/certfuzz/testcase_pipeline/tc_pipeline_base.py | 2 -- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 2 -- 4 files changed, 7 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 1297d85..d1b68ac 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -21,7 +21,6 @@ from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.iteration.iteration_linux import LinuxIteration -from certfuzz.fuzzers.zzuf import ZzufFuzzer from certfuzz.fuzzers.bytemut import ByteMutFuzzer from certfuzz.runners.zzufrun import ZzufRunner diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index f92711e..e2decd1 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -12,8 +12,6 @@ from certfuzz.fuzztools.zzuflog import ZzufLog from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.testcase_pipeline.tc_pipeline_linux import LinuxTestCasePipeline -from certfuzz.runners.zzufrun import ZzufRunner -from certfuzz.fuzzers.bytemut import ByteMutFuzzer logger = logging.getLogger(__name__) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index 80133a9..d505890 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -7,13 +7,11 @@ import abc import logging import os -import shutil from certfuzz.analyzers.errors import AnalyzerEmptyOutputError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file from certfuzz.fuzztools import filetools from certfuzz.helpers.coroutine import coroutine -from certfuzz.testcase_pipeline.errors import TestCasePipelineError from certfuzz.file_handlers.tmp_reaper import TmpReaper diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 8004ed3..f1157d9 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -20,8 +20,6 @@ from certfuzz.minimizer.errors import MinimizerError from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase -from certfuzz.testcase_pipeline.errors import TestCasePipelineError -import shutil logger = logging.getLogger(__name__) From b80cd0a13c55eecd218a1e092940611ee08cd574 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 12 Jan 2016 15:31:59 -0500 Subject: [PATCH 0695/1169] create certfuzz.reporters package with basic copy file reporter --- src/certfuzz/reporters/__init__.py | 0 src/certfuzz/reporters/copy_files.py | 47 +++++++++++++++++++++++++ src/certfuzz/reporters/errors.py | 10 ++++++ src/certfuzz/reporters/reporter_base.py | 28 +++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 src/certfuzz/reporters/__init__.py create mode 100644 src/certfuzz/reporters/copy_files.py create mode 100644 src/certfuzz/reporters/errors.py create mode 100644 src/certfuzz/reporters/reporter_base.py diff --git a/src/certfuzz/reporters/__init__.py b/src/certfuzz/reporters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/certfuzz/reporters/copy_files.py b/src/certfuzz/reporters/copy_files.py new file mode 100644 index 0000000..610b5be --- /dev/null +++ b/src/certfuzz/reporters/copy_files.py @@ -0,0 +1,47 @@ +''' +Created on Jan 12, 2016 + +@author: adh +''' +import logging +import os +import shutil + +from certfuzz.fuzztools import filetools +from certfuzz.reporters.errors import ReporterError +from certfuzz.reporters.reporter_base import ReporterBase + + +logger = logging.getLogger(__name__) + + +class CopyFilesReporter(ReporterBase): + ''' + Copies files to a location + ''' + + def __init__(self, testcase, target_dir): + ''' + Constructor + ''' + ReporterBase.__init__(self, testcase) + + self.target_dir = target_dir + + def _copy_files(self): + dst_dir = os.path.join(self.target_dir, self.testcase.signature) + # ensure target dir exists already (it might because of crash logging) + filetools.mkdir_p(dst_dir) + + src_dir = self.testcase.tempdir + if not os.path.exists(src_dir): + raise ReporterError('Testcase tempdir not found: %s', src_dir) + + src_paths = [os.path.join(src_dir, f) for f in os.listdir(src_dir)] + + for f in src_paths: + logger.debug('Copy %s -> %s', f, dst_dir) + shutil.copy2(f, dst_dir) + + def go(self): + self._copy_files() \ No newline at end of file diff --git a/src/certfuzz/reporters/errors.py b/src/certfuzz/reporters/errors.py new file mode 100644 index 0000000..9ffa085 --- /dev/null +++ b/src/certfuzz/reporters/errors.py @@ -0,0 +1,10 @@ +''' +Created on Jan 12, 2016 + +@author: adh +''' +from certfuzz.errors import CERTFuzzError + + +class ReporterError(CERTFuzzError): + pass diff --git a/src/certfuzz/reporters/reporter_base.py b/src/certfuzz/reporters/reporter_base.py new file mode 100644 index 0000000..5828df3 --- /dev/null +++ b/src/certfuzz/reporters/reporter_base.py @@ -0,0 +1,28 @@ +''' +Created on Jan 12, 2016 + +@author: adh +''' +import abc + + +class ReporterBase(object): + ''' + A BFF Reporter class + ''' + + def __init__(self, testcase): + ''' + Constructor + ''' + self.testcase = testcase + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + pass + + @abc.abstractmethod + def go(self): + pass From 883dff6d6e9e5d9642263e362bab8c273e19d1a1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 12 Jan 2016 15:32:37 -0500 Subject: [PATCH 0696/1169] pep8 cleanup --- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index f1157d9..fedd779 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -143,7 +143,7 @@ def _pre_report(self, testcase): else: uniqlogger.info('%s crash_id=%s seed=%d bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.hd_bits, testcase.hd_bytes) else: - # We don't know the HD info + # We don't know the HD info if testcase.range is not None: # We have a fuzzer that uses a range uniqlogger.info('%s crash_id=%s seed=%d range=%s', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range) From 13ca7b0aa647b8174ece6dee03e437c26bf34411 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 12 Jan 2016 15:33:12 -0500 Subject: [PATCH 0697/1169] refactor pipeline to use certfuzz.reporter module instead of _copy_file() method --- .../testcase_pipeline/tc_pipeline_base.py | 15 --------------- .../testcase_pipeline/tc_pipeline_linux.py | 5 ++++- .../testcase_pipeline/tc_pipeline_windows.py | 5 ++++- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py index d505890..c5752e7 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_base.py @@ -231,18 +231,3 @@ def go(self): testcase = self.tc_candidate_q.get() logger.debug('Sending testcase %s to pipeline', testcase.signature) self.analysis_pipeline.send(testcase) - - def _copy_files(self, testcase): - dst_dir = os.path.join(self.tc_dir, testcase.signature) - # ensure target dir exists already (it might because of crash logging) - filetools.mkdir_p(dst_dir) - - src_dir = testcase.tempdir - if not os.path.exists(src_dir): - raise TestCasePipelineError('Testcase tempdir not found: %s', src_dir) - - src_paths = [os.path.join(src_dir, f) for f in os.listdir(src_dir)] - - for f in src_paths: - logger.debug('Copy %s -> %s', f, dst_dir) - shutil.copy2(f, dst_dir) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index fedd779..87a620c 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -20,6 +20,7 @@ from certfuzz.minimizer.errors import MinimizerError from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase +from certfuzz.reporters.copy_files import CopyFilesReporter logger = logging.getLogger(__name__) @@ -152,7 +153,9 @@ def _pre_report(self, testcase): logger.info('%s first seen at %d', testcase.signature, testcase.seednum) def _report(self, testcase): - self._copy_files(testcase) + with CopyFilesReporter(testcase, self.tc_dir) as reporter: + reporter.go() + # whether it was unique or not, record some details for posterity # record the details of this crash so we can regenerate it later if needed testcase.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', testcase.seedfile.basename, testcase.seednum, testcase.range, testcase.fuzzedfile.path) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 097a9e8..8041a23 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -11,6 +11,8 @@ from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.fuzztools import filetools from certfuzz.minimizer.errors import MinimizerError +from certfuzz.reporters.copy_files import CopyFilesReporter +from coverage.report import Reporter logger = logging.getLogger(__name__) @@ -82,7 +84,8 @@ def _analyze(self, testcase): pass def _report(self, testcase): - self._copy_files(testcase) + with CopyFilesReporter(testcase, self.tc_dir) as reporter: + reporter.go() def keep_testcase(self, testcase): '''Given a testcase, decide whether it is a keeper. Returns a tuple From e7221f44dffba4546fb62941fca562853c00b671 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 12 Jan 2016 15:56:28 -0500 Subject: [PATCH 0698/1169] add unit tests for copy files reporter --- src/test_certfuzz/mocks.py | 3 ++ src/test_certfuzz/reporters/__init__.py | 0 .../reporters/test_copy_files.py | 53 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/test_certfuzz/reporters/__init__.py create mode 100644 src/test_certfuzz/reporters/test_copy_files.py diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 1a9b70c..83c2f6a 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -32,3 +32,6 @@ def read(self): class MockFuzzer(Mock): is_minimizable = False + +class MockTestcase(Mock): + signature = 'ABCDEFGHIJK' \ No newline at end of file diff --git a/src/test_certfuzz/reporters/__init__.py b/src/test_certfuzz/reporters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/test_certfuzz/reporters/test_copy_files.py b/src/test_certfuzz/reporters/test_copy_files.py new file mode 100644 index 0000000..d1f9106 --- /dev/null +++ b/src/test_certfuzz/reporters/test_copy_files.py @@ -0,0 +1,53 @@ +''' +Created on Jan 12, 2016 + +@author: adh +''' +import unittest +from certfuzz.reporters import copy_files +import tempfile +import shutil +from test_certfuzz.mocks import MockTestcase +import os + + +class Test(unittest.TestCase): + + + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + + def testCopyFiles(self): + tc = MockTestcase() + tc.tempdir = tempfile.mkdtemp(dir=self.tmpdir) + fh, tcfile = tempfile.mkstemp(dir=tc.tempdir) + os.close(fh) + + target_dir = tempfile.mkdtemp(dir=self.tmpdir) + + # target dir is empty + self.assertEqual([], os.listdir(target_dir)) + + _path, fname = os.path.split(tcfile) + + # source dir has only one file + self.assertEqual([fname], os.listdir(tc.tempdir)) + + r = copy_files.CopyFilesReporter(tc, target_dir) + with r: + r.go() + + # target dir should contain an outdir with the file we copied + outdir = os.path.join(target_dir, tc.signature) + self.assertTrue(os.path.exists(outdir)) + self.assertEqual([fname], os.listdir(outdir)) + + + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 3f524471aa7b42f0195fe70d0a0a81cb044f8a2b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 09:53:38 -0500 Subject: [PATCH 0699/1169] add test module for reporter_base, clean up tc_pipeline_base tests --- .../reporters/test_reporter_base.py | 37 +++++++++++++++++++ .../test_tc_pipeline_base.py | 23 ------------ 2 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 src/test_certfuzz/reporters/test_reporter_base.py diff --git a/src/test_certfuzz/reporters/test_reporter_base.py b/src/test_certfuzz/reporters/test_reporter_base.py new file mode 100644 index 0000000..0970975 --- /dev/null +++ b/src/test_certfuzz/reporters/test_reporter_base.py @@ -0,0 +1,37 @@ +''' +Created on Jan 13, 2016 + +@author: adh +''' +import unittest +import certfuzz.reporters.reporter_base +from certfuzz.reporters.reporter_base import ReporterBase + +class Test(unittest.TestCase): + + + def setUp(self): + pass + + + def tearDown(self): + pass + + + def testAbcMethods(self): + self.assertRaises(TypeError, ReporterBase) + + class Fail(ReporterBase): + pass + + self.assertRaises(TypeError, Fail) + + class Pass(ReporterBase): + def go(self): + pass + + Pass('dummytc') + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py index 528960f..f337bfe 100644 --- a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py +++ b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py @@ -80,29 +80,6 @@ class MockTestCase(object): self.assertEqual(i + 1, len(results)) self.assertEqual(tc, results[-1]) - def test_copy_files(self): - tcpl = TCPL_Impl(outdir=self.tmpdir) - - class MockTestCase(object): - result_dir = tempfile.mkdtemp(prefix="tc_out_", dir=self.tmpdir) - signature = "tcsignature" - tempdir = tempfile.mkdtemp(prefix="tc_in_", dir=self.tmpdir) - - tc = MockTestCase() - fd, fname = tempfile.mkstemp(dir=tc.result_dir) - os.write(fd, fname) - os.close(fd) - - self.assertEqual([os.path.basename(fname)], - os.listdir(tc.result_dir)) - tcpl.outdir = tempfile.mkdtemp(dir=self.tmpdir) - - self.assertEqual([], - os.listdir(tcpl.outdir)) - tcpl._copy_files(tc) - self.assertEqual([os.path.basename(fname)], - os.listdir(tc.result_dir)) - def test_analyze(self): class TCPL_Impl2(TCPL_Impl): def _analyze(self, testcase): From 10b994634c03b720bc50593e35e7f9bd9cfecb92 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 09:54:38 -0500 Subject: [PATCH 0700/1169] add abstract base class to reporter_base --- src/certfuzz/reporters/reporter_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/reporters/reporter_base.py b/src/certfuzz/reporters/reporter_base.py index 5828df3..3e4164a 100644 --- a/src/certfuzz/reporters/reporter_base.py +++ b/src/certfuzz/reporters/reporter_base.py @@ -5,11 +5,11 @@ ''' import abc - class ReporterBase(object): ''' A BFF Reporter class ''' + __metaclass__ = abc.ABCMeta def __init__(self, testcase): ''' From 9b8e2233eff02d55d5f6cb6ee8520892cf9555ad Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 10:42:20 -0500 Subject: [PATCH 0701/1169] minor refactor of copy_files reporter --- src/certfuzz/reporters/copy_files.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/certfuzz/reporters/copy_files.py b/src/certfuzz/reporters/copy_files.py index 610b5be..780a37a 100644 --- a/src/certfuzz/reporters/copy_files.py +++ b/src/certfuzz/reporters/copy_files.py @@ -28,7 +28,7 @@ def __init__(self, testcase, target_dir): self.target_dir = target_dir - def _copy_files(self): + def go(self): dst_dir = os.path.join(self.target_dir, self.testcase.signature) # ensure target dir exists already (it might because of crash logging) filetools.mkdir_p(dst_dir) @@ -42,6 +42,3 @@ def _copy_files(self): for f in src_paths: logger.debug('Copy %s -> %s', f, dst_dir) shutil.copy2(f, dst_dir) - - def go(self): - self._copy_files() \ No newline at end of file From a8bd7b40f09ebd3056be5ae6b5248bcaa5d152af Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 10:43:03 -0500 Subject: [PATCH 0702/1169] move testcase logging to separate reporter --- src/certfuzz/reporters/testcase_logger.py | 23 +++++++++++++++++++ .../testcase_pipeline/tc_pipeline_linux.py | 7 +++--- 2 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 src/certfuzz/reporters/testcase_logger.py diff --git a/src/certfuzz/reporters/testcase_logger.py b/src/certfuzz/reporters/testcase_logger.py new file mode 100644 index 0000000..b595ce9 --- /dev/null +++ b/src/certfuzz/reporters/testcase_logger.py @@ -0,0 +1,23 @@ +''' +Created on Jan 13, 2016 + +@author: adh +''' +from certfuzz.reporters.reporter_base import ReporterBase + +class TestcaseLoggerReporter(ReporterBase): + ''' + Invokes the testcase's logger to report out testcase data + ''' + + def go(self): + tc = self.testcase + # whether it was unique or not, record some details for posterity + # record the details of this crash so we can regenerate it later if needed + tc.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', + tc.seedfile.basename, + tc.seednum, + tc.range, + tc.fuzzedfile.path + ) + tc.logger.info('PC=%s', tc.pc) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 87a620c..25b7846 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -21,6 +21,7 @@ from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.reporters.copy_files import CopyFilesReporter +from certfuzz.reporters.testcase_logger import TestcaseLoggerReporter logger = logging.getLogger(__name__) @@ -156,10 +157,8 @@ def _report(self, testcase): with CopyFilesReporter(testcase, self.tc_dir) as reporter: reporter.go() - # whether it was unique or not, record some details for posterity - # record the details of this crash so we can regenerate it later if needed - testcase.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', testcase.seedfile.basename, testcase.seednum, testcase.range, testcase.fuzzedfile.path) - testcase.logger.info('PC=%s', testcase.pc) + with TestcaseLoggerReporter(testcase) as reporter: + reporter.go() def _post_report(self, testcase): # always clean up after yourself From 2df5cc88088459d9839b5101ac9b1a57fb8e3592 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 10:43:47 -0500 Subject: [PATCH 0703/1169] add unit tests --- src/test_certfuzz/mocks.py | 29 ++++++++--- .../reporters/test_testcase_logger.py | 49 +++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 src/test_certfuzz/reporters/test_testcase_logger.py diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 83c2f6a..de2f372 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -4,6 +4,8 @@ @organization: cert.org ''' import hashlib +import logging + class Mock(object): pass @@ -12,26 +14,39 @@ def __init__(self): self.min = 0.01 self.max = 0.10 + def __str__(self): + return '{}-{}'.format(self.min, self.max) + class MockRangefinder(Mock): def next_item(self): return MockRange() class MockSeedfile(Mock): + basename = 'basename' + root = 'root' + ext = '.ext' + tries = 0 + rangefinder = MockRangefinder() + def __init__(self, sz=1000): - self.basename = 'basename' - self.root = 'root' - self.ext = '.ext' - self.tries = 0 self.value = 'A' * sz - self.len = len(self.value) - self.rangefinder = MockRangefinder() self.md5 = hashlib.md5(self.value).hexdigest() + self.len = len(self.value) def read(self): return self.value +class MockFuzzedFile(Mock): + path = u'foo' + class MockFuzzer(Mock): is_minimizable = False class MockTestcase(Mock): - signature = 'ABCDEFGHIJK' \ No newline at end of file + signature = 'ABCDEFGHIJK' + logger = logging.getLogger('mocktestcaselogger') + seedfile = MockSeedfile() + seednum = 123456789 + range = MockRange() + fuzzedfile = MockFuzzedFile() + pc = u'dummyPCstring' diff --git a/src/test_certfuzz/reporters/test_testcase_logger.py b/src/test_certfuzz/reporters/test_testcase_logger.py new file mode 100644 index 0000000..ccbeaff --- /dev/null +++ b/src/test_certfuzz/reporters/test_testcase_logger.py @@ -0,0 +1,49 @@ +''' +Created on Jan 13, 2016 + +@author: adh +''' +import unittest +import certfuzz.reporters.testcase_logger +from test_certfuzz.mocks import MockTestcase + +class Test(unittest.TestCase): + + + def setUp(self): + pass + + + def tearDown(self): + pass + + + def test_go(self): + import logging + import io + + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + log_capture_string = io.StringIO() + hdlr = logging.StreamHandler(log_capture_string) + hdlr.setLevel(logging.DEBUG) + + logger.addHandler(hdlr) + + tc = MockTestcase() + r = certfuzz.reporters.testcase_logger.TestcaseLoggerReporter(tc) + with r: + r.go() + + log_contents = log_capture_string.getvalue() + + log_capture_string.close() + + for x in ['seen in', 'at seed', 'range', 'outfile', 'PC']: + self.assertTrue(x in log_contents, '"{}" not in log'.format(x)) + + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From f85c2405e440f22e281927b7c305611857b11ea6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 10:53:54 -0500 Subject: [PATCH 0704/1169] remove unused import --- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 8041a23..82f3fa3 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -12,7 +12,6 @@ from certfuzz.fuzztools import filetools from certfuzz.minimizer.errors import MinimizerError from certfuzz.reporters.copy_files import CopyFilesReporter -from coverage.report import Reporter logger = logging.getLogger(__name__) From 0e31614f2f2e19f04ebb0267c932640b2e0599a8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 11:10:24 -0500 Subject: [PATCH 0705/1169] Make it clear that this thing is now called BFF, even on Windows. Retiring the FOE name. --- README.md | 14 ++++----- src/windows/NEWS.txt | 5 +++- src/windows/README.txt | 66 +++++++++++++++++++++--------------------- 3 files changed, 44 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 7ddd7c1..a020fc8 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ This project contains the source code for the CERT Basic Fuzzing Framework (BFF) -and the CERT Failure Observation Engine (FOE). + +BFF for Windows was formerly known as the CERT Failure Observation Engine (FOE). If you are looking for runnable code, you should download the latest releases at: * BFF (linux, OSX) [http://www.cert.org/vulnerability-analysis/tools/bff.cfm](http://www.cert.org/vulnerability-analysis/tools/bff.cfm "BFF") -* FOE (windows) [http://www.cert.org/vulnerability-analysis/tools/foe.cfm](http://www.cert.org/vulnerability-analysis/tools/foe.cfm "FOE") # Using this code # @@ -12,7 +12,7 @@ Depending on your preferred level of difficulty and experience points, choose fr ## Easy ## -Most of the BFF and FOE code can be found in the certfuzz package `src/certfuzz`. To try out the certfuzz code in an existing installation of BFF or FOE, replace the `certfuzz` directory in your installation with the `certfuzz` directory found in this repository. +Most of the BFF code can be found in the certfuzz package `src/certfuzz`. To try out the certfuzz code in an existing installation of BFF, replace the `certfuzz` directory in your installation with the `certfuzz` directory found in this repository. ## Moderate ## @@ -30,16 +30,16 @@ If all that seems more like a challenge than a warning, go for it. See `src/experimental/README.md` for some dead ends that might be marginally useful. -# About BFF and FOE # +# About BFF # -The CERT Basic Fuzzing Framework (BFF) is a software testing tool that finds defects in applications that run on the Linux and Mac OS X platforms. The CERT Failure Observation Engine (FOE) does the same on Windows. +The CERT Basic Fuzzing Framework (BFF) is a software testing tool that finds defects in applications that run on Linux, Mac OS X and Windows. -BFF and FOE perform mutational fuzzing on software that consumes file input. They automatically collect test cases that cause software to crash in unique ways, as well as debugging information associated with the crashes. The goal of BFF and FOE is to minimize the effort required for software vendors and security researchers to efficiently discover and analyze security vulnerabilities found via fuzzing. +BFF performs mutational fuzzing on software that consumes file input. They automatically collect test cases that cause software to crash in unique ways, as well as debugging information associated with the crashes. The goal of BFF is to minimize the effort required for software vendors and security researchers to efficiently discover and analyze security vulnerabilities found via fuzzing. ## A brief history of BFF and FOE ## BFF and FOE started out as two separate but related projects within the CERT/CC -Vulnerability Analysis team. Over time, they have converged in their architecture to the point where they now share much of their code. While this convergence should eventually lead to feature parity (or nearly so), we are not there yet. +Vulnerability Analysis team. Over time, they converged in their architecture to the point where BFF 2.7 and FOE 2.1 shared much of their code. As of BFF 2.8, this integration is complete and we have retired the name FOE in favor of BFF. ## For more information diff --git a/src/windows/NEWS.txt b/src/windows/NEWS.txt index cfa2a5c..8310be6 100644 --- a/src/windows/NEWS.txt +++ b/src/windows/NEWS.txt @@ -1,4 +1,7 @@ -CERT Failure Observation Engine (FOE) +CERT Basic Fuzzing Framework for Windows (BFF) + +BFF for Windows was formerly known as CERT Failure Observation Engine (FOE) + Significant changes FOE 2.1 (September 23, 2013) diff --git a/src/windows/README.txt b/src/windows/README.txt index 8178939..a166144 100644 --- a/src/windows/README.txt +++ b/src/windows/README.txt @@ -1,4 +1,4 @@ -Failure Observation Engine (FOE) 2.1 README +CERT Basic Fuzzing Framework (BFF) for Windows 2.8 README ===== License ===== =================== @@ -17,20 +17,20 @@ See NEWS.txt Because fuzzing can fill temporary directories, put the target application in an unusable state, or trigger other operating-system-level bugs, we -recommend that FOE be used in a virtual machine. +recommend that BFF be used in a virtual machine. -Run FOE-2.1-setup.exe in a virtual machine to install FOE 2.1. +Run BFF-2.1-setup.exe in a virtual machine to install BFF 2.1. The installer should detect and attempt to download prerequisites and configure your environment appropriately. -===== Running FOE ===== +===== Running BFF ===== ======================= -1) Click the FOE2 item in the Windows Start menu. +1) Click the BFF item in the Windows Start menu. -2) Run foe2.py +2) Run bff.py 3) Run tools\quickstats.py to check fuzzing progress when you wish. @@ -38,14 +38,14 @@ configure your environment appropriately. ===== How it works ===== ======================== -When a campaign starts, FOE will gather available seed files and create +When a campaign starts, BFF will gather available seed files and create scorable sets: 1) The seed files themselves 2) The fuzz percent ranges for each seed file Each interval of a campaign will choose a seed file, and then for that file, it will choose an percent range to mangle the file. After mangling the file, -FOE will launch the target application, using the configured command line to +BFF will launch the target application, using the configured command line to have it parse the fuzzed file. If the "winrun" runner is compatible with the current platform, this is accomplished by preloading a crash-intercepting hook into the target application's process space. This allows crash detection without @@ -57,7 +57,7 @@ the Microsoft !exploitable debugger extension. If the crash is determined to be unique (by the chain of !exploitable crash hashes), then some additional analysis steps are taken: 1) A !exploitable report is created for each continuable exception. -2) If configured to do so, FOE will create a minimized test case. +2) If configured to do so, BFF will create a minimized test case. 3) The seed file and percent range that were used to fuzz are scored Seed files that produce more crashes are given a preference over less- @@ -91,7 +91,7 @@ This is a copy of the config file used for this run. It is stored for historical purposes ("Which options did I use for that run?"). version.txt -This file stores the version of FOE that was used for fuzzing. +This file stores the version of BFF that was used for fuzzing. This is the "Exploitability Classification" assigned to the crash by @@ -138,11 +138,11 @@ the target application proceeds without encountering another exception. ===== Fuzzing on your own ===== =============================== -Once you are comfortable with FOE's default ImageMagick fuzz run, you can +Once you are comfortable with BFF's default ImageMagick fuzz run, you can try fuzzing an application of your choice. The first step is to place seed -files into the FOE seedfiles directory. These are the files that will be -mangled and opened by the target application. Next modify the foe.yaml file -to suit your needs. The foe.yaml file is documented to describe what each +files into the BFF seedfiles directory. These are the files that will be +mangled and opened by the target application. Next modify the bff.yaml file +to suit your needs. The bff.yaml file is documented to describe what each of the features mean. The important parts to modify are: campaign: id: @@ -151,7 +151,7 @@ campaign: id: application name and version. campaign: use_buttonclicker: - When fuzzing a GUI application, the FOE button clicker can increase + When fuzzing a GUI application, the BFF button clicker can increase throughput and code coverage. Note that the button clicker is not configurable, but rather it has a built-in heuristic for determining which buttons to click. @@ -163,32 +163,32 @@ target: cmdline_template: This specifies the commandline syntax for invoking the target application. runner: runtimeout: - This value specifies how long FOE should wait before terminating the + This value specifies how long BFF should wait before terminating the application and moving on to the next iteration. Note that this setting only applies to the "winrun" runner (32-bit Windows XP and Server 2003 systems). debugger: runtimeout: - This value specifies how long FOE should allow the target application to + This value specifies how long BFF should allow the target application to run when it is invoked from the debugger. On platforms that use the "null" runner (64-bit Windows or Windows Vista or newer), this is the only timeout value that is used. -FOE periodically saves state of a fuzzing campaign, so it will by default -continue a cached campaign if foe.yaml has not been modified. -To clear the FOE cached state, run: -tools\clean_foe.py +BFF periodically saves state of a fuzzing campaign, so it will by default +continue a cached campaign if bff.yaml has not been modified. +To clear the BFF cached state, run: +tools\clean_BFF.py For additional options, run: -tools\clean_foe.py --help +tools\clean_BFF.py --help ===== Digging deeper into results ===== ======================================= -When FOE has produced results, you may wish to perform some additional steps. +When BFF has produced results, you may wish to perform some additional steps. Finding interesting crashes: -With some target applications, FOE may produce too many uniquely-crashing test +With some target applications, BFF may produce too many uniquely-crashing test cases to investigate manually in a reasonable amount of time. We have provided a script called drillresults.py to pick out crashes that are most likely to be exploitable and list those cases in a ranked order (most exploitable first). @@ -200,7 +200,7 @@ tools\drillresults.py --help Reproducing crashes: The tools\repro.py script can be used to reproduce a crash by running it in -the same manner that FOE did. +the same manner that BFF did. For command-line usage, run: tools\repro.py --help @@ -224,7 +224,7 @@ tools\minimize.py --help To minimize a crashing testcase to the Metasploit string pattern, run: tools\minimize.py --stringmode -When minimizing to the Metasploit pattern, FOE will use the resulting byte map +When minimizing to the Metasploit pattern, BFF will use the resulting byte map to create an additional minimized file that uses a string of 'x' characters. Note that this file is not guaranteed to produce the same crash as the original string minimization. @@ -265,7 +265,7 @@ range_list: byte ranges to be fuzzed. One range per line, hex or decimal ===== Verifying crashing results ====== ======================================= -FOE can be used to verify crashing test cases. This can be useful for +BFF can be used to verify crashing test cases. This can be useful for when a new version of an application is released or if you are the developer and you want to see how many uniquely-crashing test cases disappear when you fix a bug. To perform a verfification campaign: @@ -273,19 +273,19 @@ disappear when you fix a bug. To perform a verfification campaign: 1) Run tools\copycrashers.py to collect all of the crashing cases from a campaign. By default it will copy all of the uniquely-crashing test cases to the "seedfiles" directory, which should be empty. -2) Modify configs\foe.yaml to use the "verify" fuzzer and also specify +2) Modify configs\bff.yaml to use the "verify" fuzzer and also specify a new campaign ID. -When you run FOE, it will run each case with the target application, +When you run BFF, it will run each case with the target application, and cases that still crash will be placed in the results directory for the new campaign. -===== Manually Installing FOE ===== +===== Manually Installing BFF ===== =================================== -If you have installed FOE using the installer, you can skip this section. -To install FOE manually, you will need the following prerequisites: +If you have installed BFF using the installer, you can skip this section. +To install BFF manually, you will need the following prerequisites: - Windows XP or Server 2003 32-bit is recommended to allow exception hooking (winrun) @@ -321,7 +321,7 @@ To install FOE manually, you will need the following prerequisites: - Add debugging tools (specifically cdb.exe) to your PATH. (probably C:\Program Files\Debugging Tools for Windows (x86)\) -- Copy the foe.yaml config file from configs\examples\ to a configs +- Copy the bff.yaml config file from configs\examples\ to a configs and modify as necessary. - Copy seed files to the seedfiles directory. \ No newline at end of file From 191f63077ecf8427594a2ae097e1609bb60d44cc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 14:01:05 -0500 Subject: [PATCH 0706/1169] remove dead code --- src/certfuzz/campaign/campaign_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 29b7e30..ae1c1c5 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -169,7 +169,6 @@ def _handle_common_errors(self, etype, value, mytraceback): :param value: ''' handled = False - # self._stop_buttonclicker() if etype is KeyboardInterrupt: logger.warning('Keyboard interrupt - exiting') handled = True From 67c8fe3eabde440ece46561c20dffcae3a6d8010 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 15:25:28 -0500 Subject: [PATCH 0707/1169] create unified config files --- src/linux/conf.d/unified_config_linux.yml | 66 +++++++++++++++++++ .../examples/unified_config_windows.yml | 65 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 src/linux/conf.d/unified_config_linux.yml create mode 100644 src/windows/configs/examples/unified_config_windows.yml diff --git a/src/linux/conf.d/unified_config_linux.yml b/src/linux/conf.d/unified_config_linux.yml new file mode 100644 index 0000000..f3eb0a7 --- /dev/null +++ b/src/linux/conf.d/unified_config_linux.yml @@ -0,0 +1,66 @@ +campaign: + id: default_imagemagick_convert + workdir: ~/fuzzing + results_dir: ~/results + seedfile_dir: ~/bff/seedfiles/examples + use_minimizer: True + recycle_crashers: False +# keep_heisenbugs: False # windows only +# use_buttonclicker: False # windows only +debugger: + timeout: 60 + debugger_cls: gdb + template_dir: ~/bff/certfuzz/debuggers/templates # linux only +# max_handled_exceptions: 6 # windows only +# watchcpu: Auto # windows only +# use_debug_heap: False # windows only +fuzzer: + fuzzer_cls: bytemut + fuzz_zip_container: False + start_iteration: 0 + iteration_interval: 1 + # TODO: can we eliminate copymode? + copymode: False # linux only +killproc: # linux only + killprocname: convert + timeout: 130 +minimizer: + timeout: 3600 + minimize_to_string: False +runner: + timeout: 5 + runner_cls: zzufrun +# windows only + # exceptions: + # - 0x80000002 # EXCEPTION_DATATYPE_MISALIGNMENT + # - 0xC0000005 # STATUS_ACCESS_VIOLATION + # - 0xC000001D # STATUS_ILLEGAL_INSTRUCTION + # - 0xC0000025 # EXCEPTION_NONCONTINUABLE_EXCEPTION + # - 0xC0000026 # EXCEPTION_INVALID_DISPOSITION + # - 0xC000008C # EXCEPTION_ARRAY_BOUNDS_EXCEEDED + # - 0xC000008E # EXCEPTION_FLT_DIVIDE_BY_ZERO + # - 0xC0000090 # EXCEPTION_FLT_INVALID_OPERATION + # - 0xC0000091 # EXCEPTION_FLT_OVERFLOW + # - 0xC0000092 # EXCEPTION_FLT_STACK_CHECK + # - 0xC0000093 # EXCEPTION_FLT_UNDERFLOW + # - 0xC0000094 # STATUS_INTEGER_DIVIDE_BY_ZERO + # - 0xC0000095 # EXCEPTION_INT_OVERFLOW + # - 0xC0000096 # STATUS_PRIVILEGED_INSTRUCTION + # - 0xC00000FD # STATUS_STACK_OVERFLOW + # hideoutput: False + # watchcpu: Auto +target: + program: ~/convert + invocation_template: $PROGRAM $SEEDFILE /dev/null +valgrind: # linux only + timeout: 120 +verifier: + # keep_unique_faddr: True # windows only + keep_failed_asserts: False # linux only + use_pin_calltrace: True # linux only + keep_duplicates: False + backtrace_depth: 5 + use_valgrind: True #linux only +watchdog: + timeout: 3600 + file: /tmp/bff_watchdog \ No newline at end of file diff --git a/src/windows/configs/examples/unified_config_windows.yml b/src/windows/configs/examples/unified_config_windows.yml new file mode 100644 index 0000000..9db19f8 --- /dev/null +++ b/src/windows/configs/examples/unified_config_windows.yml @@ -0,0 +1,65 @@ +campaign: + id: default_imagemagick_convert + workdir: ~/fuzzing + results_dir: ~/results + seedfile_dir: ~/bff/seedfiles/examples + use_minimizer: True + recycle_crashers: False + keep_heisenbugs: False # windows only + use_buttonclicker: False # windows only +debugger: + timeout: 60 + debugger_cls: gdb + # template_dir: ~/bff/certfuzz/debuggers/templates # linux only + max_handled_exceptions: 6 # windows only + watchcpu: Auto # windows only + use_debug_heap: False # windows only +fuzzer: + fuzzer_cls: bytemut + fuzz_zip_container: False + start_iteration: 0 + iteration_interval: 1 + # # TODO: can we eliminate copymode? + # copymode: False # linux only +# killproc: # linux only +# killprocname: convert +# timeout: 130 +minimizer: + timeout: 3600 + minimize_to_string: False +runner: + timeout: 5 + runner_cls: zzufrun + exceptions: + - 0x80000002 # EXCEPTION_DATATYPE_MISALIGNMENT + - 0xC0000005 # STATUS_ACCESS_VIOLATION + - 0xC000001D # STATUS_ILLEGAL_INSTRUCTION + - 0xC0000025 # EXCEPTION_NONCONTINUABLE_EXCEPTION + - 0xC0000026 # EXCEPTION_INVALID_DISPOSITION + - 0xC000008C # EXCEPTION_ARRAY_BOUNDS_EXCEEDED + - 0xC000008E # EXCEPTION_FLT_DIVIDE_BY_ZERO + - 0xC0000090 # EXCEPTION_FLT_INVALID_OPERATION + - 0xC0000091 # EXCEPTION_FLT_OVERFLOW + - 0xC0000092 # EXCEPTION_FLT_STACK_CHECK + - 0xC0000093 # EXCEPTION_FLT_UNDERFLOW + - 0xC0000094 # STATUS_INTEGER_DIVIDE_BY_ZERO + - 0xC0000095 # EXCEPTION_INT_OVERFLOW + - 0xC0000096 # STATUS_PRIVILEGED_INSTRUCTION + - 0xC00000FD # STATUS_STACK_OVERFLOW + hideoutput: False + watchcpu: Auto +target: + program: ~/convert + invocation_template: $PROGRAM $SEEDFILE /dev/null +# valgrind: # linux only +# timeout: 120 +verifier: + keep_unique_faddr: True # windows only + # keep_failed_asserts: False # linux only + # use_pin_calltrace: True # linux only + keep_duplicates: False + backtrace_depth: 5 + # use_valgrind: True #linux only +# watchdog: +# timeout: 3600 +# file: /tmp/bff_watchdog \ No newline at end of file From f2348e51285c40f86dae39389a579170074dac27 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 13 Jan 2016 15:54:50 -0500 Subject: [PATCH 0708/1169] create simple loader function to read yaml and return a dict --- src/certfuzz/config/simple_loader.py | 20 ++++++++ .../config/test_simple_loader.py | 48 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/certfuzz/config/simple_loader.py create mode 100644 src/test_certfuzz/config/test_simple_loader.py diff --git a/src/certfuzz/config/simple_loader.py b/src/certfuzz/config/simple_loader.py new file mode 100644 index 0000000..dcc24c2 --- /dev/null +++ b/src/certfuzz/config/simple_loader.py @@ -0,0 +1,20 @@ +''' +Created on Jan 13, 2016 + +@author: adh +''' +import logging +import yaml +import os + +logger = logging.getLogger(__name__) + + +def load_config(yaml_file): + with open(yaml_file, 'rb') as f: + cfg = yaml.load(f) + + # add the file timestamp so we can tell if it changes later + cfg['config_timestamp'] = os.path.getmtime(yaml_file) + + return cfg diff --git a/src/test_certfuzz/config/test_simple_loader.py b/src/test_certfuzz/config/test_simple_loader.py new file mode 100644 index 0000000..19ccb14 --- /dev/null +++ b/src/test_certfuzz/config/test_simple_loader.py @@ -0,0 +1,48 @@ +''' +Created on Jan 13, 2016 + +@author: adh +''' +import unittest +import certfuzz.config.simple_loader +import tempfile +import shutil +import os +import yaml + + +class Test(unittest.TestCase): + + def setUp(self): + self.tempdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tempdir) + + def _write_yaml(self, thing=None): + if thing is None: + thing = dict([(y, x) for x, y in enumerate("abcd")]) + fd, f = tempfile.mkstemp(suffix='yaml', dir=self.tempdir) + os.close(fd) + with open(f, 'wb') as fd: + yaml.dump(thing, fd) + + return thing, f + + def test_load_config(self): + thing, f = self._write_yaml() + + self.assertTrue(os.path.exists(f)) + self.assertTrue(os.path.getsize(f) > 0) + + loaded = certfuzz.config.simple_loader.load_config(f) + + self.assertTrue('config_timestamp' in loaded) + self.assertEqual(os.path.getmtime(f), loaded.pop('config_timestamp')) + + self.assertEqual(thing, loaded) + + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 78af9943ef1b9a076853d3e863a981a4bd621ae2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 14 Jan 2016 10:25:10 -0500 Subject: [PATCH 0709/1169] windows uses new simple config loader --- src/certfuzz/campaign/campaign_base.py | 5 +++-- src/certfuzz/campaign/campaign_windows.py | 10 ---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index ae1c1c5..6fe817b 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -22,6 +22,7 @@ from certfuzz.version import __version__ from certfuzz.file_handlers.tmp_reaper import TmpReaper import gc +from certfuzz.config.simple_loader import load_config logger = logging.getLogger(__name__) @@ -100,9 +101,9 @@ def __init__(self, config_file, result_dir=None, debug=False): self._read_config_file() - @abc.abstractmethod def _read_config_file(self): logger.info('Reading config from %s', self.config_file) + self.config = load_config(self.config_file) def _common_init(self): ''' @@ -319,7 +320,7 @@ def _read_state(self, cache_file=None): if campaign: try: - if self.configdate != campaign.__dict__['configdate']: + if self.config['config_timestamp'] != campaign.__dict__['config_timestamp']: logger.warning('Config file modified. Discarding cached campaign') else: self.__dict__.update(campaign.__dict__) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 7883d87..57073ff 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -24,7 +24,6 @@ class WindowsCampaign(CampaignBase): ''' Extends CampaignBase to add windows-specific features like ButtonClicker ''' - _config_cls = WindowsConfig def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) @@ -64,15 +63,6 @@ def __init__(self, config_file, result_dir=None, debug=False): # must occur after work_dir_base, outdir_base, and campaign_id are set self._common_init() - def _read_config_file(self): - CampaignBase._read_config_file(self) - - # read configs - with self._config_cls(self.config_file) as cfgobj: - self.config = cfgobj.config - self.configdate = cfgobj.configdate - - def __getstate__(self): state = self.__dict__.copy() From 78db4a6a155e98c8741f028d112da97fc8092e65 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 14 Jan 2016 11:40:14 -0500 Subject: [PATCH 0710/1169] add fix-up path helper method --- src/certfuzz/helpers/misc.py | 11 +++++++++-- src/test_certfuzz/helpers/test_misc.py | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/helpers/misc.py b/src/certfuzz/helpers/misc.py index 07dc5b9..3b828bd 100644 --- a/src/certfuzz/helpers/misc.py +++ b/src/certfuzz/helpers/misc.py @@ -8,11 +8,20 @@ from pprint import pformat, pprint import random import string +import os my_os = platform.system() +def fixup_path(path): + ''' + Expands tildes and returns absolute path transformation of path + :param path: + ''' + return os.path.abspath(os.path.expanduser(path)) + + def quoted(string_to_wrap): return '"%s"' % string_to_wrap @@ -21,8 +30,6 @@ def print_dict(d): pprint(d) - - def random_str(length=1): chars = string.ascii_letters + string.digits return ''.join([random.choice(chars) for dummy in xrange(length)]) diff --git a/src/test_certfuzz/helpers/test_misc.py b/src/test_certfuzz/helpers/test_misc.py index 2982309..50b79cf 100644 --- a/src/test_certfuzz/helpers/test_misc.py +++ b/src/test_certfuzz/helpers/test_misc.py @@ -18,6 +18,10 @@ def setUp(self): def tearDown(self): pass + def test_fixup_path(self): + path = '~/foo' + self.assertTrue('~' not in helpers.misc.fixup_path(path)) + def test_quoted(self): for s in list('qwertyuiopasdfghjklzxcvbnm'): self.assertTrue(s in helpers.quoted(s)) From c8e4c0ff6c3bf3d68adad71d83ca2c30cf8b26d0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 25 Jan 2016 09:44:12 -0500 Subject: [PATCH 0711/1169] refactor command line template stuff into separate module --- .../fuzztools/command_line_templating.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/certfuzz/fuzztools/command_line_templating.py diff --git a/src/certfuzz/fuzztools/command_line_templating.py b/src/certfuzz/fuzztools/command_line_templating.py new file mode 100644 index 0000000..315015d --- /dev/null +++ b/src/certfuzz/fuzztools/command_line_templating.py @@ -0,0 +1,21 @@ +''' +Created on Jan 14, 2016 + +@author: adh +''' +import shlex + + +def get_command_args_list(cmd_template, infile, posix=True): + ''' + Given a command template and infile, will substitute infile into the + template, and return both the complete string and its component parts + as returned by shlex.split. The optional posix parameter is passed to + shlex.split (defaults to true). + :param cmd_template: a string.Template object containing "$SEEDFILE" + :param infile: the string to substitute for "$SEEDFILE" in cmd_template + :param posix: (optional) passed through to shlex.split + ''' + cmd = cmd_template.substitute(SEEDFILE=infile) + cmdlist = shlex.split(cmd, posix=posix) + return cmd, cmdlist \ No newline at end of file From c3b181928c12324ab7d1b1074ede8b6b3ce0d801 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 25 Jan 2016 09:46:58 -0500 Subject: [PATCH 0712/1169] remove get_command_args_list from config_windows (now in fuzztools.comman_line_templating) --- src/certfuzz/config/config_windows.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/certfuzz/config/config_windows.py b/src/certfuzz/config/config_windows.py index cc2c364..653cff6 100644 --- a/src/certfuzz/config/config_windows.py +++ b/src/certfuzz/config/config_windows.py @@ -4,7 +4,6 @@ @organization: cert.org ''' import logging -import shlex from string import Template from certfuzz.config.config_base import ConfigBase @@ -15,21 +14,6 @@ logger = logging.getLogger(__name__) -def get_command_args_list(cmd_template, infile, posix=True): - ''' - Given a command template and infile, will substitute infile into the - template, and return both the complete string and its component parts - as returned by shlex.split. The optional posix parameter is passed to - shlex.split (defaults to true). - :param cmd_template: a string.Template object containing "$SEEDFILE" - :param infile: the string to substitute for "$SEEDFILE" in cmd_template - :param posix: (optional) passed through to shlex.split - ''' - cmd = cmd_template.substitute(SEEDFILE=infile) - cmdlist = shlex.split(cmd, posix=posix) - return cmd, cmdlist - - class WindowsConfig(ConfigBase): def _add_validations(self): self.validations.append(self._validate_debugger_timeout_exceeds_runner) From 7f3f745b8e737f943041c1d4f5992a61cd42958a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 25 Jan 2016 09:47:39 -0500 Subject: [PATCH 0713/1169] add command line templating to campaign_base --- src/certfuzz/campaign/campaign_base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 6fe817b..6cd7677 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -23,6 +23,8 @@ from certfuzz.file_handlers.tmp_reaper import TmpReaper import gc from certfuzz.config.simple_loader import load_config +from string import Template +from certfuzz.helpers.misc import quoted logger = logging.getLogger(__name__) @@ -100,11 +102,22 @@ def __init__(self, config_file, result_dir=None, debug=False): self.outdir_base = os.path.abspath(result_dir) self._read_config_file() + self._fixup_config() def _read_config_file(self): logger.info('Reading config from %s', self.config_file) self.config = load_config(self.config_file) + def _fixup_config(self): + ''' + Substitutes program name into command line template + ''' + quoted_prg = quoted(self.config['target']['program']) + quoted_sf = quoted('$SEEDFILE') + t = Template(self.config['target']['cmdline_template']) + t_with_substitution = t.safe_substitute(PROGRAM=quoted_prg, SEEDFILE=quoted_sf) + self.config['target']['cmdline_template'] = t_with_substitution + def _common_init(self): ''' Initializes some additional properties common to all platforms From c2a0618bc83fe32695b3c28d227f87eeb2d288f3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 25 Jan 2016 09:48:36 -0500 Subject: [PATCH 0714/1169] refactoring to make config more consistent across platforms --- src/certfuzz/campaign/campaign_linux.py | 64 +++++++++++------ src/certfuzz/config/config_linux.py | 87 ++++++++++------------- src/certfuzz/crash/bff_crash.py | 12 ++-- src/certfuzz/iteration/iteration_linux.py | 33 ++++----- src/linux/conf.d/bff.yaml | 5 +- 5 files changed, 104 insertions(+), 97 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index d1b68ac..98acc6d 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -23,11 +23,18 @@ from certfuzz.iteration.iteration_linux import LinuxIteration from certfuzz.fuzzers.bytemut import ByteMutFuzzer from certfuzz.runners.zzufrun import ZzufRunner +import re +import shlex +from certfuzz.helpers.misc import fixup_path +from certfuzz.fuzztools.command_line_templating import get_command_args_list logger = logging.getLogger(__name__) +SEEDFILE_REPLACE_STRING = '\$SEEDFILE' + + def check_program_file_type(string, program): ''' @rtype: boolean @@ -51,39 +58,42 @@ def check_program_file_type(string, program): else: return False - class LinuxCampaign(CampaignBase): ''' Extends CampaignBase to add linux-specific features. ''' - _config_cls = LinuxConfig - def __init__(self, config_file=None, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) # pull stuff out of configs - self.campaign_id = self.config.config['campaign']['id'] - self.current_seed = self.config.start_seed - self.seed_interval = self.config.seed_interval - self.seed_dir_in = self.config.seedfile_origin_dir + self.campaign_id = self.config['campaign']['id'] + self.current_seed = self.config['zzuf']['start_seed'] + self.seed_interval = self.config['zzuf']['seed_interval'] + self.seed_dir_in = fixup_path(self.config['directories']['seedfile_origin_dir']) if self.outdir_base is None: # it wasn't spec'ed on the command line so use the config - self.outdir_base = os.path.abspath(self.config.output_dir) + self.outdir_base = fixup_path(self.config['directories']['output_dir']) + + self.work_dir_base = fixup_path(self.config['directories']['local_dir']) + self.program = fixup_path(self.config['target']['program']) + self.program_basename = os.path.basename(self.program).replace('"', '') + self.cmd_list = shlex.split(self.config['target']['cmdline']) + self.cmd_list[0] = fixup_path(self.cmd_list[0]) - self.work_dir_base = self.config.local_dir - self.program = self.config.program # must occur after work_dir_base, outdir_base, and campaign_id are set self._common_init() + def _full_path_original(self, seedfile): + # yes, two seedfile mentions are intended - adh + return os.path.join(self.work_dir_base, + self.program_basename, + seedfile, + seedfile) - def _read_config_file(self): - CampaignBase._read_config_file(self) - - with self._config_cls(self.config_file) as cfgobj: - self.config = cfgobj - self.configdate = cfgobj.configdate + def _get_command_list(self, seedfile): + return [re.sub(SEEDFILE_REPLACE_STRING, seedfile, item) for item in self.cmd_list] def _pre_enter(self): # give up if prog is a script @@ -94,7 +104,7 @@ def _pre_enter(self): self._check_for_script() def _post_enter(self): - if self.config.watchdogtimeout: + if self.config['timeouts']['watchdogtimeout']: self._setup_watchdog() check_ppid() self._cache_app() @@ -114,7 +124,9 @@ def _set_unbuffered_stdout(self): def _start_process_killer(self): logger.debug('start process killer') - with ProcessKiller(self.config.killprocname, self.config.killproctimeout) as pk: + with ProcessKiller(self.config['target']['killprocname'], + self.config['timeouts']['killproctimeout'] + ) as pk: self.pk_pid = pk.go() def _cache_app(self): @@ -122,9 +134,13 @@ def _cache_app(self): sf = self.seedfile_set.next_item() # Run the program once to cache it into memory - fullpathorig = self.config.full_path_original(sf.path) - cmdargs = self.config.get_command_list(fullpathorig) - subp.run_with_timer(cmdargs, self.config.progtimeout * 8, self.config.killprocname, use_shell=False) + fullpathorig = self._full_path_original(sf.path) + cmdargs = get_command_args_list(self.config['target']['cmdline_template'], infile=fullpathorig) +# cmdargs = self._get_command_list(fullpathorig) + subp.run_with_timer(cmdargs, + self.config['timeouts']['progtimeout'] * 8, + self.config['target']['killprocname'], + use_shell=False) # Give target time to die time.sleep(1) @@ -132,12 +148,14 @@ def _cache_app(self): def _setup_watchdog(self): logger.debug('setup watchdog') # setup our watchdog file toucher - TWDF.wdf = self.config.watchdogfile + wdf = fixup_path(self.config['directories']['watchdog_file']) + + TWDF.wdf = wdf TWDF.enable() touch_watchdog_file() # set up the watchdog timeout within the VM and restart the daemon - with WatchDog(self.config.watchdogfile, self.config.watchdogtimeout) as watchdog: + with WatchDog(wdf, self.config['timeouts']['watchdogtimeout']) as watchdog: watchdog() def _check_for_script(self): diff --git a/src/certfuzz/config/config_linux.py b/src/certfuzz/config/config_linux.py index 86e2800..470cd71 100644 --- a/src/certfuzz/config/config_linux.py +++ b/src/certfuzz/config/config_linux.py @@ -61,7 +61,6 @@ CACHED_SEEDRANGE_OBJECT_FILE = 'seedrange.pkl' CACHED_RANGEFINDER_OBJECT_FILE = 'rangefinder.pkl' CACHED_SEEDFILESET_OBJECT_FILE = 'seedfile_set.pkl' -SEEDFILE_REPLACE_STRING = '\$SEEDFILE' class LinuxConfig(ConfigBase): ''' @@ -90,14 +89,14 @@ def _set_derived_options(self): # set the attribute, casting it to the expected type setattr(self, key, dtype(val)) - self.cmd_list = shlex.split(self.cmdline) - for index, cmd_part in enumerate(self.cmd_list): - self.cmd_list[index] = os.path.expanduser(cmd_part) - if re.search(' ', self.cmd_list[0]): - self.cmd_list[0] = quoted(self.cmd_list[0]) - self.program = self.cmd_list[0] - self._cmd = self.cmd_list - self._args = self.cmd_list[1:] +# self.cmd_list = shlex.split(self.cmdline) +# for index, cmd_part in enumerate(self.cmd_list): +# self.cmd_list[index] = os.path.expanduser(cmd_part) +# if re.search(' ', self.cmd_list[0]): +# self.cmd_list[0] = quoted(self.cmd_list[0]) +# self.program = self.cmd_list[0] +# self._cmd = self.cmd_list +# self._args = self.cmd_list[1:] # for backwards compatibility self.watchdogfile = self.watchdog_file @@ -108,13 +107,13 @@ def _set_derived_options(self): self.crashexitcodesfile = os.path.join(self.local_dir, CRASH_EXIT_CODE_FILE) self.zzuf_log_file = os.path.join(self.local_dir, ZZUF_LOG_FILE) - def get_command(self, filepath): - return ' '.join(self.get_command_list(filepath)) +# def get_command(self, filepath): +# return ' '.join(self.get_command_list(filepath)) - def get_command_list(self, seedfile): - cmdlst = [self.program] - cmdlst.extend(self.get_command_args_list(seedfile)) - return cmdlst +# def get_command_list(self, seedfile): +# cmdlst = [self.program] +# cmdlst.extend(self.get_command_args_list(seedfile)) +# return cmdlst def get_command_args_list(self, seedfile): arglist = [] @@ -123,41 +122,27 @@ def get_command_args_list(self, seedfile): return arglist - def full_path_local_fuzz_dir(self, seedfile): - ''' - Returns // - @param seedfile: - ''' - return os.path.join(self.local_dir, self.program_basename, seedfile) - - def full_path_original(self, seedfile): - ''' - Returns / - @param seedfile: - ''' - return os.path.join(self.full_path_local_fuzz_dir(seedfile), seedfile) - - def get_minimized_file(self, outfile): - ''' - @rtype: string - @return: -. - ''' - (head, tail) = os.path.split(outfile) - (root, ext) = os.path.splitext(tail) - new_filename = '%s-%s%s' % (root, MINIMIZED_EXT, ext) - return os.path.join(head, new_filename) - - def get_testcase_outfile(self, seedfile, s1): - # TODO: this should become part of campaign object - ''' - @rtype: string - @return: the path to the output file for this seed: . - ''' - (dirname, basename) = os.path.split(seedfile) # @UnusedVariable - (root, ext) = os.path.splitext(basename) - new_root = '%s-%d' % (root, s1) - new_basename = '%s%s' % (new_root, ext) - self.create_tmpdir() - return os.path.join(self.tmpdir, new_basename) +# def get_minimized_file(self, outfile): +# ''' +# @rtype: string +# @return: -. +# ''' +# (head, tail) = os.path.split(outfile) +# (root, ext) = os.path.splitext(tail) +# new_filename = '%s-%s%s' % (root, MINIMIZED_EXT, ext) +# return os.path.join(head, new_filename) + +# def get_testcase_outfile(self, seedfile, s1): +# # TODO: this should become part of campaign object +# ''' +# @rtype: string +# @return: the path to the output file for this seed: . +# ''' +# (dirname, basename) = os.path.split(seedfile) # @UnusedVariable +# (root, ext) = os.path.splitext(basename) +# new_root = '%s-%d' % (root, s1) +# new_basename = '%s%s' % (new_root, ext) +# self.create_tmpdir() +# return os.path.join(self.tmpdir, new_basename) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index ca3f513..e6f86ea 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -8,6 +8,7 @@ from certfuzz.crash.crash_base import Testcase, CrashError from certfuzz.fuzztools import hostinfo, filetools +from certfuzz.fuzztools.command_line_templating import get_command_args_list try: from certfuzz.analyzers import pin_calltrace @@ -47,7 +48,7 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, self.crash_base_dir = crashers_dir self.seednum = seednum self.range = range - self.exclude_unmapped_frames = cfg.exclude_unmapped_frames + self.exclude_unmapped_frames = cfg['verifier']['exclude_unmapped_frames'] self.set_debugger_template('bt_only') self.keep_uniq_faddr = keep_faddr @@ -66,7 +67,7 @@ def __exit__(self, etype, value, traceback): def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): dbg_template_name = '%s_%s_template.txt' % (self._debugger_cls._key, option) - self.debugger_template = os.path.join(self.cfg.debugger_template_dir, dbg_template_name) + self.debugger_template = os.path.join(self.cfg['directories']['debugger_template_dir'], dbg_template_name) logger.debug('Debugger template set to %s', self.debugger_template) if not os.path.exists(self.debugger_template): raise CrashError('Debugger template does not exist at %s' % self.debugger_template) @@ -74,8 +75,9 @@ def set_debugger_template(self, option='bt_only'): def update_crash_details(self): Testcase.update_crash_details(self) - self.cmdargs = self.cfg.get_command_args_list(self.fuzzedfile.path) -# self.debugger_file = debuggers.get_debug_file(self.fuzzedfile.path) + self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'], + infile=self.fuzzedfile.path, + posix=True)[1] self.is_crash = self.confirm_crash() @@ -119,7 +121,7 @@ def confirm_crash(self): raise CrashError('Debug object not found') logger.debug('is_crash: %s is_assert_fail: %s', self.dbg.is_crash, self.dbg.is_assert_fail) - if self.cfg.savefailedasserts: + if self.cfg['verifier']['savefailedasserts']: return self.dbg.is_crash else: # only keep real crashes (not failed assertions) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index e2decd1..039f137 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -12,6 +12,7 @@ from certfuzz.fuzztools.zzuflog import ZzufLog from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.testcase_pipeline.tc_pipeline_linux import LinuxTestCasePipeline +from certfuzz.helpers.misc import fixup_path logger = logging.getLogger(__name__) @@ -48,14 +49,14 @@ def __init__(self, self.testcase_base_dir = os.path.join(self.outdir, 'crashers') - self.pipeline_options = {'use_valgrind': self.cfg.use_valgrind, - 'use_pin_calltrace': self.cfg.use_pin_calltrace, - 'minimize_crashers': self.cfg.minimizecrashers, - 'minimize_to_string': self.cfg.minimize_to_string, - 'uniq_log': self.cfg.uniq_log, - 'local_dir': self.cfg.local_dir, - 'minimizertimeout': self.cfg.minimizertimeout, - 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg.config['runoptions']['minimize'], + self.pipeline_options = {'use_valgrind': self.cfg['verifier']['use_valgrind'], + 'use_pin_calltrace': self.cfg['verifier']['use_pin_calltrace'], + 'minimize_crashers': self.cfg['verifier']['minimizecrashers'], + 'minimize_to_string': self.cfg['verifier']['minimize_to_string'], + 'uniq_log': os.path.join(self.cfg['directories']['output_dir'], 'uniquelog.txt'), + 'local_dir': fixup_path(self.cfg['directories']['local_dir']), + 'minimizertimeout': self.cfg['timeouts']['minimizertimeout'], + 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions']['minimize'], } def __enter__(self): @@ -64,15 +65,15 @@ def __enter__(self): return self.go def _pre_fuzz(self): - self._fuzz_opts = self.cfg.config['fuzzer'] + self._fuzz_opts = self.cfg['fuzzer'] IterationBase3._pre_fuzz(self) def _pre_run(self): - self._runner_options = self.cfg.config['runner'] + self._runner_options = self.cfg['runner'] if self.quiet_flag: self._runner_options['hideoutput'] = True - self._runner_cmd_template = self.cfg.config['target']['cmdline'] + self._runner_cmd_template = self.cfg['target']['cmdline'] IterationBase3._pre_run(self) @@ -99,7 +100,7 @@ def _post_run(self): # Don't generate cases for killed process or out-of-memory # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will # report the exit code in its output log. The exit code is 128 + the signal number. - analysis_needed = zzuf_log.crash_logged(self.cfg.config['zzuf']['copymode']) + analysis_needed = zzuf_log.crash_logged(self.cfg['zzuf']['copymode']) if not analysis_needed: return @@ -111,10 +112,10 @@ def _construct_testcase(self): with BffCrash(cfg=self.cfg, seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), - program=self.cfg.program, - debugger_timeout=self.cfg.debugger_timeout, - killprocname=self.cfg.killprocname, - backtrace_lines=self.cfg.backtracelevels, + program=fixup_path(self.cfg['target']['program']), + debugger_timeout=self.cfg['timeouts']['debugger_timeout'], + killprocname=self.cfg['target']['killprocname'], + backtrace_lines=self.cfg['verifier']['backtracelevels'], crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, seednum=self.seednum, diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index be38a5c..f7a2325 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -27,7 +27,8 @@ campaign: # launches the target application from an analyzer, such as gdb or valgrind # Use quotes if the target application has spaces in its name target: - cmdline: ~/convert $SEEDFILE /dev/null + program: ~/convert + cmdline_template: $PROGRAM $SEEDFILE /dev/null killprocname: convert ################################################################ @@ -182,7 +183,7 @@ runoptions: # keep_all_duplicates: False # recycle_crashers: False fuzzer: - fuzzer: zzuf + fuzzer: bytemut fuzz_zip_container: False runner: runner: zzufrun From 42130191943f69aee7de0aeb7b5b766d6ba9e63a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 25 Jan 2016 09:54:59 -0500 Subject: [PATCH 0715/1169] add command line template stuff to config_base --- src/certfuzz/config/config_base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/certfuzz/config/config_base.py b/src/certfuzz/config/config_base.py index 9c81a9e..8912723 100644 --- a/src/certfuzz/config/config_base.py +++ b/src/certfuzz/config/config_base.py @@ -8,6 +8,9 @@ import yaml from certfuzz.config.errors import ConfigError +from string import Template +from certfuzz.helpers.misc import quoted +import re logger = logging.getLogger(__name__) @@ -58,6 +61,16 @@ def validate(self): def _set_derived_options(self): if self.config is None: raise ConfigError('No config found (or config file empty?)') + # interpolate program name + # add quotes around $SEEDFILE + t = Template(self.config['target']['cmdline_template']) + self.config['target']['cmdline_template'] = t.safe_substitute(PROGRAM=quoted(self.config['target']['program']), + SEEDFILE=quoted('$SEEDFILE')) + + campaign_id = re.sub('\s+', '_', self.config['campaign']['id']) + self.config['campaign']['id'] = campaign_id + + def _add_validations(self): pass From bc1247097df1421fc6adedbe428009eafe5b43ab Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 25 Jan 2016 15:46:03 -0500 Subject: [PATCH 0716/1169] minimizer works, still broken at stderr --- src/certfuzz/campaign/campaign_base.py | 28 ++-- src/certfuzz/campaign/campaign_linux.py | 16 +- src/certfuzz/config/config_base.py | 76 --------- src/certfuzz/config/config_linux.py | 148 ------------------ src/certfuzz/config/config_windows.py | 74 --------- src/certfuzz/fuzztools/subprocess_helper.py | 1 + src/certfuzz/helpers/misc.py | 14 ++ src/certfuzz/iteration/iteration_linux.py | 2 +- src/certfuzz/iteration/iteration_windows.py | 2 +- src/certfuzz/minimizer/minimizer_base.py | 18 ++- src/certfuzz/runners/zzufrun.py | 2 +- .../testcase_pipeline/tc_pipeline_linux.py | 6 +- 12 files changed, 47 insertions(+), 340 deletions(-) delete mode 100644 src/certfuzz/config/config_base.py delete mode 100644 src/certfuzz/config/config_linux.py delete mode 100644 src/certfuzz/config/config_windows.py diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 6cd7677..f2dfba1 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -9,7 +9,6 @@ import os import re import shutil -import sys import tempfile import traceback import cPickle as pickle @@ -24,26 +23,12 @@ import gc from certfuzz.config.simple_loader import load_config from string import Template -from certfuzz.helpers.misc import quoted +from certfuzz.helpers.misc import quoted, import_module_by_name, fixup_path logger = logging.getLogger(__name__) -def import_module_by_name(name): - ''' - Imports a module at runtime given the pythonic name of the module - e.g., certfuzz.fuzzers.bytemut - :param name: - :param logger: - ''' - if logger: - logger.debug('Importing module %s', name) - __import__(name) - module = sys.modules[name] - return module - - class CampaignBase(object): ''' Provides a fuzzing campaign object. @@ -112,11 +97,18 @@ def _fixup_config(self): ''' Substitutes program name into command line template ''' + # fix target program path + self.config['target']['program'] = fixup_path(self.config['target']['program']) + logger.info('Using target program: %s',self.config['target']['program']) + quoted_prg = quoted(self.config['target']['program']) quoted_sf = quoted('$SEEDFILE') t = Template(self.config['target']['cmdline_template']) - t_with_substitution = t.safe_substitute(PROGRAM=quoted_prg, SEEDFILE=quoted_sf) - self.config['target']['cmdline_template'] = t_with_substitution + intermediate_t = t.safe_substitute(PROGRAM=quoted_prg, SEEDFILE=quoted_sf) + self.config['target']['cmdline_template'] = Template(intermediate_t) + + for k,v in self.config['directories'].iteritems(): + self.config['directories'][k] = fixup_path(v) def _common_init(self): ''' diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 98acc6d..0ef0307 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -12,7 +12,6 @@ from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.campaign.errors import CampaignScriptError -from certfuzz.config.config_linux import LinuxConfig from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file @@ -23,8 +22,6 @@ from certfuzz.iteration.iteration_linux import LinuxIteration from certfuzz.fuzzers.bytemut import ByteMutFuzzer from certfuzz.runners.zzufrun import ZzufRunner -import re -import shlex from certfuzz.helpers.misc import fixup_path from certfuzz.fuzztools.command_line_templating import get_command_args_list @@ -78,8 +75,8 @@ def __init__(self, config_file=None, result_dir=None, debug=False): self.work_dir_base = fixup_path(self.config['directories']['local_dir']) self.program = fixup_path(self.config['target']['program']) self.program_basename = os.path.basename(self.program).replace('"', '') - self.cmd_list = shlex.split(self.config['target']['cmdline']) - self.cmd_list[0] = fixup_path(self.cmd_list[0]) +# self.cmd_list = shlex.split(self.config['target']['cmdline']) +# self.cmd_list[0] = fixup_path(self.cmd_list[0]) # must occur after work_dir_base, outdir_base, and campaign_id are set @@ -92,8 +89,8 @@ def _full_path_original(self, seedfile): seedfile, seedfile) - def _get_command_list(self, seedfile): - return [re.sub(SEEDFILE_REPLACE_STRING, seedfile, item) for item in self.cmd_list] +# def _get_command_list(self, seedfile): +# return [re.sub(SEEDFILE_REPLACE_STRING, seedfile, item) for item in self.cmd_list] def _pre_enter(self): # give up if prog is a script @@ -135,8 +132,7 @@ def _cache_app(self): # Run the program once to cache it into memory fullpathorig = self._full_path_original(sf.path) - cmdargs = get_command_args_list(self.config['target']['cmdline_template'], infile=fullpathorig) -# cmdargs = self._get_command_list(fullpathorig) + cmdargs = get_command_args_list(self.config['target']['cmdline_template'], infile=fullpathorig)[1] subp.run_with_timer(cmdargs, self.config['timeouts']['progtimeout'] * 8, self.config['target']['killprocname'], @@ -148,7 +144,7 @@ def _cache_app(self): def _setup_watchdog(self): logger.debug('setup watchdog') # setup our watchdog file toucher - wdf = fixup_path(self.config['directories']['watchdog_file']) + wdf = self.config['directories']['watchdog_file'] TWDF.wdf = wdf TWDF.enable() diff --git a/src/certfuzz/config/config_base.py b/src/certfuzz/config/config_base.py deleted file mode 100644 index 8912723..0000000 --- a/src/certfuzz/config/config_base.py +++ /dev/null @@ -1,76 +0,0 @@ -''' -Created on Oct 23, 2012 - -@organization: cert.org -''' -import logging -import os.path - -import yaml -from certfuzz.config.errors import ConfigError -from string import Template -from certfuzz.helpers.misc import quoted -import re - - -logger = logging.getLogger(__name__) - - -def parse_yaml(yaml_file): - with open(yaml_file, 'r') as f: - stuff = yaml.load(f) - return stuff - - -class ConfigBase(object): - ''' - If you are inheriting this class, add validation methods to self.validations - to have them run automatically at initialization. - ''' - def __init__(self, config_file): - self.file = config_file - self.config = None - self.configdate = None - self.validations = [] - - def __enter__(self): - self.load() - self._set_derived_options() - self._add_validations() - self.validate() - return self - - def __exit__(self, etype, value, traceback): - pass - - def load(self): - logger.debug('loading config from %s', self.file) - try: - self.config = parse_yaml(self.file) - self.configdate = os.path.getmtime(self.file) - except IOError: - pass - - if self.config: - self.__dict__.update(self.config) - - def validate(self): - for validation in self.validations: - validation() - - def _set_derived_options(self): - if self.config is None: - raise ConfigError('No config found (or config file empty?)') - # interpolate program name - # add quotes around $SEEDFILE - t = Template(self.config['target']['cmdline_template']) - self.config['target']['cmdline_template'] = t.safe_substitute(PROGRAM=quoted(self.config['target']['program']), - SEEDFILE=quoted('$SEEDFILE')) - - campaign_id = re.sub('\s+', '_', self.config['campaign']['id']) - self.config['campaign']['id'] = campaign_id - - - - def _add_validations(self): - pass diff --git a/src/certfuzz/config/config_linux.py b/src/certfuzz/config/config_linux.py deleted file mode 100644 index 470cd71..0000000 --- a/src/certfuzz/config/config_linux.py +++ /dev/null @@ -1,148 +0,0 @@ -''' -Created on Oct 30, 2014 - -@organization: cert.org -''' -from functools import partial -import logging -import os -import re -import shlex -import shutil - -from certfuzz.config.config_base import ConfigBase -from certfuzz.fuzztools import filetools -from certfuzz.helpers.misc import quoted - - -logger = logging.getLogger(__name__) - -_DTYPES = {'timeouts': {'killproctimeout': int, - 'watchdogtimeout': int, - 'debugger_timeout': float, - 'progtimeout': float, - 'valgrindtimeout': int, - 'minimizertimeout': int, - }, - 'zzuf': {'copymode': bool, - 'start_seed': int, - 'seed_interval': int, - }, - 'verifier': {'backtracelevels': int, - 'keep_duplicates': bool, - 'exclude_unmapped_frames': bool, - 'minimizecrashers': bool, - 'minimize_to_string': bool, - 'use_valgrind': bool, - 'use_pin_calltrace': bool, - 'savefailedasserts': bool, - 'recycle_crashers': bool, - }, - 'directories': {'seedfile_origin_dir': str, - 'debugger_template_dir': str, - 'local_dir': str, - 'output_dir': str, - 'watchdog_file': str, - }, - 'target': { - 'killprocname': str, - 'cmdline': str, - } - } - -UNIQ_LOG = "uniquelog.txt" -LAST_SEEDFILE = 'lastseed' - -MINIMIZED_EXT = "minimal" -ZZUF_LOG_FILE = 'zzuf_log.txt' -RANGE_LOG = 'rangelog.txt' -CRASH_EXIT_CODE_FILE = "crashexitcodes" -CACHED_CONFIG_OBJECT_FILE = 'config.pkl' -CACHED_SEEDRANGE_OBJECT_FILE = 'seedrange.pkl' -CACHED_RANGEFINDER_OBJECT_FILE = 'rangefinder.pkl' -CACHED_SEEDFILESET_OBJECT_FILE = 'seedfile_set.pkl' - -class LinuxConfig(ConfigBase): - ''' - Defines a linux-specific configuration file format - ''' - def _set_derived_options(self): - ConfigBase._set_derived_options(self) - - # [campaign] - campaign_id = re.sub('\s+', '_', self.config['campaign']['id']) - self.config['campaign']['id'] = campaign_id - - # unroll ~ & relative paths - dir_dict = self.config['directories'] - for k, path in dir_dict.iteritems(): - dir_dict[k] = os.path.abspath(os.path.expanduser(path)) - - # cast the data to the expected type - for k, dtypes in _DTYPES.iteritems(): - # pick the top-level config block to work with - cfgblock = self.config[k] - for key, dtype in dtypes.iteritems(): - # get the value if it's present - # otherwise take the default type value (e.g., int() = 0, str() = '') - val = cfgblock.get(key, dtype()) - # set the attribute, casting it to the expected type - setattr(self, key, dtype(val)) - -# self.cmd_list = shlex.split(self.cmdline) -# for index, cmd_part in enumerate(self.cmd_list): -# self.cmd_list[index] = os.path.expanduser(cmd_part) -# if re.search(' ', self.cmd_list[0]): -# self.cmd_list[0] = quoted(self.cmd_list[0]) -# self.program = self.cmd_list[0] -# self._cmd = self.cmd_list -# self._args = self.cmd_list[1:] - - # for backwards compatibility - self.watchdogfile = self.watchdog_file - - # derived properties - self.program_basename = os.path.basename(self.program).replace('"', '') - self.uniq_log = os.path.join(self.output_dir, UNIQ_LOG) - self.crashexitcodesfile = os.path.join(self.local_dir, CRASH_EXIT_CODE_FILE) - self.zzuf_log_file = os.path.join(self.local_dir, ZZUF_LOG_FILE) - -# def get_command(self, filepath): -# return ' '.join(self.get_command_list(filepath)) - -# def get_command_list(self, seedfile): -# cmdlst = [self.program] -# cmdlst.extend(self.get_command_args_list(seedfile)) -# return cmdlst - - def get_command_args_list(self, seedfile): - arglist = [] - for arg in self._args: - arglist.append(re.sub(SEEDFILE_REPLACE_STRING, seedfile, arg)) - return arglist - - -# def get_minimized_file(self, outfile): -# ''' -# @rtype: string -# @return: -. -# ''' -# (head, tail) = os.path.split(outfile) -# (root, ext) = os.path.splitext(tail) -# new_filename = '%s-%s%s' % (root, MINIMIZED_EXT, ext) -# return os.path.join(head, new_filename) - -# def get_testcase_outfile(self, seedfile, s1): -# # TODO: this should become part of campaign object -# ''' -# @rtype: string -# @return: the path to the output file for this seed: . -# ''' -# (dirname, basename) = os.path.split(seedfile) # @UnusedVariable -# (root, ext) = os.path.splitext(basename) -# new_root = '%s-%d' % (root, s1) -# new_basename = '%s%s' % (new_root, ext) -# self.create_tmpdir() -# return os.path.join(self.tmpdir, new_basename) - - diff --git a/src/certfuzz/config/config_windows.py b/src/certfuzz/config/config_windows.py deleted file mode 100644 index 653cff6..0000000 --- a/src/certfuzz/config/config_windows.py +++ /dev/null @@ -1,74 +0,0 @@ -''' -Created on Feb 9, 2012 - -@organization: cert.org -''' -import logging -from string import Template - -from certfuzz.config.config_base import ConfigBase -from certfuzz.config.errors import ConfigError -from certfuzz.helpers import quoted - - -logger = logging.getLogger(__name__) - - -class WindowsConfig(ConfigBase): - def _add_validations(self): - self.validations.append(self._validate_debugger_timeout_exceeds_runner) - self.validations.append(self._validate_new_options) - - def _set_derived_options(self): - ConfigBase._set_derived_options(self) - - # interpolate program name - # add quotes around $SEEDFILE - t = Template(self.config['target']['cmdline_template']) - self.config['target']['cmdline_template'] = t.safe_substitute(PROGRAM=quoted(self.config['target']['program']), - SEEDFILE=quoted('$SEEDFILE')) - - def _validate_new_options(self): - if 'minimizer_timeout' not in self.config['runoptions']: - self.config['runoptions']['minimizer_timeout'] = 3600 - - def _validate_debugger_timeout_exceeds_runner(self): - try: - runner_section = self.config['runner'] - except KeyError: - return - - # if runner is null, we're just going to use the debugger timeout - try: - runner = runner_section['runner'] - if not runner: - return - - except KeyError: - return - - try: - run_timeout = runner_section['runtimeout'] - except KeyError: - return - - if not run_timeout: - raise ConfigError('Runner timeout cannot be zero') - - try: - debugger_section = self.config['debugger'] - except KeyError: - return - - try: - dbg_timeout = debugger_section['runtimeout'] - except KeyError: - return - - if not dbg_timeout: - raise ConfigError('Debugger timeout cannot be zero') - - if dbg_timeout < (2 * run_timeout): - logger.warning('Debugger timeout must be >= 2 * runner timeout.') - self.config['debugger']['runtimeout'] = 2.0 * run_timeout - logger.warning('Setting debugger timeout = %s instead', self.config['debugger']['runtimeout']) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 5bd7c57..016fd4a 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -62,6 +62,7 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): # remove empty args from the list [Fix for BFF-17] # ['a','','b','c'] -> ['a', 'b', 'c'] args = [arg for arg in args if arg] + for index, arg in enumerate(args): args[index] = string.replace(args[index], '"', '') diff --git a/src/certfuzz/helpers/misc.py b/src/certfuzz/helpers/misc.py index 3b828bd..7402e21 100644 --- a/src/certfuzz/helpers/misc.py +++ b/src/certfuzz/helpers/misc.py @@ -9,10 +9,24 @@ import random import string import os +import sys +logger=logging.getLogger(__name__) my_os = platform.system() +def import_module_by_name(name): + ''' + Imports a module at runtime given the pythonic name of the module + e.g., certfuzz.fuzzers.bytemut + :param name: + :param logger: + ''' + if logger: + logger.debug('Importing module %s', name) + __import__(name) + module = sys.modules[name] + return module def fixup_path(path): ''' diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 039f137..912d907 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -73,7 +73,7 @@ def _pre_run(self): if self.quiet_flag: self._runner_options['hideoutput'] = True - self._runner_cmd_template = self.cfg['target']['cmdline'] + self._runner_cmd_template = self.cfg['target']['cmdline_template'] IterationBase3._pre_run(self) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 81edbe4..84a7903 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -64,7 +64,7 @@ def __init__(self, # TODO: do we use keep_uniq_faddr at all? self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] - self.cmd_template = string.Template(cmd_template) + self.cmd_template = cmd_template if self.runner_cls is None: # null runner_cls case diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 61dc909..e3ab78e 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -23,6 +23,7 @@ from certfuzz.analyzers import pin_calltrace from certfuzz.analyzers.errors import AnalyzerEmptyOutputError from certfuzz.debuggers.output_parsers.calltracefile import Calltracefile +from certfuzz.fuzztools.command_line_templating import get_command_args_list logger = logging.getLogger(__name__) @@ -170,7 +171,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, filetools.copy_file(self.crash.fuzzedfile.path, self.tempfile) # figure out what crash signatures belong to this fuzzedfile - self.debugger_timeout = self.cfg.debugger_timeout + self.debugger_timeout = self.cfg['timeouts']['debugger_timeout'] self.crash_hashes = [] self.measured_dbg_time = None self._set_crash_hashes() @@ -348,7 +349,7 @@ def _set_crash_hashes(self): if dbg.is_crash: times.append(delta) - current_sig = self.get_signature(dbg, self.cfg.backtracelevels) + current_sig = self.get_signature(dbg, self.cfg['verifier']['backtracelevels']) # ditch the temp file if os.path.exists(f): @@ -379,15 +380,16 @@ def _set_crash_hashes(self): def run_debugger(self, infile, outfile): self.debugger_runs += 1 - cmd_args = self.cfg.get_command_args_list(infile) + cmd_args = get_command_args_list(self.cfg['target']['cmdline_template'],infile)[1] +# cmd_args = self.cfg.get_command_args_list(infile) - dbg = self._debugger_cls(self.cfg.program, + dbg = self._debugger_cls(cmd_args[0], cmd_args, outfile, self.debugger_timeout, - self.cfg.killprocname, + self.cfg['target']['killprocname'], template=self.crash.debugger_template, - exclude_unmapped_frames=self.cfg.exclude_unmapped_frames, + exclude_unmapped_frames=self.cfg['verifier']['exclude_unmapped_frames'], keep_uniq_faddr=self.keep_uniq_faddr, workingdir=self.tempdir, watchcpu=self.watchcpu @@ -468,7 +470,7 @@ def is_same_crash(self): dbg = self.run_debugger(self.tempfile, f) if dbg.is_crash: - newfuzzed_hash = self.get_signature(dbg, self.cfg.backtracelevels) + newfuzzed_hash = self.get_signature(dbg, self.cfg['verifier']['backtracelevels']) else: newfuzzed_hash = None # initialize or increment the counter for this hash @@ -614,7 +616,7 @@ def go(self): if self.use_watchdog: # touch the watchdog file so we don't reboot during long minimizations - open(self.cfg.watchdogfile, 'w').close() + open(self.cfg['directories']['watchdog_file'], 'w').close() # Fix for BFF-208 if self._time_exceeded(): diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index af5c8d7..488fbe6 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -36,7 +36,7 @@ def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): self.zzuf_log_path = os.path.join(self.workingdir, self._zzuf_log_basename) self._quiet = options.get('hideoutput', True) - self._cmd_template = Template(cmd_template) + self._cmd_template = cmd_template self._cmd = self._cmd_template.substitute(SEEDFILE=quoted(fuzzed_file)) self._cmd_parts = shlex.split(self._cmd) self._cmd_parts[0] = os.path.expanduser(self._cmd_parts[0]) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 25b7846..1c1ae55 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -68,7 +68,7 @@ def _verify(self, testcase): # crash_dir_found = filetools.find_or_create_dir(tc.result_dir) - keep_all = self.cfg.config['verifier'].get('keep_duplicates', False) + keep_all = self.cfg['verifier'].get('keep_duplicates', False) tc.should_proceed_with_analysis = keep_all or (is_new_to_campaign and not crash_dir_found) @@ -92,11 +92,11 @@ def _minimize(self, testcase): self._minimize_to_string(testcase) def _post_minimize(self, testcase): - if self.cfg.config['verifier']['recycle_crashers']: + if self.cfg['verifier']['recycle_crashers']: logger.debug('Recycling crash as seedfile') iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext - crasherseed_path = os.path.join(self.cfg.config['directories']['seedfile_origin_dir'], crasherseedname) + crasherseed_path = os.path.join(self.cfg['directories']['seedfile_origin_dir'], crasherseedname) filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) self.sf_set.add_file(crasherseed_path) From 797c925fcfccbdc3b572a4db9a2078aa327a7413 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 25 Jan 2016 16:00:28 -0500 Subject: [PATCH 0717/1169] make analyzers work with new, simpler config dict --- src/certfuzz/analyzers/analyzer_base.py | 3 ++- src/certfuzz/analyzers/callgrind/callgrind.py | 2 +- src/certfuzz/analyzers/cw_gmalloc.py | 2 +- src/certfuzz/analyzers/pin_calltrace.py | 2 +- src/certfuzz/analyzers/stderr.py | 2 +- src/certfuzz/analyzers/valgrind.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/analyzers/analyzer_base.py b/src/certfuzz/analyzers/analyzer_base.py index fcb9d50..1989052 100644 --- a/src/certfuzz/analyzers/analyzer_base.py +++ b/src/certfuzz/analyzers/analyzer_base.py @@ -8,6 +8,7 @@ from certfuzz.fuzztools import subprocess_helper as subp from certfuzz.analyzers.errors import AnalyzerOutputMissingError, AnalyzerEmptyOutputError +from certfuzz.fuzztools.command_line_templating import get_command_args_list logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -22,7 +23,7 @@ def __init__(self, cfg, crash, outfile=None, timeout=None, **options): self.cfg = cfg self.crash = crash - self.cmdargs = cfg.get_command_list(crash.fuzzedfile.path) + self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'],crash.fuzzedfile.path)[1] self.outfile = outfile self.timeout = float(timeout) self.killprocname = crash.killprocname diff --git a/src/certfuzz/analyzers/callgrind/callgrind.py b/src/certfuzz/analyzers/callgrind/callgrind.py index 858fb1f..25314bb 100644 --- a/src/certfuzz/analyzers/callgrind/callgrind.py +++ b/src/certfuzz/analyzers/callgrind/callgrind.py @@ -20,7 +20,7 @@ class Callgrind(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg.valgrindtimeout + timeout = cfg['timeouts']['valgrindtimeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True diff --git a/src/certfuzz/analyzers/cw_gmalloc.py b/src/certfuzz/analyzers/cw_gmalloc.py index 65bcd57..1d6bb5b 100644 --- a/src/certfuzz/analyzers/cw_gmalloc.py +++ b/src/certfuzz/analyzers/cw_gmalloc.py @@ -30,7 +30,7 @@ def __init__(self, cfg, crash): return None outfile = get_file(crash.fuzzedfile.path) - timeout = cfg.debugger_timeout + timeout = cfg['timeouts']['debugger_timeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout) diff --git a/src/certfuzz/analyzers/pin_calltrace.py b/src/certfuzz/analyzers/pin_calltrace.py index 2057590..4658859 100644 --- a/src/certfuzz/analyzers/pin_calltrace.py +++ b/src/certfuzz/analyzers/pin_calltrace.py @@ -21,7 +21,7 @@ class Pin_calltrace(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg.valgrindtimeout * 10 + timeout = cfg['timeouts']['valgrindtimeout'] * 10 Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True diff --git a/src/certfuzz/analyzers/stderr.py b/src/certfuzz/analyzers/stderr.py index 57a6884..1ee905a 100644 --- a/src/certfuzz/analyzers/stderr.py +++ b/src/certfuzz/analyzers/stderr.py @@ -19,7 +19,7 @@ class StdErr(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg.progtimeout + timeout = cfg['timeouts']['progtimeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout, stderr=outfile) # need to set the stderr_redirect flag on the base class diff --git a/src/certfuzz/analyzers/valgrind.py b/src/certfuzz/analyzers/valgrind.py index 8642868..d6e1867 100644 --- a/src/certfuzz/analyzers/valgrind.py +++ b/src/certfuzz/analyzers/valgrind.py @@ -20,7 +20,7 @@ class Valgrind(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg.valgrindtimeout + timeout = cfg['timeouts']['valgrindtimeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True From eaa39dc6740caf3631177c2c40ce98cef6e77855 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 08:25:28 -0500 Subject: [PATCH 0718/1169] remove unused import --- src/certfuzz/campaign/campaign_windows.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 57073ff..10c0306 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -10,7 +10,6 @@ from threading import Timer from certfuzz.campaign.campaign_base import CampaignBase -from certfuzz.config.config_windows import WindowsConfig from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.fuzzers.errors import FuzzerExhaustedError from certfuzz.iteration.iteration_windows import WindowsIteration From 9f479708378acfb7554050562be84212244162b1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 08:25:39 -0500 Subject: [PATCH 0719/1169] fix unit test --- src/test_certfuzz/analyzers/test_stderr.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/test_certfuzz/analyzers/test_stderr.py b/src/test_certfuzz/analyzers/test_stderr.py index e5441c1..8a541c9 100644 --- a/src/test_certfuzz/analyzers/test_stderr.py +++ b/src/test_certfuzz/analyzers/test_stderr.py @@ -9,6 +9,7 @@ ''' import unittest import sys +import string class Mock(object): pass @@ -24,12 +25,15 @@ def setUp(self): self.delete_file(f) self.file = '%s.stderr' % f - cfg = Mock() - cfg.progtimeout = 1 - if sys.platform == 'win32': - cfg.get_command_list = lambda x: ['c:\\cygwin\\bin\\cat.exe', '-a', 'foo'] + cfg = {'timeouts': {'progtimeout':1}, + 'target': {'cmdline_template': string.Template('PROGRAM $SEEDFILE foo')} + } + + if sys.platform=='win32': + cfg['target']['cmdline_tempate']=string.Template('c:\\cygwin\\bin\\cat.exe -a foo') else: - cfg.get_command_list = lambda x: ['cat', '-a', 'foo'] + cfg['target']['cmdline_template'] = string.Template('cat -a foo') + crash = Mock() crash.fuzzedfile = Mock() From 9e29b6be0b0aeb6c40c4dd52483e3308c6c18b24 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 08:34:11 -0500 Subject: [PATCH 0720/1169] fix imports --- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index 82f3fa3..ed39f52 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -6,12 +6,12 @@ import logging import os -from certfuzz.config.config_windows import get_command_args_list from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.fuzztools import filetools from certfuzz.minimizer.errors import MinimizerError from certfuzz.reporters.copy_files import CopyFilesReporter +from certfuzz.fuzztools.command_line_templating import get_command_args_list logger = logging.getLogger(__name__) From 270eb7fd90e41c9e3540d3f945d6016ba67ea689 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 08:34:24 -0500 Subject: [PATCH 0721/1169] add unit test --- .../fuzztools/test_command_line_templating.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/test_certfuzz/fuzztools/test_command_line_templating.py diff --git a/src/test_certfuzz/fuzztools/test_command_line_templating.py b/src/test_certfuzz/fuzztools/test_command_line_templating.py new file mode 100644 index 0000000..edffc9e --- /dev/null +++ b/src/test_certfuzz/fuzztools/test_command_line_templating.py @@ -0,0 +1,37 @@ +''' +Created on Jan 26, 2016 + +@author: adh +''' +import unittest +import certfuzz.fuzztools.command_line_templating as clt +import string + + +class Test(unittest.TestCase): + + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_command_args_list(self): + cmd = 'program arg1 arg2 $SEEDFILE arg3 arg4' + cmd_template = string.Template(cmd) + infile = 'foobar' + + result=clt.get_command_args_list(cmd_template,infile) + self.assertEqual(2, len(result)) + + (as_string,as_list) = result + + for substring in ['program','arg1','arg2','arg3','arg4','foobar']: + self.assertTrue(substring in as_string) + self.assertTrue(substring in as_list) + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file From 7a650387158d6cf0cd749cee79df3c4c1b733ce6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 08:37:33 -0500 Subject: [PATCH 0722/1169] fix unit tests --- src/test_certfuzz/runners/test_zzufrun.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test_certfuzz/runners/test_zzufrun.py b/src/test_certfuzz/runners/test_zzufrun.py index 19554f3..d75e6be 100644 --- a/src/test_certfuzz/runners/test_zzufrun.py +++ b/src/test_certfuzz/runners/test_zzufrun.py @@ -11,6 +11,7 @@ import os import stat from certfuzz.runners.errors import RunnerNotFoundError +import string class Test(unittest.TestCase): @@ -34,27 +35,27 @@ def setUp(self): self._basename_saved = certfuzz.runners.zzufrun._zzuf_basename + self.cmd_template = string.Template('foo bar') + def tearDown(self): shutil.rmtree(self.tmpdir) certfuzz.runners.zzufrun._zzuf_basename = self._basename_saved def test_quiet_flag(self): - cmd_template = 'foo bar' - zr = ZzufRunner(options={'hideoutput': True}, cmd_template=cmd_template, + zr = ZzufRunner(options={'hideoutput': True}, cmd_template=self.cmd_template, fuzzed_file=None, workingdir_base=self.tmpdir) self.assertTrue(zr._quiet) with zr: self.assertTrue('--quiet' in zr._zzuf_args) - zr = ZzufRunner(options={'hideoutput': False}, cmd_template=cmd_template, + zr = ZzufRunner(options={'hideoutput': False}, cmd_template=self.cmd_template, fuzzed_file=None, workingdir_base=self.tmpdir) self.assertFalse(zr._quiet) with zr: self.assertFalse('--quiet' in zr._zzuf_args) def test_find_zzuf(self): - cmd_template = 'foo bar' (fd, fname) = tempfile.mkstemp(prefix='zzufrun_test_', dir=self.tmpdir) os.close(fd) os.remove(fname) @@ -69,7 +70,6 @@ def test_find_zzuf(self): def test_run(self): options = {} - cmd_template = 'foo bar' touch = '/usr/bin/touch' if not os.path.exists(touch): @@ -77,7 +77,7 @@ def test_run(self): return for _ in xrange(100): - zr = ZzufRunner(options, cmd_template, self.ff, self.tmpdir) + zr = ZzufRunner(options, self.cmd_template, self.ff, self.tmpdir) fd, fname = tempfile.mkstemp(prefix='zzufrun_test_', dir=self.tmpdir) os.close(fd) os.remove(fname) From a876c5629342502a21c02e2e6c6093f3a8120ae7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 08:46:46 -0500 Subject: [PATCH 0723/1169] fix unit tests --- .../minimizer/test_minimizer_base.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/test_certfuzz/minimizer/test_minimizer_base.py b/src/test_certfuzz/minimizer/test_minimizer_base.py index dff3b04..70a3b7e 100644 --- a/src/test_certfuzz/minimizer/test_minimizer_base.py +++ b/src/test_certfuzz/minimizer/test_minimizer_base.py @@ -10,18 +10,20 @@ import shutil from certfuzz.file_handlers.basicfile import BasicFile import unittest -import certfuzz.minimizer.minimizer_base +import string class Mock(object): def __init__(self, *args, **kwargs): pass -class MockCfg(Mock): - program = 'a' - debugger_timeout = 1 - killprocname = 'a' - exclude_unmapped_frames = False - backtracelevels = 5 + +class MockCfg(dict): + def __init__(self): + self['timeouts']={'debugger_timeout': 1} + self['target']={'cmdline_template': string.Template('a b c d'), + 'killprocname': 'a'} + self['verifier']={'exclude_unmapped_frames': False, + 'backtracelevels': 5} def get_command_args_list(self, dummy): return tuple('abcd') @@ -59,7 +61,7 @@ def setUp(self): self.cfg = MockCfg() self.crash = MockCrasher() - certfuzz.minimizer.minimizer_base.Minimizer._debugger_cls = MockDebugger + Minimizer._debugger_cls = MockDebugger self.tempdir = tempfile.mkdtemp(prefix='minimizer_test_') self.crash_dst_dir = tempfile.mkdtemp(prefix='crash_', dir=self.tempdir) From 1e7ac2039eeebcd8fe9c29b98eb8a7d61f87efc2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 09:08:01 -0500 Subject: [PATCH 0724/1169] fix unit tests --- src/certfuzz/config/simple_loader.py | 4 ++++ .../campaign/test_campaign_linux.py | 21 ++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/config/simple_loader.py b/src/certfuzz/config/simple_loader.py index dcc24c2..c1ce835 100644 --- a/src/certfuzz/config/simple_loader.py +++ b/src/certfuzz/config/simple_loader.py @@ -14,6 +14,10 @@ def load_config(yaml_file): with open(yaml_file, 'rb') as f: cfg = yaml.load(f) + # yaml.load returns None if the file is empty. We need to raise an error + from errors import ConfigError + if cfg is None: + raise(ConfigError,'Config file was empty') # add the file timestamp so we can tell if it changes later cfg['config_timestamp'] = os.path.getmtime(yaml_file) diff --git a/src/test_certfuzz/campaign/test_campaign_linux.py b/src/test_certfuzz/campaign/test_campaign_linux.py index 0ca4bac..d7b8f28 100644 --- a/src/test_certfuzz/campaign/test_campaign_linux.py +++ b/src/test_certfuzz/campaign/test_campaign_linux.py @@ -8,8 +8,8 @@ import tempfile import os import shutil -from ConfigParser import NoSectionError from certfuzz.config.errors import ConfigError +import yaml class Test(unittest.TestCase): @@ -18,13 +18,17 @@ def setUp(self): self.tmpdir = tempfile.mkdtemp() fd, cfgfile = tempfile.mkstemp(suffix=".yaml", dir=self.tmpdir, text=True) os.close(fd) - import yaml + data = {'campaign': {'id': 'foo'}, - 'directories': {}, + 'directories': {'seedfile_origin_dir': tempfile.mkdtemp(prefix='seedfiles_', dir=self.tmpdir), + 'output_dir': tempfile.mkdtemp(prefix='output_', dir=self.tmpdir), + 'local_dir': tempfile.mkdtemp(prefix='local_', dir=self.tmpdir)}, 'timeouts': {}, - 'zzuf': {}, + 'zzuf': {'start_seed': 0, + 'seed_interval': 10}, 'verifier': {}, - 'target': {'cmdline': 'bar baz quux'}, + 'target': {'program': 'foo', + 'cmdline_template': 'foo bar baz quux'}, } with open(cfgfile, 'wb') as stream: yaml.dump(data, stream) @@ -35,9 +39,16 @@ def tearDown(self): shutil.rmtree(self.tmpdir) def test_init_without_config(self): + # test empty config file _fd, cfgfile = tempfile.mkstemp(suffix=".yaml", dir=self.tmpdir, text=True) self.assertRaises(ConfigError, LinuxCampaign, cfgfile) + # test non-existent config file + os.unlink(cfgfile) + self.assertFalse(os.path.exists(cfgfile)) + self.assertRaises(IOError,LinuxCampaign,cfgfile) + + def test_check_program_file_type(self): fd, fname = tempfile.mkstemp(dir=self.tmpdir, text=True) os.write(fd, 'sometext') From 6588a9e10da372540796a80f68eb0676602547cb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 09:13:23 -0500 Subject: [PATCH 0725/1169] remove old unit tests --- src/test_certfuzz/config/test_config_base.py | 90 ------------ src/test_certfuzz/config/test_config_linux.py | 138 ------------------ .../config/test_config_windows.py | 98 ------------- 3 files changed, 326 deletions(-) delete mode 100644 src/test_certfuzz/config/test_config_base.py delete mode 100644 src/test_certfuzz/config/test_config_linux.py delete mode 100644 src/test_certfuzz/config/test_config_windows.py diff --git a/src/test_certfuzz/config/test_config_base.py b/src/test_certfuzz/config/test_config_base.py deleted file mode 100644 index 0e78bb4..0000000 --- a/src/test_certfuzz/config/test_config_base.py +++ /dev/null @@ -1,90 +0,0 @@ -''' -Created on Apr 10, 2012 - -@organization: cert.org -''' - -import os -import pprint -import shutil -import tempfile -import unittest - -import yaml - -import certfuzz.config.config_base as config -from certfuzz.config.errors import ConfigError - - -_count = 0 -def _counter(): - global _count - _count += 1 - -class Test(unittest.TestCase): - - def setUp(self): - self.tempdir = tempfile.mkdtemp() - - def tearDown(self): - shutil.rmtree(self.tempdir) - - def _write_yaml(self, thing=None): - if thing is None: - thing = dict([(y, x) for x, y in enumerate("abcd")]) - fd, f = tempfile.mkstemp(suffix='yaml', dir=self.tempdir) - os.close(fd) - with open(f, 'wb') as fd: - yaml.dump(thing, fd) - - return thing, f - - def test_parse_yaml(self): - thing, f = self._write_yaml() - - self.assertTrue(os.path.exists(f)) - self.assertTrue(os.path.getsize(f) > 0) - - from_yaml = config.parse_yaml(f) - self.assertEqual(thing, from_yaml) - - def test_config_init(self): - _junk, f = self._write_yaml() - c = config.ConfigBase(f) - self.assertEqual(f, c.file) - - def test_validate(self): - dummy, f = self._write_yaml() - c = config.ConfigBase(f) - # add some validations - c.validations.append(_counter) - c.validations.append(_counter) - c.validations.append(_counter) - - # confirm that each validation got run - self.assertEqual(0, _count) - c.validate() - self.assertEqual(3, _count) - - def test_load(self): - # write another yaml file - thing, f = self._write_yaml() - # sub the new file name - with config.ConfigBase(f) as c: - # we should get the thing back again - self.assertEqual(thing, c.config) - - # load should add each of the things as - # config attributes - for k, v in thing.iteritems(): - self.assertTrue(hasattr(c, k)) - self.assertEqual(c.__getattribute__(k), v) - - def test_set_derived_options(self): - c = config.ConfigBase('foo') - self.assertEqual(None, c.config) - self.assertRaises(ConfigError, c._set_derived_options) - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/test_certfuzz/config/test_config_linux.py b/src/test_certfuzz/config/test_config_linux.py deleted file mode 100644 index 91f370d..0000000 --- a/src/test_certfuzz/config/test_config_linux.py +++ /dev/null @@ -1,138 +0,0 @@ -''' -Created on Nov 11, 2014 - -@author: adh -''' -import os -from pprint import pprint -import shutil -import tempfile -import unittest - -import yaml - -from certfuzz.config.config_linux import LinuxConfig, MINIMIZED_EXT -import certfuzz.config.config_linux as cl -from certfuzz.config.errors import ConfigError - - -CFG = ''' -campaign: - id: convert v5.2.0 -target: - cmdline: ~/convert $SEEDFILE /dev/null - killprocname: convert -directories: - seedfile_origin_dir: ~/bff/seedfiles/examples - debugger_template_dir: ~/bff/certfuzz/debuggers/templates - output_dir: ~/results - local_dir: ~/fuzzing - watchdog_file: /tmp/bff_watchdog -zzuf: - copymode: 1 - start_seed: 0 - seed_interval: 20 -verifier: - backtracelevels: 5 - keep_duplicates: False - exclude_unmapped_frames: True - savefailedasserts: False - use_valgrind: True - use_pin_calltrace: False - minimizecrashers: True - minimize_to_string: False - recycle_crashers: False -timeouts: - progtimeout: 5 - killproctimeout: 130 - debugger_timeout: 60 - valgrindtimeout: 120 - watchdogtimeout: 3600 - minimizertimeout: 3600 -''' - -class Test(unittest.TestCase): - - def setUp(self): - self.c = yaml.load(CFG) - self.tmpdir = tempfile.mkdtemp() - fd, self.yamlfile = tempfile.mkstemp(suffix='.yaml', dir=self.tmpdir, text=True) - os.close(fd) - with open(self.yamlfile, 'w') as stream: - yaml.dump(self.c, stream) - - self.cfg = LinuxConfig(self.yamlfile) - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - def test_init(self): - self.assertEqual(self.yamlfile, self.cfg.file) - self.assertEqual(None, self.cfg.config) - self.assertEqual(None, self.cfg.configdate) - for key in self.c.iterkeys(): - self.assertFalse(hasattr(self.cfg, key)) - - def test_contextmgr(self): - # expect to throw an error if the file doesn't exist - bogusfile = os.path.join(self.tmpdir, 'nonexistent_file') - cfg = LinuxConfig(bogusfile) - self.assertRaises(ConfigError, cfg.__enter__) - - with self.cfg: - self.assertNotEqual(None, self.cfg.config) - self.assertEqual(dict, type(self.cfg.config)) - self.assertNotEqual(None, self.cfg.configdate) - for key in self.c.iterkeys(): - self.assertTrue(hasattr(self.cfg, key)) - - def test_set_derived_options(self): - with self.cfg: - for blockname, dtypes in cl._DTYPES.iteritems(): - for k, dtype in dtypes.iteritems(): - self.assertTrue(hasattr(self.cfg, k)) - attr_val = getattr(self.cfg, k) - self.assertEqual(dtype, type(attr_val)) - if blockname == 'directories': - continue - self.assertEqual(self.c[blockname][k], attr_val) - self.assertTrue(self.cfg.uniq_log.endswith(cl.UNIQ_LOG)) - self.assertTrue(self.cfg.crashexitcodesfile.endswith(cl.CRASH_EXIT_CODE_FILE)) - self.assertTrue(self.cfg.zzuf_log_file.endswith(cl.ZZUF_LOG_FILE)) - - def test_get_command(self): - with self.cfg: - self.assertIn('foo', self.cfg.get_command('foo')) - - def test_get_command_list(self): - with self.cfg: - result = self.cfg.get_command_list('foo') - self.assertTrue(result[0].endswith('convert')) - self.assertEqual(result[1], 'foo') - self.assertEqual(result[2], '/dev/null') - - def test_get_command_args_list(self): - with self.cfg: - result = self.cfg.get_command_args_list('foo') - self.assertEqual(result[0], 'foo') - self.assertEqual(result[1], '/dev/null') - - def test_full_path_local_fuzz_dir(self): - with self.cfg: - result = self.cfg.full_path_local_fuzz_dir('foo') - self.assertEqual(str, type(result)) - self.assertTrue(result.endswith('foo')) - - def test_full_path_original(self): - with self.cfg: - result = self.cfg.full_path_original('foo') - self.assertEqual(str, type(result)) - self.assertTrue(result.endswith(os.path.join('foo', 'foo'))) - - def test_get_minimized_file(self): - with self.cfg: - self.assertEqual(self.cfg.get_minimized_file('foo.txt'), 'foo-%s.txt' % MINIMIZED_EXT) - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/src/test_certfuzz/config/test_config_windows.py b/src/test_certfuzz/config/test_config_windows.py deleted file mode 100644 index 97cf620..0000000 --- a/src/test_certfuzz/config/test_config_windows.py +++ /dev/null @@ -1,98 +0,0 @@ -''' -Created on Apr 2, 2012 - -@organization: cert.org -''' -import logging -import os -import shutil -import tempfile -import unittest - -import yaml - -from certfuzz.config.config_windows import WindowsConfig -from certfuzz.config.errors import ConfigError - - -logger = logging.getLogger() -hdlr = logging.FileHandler(os.devnull) -logger.addHandler(hdlr) - - -class Test(unittest.TestCase): - - def _get_minimal_config(self): - self.cfg_in = {'target': {'cmdline_template': '', - 'program': ''}, - 'runoptions': {}} - fd, f = tempfile.mkstemp(suffix='.yaml', dir=self.tempdir, text=True) - os.close(fd) - with open(f, 'w') as fd: - yaml.dump(self.cfg_in, fd) - - with WindowsConfig(f) as wcfg: - return wcfg - - def setUp(self): - self.tempdir = tempfile.mkdtemp() - - def tearDown(self): - shutil.rmtree(self.tempdir) - - def test_empty_cfg_raises_exception(self): - self.cfg_in = {} - fd, f = tempfile.mkstemp(suffix='.yaml', dir=self.tempdir, text=True) - os.close(fd) - with open(f, 'w') as fd: - yaml.dump(self.cfg_in, fd) - - wcfg = WindowsConfig(f) - self.assertRaises(ConfigError, wcfg._set_derived_options) - - def test_minimal_config(self): - try: - c = self._get_minimal_config() - except Exception, e: - self.fail('Exception on _get_minimal_config: %s' % e) - - self.assertTrue(c) - - def test_debugger_timeout_exceeds_runner(self): - c = self._get_minimal_config() - import itertools - - # no runner - c.config.update({'runner': {'runner': None}, - 'debugger': {'runtimeout': 37}}) - c.validate() - self.assertEqual(37, c.config['debugger']['runtimeout']) - - for (a, b) in itertools.product(range(10), range(10)): - c.config.update({'runner': {'runner': 'foo', 'runtimeout': a}, - 'debugger': {'runtimeout': b}}) - self.assertEqual(a, c.config['runner']['runtimeout']) - self.assertEqual(b, c.config['debugger']['runtimeout']) - - if a == 0 or b == 0: - self.assertRaises(ConfigError, c.validate) - continue - c.validate() - self.assertEqual(a, c.config['runner']['runtimeout']) - expected = max(b, (2 * a)) - self.assertEqual(expected, c.config['debugger']['runtimeout']) - - def _counter(self): - self.counter += 1 - - def test_validation(self): - c = self._get_minimal_config() - self.counter = 0 - c.validations = [self._counter] - for dummy in range(10): - c.validate() - self.assertEqual(10, self.counter) - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From a1df2e4b17e6c76da0e76e81674d8856c07634ef Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 09:13:34 -0500 Subject: [PATCH 0726/1169] add unit tests --- .../config/test_simple_loader.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test_certfuzz/config/test_simple_loader.py b/src/test_certfuzz/config/test_simple_loader.py index 19ccb14..4a83b52 100644 --- a/src/test_certfuzz/config/test_simple_loader.py +++ b/src/test_certfuzz/config/test_simple_loader.py @@ -9,6 +9,7 @@ import shutil import os import yaml +from certfuzz.config.errors import ConfigError class Test(unittest.TestCase): @@ -42,6 +43,25 @@ def test_load_config(self): self.assertEqual(thing, loaded) + def test_empty_config(self): + fd,fpath=tempfile.mkstemp(suffix='.yaml', prefix='empty_', dir=self.tempdir) + os.close(fd) + + # make sure it exists and is empty + self.assertTrue(os.path.exists(fpath)) + self.assertEqual(0,os.path.getsize(fpath)) + + self.assertRaises(ConfigError,certfuzz.config.simple_loader.load_config,fpath) + + def test_nonexistent_config(self): + fd,fpath=tempfile.mkstemp(suffix='.yaml', prefix='nonexistent_', dir=self.tempdir) + os.close(fd) + os.unlink(fpath) + self.assertFalse(os.path.exists(fpath)) + + self.assertRaises(IOError,certfuzz.config.simple_loader.load_config,fpath) + + if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] From 3b8d316cd9425d59c53a6cad87e67cc51e4cc067 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 09:35:22 -0500 Subject: [PATCH 0727/1169] relocate mocks --- .../minimizer/test_minimizer_base.py | 43 +------------------ src/test_certfuzz/mocks.py | 42 +++++++++++++++++- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/test_certfuzz/minimizer/test_minimizer_base.py b/src/test_certfuzz/minimizer/test_minimizer_base.py index 70a3b7e..7d0e536 100644 --- a/src/test_certfuzz/minimizer/test_minimizer_base.py +++ b/src/test_certfuzz/minimizer/test_minimizer_base.py @@ -8,49 +8,8 @@ from certfuzz.fuzztools import hamming from certfuzz.minimizer.minimizer_base import Minimizer import shutil -from certfuzz.file_handlers.basicfile import BasicFile import unittest -import string - -class Mock(object): - def __init__(self, *args, **kwargs): - pass - - -class MockCfg(dict): - def __init__(self): - self['timeouts']={'debugger_timeout': 1} - self['target']={'cmdline_template': string.Template('a b c d'), - 'killprocname': 'a'} - self['verifier']={'exclude_unmapped_frames': False, - 'backtracelevels': 5} - - def get_command_args_list(self, dummy): - return tuple('abcd') - -class MockCrasher(Mock): - def __init__(self): - fd, f = tempfile.mkstemp(suffix='.ext', prefix='fileroot') - os.close(fd) - self.fuzzedfile = BasicFile(f) - self.debugger_template = 'foo' - - def set_debugger_template(self, dummy): - pass - -class MockDbgOut(Mock): - is_crash = False - total_stack_corruption = False - - def get_crash_signature(self, *dummyargs): - return 'AAAAA' - -class MockDebugger(Mock): - def get(self): - return MockDebugger - - def go(self): - return MockDbgOut() +from test_certfuzz.mocks import MockCfg, MockDebugger, MockCrasher class Test(unittest.TestCase): def delete_file(self, f): diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index de2f372..585e627 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -5,9 +5,24 @@ ''' import hashlib import logging +import string +import tempfile +import os +from certfuzz.file_handlers.basicfile import BasicFile class Mock(object): - pass + def __init__(self, *args, **kwargs): + pass + +class MockCrasher(Mock): + def __init__(self): + fd, f = tempfile.mkstemp(suffix='.ext', prefix='fileroot') + os.close(fd) + self.fuzzedfile = BasicFile(f) + self.debugger_template = 'foo' + + def set_debugger_template(self, dummy): + pass class MockRange(Mock): def __init__(self): @@ -50,3 +65,28 @@ class MockTestcase(Mock): range = MockRange() fuzzedfile = MockFuzzedFile() pc = u'dummyPCstring' + +class MockDbgOut(Mock): + is_crash = False + total_stack_corruption = False + + def get_crash_signature(self, *dummyargs): + return 'AAAAA' + +class MockDebugger(Mock): + def get(self): + return MockDebugger + + def go(self): + return MockDbgOut() + +class MockCfg(dict): + def __init__(self): + self['timeouts']={'debugger_timeout': 1} + self['target']={'cmdline_template': string.Template('a b c d'), + 'killprocname': 'a'} + self['verifier']={'exclude_unmapped_frames': False, + 'backtracelevels': 5} + + def get_command_args_list(self, dummy): + return tuple('abcd') From d786b6670d8fe937fbdc7984e3eca6e5ec8fddf5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 26 Jan 2016 10:24:24 -0500 Subject: [PATCH 0728/1169] fix unit tests --- src/certfuzz/campaign/campaign_base.py | 2 +- src/certfuzz/config/simple_loader.py | 3 +- .../analyzers/test_analyzer_base.py | 19 +----------- src/test_certfuzz/analyzers/test_valgrind.py | 7 ++--- .../campaign/test_campaign_base.py | 17 +++++----- src/test_certfuzz/mocks.py | 31 ++++++++++++++----- 6 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index f2dfba1..2724d83 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -97,7 +97,7 @@ def _fixup_config(self): ''' Substitutes program name into command line template ''' - # fix target program path + # fix target program path self.config['target']['program'] = fixup_path(self.config['target']['program']) logger.info('Using target program: %s',self.config['target']['program']) diff --git a/src/certfuzz/config/simple_loader.py b/src/certfuzz/config/simple_loader.py index c1ce835..1e07bc1 100644 --- a/src/certfuzz/config/simple_loader.py +++ b/src/certfuzz/config/simple_loader.py @@ -6,6 +6,7 @@ import logging import yaml import os +from errors import ConfigError logger = logging.getLogger(__name__) @@ -15,9 +16,9 @@ def load_config(yaml_file): cfg = yaml.load(f) # yaml.load returns None if the file is empty. We need to raise an error - from errors import ConfigError if cfg is None: raise(ConfigError,'Config file was empty') + # add the file timestamp so we can tell if it changes later cfg['config_timestamp'] = os.path.getmtime(yaml_file) diff --git a/src/test_certfuzz/analyzers/test_analyzer_base.py b/src/test_certfuzz/analyzers/test_analyzer_base.py index 7d8e13d..962e364 100644 --- a/src/test_certfuzz/analyzers/test_analyzer_base.py +++ b/src/test_certfuzz/analyzers/test_analyzer_base.py @@ -6,26 +6,9 @@ import unittest from certfuzz.analyzers.analyzer_base import Analyzer +from test_certfuzz.mocks import MockCfg, MockCrash -class MockObj(object): - def __init__(self, **kwargs): - for (kw, arg) in kwargs: - self.__setattr__(kw, arg) -class MockCfg(MockObj): - - def get_command_list(self, *args): - pass - -class MockCrash(MockObj): - def __init__(self): - self.fuzzedfile = MockFile() - self.killprocname = 'killprocname' - -class MockFile(MockObj): - def __init__(self): - self.dirname = 'dirname' - self.path = 'path' class Test(unittest.TestCase): diff --git a/src/test_certfuzz/analyzers/test_valgrind.py b/src/test_certfuzz/analyzers/test_valgrind.py index b99faf4..e118993 100644 --- a/src/test_certfuzz/analyzers/test_valgrind.py +++ b/src/test_certfuzz/analyzers/test_valgrind.py @@ -7,6 +7,7 @@ @organization: cert.org ''' import unittest +from test_certfuzz.mocks import MockCfg class Mock(object): pass @@ -17,9 +18,7 @@ def delete_file(self, f): self.assertFalse(os.path.exists(f)) def setUp(self): - cfg = Mock() - cfg.valgrindtimeout = 1 - cfg.get_command_list = lambda x: ['x', 'y', 'z'] + cfg = MockCfg() crash = Mock() crash.fuzzedfile = Mock() @@ -32,7 +31,7 @@ def tearDown(self): pass def test_get_valgrind_cmdline(self): - expected = ["valgrind", "--log-file=foo.valgrind", "x", "y", "z"] + expected = ["valgrind", "--log-file=foo.valgrind", "a", "b", "c", "d"] self.assertEqual(self.vg._get_cmdline(), expected) diff --git a/src/test_certfuzz/campaign/test_campaign_base.py b/src/test_certfuzz/campaign/test_campaign_base.py index 28504a4..27da715 100644 --- a/src/test_certfuzz/campaign/test_campaign_base.py +++ b/src/test_certfuzz/campaign/test_campaign_base.py @@ -12,6 +12,8 @@ from certfuzz.campaign.campaign_base import CampaignBase import tempfile from certfuzz.campaign.errors import CampaignError +from test_certfuzz.mocks import MockCfg +import yaml class UnimplementedCampaign(CampaignBase): @@ -52,17 +54,16 @@ def _set_fuzzer(self): def _set_runner(self): pass - def _read_config_file(self): - pass - class Test(unittest.TestCase): def setUp(self): self.tmpdir = mkdtemp() - _fd, cfgfile = tempfile.mkstemp(suffix=".cfg", dir=self.tmpdir, text=True) - try: - self.campaign = ImplementedCampaign(cfgfile) - except TypeError as e: - self.fail('ImplementedCampaign does not match requirements: {}'.format(e)) + fd, cfgfile = tempfile.mkstemp(prefix='config_', suffix=".yaml", dir=self.tmpdir) + os.close(fd) + + cfg = MockCfg(templated=False) + with open(cfgfile,'wb') as f: + yaml.dump(cfg,f) + self.campaign = ImplementedCampaign(cfgfile) def tearDown(self): shutil.rmtree(self.tmpdir, ignore_errors=True) diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 585e627..6e2becf 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -24,6 +24,21 @@ def __init__(self): def set_debugger_template(self, dummy): pass +class MockObj(object): + def __init__(self, **kwargs): + for (kw, arg) in kwargs: + self.__setattr__(kw, arg) + +class MockCrash(MockObj): + def __init__(self): + self.fuzzedfile = MockFile() + self.killprocname = 'killprocname' + +class MockFile(MockObj): + def __init__(self): + self.dirname = 'dirname' + self.path = 'path' + class MockRange(Mock): def __init__(self): self.min = 0.01 @@ -81,12 +96,14 @@ def go(self): return MockDbgOut() class MockCfg(dict): - def __init__(self): - self['timeouts']={'debugger_timeout': 1} - self['target']={'cmdline_template': string.Template('a b c d'), - 'killprocname': 'a'} + def __init__(self,templated=True): + self['timeouts']={'debugger_timeout': 1, + 'valgrindtimeout': 1} + self['target']={'cmdline_template': 'a b c d', + 'killprocname': 'a', + 'program': 'foo'} self['verifier']={'exclude_unmapped_frames': False, 'backtracelevels': 5} - - def get_command_args_list(self, dummy): - return tuple('abcd') + self['directories'] ={} + if templated: + self['target']['cmdline_template'] = string.Template(self['target']['cmdline_template']) \ No newline at end of file From 471f36c62ba774e562e675b4b5696e045bc0bd34 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 08:48:37 -0500 Subject: [PATCH 0729/1169] get rid of imports in certfuzz.helpers.__init__ (+1 squashed commit) Squashed commits: [9ce38c5] get rid of imports in certfuzz.helpers.__init__ --- src/certfuzz/crash/crash_windows.py | 2 +- src/certfuzz/debuggers/nulldebugger.py | 2 +- src/certfuzz/fuzzers/fuzzer_base.py | 2 +- src/certfuzz/helpers/__init__.py | 5 --- src/test_certfuzz/helpers/test_helpers_pkg.py | 31 ------------------- src/test_certfuzz/helpers/test_misc.py | 15 +++++---- 6 files changed, 10 insertions(+), 47 deletions(-) delete mode 100644 src/test_certfuzz/helpers/test_helpers_pkg.py diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/crash/crash_windows.py index c92afa1..fbe1656 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/crash/crash_windows.py @@ -11,7 +11,7 @@ from certfuzz.crash.crash_base import Testcase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import best_effort_move -from certfuzz.helpers import random_str +from certfuzz.helpers.misc import random_str from certfuzz.debuggers.msec import MsecDebugger diff --git a/src/certfuzz/debuggers/nulldebugger.py b/src/certfuzz/debuggers/nulldebugger.py index 1b70df4..7ce2d15 100644 --- a/src/certfuzz/debuggers/nulldebugger.py +++ b/src/certfuzz/debuggers/nulldebugger.py @@ -7,7 +7,7 @@ import random from certfuzz.debuggers import allowed_exploitability_values, register -from certfuzz.helpers import random_str +from certfuzz.helpers.misc import random_str from . import Debugger diff --git a/src/certfuzz/fuzzers/fuzzer_base.py b/src/certfuzz/fuzzers/fuzzer_base.py index 51002e6..a1158ce 100644 --- a/src/certfuzz/fuzzers/fuzzer_base.py +++ b/src/certfuzz/fuzzers/fuzzer_base.py @@ -10,7 +10,7 @@ import zipfile from certfuzz.fuzztools.filetools import find_or_create_dir, write_file -from certfuzz.helpers import log_object +from certfuzz.helpers.misc import log_object MAXDEPTH = 3 diff --git a/src/certfuzz/helpers/__init__.py b/src/certfuzz/helpers/__init__.py index f12f92b..e69de29 100644 --- a/src/certfuzz/helpers/__init__.py +++ b/src/certfuzz/helpers/__init__.py @@ -1,5 +0,0 @@ -from certfuzz.helpers.misc import quoted -from certfuzz.helpers.misc import print_dict -from certfuzz.helpers.misc import random_str -from certfuzz.helpers.misc import bitswap -from certfuzz.helpers.misc import log_object diff --git a/src/test_certfuzz/helpers/test_helpers_pkg.py b/src/test_certfuzz/helpers/test_helpers_pkg.py deleted file mode 100644 index c69be1d..0000000 --- a/src/test_certfuzz/helpers/test_helpers_pkg.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Created on Oct 24, 2012 - -@organization: cert.org -''' -import unittest -from test_certfuzz import misc -import certfuzz.helpers - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_api(self): - module = certfuzz.helpers - api_list = ['quoted', - 'print_dict', - 'random_str', - 'bitswap', - 'log_object', - ] - (is_fail, msg) = misc.check_for_apis(module, api_list) - self.assertFalse(is_fail, msg) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/test_certfuzz/helpers/test_misc.py b/src/test_certfuzz/helpers/test_misc.py index 50b79cf..666bf23 100644 --- a/src/test_certfuzz/helpers/test_misc.py +++ b/src/test_certfuzz/helpers/test_misc.py @@ -5,8 +5,7 @@ ''' import unittest -import certfuzz.helpers as helpers -import platform +import certfuzz.helpers.misc as misc import string import itertools @@ -20,17 +19,17 @@ def tearDown(self): def test_fixup_path(self): path = '~/foo' - self.assertTrue('~' not in helpers.misc.fixup_path(path)) + self.assertTrue('~' not in misc.fixup_path(path)) def test_quoted(self): for s in list('qwertyuiopasdfghjklzxcvbnm'): - self.assertTrue(s in helpers.quoted(s)) - self.assertEqual('"' + s + '"', helpers.quoted(s)) + self.assertTrue(s in misc.quoted(s)) + self.assertEqual('"' + s + '"', misc.quoted(s)) def test_random_str(self): - self.assertEqual(1, len(helpers.random_str())) - random_string = helpers.random_str(100) + self.assertEqual(1, len(misc.random_str())) + random_string = misc.random_str(100) self.assertEqual(100, len(random_string)) for c in random_string: chars = string.ascii_letters + string.digits @@ -38,7 +37,7 @@ def test_random_str(self): def test_bitswap(self): for x, y in itertools.izip([0, 1, 2, 3, 4], [0, 128, 64, 192, 32]): - self.assertEqual(y, helpers.bitswap(x)) + self.assertEqual(y, misc.bitswap(x)) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] From bdc24cc3b53b24f9bf8273e71a54d550b713032f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 08:49:22 -0500 Subject: [PATCH 0730/1169] move get_command_args_list --- src/certfuzz/crash/crash_windows.py | 2 +- src/certfuzz/iteration/iteration_windows.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/crash/crash_windows.py index fbe1656..bb05e09 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/crash/crash_windows.py @@ -7,12 +7,12 @@ import logging import os -from certfuzz.config.config_windows import get_command_args_list from certfuzz.crash.crash_base import Testcase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import best_effort_move from certfuzz.helpers.misc import random_str from certfuzz.debuggers.msec import MsecDebugger +from certfuzz.fuzztools.command_line_templating import get_command_args_list logger = logging.getLogger(__name__) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 84a7903..b7d28f5 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -6,9 +6,7 @@ import glob import logging import os -import string -from certfuzz.config.config_windows import get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers.output_parsers.errors import DebuggerFileError from certfuzz.file_handlers.basicfile import BasicFile @@ -21,6 +19,7 @@ from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.runners.errors import RunnerRegistryError from certfuzz.testcase_pipeline.tc_pipeline_windows import WindowsTestCasePipeline +from certfuzz.fuzztools.command_line_templating import get_command_args_list # from certfuzz.iteration.iteration_base import IterationBase2 From a498a239810d87a24d287606a4167a7d610b855f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 08:50:08 -0500 Subject: [PATCH 0731/1169] remove unused imports --- src/certfuzz/runners/zzufrun.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 488fbe6..b8ce349 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -9,11 +9,8 @@ import os import subprocess import logging -from collections import deque from certfuzz.runners.errors import RunnerNotFoundError -import cmd import shlex -from string import Template from certfuzz.helpers.misc import quoted logger = logging.getLogger(__name__) From c287c1fe355eddc06c1e215cb1109904cc163b55 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 08:50:33 -0500 Subject: [PATCH 0732/1169] improve log message --- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 1c1ae55..8820524 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -78,8 +78,9 @@ def _verify(self, testcase): logger.debug('Original debugger file: %s', self.dbg_out_file_orig) self.success = True else: - logger.debug('%s was found, not unique', tc.signature) - + logger.info('Testcase signature %s was already seen, skipping further analysis', tc.signature) + else: + logger.debug('not a crash, continuing') def _post_verify(self, testcase): testcase.get_logger() From 374000046e49999f3a5cc82044ca655efe6019e8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 08:50:49 -0500 Subject: [PATCH 0733/1169] eliminate extraneous print statements in tests --- .../debuggers/output_parsers/test_debugger_file_base.py | 9 +-------- .../drillresults/test_testcasebundle_base.py | 3 --- src/test_certfuzz/file_handlers/test_seedfile_set.py | 9 --------- src/test_certfuzz/fuzztools/test_range.py | 2 -- 4 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/test_certfuzz/debuggers/output_parsers/test_debugger_file_base.py b/src/test_certfuzz/debuggers/output_parsers/test_debugger_file_base.py index 89fa1eb..90a1d9f 100644 --- a/src/test_certfuzz/debuggers/output_parsers/test_debugger_file_base.py +++ b/src/test_certfuzz/debuggers/output_parsers/test_debugger_file_base.py @@ -7,15 +7,9 @@ import unittest import glob import os -import logging from certfuzz.debuggers.output_parsers.debugger_file_base import detect_format from certfuzz.debuggers.output_parsers.errors import UnknownDebuggerError -# logger = logging.getLogger() -# hdlr = logging.StreamHandler() -# logger.addHandler(hdlr) -# logger.setLevel(logging.WARNING) -# debuggers.debug_file.logger.setLevel(logging.DEBUG) class Test(unittest.TestCase): @@ -37,12 +31,11 @@ def tearDown(self): def detect_format(self, filelist, expectedtype): for f in filelist: - logger.debug('File: %s', f) try: detectedtype = detect_format(f) self.assertEqual(detectedtype, expectedtype, "File %s: expected: %s got: %s" % (f, expectedtype, detectedtype)) except UnknownDebuggerError: - print "Failed to recognize type for %s" % f + self.fail("Failed to recognize type for %s" % f) def detect_format_fail(self, filelist): for f in filelist: diff --git a/src/test_certfuzz/drillresults/test_testcasebundle_base.py b/src/test_certfuzz/drillresults/test_testcasebundle_base.py index 2b87d65..f3b268f 100644 --- a/src/test_certfuzz/drillresults/test_testcasebundle_base.py +++ b/src/test_certfuzz/drillresults/test_testcasebundle_base.py @@ -119,7 +119,6 @@ def test_format_addr(self): else: trunc_faddr = faddr[-len(result):] self.assertTrue(trunc_faddr in result) - print faddr, result # 64 bit padding self.tcb._64bit_target_app = True @@ -129,7 +128,6 @@ def test_format_addr(self): result = self.tcb.format_addr(faddr_hex) self.assertEqual(16, len(result)) self.assertTrue(faddr in result) - print faddr, result def test_pc_in_mapped_address(self): self.assertEqual('', self.tcb.pc_in_mapped_address(None)) @@ -211,7 +209,6 @@ def test_score_less_interesting(self): xc['pcmodule'] = 'something' xc['EIF'] = True scores = self.tcb._score_less_interesting() - print scores self.assertTrue(50 in scores) xc['efa'] = '0x0000' diff --git a/src/test_certfuzz/file_handlers/test_seedfile_set.py b/src/test_certfuzz/file_handlers/test_seedfile_set.py index b36b455..4df125e 100644 --- a/src/test_certfuzz/file_handlers/test_seedfile_set.py +++ b/src/test_certfuzz/file_handlers/test_seedfile_set.py @@ -7,7 +7,6 @@ import tempfile import os import shutil -from pprint import pprint from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.file_handlers.directory import Directory @@ -202,16 +201,8 @@ def _same_dict(self, d1, d2): for k, v in d1.iteritems(): # print k self.assertTrue(k in d2) - if not v == d2[k]: - pprint(v) - pprint(d2[k]) - self.assertEqual(v, d2[k]) -# def test_next_item(self): -# self.assertEqual(0, len(self.sfs.things)) -# self.assertRaises(EmptySetError, self.sfs.next_key) -# self.assertRaises(EmptySetError, self.sfs.next_item) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/test_certfuzz/fuzztools/test_range.py b/src/test_certfuzz/fuzztools/test_range.py index 4b0462a..3f57622 100644 --- a/src/test_certfuzz/fuzztools/test_range.py +++ b/src/test_certfuzz/fuzztools/test_range.py @@ -5,8 +5,6 @@ ''' import unittest from certfuzz.fuzztools.range import Range -import pprint -import json class Test(unittest.TestCase): def setUp(self): From a4ac290760c6d77c6c410c46da1a4e7258f619b6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 10:18:07 -0500 Subject: [PATCH 0734/1169] move start seed and seed interval --- src/certfuzz/campaign/campaign_linux.py | 4 ++-- src/linux/conf.d/bff.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 0ef0307..7d68c29 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -64,8 +64,8 @@ def __init__(self, config_file=None, result_dir=None, debug=False): # pull stuff out of configs self.campaign_id = self.config['campaign']['id'] - self.current_seed = self.config['zzuf']['start_seed'] - self.seed_interval = self.config['zzuf']['seed_interval'] + self.current_seed = self.config['runoptions']['start_seed'] + self.seed_interval = self.config['runoptions']['seed_interval'] self.seed_dir_in = fixup_path(self.config['directories']['seedfile_origin_dir']) if self.outdir_base is None: diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index f7a2325..b2ebe20 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -81,8 +81,6 @@ directories: zzuf: copymode: 1 - start_seed: 0 - seed_interval: 20 ################################################################ # VERIFIER PARAMETERS @@ -175,7 +173,9 @@ timeouts: # adding these sections for compatibility with FOE config style runoptions: + start_seed: 0 # first_iteration: 0 + seed_interval: 20 # seed_interval: 1 minimize: True # minimizer_timeout: 3600 From 758a5911c7ea19925934200e6a6d11d6f3e2718c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 10:18:36 -0500 Subject: [PATCH 0735/1169] add opmode=copy back to zzuf runner options --- src/certfuzz/runners/zzufrun.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index b8ce349..2e1205c 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -57,6 +57,7 @@ def _construct_zzuf_args(self): '--seed=0', '--max-crashes=1', '--max-usertime=5.00', + '--opmode=copy', ]) self._zzuf_args = args From 7a9112bc2fb5037e53288cbbbf01daf8e64a5cbe Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 10:19:26 -0500 Subject: [PATCH 0736/1169] we don't use check exit anymore --- src/certfuzz/fuzztools/zzuflog.py | 5 +---- src/certfuzz/iteration/iteration_linux.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/fuzztools/zzuflog.py b/src/certfuzz/fuzztools/zzuflog.py index 64f1a7a..ca9844f 100644 --- a/src/certfuzz/fuzztools/zzuflog.py +++ b/src/certfuzz/fuzztools/zzuflog.py @@ -75,7 +75,7 @@ def _parse_line(self): self.parsed = True # set a flag that we parsed successfully return seed, rng, result - def crash_logged(self, checkexit): + def crash_logged(self): ''' Analyzes zzuf output log to figure out if this was a crash. Returns 0 if it's not really a crash. 1 if it's a crash we @@ -85,9 +85,6 @@ def crash_logged(self, checkexit): if not self.parsed: return False - if checkexit and 'exit' in self.result: - return False - # not a crash if killed if self.was_killed: return False diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 912d907..da40f75 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -100,7 +100,7 @@ def _post_run(self): # Don't generate cases for killed process or out-of-memory # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will # report the exit code in its output log. The exit code is 128 + the signal number. - analysis_needed = zzuf_log.crash_logged(self.cfg['zzuf']['copymode']) + analysis_needed = zzuf_log.crash_logged() if not analysis_needed: return From 49e951ff2d8f25c218c6835bbd29fde177a72a8e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 10:24:17 -0500 Subject: [PATCH 0737/1169] eliminate zzuf section of config --- src/linux/conf.d/bff.yaml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index b2ebe20..6bdfe13 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -59,28 +59,6 @@ directories: local_dir: ~/fuzzing watchdog_file: /tmp/bff_watchdog -################################################################ -# ZZUF FUZZER PARAMETERS -################################################################ -# Use zzuf's "copy" mode, which creates a temporary fuzzed file -# Default is 1, where zzuf will determine the file to fuzz, mangle it as -# a randomly-named file in $TMPDIR and then open that with the target app. -# This mode is compatible with more applications, but it can be slower in -# some cases, due to the extra file i/o required. -# If set to 0, zzuf will use LD_PRELOAD to mangle input in memory, rather -# than creating a temporary file. Note that this mode can interfere with -# some target applications. -# OSX Cocoa applications require copymode=1 - -# The zzuf seed number to start with - -# How many iterations you want zzuf to try per seedfile/range selection -# If you have a large number of seed files and/or the target application -# is slow, you may wish to make this value smaller to get better coverage -# in a reasonable amount of time. - -zzuf: - copymode: 1 ################################################################ # VERIFIER PARAMETERS From aff3cedf26f178e855d344506f3a14cc33a36f70 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 10:29:27 -0500 Subject: [PATCH 0738/1169] replace timeouts/progtimeout with runner/runtimeout --- src/certfuzz/analyzers/stderr.py | 2 +- src/certfuzz/campaign/campaign_linux.py | 2 +- src/linux/conf.d/bff.yaml | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/analyzers/stderr.py b/src/certfuzz/analyzers/stderr.py index 1ee905a..e4313c0 100644 --- a/src/certfuzz/analyzers/stderr.py +++ b/src/certfuzz/analyzers/stderr.py @@ -19,7 +19,7 @@ class StdErr(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg['timeouts']['progtimeout'] + timeout = cfg['runner']['runtimeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout, stderr=outfile) # need to set the stderr_redirect flag on the base class diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 7d68c29..cdcc28d 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -134,7 +134,7 @@ def _cache_app(self): fullpathorig = self._full_path_original(sf.path) cmdargs = get_command_args_list(self.config['target']['cmdline_template'], infile=fullpathorig)[1] subp.run_with_timer(cmdargs, - self.config['timeouts']['progtimeout'] * 8, + self.config['runner']['runtimeout'] * 8, self.config['target']['killprocname'], use_shell=False) diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 6bdfe13..b9e629f 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -141,7 +141,6 @@ verifier: # it has made at that point) and return to fuzzing. timeouts: - progtimeout: 5 killproctimeout: 130 debugger_timeout: 60 valgrindtimeout: 120 From fe2676c42ecfb04f6b552b56439fd9fb4f091bfa Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 10:32:20 -0500 Subject: [PATCH 0739/1169] replace timeouts/killproctimeout with runoptions/killproctimeout --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/linux/conf.d/bff.yaml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index cdcc28d..99489d6 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -122,7 +122,7 @@ def _set_unbuffered_stdout(self): def _start_process_killer(self): logger.debug('start process killer') with ProcessKiller(self.config['target']['killprocname'], - self.config['timeouts']['killproctimeout'] + self.config['runoptions']['killproctimeout'] ) as pk: self.pk_pid = pk.go() diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index b9e629f..6a7423e 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -141,7 +141,6 @@ verifier: # it has made at that point) and return to fuzzing. timeouts: - killproctimeout: 130 debugger_timeout: 60 valgrindtimeout: 120 watchdogtimeout: 3600 @@ -159,6 +158,8 @@ runoptions: # keep_unique_faddr: False # keep_all_duplicates: False # recycle_crashers: False + killproctimeout: 130 + fuzzer: fuzzer: bytemut fuzz_zip_container: False From 7a5d283558e00747c6bd89ccb0a700df2905a9c2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 10:35:32 -0500 Subject: [PATCH 0740/1169] replace timeouts/debugger_timeout with debugger/runtimeout --- src/certfuzz/analyzers/cw_gmalloc.py | 2 +- src/certfuzz/iteration/iteration_linux.py | 2 +- src/certfuzz/minimizer/minimizer_base.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/analyzers/cw_gmalloc.py b/src/certfuzz/analyzers/cw_gmalloc.py index 1d6bb5b..80cc0af 100644 --- a/src/certfuzz/analyzers/cw_gmalloc.py +++ b/src/certfuzz/analyzers/cw_gmalloc.py @@ -30,7 +30,7 @@ def __init__(self, cfg, crash): return None outfile = get_file(crash.fuzzedfile.path) - timeout = cfg['timeouts']['debugger_timeout'] + timeout = cfg['debugger']['runtimeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index da40f75..f1de222 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -113,7 +113,7 @@ def _construct_testcase(self): seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), program=fixup_path(self.cfg['target']['program']), - debugger_timeout=self.cfg['timeouts']['debugger_timeout'], + debugger_timeout=self.cfg['debugger']['runtimeout'], killprocname=self.cfg['target']['killprocname'], backtrace_lines=self.cfg['verifier']['backtracelevels'], crashers_dir=self.testcase_base_dir, diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index e3ab78e..4af16f6 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -171,7 +171,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, filetools.copy_file(self.crash.fuzzedfile.path, self.tempfile) # figure out what crash signatures belong to this fuzzedfile - self.debugger_timeout = self.cfg['timeouts']['debugger_timeout'] + self.debugger_timeout = self.cfg['debugger']['runtimeout'] self.crash_hashes = [] self.measured_dbg_time = None self._set_crash_hashes() From 2ee66c94222072c2d9b39cb9504e2180e39dcbe0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 10:39:10 -0500 Subject: [PATCH 0741/1169] replace timeouts/watchdogtimetout with runoptions/watchdogtimeout --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/linux/conf.d/bff.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 99489d6..3a15f5e 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -101,7 +101,7 @@ def _pre_enter(self): self._check_for_script() def _post_enter(self): - if self.config['timeouts']['watchdogtimeout']: + if self.config['runoptions']['watchdogtimeout']: self._setup_watchdog() check_ppid() self._cache_app() diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 6a7423e..0335950 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -141,9 +141,7 @@ verifier: # it has made at that point) and return to fuzzing. timeouts: - debugger_timeout: 60 valgrindtimeout: 120 - watchdogtimeout: 3600 minimizertimeout: 3600 # adding these sections for compatibility with FOE config style @@ -159,6 +157,8 @@ runoptions: # keep_all_duplicates: False # recycle_crashers: False killproctimeout: 130 + watchdogtimeout: 3600 + fuzzer: fuzzer: bytemut From 75cb01dc8e76ef146d97049bda2cafb6f2e70d88 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 11:09:53 -0500 Subject: [PATCH 0742/1169] move valgrindtimeout to verifier section --- src/certfuzz/analyzers/callgrind/callgrind.py | 2 +- src/certfuzz/analyzers/pin_calltrace.py | 2 +- src/certfuzz/analyzers/valgrind.py | 2 +- src/linux/conf.d/bff.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/analyzers/callgrind/callgrind.py b/src/certfuzz/analyzers/callgrind/callgrind.py index 25314bb..6dc0fa8 100644 --- a/src/certfuzz/analyzers/callgrind/callgrind.py +++ b/src/certfuzz/analyzers/callgrind/callgrind.py @@ -20,7 +20,7 @@ class Callgrind(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg['timeouts']['valgrindtimeout'] + timeout = cfg['verifier']['valgrind_timeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True diff --git a/src/certfuzz/analyzers/pin_calltrace.py b/src/certfuzz/analyzers/pin_calltrace.py index 4658859..128e92e 100644 --- a/src/certfuzz/analyzers/pin_calltrace.py +++ b/src/certfuzz/analyzers/pin_calltrace.py @@ -21,7 +21,7 @@ class Pin_calltrace(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg['timeouts']['valgrindtimeout'] * 10 + timeout = cfg['runoptions']['valgrind_timeout'] * 10 Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True diff --git a/src/certfuzz/analyzers/valgrind.py b/src/certfuzz/analyzers/valgrind.py index d6e1867..3d4348c 100644 --- a/src/certfuzz/analyzers/valgrind.py +++ b/src/certfuzz/analyzers/valgrind.py @@ -20,7 +20,7 @@ class Valgrind(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg['timeouts']['valgrindtimeout'] + timeout = cfg['verifier']['valgrind_timeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 0335950..8f429c9 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -129,6 +129,7 @@ verifier: # CERT Triage Tools exploitable output. # maximum time (seconds) to let the program run to capture valgrind output: + valgrind_timeout: 120 # Set value for the Linux watchdog timer. If watchdog_file is not touched # for a period longer than this value (seconds), then the vm is rebooted @@ -141,7 +142,6 @@ verifier: # it has made at that point) and return to fuzzing. timeouts: - valgrindtimeout: 120 minimizertimeout: 3600 # adding these sections for compatibility with FOE config style From d5f9893637283e197e5ffab86fc52a7ddd124bdd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 11:12:13 -0500 Subject: [PATCH 0743/1169] use first_iteration instead of start_seed, move minimizer timeout to run options --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/iteration/iteration_linux.py | 2 +- src/linux/conf.d/bff.yaml | 48 +++++++---------------- 3 files changed, 16 insertions(+), 36 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 3a15f5e..2246059 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -64,7 +64,7 @@ def __init__(self, config_file=None, result_dir=None, debug=False): # pull stuff out of configs self.campaign_id = self.config['campaign']['id'] - self.current_seed = self.config['runoptions']['start_seed'] + self.current_seed = self.config['runoptions']['first_iteration'] self.seed_interval = self.config['runoptions']['seed_interval'] self.seed_dir_in = fixup_path(self.config['directories']['seedfile_origin_dir']) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index f1de222..5aefe2f 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -55,7 +55,7 @@ def __init__(self, 'minimize_to_string': self.cfg['verifier']['minimize_to_string'], 'uniq_log': os.path.join(self.cfg['directories']['output_dir'], 'uniquelog.txt'), 'local_dir': fixup_path(self.cfg['directories']['local_dir']), - 'minimizertimeout': self.cfg['timeouts']['minimizertimeout'], + 'minimizertimeout': self.cfg['runoptions']['minimizer_timeout'], 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions']['minimize'], } diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 8f429c9..7751d3f 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -112,51 +112,31 @@ verifier: minimize_to_string: False recycle_crashers: False keep_duplicates: False - - -################################################################ -# APPLICATION TIMEOUTS -################################################################ -# -# maximum program execution time (seconds) that BFF will allow: - -# maximum time (seconds) that process specified by "killprocname" option -# can run before it's killed via killproc.sh, -# which is used to kill stray processes. Normally, zzuf will kill -# the target process if the above timeout is reached. - -# maximum time (seconds) to let the program run to capture debugger and -# CERT Triage Tools exploitable output. - # maximum time (seconds) to let the program run to capture valgrind output: valgrind_timeout: 120 -# Set value for the Linux watchdog timer. If watchdog_file is not touched -# for a period longer than this value (seconds), then the vm is rebooted -# by the watchdog. -# Set to 0 to disable watchdog functionality. - +runoptions: + first_iteration: 0 + seed_interval: 20 + minimize: True # Minimization can sometimes take a long time, and time spent minimizing # is time not spent fuzzing. If a minimization run exceeds this time # (in seconds) the minimization will terminate (keeping whatever progress # it has made at that point) and return to fuzzing. - -timeouts: - minimizertimeout: 3600 - -# adding these sections for compatibility with FOE config style - -runoptions: - start_seed: 0 -# first_iteration: 0 - seed_interval: 20 -# seed_interval: 1 - minimize: True -# minimizer_timeout: 3600 + minimizer_timeout: 3600 # keep_unique_faddr: False # keep_all_duplicates: False # recycle_crashers: False + +# maximum time (seconds) that process specified by "killprocname" option +# can run before it's killed via killproc.sh, +# which is used to kill stray processes. Normally, zzuf will kill +# the target process if the above timeout is reached. killproctimeout: 130 +# Set value for the Linux watchdog timer. If watchdog_file is not touched +# for a period longer than this value (seconds), then the vm is rebooted +# by the watchdog. +# Set to 0 to disable watchdog functionality. watchdogtimeout: 3600 From 73610ffd8c2c1c98e4fcc3502158074d2deebf23 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 11:32:52 -0500 Subject: [PATCH 0744/1169] make backtracelevels part of debugger config instead of verifier --- src/certfuzz/iteration/iteration_linux.py | 2 +- src/certfuzz/minimizer/minimizer_base.py | 4 ++-- src/linux/conf.d/bff.yaml | 15 +++++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 5aefe2f..da96546 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -115,7 +115,7 @@ def _construct_testcase(self): program=fixup_path(self.cfg['target']['program']), debugger_timeout=self.cfg['debugger']['runtimeout'], killprocname=self.cfg['target']['killprocname'], - backtrace_lines=self.cfg['verifier']['backtracelevels'], + backtrace_lines=self.cfg['debugger']['backtracelevels'], crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, seednum=self.seednum, diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 4af16f6..e4f2ba0 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -349,7 +349,7 @@ def _set_crash_hashes(self): if dbg.is_crash: times.append(delta) - current_sig = self.get_signature(dbg, self.cfg['verifier']['backtracelevels']) + current_sig = self.get_signature(dbg, self.cfg['debugger']['backtracelevels']) # ditch the temp file if os.path.exists(f): @@ -470,7 +470,7 @@ def is_same_crash(self): dbg = self.run_debugger(self.tempfile, f) if dbg.is_crash: - newfuzzed_hash = self.get_signature(dbg, self.cfg['verifier']['backtracelevels']) + newfuzzed_hash = self.get_signature(dbg, self.cfg['debugger']['backtracelevels']) else: newfuzzed_hash = None # initialize or increment the counter for this hash diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 7751d3f..0fee0e0 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -64,11 +64,6 @@ directories: # VERIFIER PARAMETERS ################################################################ # -# number of backtrace levels to hash for uniqueness. -# Increase this number for more crash uniqueness granularity. -# Decrease this number if you think that you are getting too many -# duplicate crashes. - # Include backtrace frames that aren't part of a loaded library? # Set this value to False if you wish to consider unmapped stack # frames in the crash hashes. This can be useful for target application @@ -103,7 +98,6 @@ directories: # rather than finding new underlying vulnerabilities. verifier: - backtracelevels: 5 exclude_unmapped_frames: True savefailedasserts: False use_valgrind: True @@ -145,7 +139,16 @@ fuzzer: fuzz_zip_container: False runner: runner: zzufrun +# maximum program execution time (seconds) that BFF will allow: runtimeout: 5 debugger: debugger: gdb +# maximum time (seconds) to let the program run to capture debugger and +# CERT Triage Tools exploitable output. runtimeout: 60 +# number of backtrace levels to hash for uniqueness. +# Increase this number for more crash uniqueness granularity. +# Decrease this number if you think that you are getting too many +# duplicate crashes. + backtracelevels: 5 + From e4106c4c843131ce8c9091133a2e94f47c8ce826 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 11:38:06 -0500 Subject: [PATCH 0745/1169] rename seedfile_origin_dir -> seedfile_dir --- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 2 +- src/linux/conf.d/bff.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 8820524..e571c37 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -97,7 +97,7 @@ def _post_minimize(self, testcase): logger.debug('Recycling crash as seedfile') iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext - crasherseed_path = os.path.join(self.cfg['directories']['seedfile_origin_dir'], crasherseedname) + crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) self.sf_set.add_file(crasherseed_path) diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 0fee0e0..c0bf32b 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -53,7 +53,7 @@ target: # if fuzzer is still running directories: - seedfile_origin_dir: ~/bff/seedfiles/examples + seedfile_dir: ~/bff/seedfiles/examples debugger_template_dir: ~/bff/certfuzz/debuggers/templates output_dir: ~/results local_dir: ~/fuzzing From ee654e0772341ac3ba2b9c422e909d621a5f0f28 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 11:39:46 -0500 Subject: [PATCH 0746/1169] rename local_dir -> working_dir --- src/certfuzz/iteration/iteration_linux.py | 2 +- src/linux/conf.d/bff.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index da96546..47faf0f 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -54,7 +54,7 @@ def __init__(self, 'minimize_crashers': self.cfg['verifier']['minimizecrashers'], 'minimize_to_string': self.cfg['verifier']['minimize_to_string'], 'uniq_log': os.path.join(self.cfg['directories']['output_dir'], 'uniquelog.txt'), - 'local_dir': fixup_path(self.cfg['directories']['local_dir']), + 'local_dir': fixup_path(self.cfg['directories']['working_dir']), 'minimizertimeout': self.cfg['runoptions']['minimizer_timeout'], 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions']['minimize'], } diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index c0bf32b..5db47bf 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -56,7 +56,7 @@ directories: seedfile_dir: ~/bff/seedfiles/examples debugger_template_dir: ~/bff/certfuzz/debuggers/templates output_dir: ~/results - local_dir: ~/fuzzing + working_dir: ~/fuzzing watchdog_file: /tmp/bff_watchdog From 6ce3c65a3f5d3f4ed48dc831c1dacbfbd54a98be Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 13:22:29 -0500 Subject: [PATCH 0747/1169] rename output_dir -> results_dir --- src/certfuzz/iteration/iteration_linux.py | 2 +- src/linux/conf.d/bff.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 47faf0f..5b44269 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -53,7 +53,7 @@ def __init__(self, 'use_pin_calltrace': self.cfg['verifier']['use_pin_calltrace'], 'minimize_crashers': self.cfg['verifier']['minimizecrashers'], 'minimize_to_string': self.cfg['verifier']['minimize_to_string'], - 'uniq_log': os.path.join(self.cfg['directories']['output_dir'], 'uniquelog.txt'), + 'uniq_log': os.path.join(self.cfg['directories']['results_dir'], 'uniquelog.txt'), 'local_dir': fixup_path(self.cfg['directories']['working_dir']), 'minimizertimeout': self.cfg['runoptions']['minimizer_timeout'], 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions']['minimize'], diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 5db47bf..44b0aa0 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -55,8 +55,8 @@ target: directories: seedfile_dir: ~/bff/seedfiles/examples debugger_template_dir: ~/bff/certfuzz/debuggers/templates - output_dir: ~/results working_dir: ~/fuzzing + results_dir: ~/results watchdog_file: /tmp/bff_watchdog From f7907dd76e366bce15406fdd58e43013fb703e76 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 13:22:51 -0500 Subject: [PATCH 0748/1169] reorder options --- src/linux/conf.d/bff.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 44b0aa0..e281c75 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -54,9 +54,9 @@ target: directories: seedfile_dir: ~/bff/seedfiles/examples - debugger_template_dir: ~/bff/certfuzz/debuggers/templates working_dir: ~/fuzzing results_dir: ~/results + debugger_template_dir: ~/bff/certfuzz/debuggers/templates watchdog_file: /tmp/bff_watchdog From e8a593956efdb2ace175708937e92fdd9c237f92 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 13:23:13 -0500 Subject: [PATCH 0749/1169] move recycle_crashsers to run options --- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 2 +- src/linux/conf.d/bff.yaml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index e571c37..77b583e 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -93,7 +93,7 @@ def _minimize(self, testcase): self._minimize_to_string(testcase) def _post_minimize(self, testcase): - if self.cfg['verifier']['recycle_crashers']: + if self.cfg['runoptions']['recycle_crashers']: logger.debug('Recycling crash as seedfile') iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index e281c75..a8d4fa0 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -104,7 +104,6 @@ verifier: use_pin_calltrace: False minimizecrashers: True minimize_to_string: False - recycle_crashers: False keep_duplicates: False # maximum time (seconds) to let the program run to capture valgrind output: valgrind_timeout: 120 @@ -120,7 +119,7 @@ runoptions: minimizer_timeout: 3600 # keep_unique_faddr: False # keep_all_duplicates: False -# recycle_crashers: False + recycle_crashers: False # maximum time (seconds) that process specified by "killprocname" option # can run before it's killed via killproc.sh, From 11e16d4d06ad176f097588a2b61f4560e94776c0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 13:26:34 -0500 Subject: [PATCH 0750/1169] replace verifier/minimize_crashers with runoptions/minimize --- src/certfuzz/iteration/iteration_linux.py | 2 +- src/linux/conf.d/bff.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 5b44269..67a8c5f 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -51,7 +51,7 @@ def __init__(self, self.pipeline_options = {'use_valgrind': self.cfg['verifier']['use_valgrind'], 'use_pin_calltrace': self.cfg['verifier']['use_pin_calltrace'], - 'minimize_crashers': self.cfg['verifier']['minimizecrashers'], + 'minimize_crashers': self.cfg['runoptions']['minimize'], 'minimize_to_string': self.cfg['verifier']['minimize_to_string'], 'uniq_log': os.path.join(self.cfg['directories']['results_dir'], 'uniquelog.txt'), 'local_dir': fixup_path(self.cfg['directories']['working_dir']), diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index a8d4fa0..2901f75 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -102,7 +102,6 @@ verifier: savefailedasserts: False use_valgrind: True use_pin_calltrace: False - minimizecrashers: True minimize_to_string: False keep_duplicates: False # maximum time (seconds) to let the program run to capture valgrind output: From ad93d656c9633347f365bd086c5b7432d0c6937a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 27 Jan 2016 13:27:50 -0500 Subject: [PATCH 0751/1169] move minimize_to_string to run options --- src/certfuzz/iteration/iteration_linux.py | 2 +- src/linux/conf.d/bff.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 67a8c5f..6e0df94 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -52,7 +52,7 @@ def __init__(self, self.pipeline_options = {'use_valgrind': self.cfg['verifier']['use_valgrind'], 'use_pin_calltrace': self.cfg['verifier']['use_pin_calltrace'], 'minimize_crashers': self.cfg['runoptions']['minimize'], - 'minimize_to_string': self.cfg['verifier']['minimize_to_string'], + 'minimize_to_string': self.cfg['runoptions']['minimize_to_string'], 'uniq_log': os.path.join(self.cfg['directories']['results_dir'], 'uniquelog.txt'), 'local_dir': fixup_path(self.cfg['directories']['working_dir']), 'minimizertimeout': self.cfg['runoptions']['minimizer_timeout'], diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 2901f75..aef012c 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -102,7 +102,6 @@ verifier: savefailedasserts: False use_valgrind: True use_pin_calltrace: False - minimize_to_string: False keep_duplicates: False # maximum time (seconds) to let the program run to capture valgrind output: valgrind_timeout: 120 @@ -116,6 +115,7 @@ runoptions: # (in seconds) the minimization will terminate (keeping whatever progress # it has made at that point) and return to fuzzing. minimizer_timeout: 3600 + minimize_to_string: False # keep_unique_faddr: False # keep_all_duplicates: False recycle_crashers: False From 877ec834c3c271d502223af7b7e6c5bc0d0b9803 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 28 Jan 2016 14:57:35 -0500 Subject: [PATCH 0752/1169] rename keep_duplicates to keep_all_duplicates --- src/certfuzz/testcase_pipeline/tc_pipeline_windows.py | 2 +- src/linux/conf.d/bff.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index ed39f52..f3add6a 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -94,7 +94,7 @@ def keep_testcase(self, testcase): @return (bool,str) ''' if testcase.is_crash: - if self.options['keep_duplicates']: + if self.options['keep_all_duplicates']: return (True, 'keep duplicates') elif self.uniq_func(testcase.signature): # Check if crasher directory exists already diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index aef012c..e31536f 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -102,7 +102,7 @@ verifier: savefailedasserts: False use_valgrind: True use_pin_calltrace: False - keep_duplicates: False + # keep_duplicates: False # maximum time (seconds) to let the program run to capture valgrind output: valgrind_timeout: 120 @@ -117,7 +117,7 @@ runoptions: minimizer_timeout: 3600 minimize_to_string: False # keep_unique_faddr: False -# keep_all_duplicates: False + keep_all_duplicates: False recycle_crashers: False # maximum time (seconds) that process specified by "killprocname" option From 5d39c8c1c4729fde1813444e6bc68e7ff19c029f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 28 Jan 2016 14:58:08 -0500 Subject: [PATCH 0753/1169] fix a few missed config var renames --- src/certfuzz/campaign/campaign_linux.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 2246059..82f5d4f 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -66,13 +66,13 @@ def __init__(self, config_file=None, result_dir=None, debug=False): self.campaign_id = self.config['campaign']['id'] self.current_seed = self.config['runoptions']['first_iteration'] self.seed_interval = self.config['runoptions']['seed_interval'] - self.seed_dir_in = fixup_path(self.config['directories']['seedfile_origin_dir']) + self.seed_dir_in = fixup_path(self.config['directories']['seedfile_dir']) if self.outdir_base is None: # it wasn't spec'ed on the command line so use the config - self.outdir_base = fixup_path(self.config['directories']['output_dir']) + self.outdir_base = fixup_path(self.config['directories']['results_dir']) - self.work_dir_base = fixup_path(self.config['directories']['local_dir']) + self.work_dir_base = fixup_path(self.config['directories']['working_dir']) self.program = fixup_path(self.config['target']['program']) self.program_basename = os.path.basename(self.program).replace('"', '') # self.cmd_list = shlex.split(self.config['target']['cmdline']) @@ -151,7 +151,7 @@ def _setup_watchdog(self): touch_watchdog_file() # set up the watchdog timeout within the VM and restart the daemon - with WatchDog(wdf, self.config['timeouts']['watchdogtimeout']) as watchdog: + with WatchDog(wdf, self.config['runoptions']['watchdogtimeout']) as watchdog: watchdog() def _check_for_script(self): From ee6264303ba3516d646d145df71795a2e10adb8a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 28 Jan 2016 14:58:38 -0500 Subject: [PATCH 0754/1169] fix unit tests --- src/test_certfuzz/analyzers/test_stderr.py | 2 +- src/test_certfuzz/campaign/test_campaign_linux.py | 10 +++++----- src/test_certfuzz/fuzztools/test_zzuflog.py | 10 +++++----- src/test_certfuzz/mocks.py | 7 ++++--- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/test_certfuzz/analyzers/test_stderr.py b/src/test_certfuzz/analyzers/test_stderr.py index 8a541c9..1e82ad0 100644 --- a/src/test_certfuzz/analyzers/test_stderr.py +++ b/src/test_certfuzz/analyzers/test_stderr.py @@ -25,7 +25,7 @@ def setUp(self): self.delete_file(f) self.file = '%s.stderr' % f - cfg = {'timeouts': {'progtimeout':1}, + cfg = {'runner': {'runtimeout':1}, 'target': {'cmdline_template': string.Template('PROGRAM $SEEDFILE foo')} } diff --git a/src/test_certfuzz/campaign/test_campaign_linux.py b/src/test_certfuzz/campaign/test_campaign_linux.py index d7b8f28..863b497 100644 --- a/src/test_certfuzz/campaign/test_campaign_linux.py +++ b/src/test_certfuzz/campaign/test_campaign_linux.py @@ -20,15 +20,15 @@ def setUp(self): os.close(fd) data = {'campaign': {'id': 'foo'}, - 'directories': {'seedfile_origin_dir': tempfile.mkdtemp(prefix='seedfiles_', dir=self.tmpdir), - 'output_dir': tempfile.mkdtemp(prefix='output_', dir=self.tmpdir), - 'local_dir': tempfile.mkdtemp(prefix='local_', dir=self.tmpdir)}, + 'directories': {'seedfile_dir': tempfile.mkdtemp(prefix='seedfiles_', dir=self.tmpdir), + 'results_dir': tempfile.mkdtemp(prefix='output_', dir=self.tmpdir), + 'working_dir': tempfile.mkdtemp(prefix='local_', dir=self.tmpdir)}, 'timeouts': {}, - 'zzuf': {'start_seed': 0, - 'seed_interval': 10}, 'verifier': {}, 'target': {'program': 'foo', 'cmdline_template': 'foo bar baz quux'}, + 'runoptions':{'first_iteration':0, + 'seed_interval': 10} } with open(cfgfile, 'wb') as stream: yaml.dump(data, stream) diff --git a/src/test_certfuzz/fuzztools/test_zzuflog.py b/src/test_certfuzz/fuzztools/test_zzuflog.py index e2714c8..b70e001 100644 --- a/src/test_certfuzz/fuzztools/test_zzuflog.py +++ b/src/test_certfuzz/fuzztools/test_zzuflog.py @@ -112,31 +112,31 @@ def test_read_zzuf_log(self): def test_crash_logged(self): self.log.result = "a" self.log._set_exitcode() - self.assertFalse(self.log.crash_logged(False)) + self.assertFalse(self.log.crash_logged()) # _was_killed => true # should be false self.log.result = "signal 9" self.log._set_exitcode() - self.assertFalse(self.log.crash_logged(False)) + self.assertFalse(self.log.crash_logged()) # _was_out_of_memory => true # should be false self.log.result = "signal 15" self.log._set_exitcode() - self.assertFalse(self.log.crash_logged(False)) + self.assertFalse(self.log.crash_logged()) # should be false since infile is empty self.log.result = "a" self.log._set_exitcode() self.assertFalse(self.log.parsed) - self.assertFalse(self.log.crash_logged(False)) + self.assertFalse(self.log.crash_logged()) # should be true self.log.result = "a" self.log._set_exitcode() self.log.parsed = True # have to fake it since infile is empty - self.assertTrue(self.log.crash_logged(False)) + self.assertTrue(self.log.crash_logged()) # def test_crash_exit(self): # crash_exit_code_list = [77, 88, 99] diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 6e2becf..7f5b3f9 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -97,13 +97,14 @@ def go(self): class MockCfg(dict): def __init__(self,templated=True): - self['timeouts']={'debugger_timeout': 1, - 'valgrindtimeout': 1} + self['debugger']={'runtimeout': 1, + 'backtracelevels': 5, + } self['target']={'cmdline_template': 'a b c d', 'killprocname': 'a', 'program': 'foo'} self['verifier']={'exclude_unmapped_frames': False, - 'backtracelevels': 5} + 'valgrind_timeout': 1} self['directories'] ={} if templated: self['target']['cmdline_template'] = string.Template(self['target']['cmdline_template']) \ No newline at end of file From e156d4230364f5957cd45d817386f5eeba88bee3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 28 Jan 2016 15:30:32 -0500 Subject: [PATCH 0755/1169] rename 'verifier' to 'analyzer' in config --- src/certfuzz/analyzers/callgrind/callgrind.py | 2 +- src/certfuzz/analyzers/valgrind.py | 2 +- src/certfuzz/crash/bff_crash.py | 4 ++-- src/certfuzz/iteration/iteration_linux.py | 4 ++-- src/certfuzz/minimizer/minimizer_base.py | 2 +- src/certfuzz/testcase_pipeline/tc_pipeline_linux.py | 2 +- src/linux/conf.d/bff.yaml | 2 +- src/test_certfuzz/mocks.py | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/analyzers/callgrind/callgrind.py b/src/certfuzz/analyzers/callgrind/callgrind.py index 6dc0fa8..33830ab 100644 --- a/src/certfuzz/analyzers/callgrind/callgrind.py +++ b/src/certfuzz/analyzers/callgrind/callgrind.py @@ -20,7 +20,7 @@ class Callgrind(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg['verifier']['valgrind_timeout'] + timeout = cfg['analyzer']['valgrind_timeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True diff --git a/src/certfuzz/analyzers/valgrind.py b/src/certfuzz/analyzers/valgrind.py index 3d4348c..13b0a64 100644 --- a/src/certfuzz/analyzers/valgrind.py +++ b/src/certfuzz/analyzers/valgrind.py @@ -20,7 +20,7 @@ class Valgrind(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg['verifier']['valgrind_timeout'] + timeout = cfg['analyzer']['valgrind_timeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index e6f86ea..898800d 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -48,7 +48,7 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, self.crash_base_dir = crashers_dir self.seednum = seednum self.range = range - self.exclude_unmapped_frames = cfg['verifier']['exclude_unmapped_frames'] + self.exclude_unmapped_frames = cfg['analyzer']['exclude_unmapped_frames'] self.set_debugger_template('bt_only') self.keep_uniq_faddr = keep_faddr @@ -121,7 +121,7 @@ def confirm_crash(self): raise CrashError('Debug object not found') logger.debug('is_crash: %s is_assert_fail: %s', self.dbg.is_crash, self.dbg.is_assert_fail) - if self.cfg['verifier']['savefailedasserts']: + if self.cfg['analyzer']['savefailedasserts']: return self.dbg.is_crash else: # only keep real crashes (not failed assertions) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 6e0df94..c3ebd43 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -49,8 +49,8 @@ def __init__(self, self.testcase_base_dir = os.path.join(self.outdir, 'crashers') - self.pipeline_options = {'use_valgrind': self.cfg['verifier']['use_valgrind'], - 'use_pin_calltrace': self.cfg['verifier']['use_pin_calltrace'], + self.pipeline_options = {'use_valgrind': self.cfg['analyzer']['use_valgrind'], + 'use_pin_calltrace': self.cfg['analyzer']['use_pin_calltrace'], 'minimize_crashers': self.cfg['runoptions']['minimize'], 'minimize_to_string': self.cfg['runoptions']['minimize_to_string'], 'uniq_log': os.path.join(self.cfg['directories']['results_dir'], 'uniquelog.txt'), diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index e4f2ba0..d343608 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -389,7 +389,7 @@ def run_debugger(self, infile, outfile): self.debugger_timeout, self.cfg['target']['killprocname'], template=self.crash.debugger_template, - exclude_unmapped_frames=self.cfg['verifier']['exclude_unmapped_frames'], + exclude_unmapped_frames=self.cfg['analyzer']['exclude_unmapped_frames'], keep_uniq_faddr=self.keep_uniq_faddr, workingdir=self.tempdir, watchcpu=self.watchcpu diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py index 77b583e..90dea69 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py @@ -68,7 +68,7 @@ def _verify(self, testcase): # crash_dir_found = filetools.find_or_create_dir(tc.result_dir) - keep_all = self.cfg['verifier'].get('keep_duplicates', False) + keep_all = self.cfg['analyzer'].get('keep_duplicates', False) tc.should_proceed_with_analysis = keep_all or (is_new_to_campaign and not crash_dir_found) diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index e31536f..8a29854 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -97,7 +97,7 @@ directories: # just demonstrate weaknesses in backtrace-based uniqueness determination # rather than finding new underlying vulnerabilities. -verifier: +analyzer: exclude_unmapped_frames: True savefailedasserts: False use_valgrind: True diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 7f5b3f9..1ba4bc1 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -103,7 +103,7 @@ def __init__(self,templated=True): self['target']={'cmdline_template': 'a b c d', 'killprocname': 'a', 'program': 'foo'} - self['verifier']={'exclude_unmapped_frames': False, + self['analyzer']={'exclude_unmapped_frames': False, 'valgrind_timeout': 1} self['directories'] ={} if templated: From 99eb55f37d7f9b44aa07cf36d1f16d6132bb77fe Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 28 Jan 2016 16:03:47 -0500 Subject: [PATCH 0756/1169] reinstate underscores in output dir names --- src/certfuzz/campaign/campaign_base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 2724d83..c930bfb 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -114,12 +114,13 @@ def _common_init(self): ''' Initializes some additional properties common to all platforms ''' - self.outdir = os.path.join(self.outdir_base, self.campaign_id) + _campaign_id_with_underscores = re.sub('\W', '_', self.campaign_id) + self.outdir = os.path.join(self.outdir_base, _campaign_id_with_underscores) logger.debug('outdir=%s', self.outdir) self.sf_set_out = os.path.join(self.outdir, 'seedfiles') if not self.cached_state_file: - cachefile = 'campaign_%s.pkl' % re.sub('\W', '_', self.campaign_id) + cachefile = 'campaign_%s.pkl' % _campaign_id_with_underscores self.cached_state_file = os.path.join(self.work_dir_base, cachefile) if not self.seed_interval: self.seed_interval = 1 From dc7c020b5a1b2ccc9cf23dd1ad7813e954926688 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 28 Jan 2016 16:16:11 -0500 Subject: [PATCH 0757/1169] fixup windows standalone minimizer to deal with new config stuff --- src/certfuzz/tools/windows/minimize.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index fe2f3fd..4ee7dab 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -7,7 +7,8 @@ import logging import os import string -import sys +from certfuzz.fuzztools.command_line_templating import get_command_args_list +from certfuzz.config.simple_loader import load_config try: @@ -15,7 +16,6 @@ from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer - from certfuzz.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport except ImportError: @@ -28,7 +28,6 @@ from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer - from certfuzz.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport @@ -142,9 +141,7 @@ def main(): else: parser.error('fuzzedfile must be specified') - with WindowsConfig(cfg_file) as configobj: - config = configobj.config - cfg = _create_minimizer_cfg(config) + cfg = load_config(cfg_file) if options.target: seedfile = BasicFile(options.target) @@ -155,10 +152,10 @@ def main(): filename_modifier = '' retries = 0 debugger_class = msec.MsecDebugger - template = string.Template(config['target']['cmdline_template']) + template = string.Template(cfg['target']['cmdline_template']) cmd_as_args = get_command_args_list(template, fuzzed_file.path)[1] with WindowsCrash(template, seedfile, fuzzed_file, cmd_as_args, None, debugger_class, - config['debugger'], outdir, options.keep_uniq_faddr, config['target']['program'], + cfg['debugger'], outdir, options.keep_uniq_faddr, cfg['target']['program'], retries) as crash: filetools.make_directories(crash.tempdir) logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) From 1b9bb1d61fe7c6af09b6a8e550cf466b3db354e8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 28 Jan 2016 16:19:53 -0500 Subject: [PATCH 0758/1169] don't replace dots in campaign id, just spaces --- src/certfuzz/campaign/campaign_base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index c930bfb..6e7dd8f 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -114,8 +114,10 @@ def _common_init(self): ''' Initializes some additional properties common to all platforms ''' + _campaign_id_no_space=re.sub('\s', '_', self.campaign_id) _campaign_id_with_underscores = re.sub('\W', '_', self.campaign_id) - self.outdir = os.path.join(self.outdir_base, _campaign_id_with_underscores) + + self.outdir = os.path.join(self.outdir_base, _campaign_id_no_space) logger.debug('outdir=%s', self.outdir) self.sf_set_out = os.path.join(self.outdir, 'seedfiles') From 48582bc0247fac7fc5302ed0a800b9c0631ef2f9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 10:54:39 -0500 Subject: [PATCH 0759/1169] refactor config dict fix up to its own function --- src/certfuzz/campaign/campaign_base.py | 24 +++-------------- src/certfuzz/config/simple_loader.py | 27 +++++++++++++++++++ src/certfuzz/tools/windows/minimize.py | 14 +++++----- .../analyzers/test_analyzer_base.py | 5 ++-- src/test_certfuzz/analyzers/test_valgrind.py | 13 +++++---- .../config/test_simple_loader.py | 24 +++++++++++++++++ .../minimizer/test_minimizer_base.py | 4 +-- src/test_certfuzz/mocks.py | 11 +++++--- 8 files changed, 82 insertions(+), 40 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 6e7dd8f..5877b78 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -21,9 +21,8 @@ from certfuzz.version import __version__ from certfuzz.file_handlers.tmp_reaper import TmpReaper import gc -from certfuzz.config.simple_loader import load_config -from string import Template -from certfuzz.helpers.misc import quoted, import_module_by_name, fixup_path +from certfuzz.config.simple_loader import load_and_fix_config +from certfuzz.helpers.misc import import_module_by_name logger = logging.getLogger(__name__) @@ -87,28 +86,11 @@ def __init__(self, config_file, result_dir=None, debug=False): self.outdir_base = os.path.abspath(result_dir) self._read_config_file() - self._fixup_config() def _read_config_file(self): logger.info('Reading config from %s', self.config_file) - self.config = load_config(self.config_file) - - def _fixup_config(self): - ''' - Substitutes program name into command line template - ''' - # fix target program path - self.config['target']['program'] = fixup_path(self.config['target']['program']) + self.config = load_and_fix_config(self.config_file) logger.info('Using target program: %s',self.config['target']['program']) - - quoted_prg = quoted(self.config['target']['program']) - quoted_sf = quoted('$SEEDFILE') - t = Template(self.config['target']['cmdline_template']) - intermediate_t = t.safe_substitute(PROGRAM=quoted_prg, SEEDFILE=quoted_sf) - self.config['target']['cmdline_template'] = Template(intermediate_t) - - for k,v in self.config['directories'].iteritems(): - self.config['directories'][k] = fixup_path(v) def _common_init(self): ''' diff --git a/src/certfuzz/config/simple_loader.py b/src/certfuzz/config/simple_loader.py index 1e07bc1..3bf3eba 100644 --- a/src/certfuzz/config/simple_loader.py +++ b/src/certfuzz/config/simple_loader.py @@ -7,6 +7,9 @@ import yaml import os from errors import ConfigError +from certfuzz.helpers.misc import fixup_path, quoted +from string import Template +from copy import deepcopy logger = logging.getLogger(__name__) @@ -23,3 +26,27 @@ def load_config(yaml_file): cfg['config_timestamp'] = os.path.getmtime(yaml_file) return cfg + +def fixup_config(cfg): + ''' + Substitutes program name into command line template + returns modified dict + ''' + # copy the dictionary + cfgdict = deepcopy(cfg) + # fix target program path + cfgdict['target']['program'] = fixup_path(cfgdict['target']['program']) + + quoted_prg = quoted(cfgdict['target']['program']) + quoted_sf = quoted('$SEEDFILE') + t = Template(cfgdict['target']['cmdline_template']) + intermediate_t = t.safe_substitute(PROGRAM=quoted_prg, SEEDFILE=quoted_sf) + cfgdict['target']['cmdline_template'] = Template(intermediate_t) + + for k,v in cfgdict['directories'].iteritems(): + cfgdict['directories'][k] = fixup_path(v) + + return cfgdict + +def load_and_fix_config(yaml_file): + return fixup_config(load_config(yaml_file)) \ No newline at end of file diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 4ee7dab..d93d624 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -7,8 +7,6 @@ import logging import os import string -from certfuzz.fuzztools.command_line_templating import get_command_args_list -from certfuzz.config.simple_loader import load_config try: @@ -18,6 +16,8 @@ from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport + from certfuzz.fuzztools.command_line_templating import get_command_args_list + from certfuzz.config.simple_loader import load_and_fix_config except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH import sys @@ -30,6 +30,8 @@ from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer from certfuzz.crash.crash_windows import WindowsCrash from certfuzz.debuggers import msec # @UnusedImport + from certfuzz.fuzztools.command_line_templating import get_command_args_list + from certfuzz.config.simple_loader import load_and_fix_config logger = logging.getLogger() logger.setLevel(logging.WARNING) @@ -141,7 +143,7 @@ def main(): else: parser.error('fuzzedfile must be specified') - cfg = load_config(cfg_file) + cfg = load_and_fix_config(cfg_file) if options.target: seedfile = BasicFile(options.target) @@ -152,9 +154,9 @@ def main(): filename_modifier = '' retries = 0 debugger_class = msec.MsecDebugger - template = string.Template(cfg['target']['cmdline_template']) - cmd_as_args = get_command_args_list(template, fuzzed_file.path)[1] - with WindowsCrash(template, seedfile, fuzzed_file, cmd_as_args, None, debugger_class, + + cmd_as_args = get_command_args_list(cfg['target']['cmdline_template'], fuzzed_file.path)[1] + with WindowsCrash(cfg['target']['cmdline_template'], seedfile, fuzzed_file, cmd_as_args, None, debugger_class, cfg['debugger'], outdir, options.keep_uniq_faddr, cfg['target']['program'], retries) as crash: filetools.make_directories(crash.tempdir) diff --git a/src/test_certfuzz/analyzers/test_analyzer_base.py b/src/test_certfuzz/analyzers/test_analyzer_base.py index 962e364..166c9fa 100644 --- a/src/test_certfuzz/analyzers/test_analyzer_base.py +++ b/src/test_certfuzz/analyzers/test_analyzer_base.py @@ -6,14 +6,13 @@ import unittest from certfuzz.analyzers.analyzer_base import Analyzer -from test_certfuzz.mocks import MockCfg, MockCrash - +from test_certfuzz.mocks import MockCrash, MockFixupCfg class Test(unittest.TestCase): def setUp(self): - cfg = MockCfg() + cfg = MockFixupCfg() crash = MockCrash() self.analyzer = Analyzer(cfg, crash, timeout=0) self.assertTrue(self.analyzer, 'Analyzer does not exist') diff --git a/src/test_certfuzz/analyzers/test_valgrind.py b/src/test_certfuzz/analyzers/test_valgrind.py index e118993..c6aaa2c 100644 --- a/src/test_certfuzz/analyzers/test_valgrind.py +++ b/src/test_certfuzz/analyzers/test_valgrind.py @@ -7,7 +7,7 @@ @organization: cert.org ''' import unittest -from test_certfuzz.mocks import MockCfg +from test_certfuzz.mocks import MockFixupCfg class Mock(object): pass @@ -18,7 +18,7 @@ def delete_file(self, f): self.assertFalse(os.path.exists(f)) def setUp(self): - cfg = MockCfg() + cfg = MockFixupCfg() crash = Mock() crash.fuzzedfile = Mock() @@ -31,9 +31,12 @@ def tearDown(self): pass def test_get_valgrind_cmdline(self): - expected = ["valgrind", "--log-file=foo.valgrind", "a", "b", "c", "d"] - - self.assertEqual(self.vg._get_cmdline(), expected) + result = self.vg._get_cmdline() + self.assertTrue('valgrind' in result) + self.assertTrue('b' in result) + self.assertTrue('c' in result) + self.assertTrue('d' in result) + self.assertTrue('foo' in result) def test_get_valgrind(self): # cannot test directly, see test_get_valgrind_cmdline diff --git a/src/test_certfuzz/config/test_simple_loader.py b/src/test_certfuzz/config/test_simple_loader.py index 4a83b52..369085a 100644 --- a/src/test_certfuzz/config/test_simple_loader.py +++ b/src/test_certfuzz/config/test_simple_loader.py @@ -10,6 +10,9 @@ import os import yaml from certfuzz.config.errors import ConfigError +from certfuzz.config.simple_loader import fixup_config +from test_certfuzz.mocks import MockCfg +import string class Test(unittest.TestCase): @@ -60,7 +63,28 @@ def test_nonexistent_config(self): self.assertFalse(os.path.exists(fpath)) self.assertRaises(IOError,certfuzz.config.simple_loader.load_config,fpath) + + def test_fix_config(self): + cfgdict = MockCfg(templated=False) + cfgdict2 = MockCfg(templated=False) + + # make sure cmdline_template is just a normal string + self.assertEqual(type(''),type(cfgdict['target']['cmdline_template'])) + + x = fixup_config(cfgdict) + # fixup config should return a different dict + + # make sure cfgdict didn't change + self.assertEqual(cfgdict,cfgdict2) + + # fixup_config should not alter cfgdict but return a new dict + self.assertNotEqual(cfgdict,x) + # make sure cmdline_template is a string.Template + y = string.Template('foo') + self.assertEqual(type(y),type(x['target']['cmdline_template'])) + + self.assertTrue(x['target']['program'].endswith(cfgdict['target']['program'])) if __name__ == "__main__": diff --git a/src/test_certfuzz/minimizer/test_minimizer_base.py b/src/test_certfuzz/minimizer/test_minimizer_base.py index 7d0e536..bbb28ce 100644 --- a/src/test_certfuzz/minimizer/test_minimizer_base.py +++ b/src/test_certfuzz/minimizer/test_minimizer_base.py @@ -9,7 +9,7 @@ from certfuzz.minimizer.minimizer_base import Minimizer import shutil import unittest -from test_certfuzz.mocks import MockCfg, MockDebugger, MockCrasher +from test_certfuzz.mocks import MockDebugger, MockCrasher, MockFixupCfg class Test(unittest.TestCase): def delete_file(self, f): @@ -17,7 +17,7 @@ def delete_file(self, f): self.assertFalse(os.path.exists(f)) def setUp(self): - self.cfg = MockCfg() + self.cfg = MockFixupCfg() self.crash = MockCrasher() Minimizer._debugger_cls = MockDebugger diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 1ba4bc1..fbf0dd0 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -9,6 +9,7 @@ import tempfile import os from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.config.simple_loader import fixup_config class Mock(object): def __init__(self, *args, **kwargs): @@ -95,16 +96,20 @@ def get(self): def go(self): return MockDbgOut() + class MockCfg(dict): def __init__(self,templated=True): self['debugger']={'runtimeout': 1, 'backtracelevels': 5, } - self['target']={'cmdline_template': 'a b c d', + self['target']={'cmdline_template': '$PROGRAM b c d $SEEDFILE', 'killprocname': 'a', - 'program': 'foo'} + 'program': 'a'} self['analyzer']={'exclude_unmapped_frames': False, 'valgrind_timeout': 1} self['directories'] ={} if templated: - self['target']['cmdline_template'] = string.Template(self['target']['cmdline_template']) \ No newline at end of file + self['target']['cmdline_template'] = string.Template(self['target']['cmdline_template']) + +def MockFixupCfg(): + return fixup_config(MockCfg(templated=False)) From 2747651ddb7301738dc91adb493e9ef4037a35eb Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 11:26:27 -0500 Subject: [PATCH 0760/1169] add docstring --- src/certfuzz/config/simple_loader.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/config/simple_loader.py b/src/certfuzz/config/simple_loader.py index 3bf3eba..019fdf1 100644 --- a/src/certfuzz/config/simple_loader.py +++ b/src/certfuzz/config/simple_loader.py @@ -15,6 +15,10 @@ def load_config(yaml_file): + ''' + Reads config from yaml_file, returns dict + :param yaml_file: path to a yaml file containing the configuration + ''' with open(yaml_file, 'rb') as f: cfg = yaml.load(f) From d16239b18b8a68dac9ffc6c78face6dfafa0a702 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 11:31:03 -0500 Subject: [PATCH 0761/1169] windows crash doesn't use debugger_class anymore --- src/certfuzz/tools/windows/minimize.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index d93d624..3372f6a 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -156,9 +156,17 @@ def main(): debugger_class = msec.MsecDebugger cmd_as_args = get_command_args_list(cfg['target']['cmdline_template'], fuzzed_file.path)[1] - with WindowsCrash(cfg['target']['cmdline_template'], seedfile, fuzzed_file, cmd_as_args, None, debugger_class, - cfg['debugger'], outdir, options.keep_uniq_faddr, cfg['target']['program'], - retries) as crash: + with WindowsCrash(cmd_template=cfg['target']['cmdline_template'], + seedfile=seedfile, + fuzzedfile=fuzzed_file, + cmdlist=cmd_as_args, + fuzzer=None, + dbg_opts=cfg['debugger'], + workingdir_base=outdir, + keep_faddr=options.keep_uniq_faddr, + program=cfg['target']['program'], + heisenbug_retries=retries + ) as crash: filetools.make_directories(crash.tempdir) logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) filetools.copy_file(fuzzed_file.path, crash.tempdir) From 7acc32f81206593681d4e58cb3449e6d98309837 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jan 2016 14:48:43 -0500 Subject: [PATCH 0762/1169] Get rid of remaining multi-line echo statements. --- src/linux/welcome.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/linux/welcome.sh b/src/linux/welcome.sh index 30f6858..8ecb91b 100755 --- a/src/linux/welcome.sh +++ b/src/linux/welcome.sh @@ -27,10 +27,7 @@ echo "Output directory: " `egrep -m1 '^ output_dir' $currentcfg | sed 's/^ " if [[ -n "$xterm" ]]; then - echo "Run ./batch.sh to begin fuzzing. -" + echo -e "Run ./batch.sh to begin fuzzing." elif [[ "$platform" =~ "Darwin" ]]; then - echo "X is not detected. Please install X before running BFF -See: https://support.apple.com/kb/HT5293 -" + echo -e "X is not detected. Please install X before running BFF\nSee: https://support.apple.com/kb/HT5293\n" fi From fe2bd4b51d54925c48db728dfd389e9d209dfeed Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 13:39:53 -0500 Subject: [PATCH 0763/1169] refactor some more common things out of platform-specific --- src/certfuzz/campaign/campaign_base.py | 21 +++++++++++++++++++++ src/certfuzz/campaign/campaign_linux.py | 20 +------------------- src/certfuzz/campaign/campaign_windows.py | 19 ------------------- 3 files changed, 22 insertions(+), 38 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 5877b78..63efe6b 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -96,6 +96,22 @@ def _common_init(self): ''' Initializes some additional properties common to all platforms ''' + # pull stuff out of configs + self.campaign_id = self.config['campaign']['id'] + + self.current_seed = self.config['runoptions']['first_iteration'] + self.seed_interval = self.config['runoptions']['seed_interval'] + + self.seed_dir_in = self.config['directories']['seedfile_dir'] + if self.outdir_base is None: + # it wasn't spec'ed on the command line so use the config + self.outdir_base = self.config['directories']['results_dir'] + + self.work_dir_base = self.config['directories']['working_dir'] + self.program = self.config['target']['program'] + self.cmd_template = self.config['target']['cmdline_template'] + + _campaign_id_no_space=re.sub('\s', '_', self.campaign_id) _campaign_id_with_underscores = re.sub('\W', '_', self.campaign_id) @@ -111,6 +127,11 @@ def _common_init(self): if not self.current_seed: self.current_seed = 0 + self.fuzzer_module_name = 'certfuzz.fuzzers.{}'.format(self.config['fuzzer']['fuzzer']) + if self.config['runner']['runner']: + self.runner_module_name = 'certfuzz.runners.{}'.format(self.config['runner']['runner']) + self.debugger_module_name = 'certfuzz.debuggers.{}'.format(self.config['debugger']['debugger']) + @abc.abstractmethod def _pre_enter(self): ''' diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 82f5d4f..df251fb 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -62,23 +62,11 @@ class LinuxCampaign(CampaignBase): def __init__(self, config_file=None, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) - # pull stuff out of configs - self.campaign_id = self.config['campaign']['id'] - self.current_seed = self.config['runoptions']['first_iteration'] - self.seed_interval = self.config['runoptions']['seed_interval'] - self.seed_dir_in = fixup_path(self.config['directories']['seedfile_dir']) - - if self.outdir_base is None: - # it wasn't spec'ed on the command line so use the config - self.outdir_base = fixup_path(self.config['directories']['results_dir']) - - self.work_dir_base = fixup_path(self.config['directories']['working_dir']) - self.program = fixup_path(self.config['target']['program']) + self.program_basename = os.path.basename(self.program).replace('"', '') # self.cmd_list = shlex.split(self.config['target']['cmdline']) # self.cmd_list[0] = fixup_path(self.cmd_list[0]) - # must occur after work_dir_base, outdir_base, and campaign_id are set self._common_init() @@ -160,12 +148,6 @@ def _check_for_script(self): logger.warning("Target application is a shell script.") raise CampaignScriptError() - def _set_fuzzer(self): - ''' - Overrides parent class - ''' - pass - def _set_runner(self): ''' Overrides parent class diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 10c0306..8cce29a 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -29,35 +29,16 @@ def __init__(self, config_file, result_dir=None, debug=False): self.gui_app = False - # pull stuff out of configs - self.campaign_id = self.config['campaign']['id'] - self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker') if not self.use_buttonclicker: self.use_buttonclicker = False - self.current_seed = self.config['runoptions'].get('first_iteration') - self.seed_interval = self.config['runoptions'].get('seed_interval') - - if self.outdir_base is None: - # it wasn't spec'ed on the command line so use the config - self.outdir_base = os.path.abspath(self.config['directories']['results_dir']) - self.work_dir_base = os.path.abspath(self.config['directories']['working_dir']) - - self.seed_dir_in = self.config['directories']['seedfile_dir'] self.keep_duplicates = self.config['runoptions']['keep_all_duplicates'] self.keep_heisenbugs = self.config['campaign']['keep_heisenbugs'] self.should_keep_u_faddr = self.config['runoptions']['keep_unique_faddr'] - self.program = self.config['target']['program'] - self.cmd_template = self.config['target']['cmdline_template'] - - self.fuzzer_module_name = 'certfuzz.fuzzers.{}'.format(self.config['fuzzer']['fuzzer']) - if self.config['runner']['runner']: - self.runner_module_name = 'certfuzz.runners.{}'.format(self.config['runner']['runner']) - self.debugger_module_name = 'certfuzz.debuggers.{}'.format(self.config['debugger']['debugger']) # must occur after work_dir_base, outdir_base, and campaign_id are set self._common_init() From 86f8e3f531ae28332f6976c28dda8ecc4dd09b11 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 13:50:04 -0500 Subject: [PATCH 0764/1169] rename keep_all_duplicates to keep_duplicates --- src/certfuzz/campaign/campaign_windows.py | 7 ------ src/certfuzz/iteration/iteration_linux.py | 20 ++++++++-------- src/certfuzz/iteration/iteration_windows.py | 24 +++++++++---------- .../testcase_pipeline/tc_pipeline_windows.py | 2 +- src/linux/conf.d/bff.yaml | 2 +- src/windows/configs/examples/bff.yaml | 4 ++-- 6 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 8cce29a..415bccf 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -35,11 +35,6 @@ def __init__(self, config_file, result_dir=None, debug=False): - self.keep_duplicates = self.config['runoptions']['keep_all_duplicates'] - self.keep_heisenbugs = self.config['campaign']['keep_heisenbugs'] - self.should_keep_u_faddr = self.config['runoptions']['keep_unique_faddr'] - - # must occur after work_dir_base, outdir_base, and campaign_id are set self._common_init() @@ -181,8 +176,6 @@ def _do_iteration(self, sf, range_obj, seednum): config=self.config, fuzzer_cls=self.fuzzer_cls, runner_cls=self.runner_cls, - keep_heisenbugs=self.keep_heisenbugs, - keep_duplicates=self.keep_duplicates, cmd_template=self.cmd_template, debug=self.debug, ) as iteration: diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index c3ebd43..6feb88c 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -28,21 +28,21 @@ def __init__(self, outdir=None, sf_set=None, uniq_func=None, - cfg=None, + config=None, fuzzer_cls=None, runner_cls=None, ): IterationBase3.__init__(self, - seedfile, - seednum, - workdirbase, - outdir, - sf_set, - uniq_func, - cfg, - fuzzer_cls, - runner_cls, + seedfile=seedfile, + seednum=seednum, + workdirbase=workdirbase, + outdir=outdir, + sf_set=sf_set, + uniq_func=uniq_func, + config=config, + fuzzer_cls=fuzzer_cls, + runner_cls=runner_cls, ) self.quiet_flag = self._iteration_counter < 2 diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index b7d28f5..aa41b5e 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -42,21 +42,19 @@ def __init__(self, config=None, fuzzer_cls=None, runner_cls=None, - keep_heisenbugs=None, - keep_duplicates=None, cmd_template=None, debug=False, ): IterationBase3.__init__(self, - seedfile, - seednum, - workdirbase, - outdir, - sf_set, - uniq_func, - config, - fuzzer_cls, - runner_cls, + seedfile=seedfile, + seednum=seednum, + workdirbase=workdirbase, + outdir=outdir, + sf_set=sf_set, + uniq_func=uniq_func, + config=config, + fuzzer_cls=fuzzer_cls, + runner_cls=runner_cls, ) self.debug = debug @@ -72,8 +70,8 @@ def __init__(self, # runner_cls is not null self.retries = 4 - self.pipeline_options = {'keep_duplicates': keep_duplicates, - 'keep_heisenbugs': keep_heisenbugs, + self.pipeline_options = {'keep_duplicates': self.config['runoptions']['keep_duplicates'], + 'keep_heisenbugs': self.config['runoptions']['keep_heisenbugs'], 'minimizable': False, 'cmd_template': self.cmd_template, 'used_runner': self.runner_cls is not None, diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py index f3add6a..ed39f52 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py @@ -94,7 +94,7 @@ def keep_testcase(self, testcase): @return (bool,str) ''' if testcase.is_crash: - if self.options['keep_all_duplicates']: + if self.options['keep_duplicates']: return (True, 'keep duplicates') elif self.uniq_func(testcase.signature): # Check if crasher directory exists already diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 8a29854..d2703be 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -117,7 +117,7 @@ runoptions: minimizer_timeout: 3600 minimize_to_string: False # keep_unique_faddr: False - keep_all_duplicates: False + keep_duplicates: False recycle_crashers: False # maximum time (seconds) that process specified by "killprocname" option diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index fbe483f..15295a2 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -72,7 +72,7 @@ directories: # a minimization run before giving up # keep_unique_faddr: Consider the Exception Faulting Address value as # part of the crash hash -# keep_all_duplicates: Keep all duplicate crashing cases +# keep_duplicates: Keep all duplicate crashing cases # recycle_crashers: Recycle uniquely-crashing testcases into the pool # of available seed files to fuzz ##################################################################### @@ -82,7 +82,7 @@ runoptions: minimize: True minimizer_timeout: 3600 keep_unique_faddr: False - keep_all_duplicates: False + keep_duplicates: False recycle_crashers: False ##################################################################### From e372cf1d28b2562cc45b58f6e85e44f862fe4e1f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 14:30:22 -0500 Subject: [PATCH 0765/1169] refactor common init stuff --- src/certfuzz/campaign/campaign_base.py | 18 ++++++------------ src/certfuzz/campaign/campaign_linux.py | 15 +++------------ src/certfuzz/campaign/campaign_windows.py | 12 +----------- 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 63efe6b..4f1c9e3 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -34,7 +34,6 @@ class CampaignBase(object): ''' __metaclass__ = abc.ABCMeta - @abc.abstractmethod def __init__(self, config_file, result_dir=None, debug=False): ''' Typically one would invoke a campaign as follows: @@ -75,6 +74,7 @@ def __init__(self, config_file, result_dir=None, debug=False): # TODO: consider making this configurable self.status_interval = 100 + self.gui_app = False self.seed_interval = None self.current_seed = None @@ -87,16 +87,6 @@ def __init__(self, config_file, result_dir=None, debug=False): self._read_config_file() - def _read_config_file(self): - logger.info('Reading config from %s', self.config_file) - self.config = load_and_fix_config(self.config_file) - logger.info('Using target program: %s',self.config['target']['program']) - - def _common_init(self): - ''' - Initializes some additional properties common to all platforms - ''' - # pull stuff out of configs self.campaign_id = self.config['campaign']['id'] self.current_seed = self.config['runoptions']['first_iteration'] @@ -110,7 +100,6 @@ def _common_init(self): self.work_dir_base = self.config['directories']['working_dir'] self.program = self.config['target']['program'] self.cmd_template = self.config['target']['cmdline_template'] - _campaign_id_no_space=re.sub('\s', '_', self.campaign_id) _campaign_id_with_underscores = re.sub('\W', '_', self.campaign_id) @@ -132,6 +121,11 @@ def _common_init(self): self.runner_module_name = 'certfuzz.runners.{}'.format(self.config['runner']['runner']) self.debugger_module_name = 'certfuzz.debuggers.{}'.format(self.config['debugger']['debugger']) + def _read_config_file(self): + logger.info('Reading config from %s', self.config_file) + self.config = load_and_fix_config(self.config_file) + logger.info('Using target program: %s',self.config['target']['program']) + @abc.abstractmethod def _pre_enter(self): ''' diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index df251fb..9a6bdfb 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -59,21 +59,12 @@ class LinuxCampaign(CampaignBase): ''' Extends CampaignBase to add linux-specific features. ''' - def __init__(self, config_file=None, result_dir=None, debug=False): - CampaignBase.__init__(self, config_file, result_dir, debug) - - - self.program_basename = os.path.basename(self.program).replace('"', '') -# self.cmd_list = shlex.split(self.config['target']['cmdline']) -# self.cmd_list[0] = fixup_path(self.cmd_list[0]) - - # must occur after work_dir_base, outdir_base, and campaign_id are set - self._common_init() def _full_path_original(self, seedfile): # yes, two seedfile mentions are intended - adh + program_basename=os.path.basename(self.program).replace('"', '') return os.path.join(self.work_dir_base, - self.program_basename, + program_basename, seedfile, seedfile) @@ -193,8 +184,8 @@ def _do_iteration(self, seedfile, range_obj, seednum): outdir=self.outdir, sf_set=self.seedfile_set, uniq_func=self._crash_is_unique, - cfg=self.config, fuzzer_cls=ByteMutFuzzer, + config=self.config, runner_cls=ZzufRunner, ) as iteration: iteration() diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 415bccf..761c70c 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -26,17 +26,7 @@ class WindowsCampaign(CampaignBase): def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) - - self.gui_app = False - - self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker') - if not self.use_buttonclicker: - self.use_buttonclicker = False - - - - # must occur after work_dir_base, outdir_base, and campaign_id are set - self._common_init() + self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker',False) def __getstate__(self): state = self.__dict__.copy() From 8daa21063d0f5d9414a6ae89d359bc2fb3674f49 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 14:31:06 -0500 Subject: [PATCH 0766/1169] make fuzzer class configurable --- src/certfuzz/campaign/campaign_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 9a6bdfb..3a69401 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -184,8 +184,8 @@ def _do_iteration(self, seedfile, range_obj, seednum): outdir=self.outdir, sf_set=self.seedfile_set, uniq_func=self._crash_is_unique, - fuzzer_cls=ByteMutFuzzer, config=self.config, + fuzzer_cls=self.fuzzer_cls, runner_cls=ZzufRunner, ) as iteration: iteration() From 70666e021a21a5f0643530f648aed2a080f0aa3f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 14:31:41 -0500 Subject: [PATCH 0767/1169] range id isn't guaranteed to exist --- src/certfuzz/iteration/iteration_base3.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 20e4bae..a6ce82e 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -141,14 +141,16 @@ def run(self): def record_success(self): self.sf_set.record_success(key=self.seedfile.md5) - self.seedfile.rangefinder.record_success(key=self.r.id) + if hasattr(self.r, 'id'): + self.seedfile.rangefinder.record_success(key=self.r.id) def record_failure(self): self.record_tries() def record_tries(self): self.sf_set.record_tries(key=self.seedfile.md5, tries=1) - self.seedfile.rangefinder.record_tries(key=self.r.id, tries=1) + if hasattr(self.r, 'id'): + self.seedfile.rangefinder.record_tries(key=self.r.id, tries=1) def process_testcases(self): if not len(self.testcases): From 1f4c57c604597b7115700891b53dadbd725ab27d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 14:48:00 -0500 Subject: [PATCH 0768/1169] fix unit tests --- .../campaign/test_campaign_base.py | 3 --- .../campaign/test_campaign_linux.py | 24 ++++++++++--------- src/test_certfuzz/mocks.py | 10 +++++++- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/test_certfuzz/campaign/test_campaign_base.py b/src/test_certfuzz/campaign/test_campaign_base.py index 27da715..64b3614 100644 --- a/src/test_certfuzz/campaign/test_campaign_base.py +++ b/src/test_certfuzz/campaign/test_campaign_base.py @@ -24,9 +24,6 @@ class ImplementedCampaign(CampaignBase): def __getstate__(self): pass - def __init__(self, config_file, result_dir=None): - return CampaignBase.__init__(self, config_file, result_dir) - def __setstate__(self): pass diff --git a/src/test_certfuzz/campaign/test_campaign_linux.py b/src/test_certfuzz/campaign/test_campaign_linux.py index 863b497..c7f5606 100644 --- a/src/test_certfuzz/campaign/test_campaign_linux.py +++ b/src/test_certfuzz/campaign/test_campaign_linux.py @@ -10,6 +10,7 @@ import shutil from certfuzz.config.errors import ConfigError import yaml +from test_certfuzz.mocks import MockCfg class Test(unittest.TestCase): @@ -19,17 +20,18 @@ def setUp(self): fd, cfgfile = tempfile.mkstemp(suffix=".yaml", dir=self.tmpdir, text=True) os.close(fd) - data = {'campaign': {'id': 'foo'}, - 'directories': {'seedfile_dir': tempfile.mkdtemp(prefix='seedfiles_', dir=self.tmpdir), - 'results_dir': tempfile.mkdtemp(prefix='output_', dir=self.tmpdir), - 'working_dir': tempfile.mkdtemp(prefix='local_', dir=self.tmpdir)}, - 'timeouts': {}, - 'verifier': {}, - 'target': {'program': 'foo', - 'cmdline_template': 'foo bar baz quux'}, - 'runoptions':{'first_iteration':0, - 'seed_interval': 10} - } + data = MockCfg(templated=False) +# data = {'campaign': {'id': 'foo'}, +# 'directories': {'seedfile_dir': tempfile.mkdtemp(prefix='seedfiles_', dir=self.tmpdir), +# 'results_dir': tempfile.mkdtemp(prefix='output_', dir=self.tmpdir), +# 'working_dir': tempfile.mkdtemp(prefix='local_', dir=self.tmpdir)}, +# 'timeouts': {}, +# 'verifier': {}, +# 'target': {'program': 'foo', +# 'cmdline_template': 'foo bar baz quux'}, +# 'runoptions':{'first_iteration':0, +# 'seed_interval': 10} +# } with open(cfgfile, 'wb') as stream: yaml.dump(data, stream) diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index fbf0dd0..03a48a5 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -101,13 +101,21 @@ class MockCfg(dict): def __init__(self,templated=True): self['debugger']={'runtimeout': 1, 'backtracelevels': 5, + 'debugger': 'gdb', } self['target']={'cmdline_template': '$PROGRAM b c d $SEEDFILE', 'killprocname': 'a', 'program': 'a'} self['analyzer']={'exclude_unmapped_frames': False, 'valgrind_timeout': 1} - self['directories'] ={} + self['directories'] ={'seedfile_dir': '', + 'results_dir': '', + 'working_dir': ''} + self['fuzzer']={'fuzzer': 'bytemut'} + self['campaign']={'id': 'xyz'} + self['runoptions']={'first_iteration': 0, + 'seed_interval': 10} + self['runner']={'runner': 'zzufrun'} if templated: self['target']['cmdline_template'] = string.Template(self['target']['cmdline_template']) From dc2490d8525a80fc85f7f9ea66078caa2ec3e1b4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 15:07:29 -0500 Subject: [PATCH 0769/1169] restore sendfile removal from set when fuzzer exhausted error is observed --- src/certfuzz/campaign/campaign_linux.py | 9 ++++++++- src/certfuzz/campaign/campaign_windows.py | 10 +++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 3a69401..bc0339f 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -24,6 +24,7 @@ from certfuzz.runners.zzufrun import ZzufRunner from certfuzz.helpers.misc import fixup_path from certfuzz.fuzztools.command_line_templating import get_command_args_list +from certfuzz.fuzzers.errors import FuzzerExhaustedError logger = logging.getLogger(__name__) @@ -188,4 +189,10 @@ def _do_iteration(self, seedfile, range_obj, seednum): fuzzer_cls=self.fuzzer_cls, runner_cls=ZzufRunner, ) as iteration: - iteration() + try: + iteration() + except FuzzerExhaustedError: + # Some fuzzers run out of things to do. They should + # raise a FuzzerExhaustedError when that happens. + logger.info('Done with %s, removing from set', seedfile.basename) + self.seedfile_set.remove_file(seedfile) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 761c70c..efc7793 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -153,11 +153,11 @@ def _stop_buttonclicker(self): if self.use_buttonclicker: os.system('taskkill /im buttonclicker.exe') - def _do_iteration(self, sf, range_obj, seednum): + def _do_iteration(self, seedfile, range_obj, seednum): # use a with...as to ensure we always hit # the __enter__ and __exit__ methods of the # newly created WindowsIteration() - with WindowsIteration(seedfile=sf, + with WindowsIteration(seedfile=seedfile, seednum=seednum, workdirbase=self.working_dir, outdir=self.outdir, @@ -174,9 +174,9 @@ def _do_iteration(self, sf, range_obj, seednum): except FuzzerExhaustedError: # Some fuzzers run out of things to do. They should # raise a FuzzerExhaustedError when that happens. - logger.info('Done with %s, removing from set', sf.basename) - # FIXME - # self.seedfile_set.del_item(sf.md5) + logger.info('Done with %s, removing from set', seedfile.basename) + self.seedfile_set.remove_file(seedfile) + if not seednum % self.status_interval: logger.info('Iteration: %d Crashes found: %d', self.current_seed, len(self.crashes_seen)) From 564b3e1b7e11c8170919c8e1aba50c32ef1a362d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jan 2016 15:19:43 -0500 Subject: [PATCH 0770/1169] Fix up remaining multi-line echo. Change output_dir to results_dir --- src/linux/welcome.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/linux/welcome.sh b/src/linux/welcome.sh index 8ecb91b..f477a15 100755 --- a/src/linux/welcome.sh +++ b/src/linux/welcome.sh @@ -22,12 +22,10 @@ else fi echo "Target commandline: " `egrep -m1 '^ cmdline' $currentcfg | sed 's/^ cmdline://'` -echo "Output directory: " `egrep -m1 '^ output_dir' $currentcfg | sed 's/^ output_dir://'` - -" +echo -e "Output directory: " `egrep -m1 '^ results_dir' $currentcfg | sed 's/^ results_dir://'` "\n\n" if [[ -n "$xterm" ]]; then - echo -e "Run ./batch.sh to begin fuzzing." + echo -e "Run ./batch.sh to begin fuzzing.\n" elif [[ "$platform" =~ "Darwin" ]]; then echo -e "X is not detected. Please install X before running BFF\nSee: https://support.apple.com/kb/HT5293\n" fi From f8c707670852eff8a2d47a19a42852993055ab66 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jan 2016 16:03:06 -0500 Subject: [PATCH 0771/1169] $PROGRAM variable expansion for welcome.sh --- src/linux/welcome.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/linux/welcome.sh b/src/linux/welcome.sh index f477a15..b16ae48 100755 --- a/src/linux/welcome.sh +++ b/src/linux/welcome.sh @@ -21,7 +21,8 @@ else echo "Using configuration file: $currentcfg" fi -echo "Target commandline: " `egrep -m1 '^ cmdline' $currentcfg | sed 's/^ cmdline://'` +program=`egrep -m1 '^ program:' $currentcfg | sed 's/^ program://'` +echo "Target commandline: " `egrep -m1 '^ cmdline' $currentcfg | sed 's/^ cmdline_template://' | sed "s|"'$PROGRAM'"|$program|"` echo -e "Output directory: " `egrep -m1 '^ results_dir' $currentcfg | sed 's/^ results_dir://'` "\n\n" if [[ -n "$xterm" ]]; then From 766971be53df01b7172dc68064b710b228417a72 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 29 Jan 2016 16:11:09 -0500 Subject: [PATCH 0772/1169] fix unit test --- src/certfuzz/iteration/iteration_windows.py | 8 ++++---- src/test_certfuzz/iteration/test_iteration_windows.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index aa41b5e..17fcc96 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -59,7 +59,7 @@ def __init__(self, self.debug = debug # TODO: do we use keep_uniq_faddr at all? - self.keep_uniq_faddr = config['runoptions']['keep_unique_faddr'] + self.keep_uniq_faddr = config['runoptions'].get('keep_unique_faddr',False) self.cmd_template = cmd_template @@ -70,12 +70,12 @@ def __init__(self, # runner_cls is not null self.retries = 4 - self.pipeline_options = {'keep_duplicates': self.config['runoptions']['keep_duplicates'], - 'keep_heisenbugs': self.config['runoptions']['keep_heisenbugs'], + self.pipeline_options = {'keep_duplicates': self.cfg['runoptions'].get('keep_duplicates',False), + 'keep_heisenbugs': self.cfg['runoptions'].get('keep_heisenbugs',False), 'minimizable': False, 'cmd_template': self.cmd_template, 'used_runner': self.runner_cls is not None, - 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions']['minimize'] + 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize',False) } def __exit__(self, etype, value, traceback): diff --git a/src/test_certfuzz/iteration/test_iteration_windows.py b/src/test_certfuzz/iteration/test_iteration_windows.py index 79fa402..645cb0e 100644 --- a/src/test_certfuzz/iteration/test_iteration_windows.py +++ b/src/test_certfuzz/iteration/test_iteration_windows.py @@ -13,6 +13,8 @@ class Test(unittest.TestCase): def setUp(self): # args: + + # seedfile=None, # seednum=None, # workdirbase=None, @@ -22,12 +24,10 @@ def setUp(self): # config=None, # fuzzer_cls=None, # runner_cls=None, -# keep_heisenbugs=None, -# keep_duplicates=None, # cmd_template=None, # debug=False, - args = list('ABCDEFGHIJKLM') + args = list('ABCDEFGHILM') args[6] = {'runoptions': {'keep_unique_faddr': False}} args[7] = MockFuzzer self.iteration = WindowsIteration(*args) From 5c64e24bfd9943db40278f304b10804bd3841e3c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 1 Feb 2016 17:07:42 -0500 Subject: [PATCH 0773/1169] Only "fuzz" the file to be fuzzed. Given that zzuf isn't actually mutating anything at this point, this probably doesn't matter so much. --- src/certfuzz/runners/zzufrun.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 2e1205c..b7acc20 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -58,6 +58,7 @@ def _construct_zzuf_args(self): '--max-crashes=1', '--max-usertime=5.00', '--opmode=copy', + '--include=%s' % self.fuzzed_file, ]) self._zzuf_args = args From 44af258ad4c31a94c082c032783a32ab5e30d7b4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 08:19:13 -0500 Subject: [PATCH 0774/1169] cmdargs is not the same as cmdlist...the latter includes the program, the former does not --- src/certfuzz/crash/bff_crash.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/crash/bff_crash.py index 898800d..7f338d6 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/crash/bff_crash.py @@ -75,10 +75,10 @@ def set_debugger_template(self, option='bt_only'): def update_crash_details(self): Testcase.update_crash_details(self) - self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'], + cmdlist = get_command_args_list(self.cfg['target']['cmdline_template'], infile=self.fuzzedfile.path, posix=True)[1] - + self.cmdargs = cmdlist[1:] self.is_crash = self.confirm_crash() if self.is_crash: From a065570e17296d7ffe93ead090d5ff07a42e6cb8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 07:54:13 -0500 Subject: [PATCH 0775/1169] rename certfuzz.crash -> certfuzz.testcase (+8 squashed commits) Squashed commits: [4a20e88] rename certfuzz.testcase_pipeline -> certfuzz.tc_pipeline [d53fc51] rename bff_crash -> testcase_linux [d24b17a] rename crash_base -> testcase_base2 [df2741d] rename crash_windows -> testcase_windows [aae1557] rename BffCrash -> LinuxTestcase [c6137d6] rename WindowsCrash -> WindowsTestcase [1de9be7] rename testcase tests [61c6e2a] at least import the modules in test cases rename testcase tests rename WindowsCrash -> WindowsTestcase rename BffCrash -> LinuxTestcase rename crash_windows -> testcase_windows rename crash_base -> testcase_base2 rename bff_crash -> testcase_linux rename certfuzz.testcase_pipeline -> certfuzz.tc_pipeline rename certfuzz.crash -> certfuzz.testcase --- src/certfuzz/iteration/iteration_linux.py | 6 +- src/certfuzz/iteration/iteration_windows.py | 6 +- .../{crash => tc_pipeline}/__init__.py | 0 .../errors.py | 0 .../tc_pipeline_base.py | 0 .../tc_pipeline_linux.py | 2 +- .../tc_pipeline_windows.py | 2 +- .../__init__.py | 0 src/certfuzz/{crash => testcase}/bff_crash.py | 8 +- .../{crash => testcase}/crash_base.py | 4 +- .../{crash => testcase}/crash_windows.py | 4 +- src/certfuzz/{crash => testcase}/errors.py | 0 .../{crash => testcase}/testcase_base.py | 0 src/certfuzz/testcase/testcase_base2.py | 147 ++++++++++ src/certfuzz/testcase/testcase_linux.py | 191 +++++++++++++ src/certfuzz/testcase/testcase_windows.py | 270 ++++++++++++++++++ src/certfuzz/tools/linux/minimize.py | 4 +- src/certfuzz/tools/windows/minimize.py | 6 +- .../{crash => testcase}/__init__.py | 0 .../{crash => testcase}/test_testcase_base.py | 1 + .../test_testcase_base2.py} | 1 + .../test_testcase_linux.py} | 2 +- .../test_testcase_windows.py} | 2 +- .../test_tc_pipeline_base.py | 12 +- .../test_tc_pipeline_linux.py | 2 +- .../test_tc_pipeline_windows.py | 2 +- 26 files changed, 641 insertions(+), 31 deletions(-) rename src/certfuzz/{crash => tc_pipeline}/__init__.py (100%) rename src/certfuzz/{testcase_pipeline => tc_pipeline}/errors.py (100%) rename src/certfuzz/{testcase_pipeline => tc_pipeline}/tc_pipeline_base.py (100%) rename src/certfuzz/{testcase_pipeline => tc_pipeline}/tc_pipeline_linux.py (99%) rename src/certfuzz/{testcase_pipeline => tc_pipeline}/tc_pipeline_windows.py (98%) rename src/certfuzz/{testcase_pipeline => testcase}/__init__.py (100%) rename src/certfuzz/{crash => testcase}/bff_crash.py (97%) rename src/certfuzz/{crash => testcase}/crash_base.py (97%) rename src/certfuzz/{crash => testcase}/crash_windows.py (99%) rename src/certfuzz/{crash => testcase}/errors.py (100%) rename src/certfuzz/{crash => testcase}/testcase_base.py (100%) create mode 100644 src/certfuzz/testcase/testcase_base2.py create mode 100644 src/certfuzz/testcase/testcase_linux.py create mode 100644 src/certfuzz/testcase/testcase_windows.py rename src/test_certfuzz/{crash => testcase}/__init__.py (100%) rename src/test_certfuzz/{crash => testcase}/test_testcase_base.py (88%) rename src/test_certfuzz/{crash/test_crash_base.py => testcase/test_testcase_base2.py} (88%) rename src/test_certfuzz/{crash/test_bff_crash.py => testcase/test_testcase_linux.py} (96%) rename src/test_certfuzz/{crash/test_crash_windows.py => testcase/test_testcase_windows.py} (88%) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 6feb88c..ab0e5cf 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -6,12 +6,12 @@ import logging import os -from certfuzz.crash.bff_crash import BffCrash +from certfuzz.testcase.testcase_linux import LinuxTestcase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.fuzztools.zzuflog import ZzufLog from certfuzz.iteration.iteration_base3 import IterationBase3 -from certfuzz.testcase_pipeline.tc_pipeline_linux import LinuxTestCasePipeline +from certfuzz.tc_pipeline.tc_pipeline_linux import LinuxTestCasePipeline from certfuzz.helpers.misc import fixup_path @@ -109,7 +109,7 @@ def _post_run(self): def _construct_testcase(self): logger.info('Building testcase object') - with BffCrash(cfg=self.cfg, + with LinuxTestcase(cfg=self.cfg, seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), program=fixup_path(self.cfg['target']['program']), diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 17fcc96..3d5fd8d 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -7,7 +7,7 @@ import logging import os -from certfuzz.crash.crash_windows import WindowsCrash +from certfuzz.testcase.testcase_windows import WindowsTestcase from certfuzz.debuggers.output_parsers.errors import DebuggerFileError from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.file_handlers.tmp_reaper import TmpReaper @@ -18,7 +18,7 @@ from certfuzz.fuzztools.filetools import delete_files_or_dirs from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.runners.errors import RunnerRegistryError -from certfuzz.testcase_pipeline.tc_pipeline_windows import WindowsTestCasePipeline +from certfuzz.tc_pipeline.tc_pipeline_windows import WindowsTestCasePipeline from certfuzz.fuzztools.command_line_templating import get_command_args_list @@ -170,7 +170,7 @@ def _post_run(self): def _construct_testcase(self): logger.debug('Building testcase object') - with WindowsCrash(cmd_template=self.cmd_template, + with WindowsTestcase(cmd_template=self.cmd_template, seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), cmdlist=get_command_args_list(self.cmd_template, self.fuzzer.output_file_path)[1], diff --git a/src/certfuzz/crash/__init__.py b/src/certfuzz/tc_pipeline/__init__.py similarity index 100% rename from src/certfuzz/crash/__init__.py rename to src/certfuzz/tc_pipeline/__init__.py diff --git a/src/certfuzz/testcase_pipeline/errors.py b/src/certfuzz/tc_pipeline/errors.py similarity index 100% rename from src/certfuzz/testcase_pipeline/errors.py rename to src/certfuzz/tc_pipeline/errors.py diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py similarity index 100% rename from src/certfuzz/testcase_pipeline/tc_pipeline_base.py rename to src/certfuzz/tc_pipeline/tc_pipeline_base.py diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py similarity index 99% rename from src/certfuzz/testcase_pipeline/tc_pipeline_linux.py rename to src/certfuzz/tc_pipeline/tc_pipeline_linux.py index 90dea69..df52111 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -19,7 +19,7 @@ from certfuzz.fuzztools import filetools from certfuzz.minimizer.errors import MinimizerError from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer -from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase +from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.reporters.copy_files import CopyFilesReporter from certfuzz.reporters.testcase_logger import TestcaseLoggerReporter diff --git a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py similarity index 98% rename from src/certfuzz/testcase_pipeline/tc_pipeline_windows.py rename to src/certfuzz/tc_pipeline/tc_pipeline_windows.py index ed39f52..6e3b666 100644 --- a/src/certfuzz/testcase_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -7,7 +7,7 @@ import os from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer -from certfuzz.testcase_pipeline.tc_pipeline_base import TestCasePipelineBase +from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.fuzztools import filetools from certfuzz.minimizer.errors import MinimizerError from certfuzz.reporters.copy_files import CopyFilesReporter diff --git a/src/certfuzz/testcase_pipeline/__init__.py b/src/certfuzz/testcase/__init__.py similarity index 100% rename from src/certfuzz/testcase_pipeline/__init__.py rename to src/certfuzz/testcase/__init__.py diff --git a/src/certfuzz/crash/bff_crash.py b/src/certfuzz/testcase/bff_crash.py similarity index 97% rename from src/certfuzz/crash/bff_crash.py rename to src/certfuzz/testcase/bff_crash.py index 7f338d6..880688e 100644 --- a/src/certfuzz/crash/bff_crash.py +++ b/src/certfuzz/testcase/bff_crash.py @@ -6,7 +6,7 @@ import logging import os -from certfuzz.crash.crash_base import Testcase, CrashError +from certfuzz.testcase.testcase_base2 import Testcase, CrashError from certfuzz.fuzztools import hostinfo, filetools from certfuzz.fuzztools.command_line_templating import get_command_args_list @@ -27,7 +27,7 @@ from certfuzz.debuggers.crashwrangler import CrashWrangler as debugger_cls -class BffCrash(Testcase): +class LinuxTestcase(Testcase): ''' classdocs ''' @@ -75,10 +75,10 @@ def set_debugger_template(self, option='bt_only'): def update_crash_details(self): Testcase.update_crash_details(self) - cmdlist = get_command_args_list(self.cfg['target']['cmdline_template'], + self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'], infile=self.fuzzedfile.path, posix=True)[1] - self.cmdargs = cmdlist[1:] + self.is_crash = self.confirm_crash() if self.is_crash: diff --git a/src/certfuzz/crash/crash_base.py b/src/certfuzz/testcase/crash_base.py similarity index 97% rename from src/certfuzz/crash/crash_base.py rename to src/certfuzz/testcase/crash_base.py index 6996a3e..48ead05 100644 --- a/src/certfuzz/crash/crash_base.py +++ b/src/certfuzz/testcase/crash_base.py @@ -7,8 +7,8 @@ import os import tempfile -from certfuzz.crash.errors import CrashError -from certfuzz.crash.testcase_base import TestCaseBase +from certfuzz.testcase.errors import CrashError +from certfuzz.testcase.testcase_base import TestCaseBase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools import filetools diff --git a/src/certfuzz/crash/crash_windows.py b/src/certfuzz/testcase/crash_windows.py similarity index 99% rename from src/certfuzz/crash/crash_windows.py rename to src/certfuzz/testcase/crash_windows.py index bb05e09..c25eae5 100644 --- a/src/certfuzz/crash/crash_windows.py +++ b/src/certfuzz/testcase/crash_windows.py @@ -7,7 +7,7 @@ import logging import os -from certfuzz.crash.crash_base import Testcase +from certfuzz.testcase.crash_base import Testcase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import best_effort_move from certfuzz.helpers.misc import random_str @@ -38,7 +38,7 @@ def logerror(func, path, excinfo): } -class WindowsCrash(Testcase): +class WindowsTestcase(Testcase): tmpdir_pfx = 'bff-crash-' _debugger_cls = MsecDebugger diff --git a/src/certfuzz/crash/errors.py b/src/certfuzz/testcase/errors.py similarity index 100% rename from src/certfuzz/crash/errors.py rename to src/certfuzz/testcase/errors.py diff --git a/src/certfuzz/crash/testcase_base.py b/src/certfuzz/testcase/testcase_base.py similarity index 100% rename from src/certfuzz/crash/testcase_base.py rename to src/certfuzz/testcase/testcase_base.py diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py new file mode 100644 index 0000000..48ead05 --- /dev/null +++ b/src/certfuzz/testcase/testcase_base2.py @@ -0,0 +1,147 @@ +''' +Created on Oct 11, 2012 + +@organization: cert.org +''' +import logging +import os +import tempfile + +from certfuzz.testcase.errors import CrashError +from certfuzz.testcase.testcase_base import TestCaseBase +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.fuzztools import filetools + + +logger = logging.getLogger(__name__) + + +class Testcase(TestCaseBase): + tmpdir_pfx = 'crash-' + _debugger_cls = None + + def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): + logger.debug('Inititalize Testcase') + TestCaseBase.__init__(self, seedfile, fuzzedfile) + + self.debugger_timeout = dbg_timeout + + self.debugger_template = None + # All crashes are heisenbugs until proven otherwise + self.is_heisenbug = True + + self.workdir_base = tempfile.gettempdir() + + # set some defaults + # Not a crash until we're sure + self.is_crash = False + self.debugger_file = None + self.is_unique = False + self.should_proceed_with_analysis = False + self.is_corrupt_stack = False + self.copy_fuzzedfile = True + self.pc = None + self.logger = None + self.result_dir = None + self.debugger_missed_stack_corruption = False + self.total_stack_corruption = False + self.pc_in_function = False + + def _create_workdir_base(self): + # make sure the workdir_base exists + if not os.path.exists(self.workdir_base): + filetools.make_directories([self.workdir_base]) + + def __enter__(self): + self._create_workdir_base() + self.update_crash_details() + return self + + def __exit__(self, etype, value, traceback): + pass + + def _get_output_dir(self, *args): + raise NotImplementedError + + def _rename_dbg_file(self): + raise NotImplementedError + + def _rename_fuzzed_file(self): + raise NotImplementedError + + def _set_attr_from_dbg(self, attrname): + raise NotImplementedError + + def _verify_crash_base_dir(self): + raise NotImplementedError + + def calculate_hamming_distances(self): + TestCaseBase.calculate_hamming_distances(self) + self.logger.info("bitwise_hd=%d", self.hd_bits) + self.logger.info("bytewise_hd=%d", self.hd_bytes) + + def calculate_hamming_distances_a(self): + TestCaseBase.calculate_hamming_distances_a(self) + self.logger.info("bitwise_hd=%d", self.hd_bits) + self.logger.info("bytewise_hd=%d", self.hd_bytes) + + def clean_tmpdir(self): + logger.debug('Cleaning up %s', self.tempdir) + if os.path.exists(self.tempdir): + filetools.delete_files_or_dirs([self.tempdir]) + else: + logger.debug('No tempdir at %s', self.tempdir) + + if os.path.exists(self.tempdir): + logger.debug('Unable to remove tempdir %s', self.tempdir) + + def confirm_crash(self): + raise NotImplementedError + + def copy_files_to_temp(self): + if self.fuzzedfile and self.copy_fuzzedfile: + filetools.copy_file(self.fuzzedfile.path, self.tempdir) + + if self.seedfile: + filetools.copy_file(self.seedfile.path, self.tempdir) + + new_fuzzedfile = os.path.join(self.tempdir, self.fuzzedfile.basename) + self.fuzzedfile = BasicFile(new_fuzzedfile) + + def copy_files(self, outdir): + crash_files = os.listdir(self.tempdir) + for file in crash_files: + filepath = os.path.join(self.tempdir, file) + if os.path.isfile(filepath): + filetools.copy_file(filepath, outdir) + + def debug(self, tries_remaining=None): + raise NotImplementedError + + def debug_once(self): + raise NotImplementedError + + def delete_files(self): + if os.path.isdir(self.fuzzedfile.dirname): + logger.debug('Deleting files from %s', self.fuzzedfile.dirname) + filetools.delete_files_or_dirs([self.fuzzedfile.dirname]) + + def get_debug_output(self, f): + raise NotImplementedError + + def get_logger(self): + raise NotImplementedError + + def get_result_dir(self): + raise NotImplementedError + + def get_signature(self): + raise NotImplementedError + + def set_debugger_template(self, option='bt_only'): + raise NotImplementedError + + def update_crash_details(self): + self.tempdir = tempfile.mkdtemp(prefix=self.tmpdir_pfx, dir=self.workdir_base) + self.copy_files_to_temp() +# raise NotImplementedError diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py new file mode 100644 index 0000000..b6ab370 --- /dev/null +++ b/src/certfuzz/testcase/testcase_linux.py @@ -0,0 +1,191 @@ +''' +Created on Jul 19, 2011 + +@organization: cert.org +''' +import logging +import os + +from certfuzz.testcase.crash_base import Testcase, CrashError +from certfuzz.fuzztools import hostinfo, filetools +from certfuzz.fuzztools.command_line_templating import get_command_args_list + +try: + from certfuzz.analyzers import pin_calltrace + from certfuzz.analyzers.errors import AnalyzerEmptyOutputError + from certfuzz.debuggers.output_parsers.calltracefile import Calltracefile +except ImportError: + pass + +logger = logging.getLogger(__name__) + +host_info = hostinfo.HostInfo() + +if host_info.is_linux(): + from certfuzz.debuggers.gdb import GDB as debugger_cls +elif host_info.is_osx(): + from certfuzz.debuggers.crashwrangler import CrashWrangler as debugger_cls + + +class BffCrash(Testcase): + ''' + classdocs + ''' + tmpdir_pfx = 'bff-crash-' + _debugger_cls = debugger_cls + + def __init__(self, cfg, seedfile, fuzzedfile, program, + debugger_timeout, killprocname, backtrace_lines, + crashers_dir, workdir_base, seednum=None, range=None, keep_faddr=False): + ''' + Constructor + ''' + Testcase.__init__(self, seedfile, fuzzedfile, debugger_timeout) + self.cfg = cfg + self.program = program + self.killprocname = killprocname + self.backtrace_lines = backtrace_lines + self.crash_base_dir = crashers_dir + self.seednum = seednum + self.range = range + self.exclude_unmapped_frames = cfg['analyzer']['exclude_unmapped_frames'] + self.set_debugger_template('bt_only') + self.keep_uniq_faddr = keep_faddr + + self.cmdargs = None +# self.debugger_file = None + self.is_crash = False + self.signature = None + self.faddr = None + self.pc = None + self.result_dir = None + + def __exit__(self, etype, value, traceback): + pass +# self.clean_tmpdir() + + def set_debugger_template(self, option='bt_only'): + if host_info.is_linux(): + dbg_template_name = '%s_%s_template.txt' % (self._debugger_cls._key, option) + self.debugger_template = os.path.join(self.cfg['directories']['debugger_template_dir'], dbg_template_name) + logger.debug('Debugger template set to %s', self.debugger_template) + if not os.path.exists(self.debugger_template): + raise CrashError('Debugger template does not exist at %s' % self.debugger_template) + + def update_crash_details(self): + Testcase.update_crash_details(self) + + cmdlist = get_command_args_list(self.cfg['target']['cmdline_template'], + infile=self.fuzzedfile.path, + posix=True)[1] + self.cmdargs = cmdlist[1:] + self.is_crash = self.confirm_crash() + + if self.is_crash: + self.signature = self.get_signature() + self.pc = self.dbg.registers_hex.get(self.dbg.pc_name) + self.result_dir = self.get_result_dir() + self.debugger_missed_stack_corruption = self.dbg.debugger_missed_stack_corruption + self.total_stack_corruption = self.dbg.total_stack_corruption + self.pc_in_function = self.dbg.pc_in_function + self.faddr = self.dbg.faddr + logger.debug('sig: %s', self.signature) + logger.debug('pc: %s', self.pc) + logger.debug('result_dir: %s', self.result_dir) + else: + # clean up on non-crashes + self.delete_files() + + return self.is_crash + + def get_debug_output(self, outfile_base): + # get debugger output + logger.debug('Debugger template: %s outfile_base: %s', + self.debugger_template, outfile_base) + debugger_obj = self._debugger_cls(self.program, + self.cmdargs, + outfile_base, + self.debugger_timeout, + self.killprocname, + template=self.debugger_template, + exclude_unmapped_frames=self.exclude_unmapped_frames, + keep_uniq_faddr=self.keep_uniq_faddr + ) + self.dbg = debugger_obj.go() + + def confirm_crash(self): + # get debugger output + self.get_debug_output(self.fuzzedfile.path) + + if not self.dbg: + raise CrashError('Debug object not found') + + logger.debug('is_crash: %s is_assert_fail: %s', self.dbg.is_crash, self.dbg.is_assert_fail) + if self.cfg['analyzer']['savefailedasserts']: + return self.dbg.is_crash + else: + # only keep real crashes (not failed assertions) + return self.dbg.is_crash and not self.dbg.is_assert_fail + + def __repr__(self): + as_list = ['%s:%s' % (k, v) for (k, v) in self.__dict__.items()] + return str('\n'.join(as_list)) + + def get_signature(self): + ''' + Runs the debugger on the crash and gets its signature. + @raise CrasherHasNoSignatureError: if it's a valid crash, but we don't get a signature + ''' + if not self.signature: + self.signature = self.dbg.get_crash_signature(self.backtrace_lines) + if self.signature: + logger.debug("Testcase signature is %s", self.signature) + else: + raise CrashError('Testcase has no signature.') + if self.dbg.total_stack_corruption: + # total_stack_corruption. Use pin calltrace to get a backtrace + analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self) + try: + analyzer_instance.go() + except AnalyzerEmptyOutputError: + logger.warning('Unexpected empty output from pin. Cannot determine call trace.') + return self.signature + + calltrace = Calltracefile(analyzer_instance.outfile) + pinsignature = calltrace.get_crash_signature(self.backtrace_lines * 10) + if pinsignature: + self.signature = pinsignature + return self.signature + + def _verify_crash_base_dir(self): + if not self.crash_base_dir: + raise CrashError('crash_base_dir not set') + if not os.path.exists(self.crash_base_dir): + filetools.make_directories(self.crash_base_dir) + + def get_result_dir(self): + assert self.crash_base_dir + assert self.signature + self._verify_crash_base_dir() + self.result_dir = os.path.join(self.crash_base_dir, self.signature) + + return self.result_dir + + def get_logger(self): + ''' + sets self.logger to a logger specific to this crash + ''' + self.logger = logging.getLogger(self.signature) + if len(self.logger.handlers) == 0: + if not os.path.exists(self.result_dir): + logger.error('Result path not found: %s', self.result_dir) + raise CrashError('Result path not found: {}'.format(self.result_dir)) + logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) + logfile = '%s.log' % self.signature + logger.debug('logfile=%s', logfile) + logpath = os.path.join(self.result_dir, logfile) + logger.debug('logpath=%s', logpath) + hdlr = logging.FileHandler(logpath) + self.logger.addHandler(hdlr) + + return self.logger diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py new file mode 100644 index 0000000..b5d125f --- /dev/null +++ b/src/certfuzz/testcase/testcase_windows.py @@ -0,0 +1,270 @@ +''' +Created on Feb 9, 2012 + +@organization: cert.org +''' +import hashlib +import logging +import os + +from certfuzz.testcase.testcase_base2 import Testcase +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.fuzztools.filetools import best_effort_move +from certfuzz.helpers.misc import random_str +from certfuzz.debuggers.msec import MsecDebugger +from certfuzz.fuzztools.command_line_templating import get_command_args_list + + +logger = logging.getLogger(__name__) + + +def logerror(func, path, excinfo): + logger.warning('%s failed to remove %s: %s', func, path, excinfo) + +short_exp = { + 'UNKNOWN': 'UNK', + 'PROBABLY_NOT_EXPLOITABLE': 'PNE', + 'PROBABLY_EXPLOITABLE': 'PEX', + 'EXPLOITABLE': 'EXP', + 'HEISENBUG': 'HSB', + } + +exp_rank = { + 'EXPLOITABLE': 1, + 'PROBABLY_EXPLOITABLE': 2, + 'UNKNOWN': 3, + 'PROBABLY_NOT_EXPLOITABLE': 4, + 'HEISENBUG': 5, + } + + +class WindowsCrash(Testcase): + tmpdir_pfx = 'bff-crash-' + _debugger_cls = MsecDebugger + + # TODO: do we still need fuzzer as an arg? + def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, + dbg_opts, workingdir_base, keep_faddr, program, + heisenbug_retries=4, copy_fuzzedfile=True): + + dbg_timeout = dbg_opts['runtimeout'] + + Testcase.__init__(self, seedfile, fuzzedfile, dbg_timeout) + + self.dbg_opts = dbg_opts + self.copy_fuzzedfile = copy_fuzzedfile + + self.cmdargs = cmdlist + self.workdir_base = workingdir_base + + self.keep_uniq_faddr = keep_faddr + self.program = program + self.dbg_result = {} + self.crash_hash = None + self.result_dir = None + self.faddr = None + self.dbg_file = '' + self.cmd_template = cmd_template + try: + self.max_handled_exceptions = self.dbg_opts['max_handled_exceptions'] + except KeyError: + self.max_handled_exceptions = 6 + try: + self.watchcpu = self.dbg_opts['watchcpu'] + except KeyError: + self.watchcpu = False + self.exception_depth = 0 + self.reached_secondchance = False + self.parsed_outputs = [] + + self.max_depth = heisenbug_retries + + def _get_file_basename(self): + ''' + If self.copy_fuzzedfile is set, indicating that the fuzzer modifies the + seedfile, then return the fuzzedfile basename. Otherwise return the + seedfile basename. + ''' + if self.copy_fuzzedfile: + return self.fuzzedfile.basename + return self.seedfile.basename + + def update_crash_details(self): + ''' + Resets various properties of the crash object and regenerates crash data + as needed. Used in both object runtime context and for refresh after + a crash object is copied. + ''' + Testcase.update_crash_details(self) + # Reset properties that need to be regenerated + self.exception_depth = 0 + self.parsed_outputs = [] + self.exp = None + fname = self._get_file_basename() + outfile_base = os.path.join(self.tempdir, fname) + # Regenerate target commandline with new crasher file + self.cmdargs = get_command_args_list(self.cmd_template, outfile_base)[1] + self.debug() + self._rename_fuzzed_file() + self._rename_dbg_file() + + def set_debugger_template(self, *args): + pass + + def debug_once(self): + outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) + + debugger = self._debugger_cls(program=self.program, + cmd_args=self.cmdargs, + outfile_base=outfile_base, + timeout=self.debugger_timeout, + killprocname=None, + exception_depth=self.exception_depth, + workingdir=self.tempdir, + watchcpu=self.watchcpu) + self.parsed_outputs.append(debugger.go()) + + self.reached_secondchance = self.parsed_outputs[self.exception_depth].secondchance + + if self.reached_secondchance and self.exception_depth > 0: + # No need to process second-chance exception + # Note that some exceptions, such as Illegal Instructions have no first-chance: + # In those cases, proceed... + return + + # Store highest exploitability of every exception in the chain + current_exception_exp = self.parsed_outputs[self.exception_depth].exp + if current_exception_exp: + if not self.exp: + self.exp = current_exception_exp + elif exp_rank[current_exception_exp] < exp_rank[self.exp]: + self.exp = current_exception_exp + + current_exception_hash = self.parsed_outputs[self.exception_depth].crash_hash + current_exception_faddr = self.parsed_outputs[self.exception_depth].faddr + if current_exception_hash: + # We have a hash for the current exception + if self.exception_depth == 0: + # First exception - start exception hash chain from scratch + self.crash_hash = current_exception_hash + elif self.crash_hash: + # Append to the exception hash chain + self.crash_hash = self.crash_hash + '_' + current_exception_hash + + if self.keep_uniq_faddr and current_exception_faddr: + self.crash_hash += '.' + current_exception_faddr + + # The first exception is the one that is representative for the crasher + if self.exception_depth == 0: + self.dbg_file = debugger.outfile + # add debugger results to our own attributes + self.is_crash = self.parsed_outputs[0].is_crash + self.dbg_type = self.parsed_outputs[0]._key + # self.exp = self.parsed_outputs[0].exp + self.faddr = self.parsed_outputs[0].faddr + # self.crash_hash = self.parsed_outputs[0].crash_hash + + def get_signature(self): + self.signature = self.crash_hash + return self.crash_hash + + def debug(self, tries_remaining=None): + if tries_remaining is None: + tries_remaining = self.max_depth + + logger.debug("tries left: %d", tries_remaining) + self.debug_once() + + if self.is_crash: + self.is_heisenbug = False + + logger.debug("checking for handled exceptions...") + while self.exception_depth < self.max_handled_exceptions: + self.exception_depth += 1 + self.debug_once() + if self.reached_secondchance or not self.parsed_outputs[self.exception_depth].is_crash: + logger.debug("no more handled exceptions") + break + # get the signature now that we've got all of the exceptions + self.get_signature() + else: + # if we are still a heisenbug after debugging + # we might need to try again + # or give up if we've tried enough already + if tries_remaining: + # keep diving + logger.debug("possible heisenbug (%d tries left)", tries_remaining) + self.debug(tries_remaining - 1) + else: + # we're at the bottom + self._set_heisenbug_properties() + logger.debug("heisenbug found") + + def _set_heisenbug_properties(self): + self.exp = 'HEISENBUG' + try: + fuzzed_content = self.fuzzedfile.read() + except Exception, e: + # for whatever reason we couldn't get the real content, + # and since we're just generating a string here + # any string will do + logger.warning('Unable to get md5 of %s, using random string for heisenbug signature: %s', self.fuzzedfile.path, e) + fuzzed_content = random_str(64) + self.is_heisenbug = True + self.signature = hashlib.md5(fuzzed_content).hexdigest() + + def _get_output_dir(self, target_base): + logger.debug('target_base: %s', target_base) + logger.debug('exp: %s', self.exp) + logger.debug('signature: %s', self.signature) + + self.target_dir = os.path.join(target_base, self.exp, self.signature) + if len(self.target_dir) > 170: + # Don't make a path too deep. Windows won't support it + self.target_dir = self.target_dir[:170] + '__' + logger.debug('target_dir: %s', self.target_dir) + return self.target_dir + + def _rename_fuzzed_file(self): + if not self.faddr: + return + + logger.debug('Attempting to rename %s', self.fuzzedfile.path) + new_basename = '%s-%s%s' % (self.fuzzedfile.root, self.faddr, self.fuzzedfile.ext) + new_fuzzed_file = os.path.join(self.fuzzedfile.dirname, new_basename) + logger.debug('renaming %s -> %s', self.fuzzedfile.path, new_fuzzed_file) + + # best_effort move returns a tuple of booleans indicating (copied, deleted) + # we only care about copied + copied = best_effort_move(self.fuzzedfile.path, new_fuzzed_file)[0] + + if copied: + # replace fuzzed file + self.fuzzedfile = BasicFile(new_fuzzed_file) + + def _rename_dbg_file(self): + if self.faddr: + faddr_str = '%s' % self.faddr + else: + faddr_str = '' + + (path, basename) = os.path.split(self.dbg_file) + (basename, dbgext) = os.path.splitext(basename) + (root, ext) = os.path.splitext(basename) + + exp_str = short_exp[self.exp] + + parts = [root] + if faddr_str: + parts.append(faddr_str) + if exp_str: + parts.append(exp_str) + new_basename = '-'.join(parts) + ext + dbgext + + new_dbg_file = os.path.join(path, new_basename) + + # best_effort move returns a tuple of booleans indicating (copied, deleted) + # we only care about copied + copied = best_effort_move(self.dbg_file, new_dbg_file)[0] + if copied: + self.dbg_file = new_dbg_file diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index f249710..f07135e 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -9,7 +9,7 @@ from certfuzz import debuggers from certfuzz.config.config_linux import LinuxConfig -from certfuzz.crash.bff_crash import BffCrash +from certfuzz.testcase.testcase_linux import LinuxTestcase from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.file_handlers.basicfile import BasicFile @@ -137,7 +137,7 @@ def main(): crashers_dir = '.' - with BffCrash(cfg, seedfile, fuzzed_file, cfg.program, + with LinuxTestcase(cfg, seedfile, fuzzed_file, cfg.program, cfg.debugger_timeout, cfg.killprocname, cfg.backtracelevels, crashers_dir, options.keep_uniq_faddr) as crash: diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 3372f6a..91944a8 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -14,7 +14,7 @@ from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer - from certfuzz.crash.crash_windows import WindowsCrash + from certfuzz.testcase.testcase_windows import WindowsTestcase from certfuzz.debuggers import msec # @UnusedImport from certfuzz.fuzztools.command_line_templating import get_command_args_list from certfuzz.config.simple_loader import load_and_fix_config @@ -28,7 +28,7 @@ from certfuzz.fuzztools import filetools, text from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer - from certfuzz.crash.crash_windows import WindowsCrash + from certfuzz.testcase.testcase_windows import WindowsTestcase from certfuzz.debuggers import msec # @UnusedImport from certfuzz.fuzztools.command_line_templating import get_command_args_list from certfuzz.config.simple_loader import load_and_fix_config @@ -156,7 +156,7 @@ def main(): debugger_class = msec.MsecDebugger cmd_as_args = get_command_args_list(cfg['target']['cmdline_template'], fuzzed_file.path)[1] - with WindowsCrash(cmd_template=cfg['target']['cmdline_template'], + with WindowsTestcase(cmd_template=cfg['target']['cmdline_template'], seedfile=seedfile, fuzzedfile=fuzzed_file, cmdlist=cmd_as_args, diff --git a/src/test_certfuzz/crash/__init__.py b/src/test_certfuzz/testcase/__init__.py similarity index 100% rename from src/test_certfuzz/crash/__init__.py rename to src/test_certfuzz/testcase/__init__.py diff --git a/src/test_certfuzz/crash/test_testcase_base.py b/src/test_certfuzz/testcase/test_testcase_base.py similarity index 88% rename from src/test_certfuzz/crash/test_testcase_base.py rename to src/test_certfuzz/testcase/test_testcase_base.py index 4134c48..c5016fb 100644 --- a/src/test_certfuzz/crash/test_testcase_base.py +++ b/src/test_certfuzz/testcase/test_testcase_base.py @@ -4,6 +4,7 @@ @organization: cert.org ''' import unittest +import certfuzz.testcase.testcase_base class Test(unittest.TestCase): diff --git a/src/test_certfuzz/crash/test_crash_base.py b/src/test_certfuzz/testcase/test_testcase_base2.py similarity index 88% rename from src/test_certfuzz/crash/test_crash_base.py rename to src/test_certfuzz/testcase/test_testcase_base2.py index 6e33993..3859b99 100644 --- a/src/test_certfuzz/crash/test_crash_base.py +++ b/src/test_certfuzz/testcase/test_testcase_base2.py @@ -4,6 +4,7 @@ @organization: cert.org ''' import unittest +import certfuzz.testcase.testcase_base2 class Test(unittest.TestCase): diff --git a/src/test_certfuzz/crash/test_bff_crash.py b/src/test_certfuzz/testcase/test_testcase_linux.py similarity index 96% rename from src/test_certfuzz/crash/test_bff_crash.py rename to src/test_certfuzz/testcase/test_testcase_linux.py index 77fb5f4..414800f 100644 --- a/src/test_certfuzz/crash/test_bff_crash.py +++ b/src/test_certfuzz/testcase/test_testcase_linux.py @@ -5,11 +5,11 @@ ''' import unittest -# from certfuzz.crash.bff_crash import Crash from test_certfuzz.mocks import Mock import tempfile import os import shutil +import certfuzz.testcase.testcase_linux class MockConfig(Mock): def __init__(self, tmpdir): diff --git a/src/test_certfuzz/crash/test_crash_windows.py b/src/test_certfuzz/testcase/test_testcase_windows.py similarity index 88% rename from src/test_certfuzz/crash/test_crash_windows.py rename to src/test_certfuzz/testcase/test_testcase_windows.py index cf0bf77..8157ecc 100644 --- a/src/test_certfuzz/crash/test_crash_windows.py +++ b/src/test_certfuzz/testcase/test_testcase_windows.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -#import certfuzz.crash.crash_windows +import certfuzz.testcase.testcase_windows class Test(unittest.TestCase): diff --git a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py index f337bfe..2c4800e 100644 --- a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py +++ b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py @@ -4,14 +4,14 @@ @organization: cert.org ''' import unittest -import certfuzz.testcase_pipeline.tc_pipeline_base +import certfuzz.tc_pipeline.tc_pipeline_base import tempfile import shutil import os -import certfuzz.testcase_pipeline.tc_pipeline_base +import certfuzz.tc_pipeline.tc_pipeline_base -class TCPL_Impl(certfuzz.testcase_pipeline.tc_pipeline_base.TestCasePipelineBase): +class TCPL_Impl(certfuzz.tc_pipeline.tc_pipeline_base.TestCasePipelineBase): def _setup_analyzers(self): pass @@ -37,7 +37,7 @@ def tearDown(self): shutil.rmtree(self.tmpdir) def test_abstract_class(self): - cls = certfuzz.testcase_pipeline.tc_pipeline_base.TestCasePipelineBase + cls = certfuzz.tc_pipeline.tc_pipeline_base.TestCasePipelineBase # should fail since we haven't implemented the abc methods self.assertRaises(TypeError, cls) @@ -83,7 +83,7 @@ class MockTestCase(object): def test_analyze(self): class TCPL_Impl2(TCPL_Impl): def _analyze(self, testcase): - certfuzz.testcase_pipeline.tc_pipeline_base.TestCasePipelineBase._analyze(self, testcase) + certfuzz.tc_pipeline.tc_pipeline_base.TestCasePipelineBase._analyze(self, testcase) tcpl = TCPL_Impl2(outdir=self.tmpdir) @@ -105,7 +105,7 @@ def inc_cc(): touch_watchdog_call_count.append(1) # monkey patch touch_watchdog_file - certfuzz.testcase_pipeline.tc_pipeline_base.touch_watchdog_file = inc_cc + certfuzz.tc_pipeline.tc_pipeline_base.touch_watchdog_file = inc_cc tc = MockTestCase() tcpl.analyzer_classes = [MockAnalyzer for _ in xrange(5)] diff --git a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_linux.py b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_linux.py index 3ffc6cf..ba3e9bd 100644 --- a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_linux.py +++ b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_linux.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -import certfuzz.testcase_pipeline.tc_pipeline_linux +import certfuzz.tc_pipeline.tc_pipeline_linux class Test(unittest.TestCase): diff --git a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_windows.py b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_windows.py index f9f41a7..e470985 100644 --- a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_windows.py +++ b/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_windows.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -import certfuzz.testcase_pipeline.tc_pipeline_windows +import certfuzz.tc_pipeline.tc_pipeline_windows class Test(unittest.TestCase): From e3645e820f0022164b65f284b9911f35b7407a55 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 09:13:11 -0500 Subject: [PATCH 0776/1169] fix import of get_command_args_list --- src/certfuzz/runners/winrun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 89ed364..58f1742 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -2,6 +2,7 @@ import platform from .errors import RunnerPlatformVersionError +from certfuzz.fuzztools.command_line_templating import get_command_args_list if not platform.version().startswith('5.'): raise RunnerPlatformVersionError('Incompatible OS: winrun only works on Windows XP and 2003') @@ -17,7 +18,6 @@ import time from certfuzz.runners.errors import RunnerArchitectureError, RunnerRegistryError from certfuzz.runners.errors import RunnerError -from certfuzz.config.config_windows import get_command_args_list from certfuzz.fuzztools.filetools import find_or_create_dir logger = logging.getLogger(__name__) From 7b8e36adc72a2cea6fb69ea55a1ab8be3fc227f1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 09:44:21 -0500 Subject: [PATCH 0777/1169] missed a couple renames in previous commit --- src/certfuzz/testcase/testcase_linux.py | 2 +- src/certfuzz/testcase/testcase_windows.py | 2 +- .../{testcase_pipeline => tc_pipeline}/__init__.py | 0 .../{testcase_pipeline => tc_pipeline}/test_tc_pipeline_base.py | 0 .../test_tc_pipeline_linux.py | 0 .../test_tc_pipeline_windows.py | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename src/test_certfuzz/{testcase_pipeline => tc_pipeline}/__init__.py (100%) rename src/test_certfuzz/{testcase_pipeline => tc_pipeline}/test_tc_pipeline_base.py (100%) rename src/test_certfuzz/{testcase_pipeline => tc_pipeline}/test_tc_pipeline_linux.py (100%) rename src/test_certfuzz/{testcase_pipeline => tc_pipeline}/test_tc_pipeline_windows.py (100%) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index b6ab370..0c98666 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -27,7 +27,7 @@ from certfuzz.debuggers.crashwrangler import CrashWrangler as debugger_cls -class BffCrash(Testcase): +class LinuxTestcase(Testcase): ''' classdocs ''' diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index b5d125f..e2c7578 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -38,7 +38,7 @@ def logerror(func, path, excinfo): } -class WindowsCrash(Testcase): +class WindowsTestcase(Testcase): tmpdir_pfx = 'bff-crash-' _debugger_cls = MsecDebugger diff --git a/src/test_certfuzz/testcase_pipeline/__init__.py b/src/test_certfuzz/tc_pipeline/__init__.py similarity index 100% rename from src/test_certfuzz/testcase_pipeline/__init__.py rename to src/test_certfuzz/tc_pipeline/__init__.py diff --git a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py similarity index 100% rename from src/test_certfuzz/testcase_pipeline/test_tc_pipeline_base.py rename to src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py diff --git a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_linux.py b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_linux.py similarity index 100% rename from src/test_certfuzz/testcase_pipeline/test_tc_pipeline_linux.py rename to src/test_certfuzz/tc_pipeline/test_tc_pipeline_linux.py diff --git a/src/test_certfuzz/testcase_pipeline/test_tc_pipeline_windows.py b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_windows.py similarity index 100% rename from src/test_certfuzz/testcase_pipeline/test_tc_pipeline_windows.py rename to src/test_certfuzz/tc_pipeline/test_tc_pipeline_windows.py From 237520e10154c7a1008d0d95786c550ee2110b44 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 10:55:53 -0500 Subject: [PATCH 0778/1169] config handling changes into linux standalone minimizer --- src/certfuzz/testcase/bff_crash.py | 191 ----------------- src/certfuzz/testcase/crash_base.py | 147 ------------- src/certfuzz/testcase/crash_windows.py | 270 ------------------------ src/certfuzz/testcase/testcase_linux.py | 2 +- src/certfuzz/tools/linux/minimize.py | 20 +- 5 files changed, 14 insertions(+), 616 deletions(-) delete mode 100644 src/certfuzz/testcase/bff_crash.py delete mode 100644 src/certfuzz/testcase/crash_base.py delete mode 100644 src/certfuzz/testcase/crash_windows.py diff --git a/src/certfuzz/testcase/bff_crash.py b/src/certfuzz/testcase/bff_crash.py deleted file mode 100644 index 880688e..0000000 --- a/src/certfuzz/testcase/bff_crash.py +++ /dev/null @@ -1,191 +0,0 @@ -''' -Created on Jul 19, 2011 - -@organization: cert.org -''' -import logging -import os - -from certfuzz.testcase.testcase_base2 import Testcase, CrashError -from certfuzz.fuzztools import hostinfo, filetools -from certfuzz.fuzztools.command_line_templating import get_command_args_list - -try: - from certfuzz.analyzers import pin_calltrace - from certfuzz.analyzers.errors import AnalyzerEmptyOutputError - from certfuzz.debuggers.output_parsers.calltracefile import Calltracefile -except ImportError: - pass - -logger = logging.getLogger(__name__) - -host_info = hostinfo.HostInfo() - -if host_info.is_linux(): - from certfuzz.debuggers.gdb import GDB as debugger_cls -elif host_info.is_osx(): - from certfuzz.debuggers.crashwrangler import CrashWrangler as debugger_cls - - -class LinuxTestcase(Testcase): - ''' - classdocs - ''' - tmpdir_pfx = 'bff-crash-' - _debugger_cls = debugger_cls - - def __init__(self, cfg, seedfile, fuzzedfile, program, - debugger_timeout, killprocname, backtrace_lines, - crashers_dir, workdir_base, seednum=None, range=None, keep_faddr=False): - ''' - Constructor - ''' - Testcase.__init__(self, seedfile, fuzzedfile, debugger_timeout) - self.cfg = cfg - self.program = program - self.killprocname = killprocname - self.backtrace_lines = backtrace_lines - self.crash_base_dir = crashers_dir - self.seednum = seednum - self.range = range - self.exclude_unmapped_frames = cfg['analyzer']['exclude_unmapped_frames'] - self.set_debugger_template('bt_only') - self.keep_uniq_faddr = keep_faddr - - self.cmdargs = None -# self.debugger_file = None - self.is_crash = False - self.signature = None - self.faddr = None - self.pc = None - self.result_dir = None - - def __exit__(self, etype, value, traceback): - pass -# self.clean_tmpdir() - - def set_debugger_template(self, option='bt_only'): - if host_info.is_linux(): - dbg_template_name = '%s_%s_template.txt' % (self._debugger_cls._key, option) - self.debugger_template = os.path.join(self.cfg['directories']['debugger_template_dir'], dbg_template_name) - logger.debug('Debugger template set to %s', self.debugger_template) - if not os.path.exists(self.debugger_template): - raise CrashError('Debugger template does not exist at %s' % self.debugger_template) - - def update_crash_details(self): - Testcase.update_crash_details(self) - - self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'], - infile=self.fuzzedfile.path, - posix=True)[1] - - self.is_crash = self.confirm_crash() - - if self.is_crash: - self.signature = self.get_signature() - self.pc = self.dbg.registers_hex.get(self.dbg.pc_name) - self.result_dir = self.get_result_dir() - self.debugger_missed_stack_corruption = self.dbg.debugger_missed_stack_corruption - self.total_stack_corruption = self.dbg.total_stack_corruption - self.pc_in_function = self.dbg.pc_in_function - self.faddr = self.dbg.faddr - logger.debug('sig: %s', self.signature) - logger.debug('pc: %s', self.pc) - logger.debug('result_dir: %s', self.result_dir) - else: - # clean up on non-crashes - self.delete_files() - - return self.is_crash - - def get_debug_output(self, outfile_base): - # get debugger output - logger.debug('Debugger template: %s outfile_base: %s', - self.debugger_template, outfile_base) - debugger_obj = self._debugger_cls(self.program, - self.cmdargs, - outfile_base, - self.debugger_timeout, - self.killprocname, - template=self.debugger_template, - exclude_unmapped_frames=self.exclude_unmapped_frames, - keep_uniq_faddr=self.keep_uniq_faddr - ) - self.dbg = debugger_obj.go() - - def confirm_crash(self): - # get debugger output - self.get_debug_output(self.fuzzedfile.path) - - if not self.dbg: - raise CrashError('Debug object not found') - - logger.debug('is_crash: %s is_assert_fail: %s', self.dbg.is_crash, self.dbg.is_assert_fail) - if self.cfg['analyzer']['savefailedasserts']: - return self.dbg.is_crash - else: - # only keep real crashes (not failed assertions) - return self.dbg.is_crash and not self.dbg.is_assert_fail - - def __repr__(self): - as_list = ['%s:%s' % (k, v) for (k, v) in self.__dict__.items()] - return str('\n'.join(as_list)) - - def get_signature(self): - ''' - Runs the debugger on the crash and gets its signature. - @raise CrasherHasNoSignatureError: if it's a valid crash, but we don't get a signature - ''' - if not self.signature: - self.signature = self.dbg.get_crash_signature(self.backtrace_lines) - if self.signature: - logger.debug("Testcase signature is %s", self.signature) - else: - raise CrashError('Testcase has no signature.') - if self.dbg.total_stack_corruption: - # total_stack_corruption. Use pin calltrace to get a backtrace - analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self) - try: - analyzer_instance.go() - except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from pin. Cannot determine call trace.') - return self.signature - - calltrace = Calltracefile(analyzer_instance.outfile) - pinsignature = calltrace.get_crash_signature(self.backtrace_lines * 10) - if pinsignature: - self.signature = pinsignature - return self.signature - - def _verify_crash_base_dir(self): - if not self.crash_base_dir: - raise CrashError('crash_base_dir not set') - if not os.path.exists(self.crash_base_dir): - filetools.make_directories(self.crash_base_dir) - - def get_result_dir(self): - assert self.crash_base_dir - assert self.signature - self._verify_crash_base_dir() - self.result_dir = os.path.join(self.crash_base_dir, self.signature) - - return self.result_dir - - def get_logger(self): - ''' - sets self.logger to a logger specific to this crash - ''' - self.logger = logging.getLogger(self.signature) - if len(self.logger.handlers) == 0: - if not os.path.exists(self.result_dir): - logger.error('Result path not found: %s', self.result_dir) - raise CrashError('Result path not found: {}'.format(self.result_dir)) - logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) - logfile = '%s.log' % self.signature - logger.debug('logfile=%s', logfile) - logpath = os.path.join(self.result_dir, logfile) - logger.debug('logpath=%s', logpath) - hdlr = logging.FileHandler(logpath) - self.logger.addHandler(hdlr) - - return self.logger diff --git a/src/certfuzz/testcase/crash_base.py b/src/certfuzz/testcase/crash_base.py deleted file mode 100644 index 48ead05..0000000 --- a/src/certfuzz/testcase/crash_base.py +++ /dev/null @@ -1,147 +0,0 @@ -''' -Created on Oct 11, 2012 - -@organization: cert.org -''' -import logging -import os -import tempfile - -from certfuzz.testcase.errors import CrashError -from certfuzz.testcase.testcase_base import TestCaseBase -from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.fuzztools import filetools - - -logger = logging.getLogger(__name__) - - -class Testcase(TestCaseBase): - tmpdir_pfx = 'crash-' - _debugger_cls = None - - def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): - logger.debug('Inititalize Testcase') - TestCaseBase.__init__(self, seedfile, fuzzedfile) - - self.debugger_timeout = dbg_timeout - - self.debugger_template = None - # All crashes are heisenbugs until proven otherwise - self.is_heisenbug = True - - self.workdir_base = tempfile.gettempdir() - - # set some defaults - # Not a crash until we're sure - self.is_crash = False - self.debugger_file = None - self.is_unique = False - self.should_proceed_with_analysis = False - self.is_corrupt_stack = False - self.copy_fuzzedfile = True - self.pc = None - self.logger = None - self.result_dir = None - self.debugger_missed_stack_corruption = False - self.total_stack_corruption = False - self.pc_in_function = False - - def _create_workdir_base(self): - # make sure the workdir_base exists - if not os.path.exists(self.workdir_base): - filetools.make_directories([self.workdir_base]) - - def __enter__(self): - self._create_workdir_base() - self.update_crash_details() - return self - - def __exit__(self, etype, value, traceback): - pass - - def _get_output_dir(self, *args): - raise NotImplementedError - - def _rename_dbg_file(self): - raise NotImplementedError - - def _rename_fuzzed_file(self): - raise NotImplementedError - - def _set_attr_from_dbg(self, attrname): - raise NotImplementedError - - def _verify_crash_base_dir(self): - raise NotImplementedError - - def calculate_hamming_distances(self): - TestCaseBase.calculate_hamming_distances(self) - self.logger.info("bitwise_hd=%d", self.hd_bits) - self.logger.info("bytewise_hd=%d", self.hd_bytes) - - def calculate_hamming_distances_a(self): - TestCaseBase.calculate_hamming_distances_a(self) - self.logger.info("bitwise_hd=%d", self.hd_bits) - self.logger.info("bytewise_hd=%d", self.hd_bytes) - - def clean_tmpdir(self): - logger.debug('Cleaning up %s', self.tempdir) - if os.path.exists(self.tempdir): - filetools.delete_files_or_dirs([self.tempdir]) - else: - logger.debug('No tempdir at %s', self.tempdir) - - if os.path.exists(self.tempdir): - logger.debug('Unable to remove tempdir %s', self.tempdir) - - def confirm_crash(self): - raise NotImplementedError - - def copy_files_to_temp(self): - if self.fuzzedfile and self.copy_fuzzedfile: - filetools.copy_file(self.fuzzedfile.path, self.tempdir) - - if self.seedfile: - filetools.copy_file(self.seedfile.path, self.tempdir) - - new_fuzzedfile = os.path.join(self.tempdir, self.fuzzedfile.basename) - self.fuzzedfile = BasicFile(new_fuzzedfile) - - def copy_files(self, outdir): - crash_files = os.listdir(self.tempdir) - for file in crash_files: - filepath = os.path.join(self.tempdir, file) - if os.path.isfile(filepath): - filetools.copy_file(filepath, outdir) - - def debug(self, tries_remaining=None): - raise NotImplementedError - - def debug_once(self): - raise NotImplementedError - - def delete_files(self): - if os.path.isdir(self.fuzzedfile.dirname): - logger.debug('Deleting files from %s', self.fuzzedfile.dirname) - filetools.delete_files_or_dirs([self.fuzzedfile.dirname]) - - def get_debug_output(self, f): - raise NotImplementedError - - def get_logger(self): - raise NotImplementedError - - def get_result_dir(self): - raise NotImplementedError - - def get_signature(self): - raise NotImplementedError - - def set_debugger_template(self, option='bt_only'): - raise NotImplementedError - - def update_crash_details(self): - self.tempdir = tempfile.mkdtemp(prefix=self.tmpdir_pfx, dir=self.workdir_base) - self.copy_files_to_temp() -# raise NotImplementedError diff --git a/src/certfuzz/testcase/crash_windows.py b/src/certfuzz/testcase/crash_windows.py deleted file mode 100644 index c25eae5..0000000 --- a/src/certfuzz/testcase/crash_windows.py +++ /dev/null @@ -1,270 +0,0 @@ -''' -Created on Feb 9, 2012 - -@organization: cert.org -''' -import hashlib -import logging -import os - -from certfuzz.testcase.crash_base import Testcase -from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.fuzztools.filetools import best_effort_move -from certfuzz.helpers.misc import random_str -from certfuzz.debuggers.msec import MsecDebugger -from certfuzz.fuzztools.command_line_templating import get_command_args_list - - -logger = logging.getLogger(__name__) - - -def logerror(func, path, excinfo): - logger.warning('%s failed to remove %s: %s', func, path, excinfo) - -short_exp = { - 'UNKNOWN': 'UNK', - 'PROBABLY_NOT_EXPLOITABLE': 'PNE', - 'PROBABLY_EXPLOITABLE': 'PEX', - 'EXPLOITABLE': 'EXP', - 'HEISENBUG': 'HSB', - } - -exp_rank = { - 'EXPLOITABLE': 1, - 'PROBABLY_EXPLOITABLE': 2, - 'UNKNOWN': 3, - 'PROBABLY_NOT_EXPLOITABLE': 4, - 'HEISENBUG': 5, - } - - -class WindowsTestcase(Testcase): - tmpdir_pfx = 'bff-crash-' - _debugger_cls = MsecDebugger - - # TODO: do we still need fuzzer as an arg? - def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, - dbg_opts, workingdir_base, keep_faddr, program, - heisenbug_retries=4, copy_fuzzedfile=True): - - dbg_timeout = dbg_opts['runtimeout'] - - Testcase.__init__(self, seedfile, fuzzedfile, dbg_timeout) - - self.dbg_opts = dbg_opts - self.copy_fuzzedfile = copy_fuzzedfile - - self.cmdargs = cmdlist - self.workdir_base = workingdir_base - - self.keep_uniq_faddr = keep_faddr - self.program = program - self.dbg_result = {} - self.crash_hash = None - self.result_dir = None - self.faddr = None - self.dbg_file = '' - self.cmd_template = cmd_template - try: - self.max_handled_exceptions = self.dbg_opts['max_handled_exceptions'] - except KeyError: - self.max_handled_exceptions = 6 - try: - self.watchcpu = self.dbg_opts['watchcpu'] - except KeyError: - self.watchcpu = False - self.exception_depth = 0 - self.reached_secondchance = False - self.parsed_outputs = [] - - self.max_depth = heisenbug_retries - - def _get_file_basename(self): - ''' - If self.copy_fuzzedfile is set, indicating that the fuzzer modifies the - seedfile, then return the fuzzedfile basename. Otherwise return the - seedfile basename. - ''' - if self.copy_fuzzedfile: - return self.fuzzedfile.basename - return self.seedfile.basename - - def update_crash_details(self): - ''' - Resets various properties of the crash object and regenerates crash data - as needed. Used in both object runtime context and for refresh after - a crash object is copied. - ''' - Testcase.update_crash_details(self) - # Reset properties that need to be regenerated - self.exception_depth = 0 - self.parsed_outputs = [] - self.exp = None - fname = self._get_file_basename() - outfile_base = os.path.join(self.tempdir, fname) - # Regenerate target commandline with new crasher file - self.cmdargs = get_command_args_list(self.cmd_template, outfile_base)[1] - self.debug() - self._rename_fuzzed_file() - self._rename_dbg_file() - - def set_debugger_template(self, *args): - pass - - def debug_once(self): - outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) - - debugger = self._debugger_cls(program=self.program, - cmd_args=self.cmdargs, - outfile_base=outfile_base, - timeout=self.debugger_timeout, - killprocname=None, - exception_depth=self.exception_depth, - workingdir=self.tempdir, - watchcpu=self.watchcpu) - self.parsed_outputs.append(debugger.go()) - - self.reached_secondchance = self.parsed_outputs[self.exception_depth].secondchance - - if self.reached_secondchance and self.exception_depth > 0: - # No need to process second-chance exception - # Note that some exceptions, such as Illegal Instructions have no first-chance: - # In those cases, proceed... - return - - # Store highest exploitability of every exception in the chain - current_exception_exp = self.parsed_outputs[self.exception_depth].exp - if current_exception_exp: - if not self.exp: - self.exp = current_exception_exp - elif exp_rank[current_exception_exp] < exp_rank[self.exp]: - self.exp = current_exception_exp - - current_exception_hash = self.parsed_outputs[self.exception_depth].crash_hash - current_exception_faddr = self.parsed_outputs[self.exception_depth].faddr - if current_exception_hash: - # We have a hash for the current exception - if self.exception_depth == 0: - # First exception - start exception hash chain from scratch - self.crash_hash = current_exception_hash - elif self.crash_hash: - # Append to the exception hash chain - self.crash_hash = self.crash_hash + '_' + current_exception_hash - - if self.keep_uniq_faddr and current_exception_faddr: - self.crash_hash += '.' + current_exception_faddr - - # The first exception is the one that is representative for the crasher - if self.exception_depth == 0: - self.dbg_file = debugger.outfile - # add debugger results to our own attributes - self.is_crash = self.parsed_outputs[0].is_crash - self.dbg_type = self.parsed_outputs[0]._key - # self.exp = self.parsed_outputs[0].exp - self.faddr = self.parsed_outputs[0].faddr - # self.crash_hash = self.parsed_outputs[0].crash_hash - - def get_signature(self): - self.signature = self.crash_hash - return self.crash_hash - - def debug(self, tries_remaining=None): - if tries_remaining is None: - tries_remaining = self.max_depth - - logger.debug("tries left: %d", tries_remaining) - self.debug_once() - - if self.is_crash: - self.is_heisenbug = False - - logger.debug("checking for handled exceptions...") - while self.exception_depth < self.max_handled_exceptions: - self.exception_depth += 1 - self.debug_once() - if self.reached_secondchance or not self.parsed_outputs[self.exception_depth].is_crash: - logger.debug("no more handled exceptions") - break - # get the signature now that we've got all of the exceptions - self.get_signature() - else: - # if we are still a heisenbug after debugging - # we might need to try again - # or give up if we've tried enough already - if tries_remaining: - # keep diving - logger.debug("possible heisenbug (%d tries left)", tries_remaining) - self.debug(tries_remaining - 1) - else: - # we're at the bottom - self._set_heisenbug_properties() - logger.debug("heisenbug found") - - def _set_heisenbug_properties(self): - self.exp = 'HEISENBUG' - try: - fuzzed_content = self.fuzzedfile.read() - except Exception, e: - # for whatever reason we couldn't get the real content, - # and since we're just generating a string here - # any string will do - logger.warning('Unable to get md5 of %s, using random string for heisenbug signature: %s', self.fuzzedfile.path, e) - fuzzed_content = random_str(64) - self.is_heisenbug = True - self.signature = hashlib.md5(fuzzed_content).hexdigest() - - def _get_output_dir(self, target_base): - logger.debug('target_base: %s', target_base) - logger.debug('exp: %s', self.exp) - logger.debug('signature: %s', self.signature) - - self.target_dir = os.path.join(target_base, self.exp, self.signature) - if len(self.target_dir) > 170: - # Don't make a path too deep. Windows won't support it - self.target_dir = self.target_dir[:170] + '__' - logger.debug('target_dir: %s', self.target_dir) - return self.target_dir - - def _rename_fuzzed_file(self): - if not self.faddr: - return - - logger.debug('Attempting to rename %s', self.fuzzedfile.path) - new_basename = '%s-%s%s' % (self.fuzzedfile.root, self.faddr, self.fuzzedfile.ext) - new_fuzzed_file = os.path.join(self.fuzzedfile.dirname, new_basename) - logger.debug('renaming %s -> %s', self.fuzzedfile.path, new_fuzzed_file) - - # best_effort move returns a tuple of booleans indicating (copied, deleted) - # we only care about copied - copied = best_effort_move(self.fuzzedfile.path, new_fuzzed_file)[0] - - if copied: - # replace fuzzed file - self.fuzzedfile = BasicFile(new_fuzzed_file) - - def _rename_dbg_file(self): - if self.faddr: - faddr_str = '%s' % self.faddr - else: - faddr_str = '' - - (path, basename) = os.path.split(self.dbg_file) - (basename, dbgext) = os.path.splitext(basename) - (root, ext) = os.path.splitext(basename) - - exp_str = short_exp[self.exp] - - parts = [root] - if faddr_str: - parts.append(faddr_str) - if exp_str: - parts.append(exp_str) - new_basename = '-'.join(parts) + ext + dbgext - - new_dbg_file = os.path.join(path, new_basename) - - # best_effort move returns a tuple of booleans indicating (copied, deleted) - # we only care about copied - copied = best_effort_move(self.dbg_file, new_dbg_file)[0] - if copied: - self.dbg_file = new_dbg_file diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 0c98666..55be645 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -6,7 +6,7 @@ import logging import os -from certfuzz.testcase.crash_base import Testcase, CrashError +from certfuzz.testcase.testcase_base2 import Testcase, CrashError from certfuzz.fuzztools import hostinfo, filetools from certfuzz.fuzztools.command_line_templating import get_command_args_list diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index f07135e..54a9a56 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -8,13 +8,13 @@ import sys from certfuzz import debuggers -from certfuzz.config.config_linux import LinuxConfig from certfuzz.testcase.testcase_linux import LinuxTestcase from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools import filetools, text from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer +from certfuzz.config.simple_loader import load_and_fix_config mydir = os.path.dirname(os.path.abspath(__file__)) @@ -123,9 +123,6 @@ def main(): else: parser.error('fuzzedfile must be specified') - cfg = LinuxConfig(cfg_file) - with cfg: - pass if options.target: seedfile = BasicFile(options.target) @@ -137,9 +134,18 @@ def main(): crashers_dir = '.' - with LinuxTestcase(cfg, seedfile, fuzzed_file, cfg.program, - cfg.debugger_timeout, cfg.killprocname, cfg.backtracelevels, - crashers_dir, options.keep_uniq_faddr) as crash: + cfg = load_and_fix_config(cfg_file) + + with LinuxTestcase(cfg=cfg, + seedfile=seedfile, + fuzzedfile=fuzzed_file, + program=cfg['target']['program'], + debugger_timeout=cfg['debugger']['runtimeout'], + killprocname=cfg['target']['killprocname'], + backtrace_lines=cfg['debugger']['backtracelevels'], + crashers_dir=crashers_dir, + workdir_base=None, + keep_faddr=options.keep_uniq_faddr) as crash: filetools.make_directories(crash.tempdir) logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) From 50d421e09cfce06f5719fa499d6ff55f01f3886b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 10:56:37 -0500 Subject: [PATCH 0779/1169] path mixups happen in config loader now. also fix import from testcase rename --- src/certfuzz/iteration/iteration_linux.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index ab0e5cf..1c26358 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -6,13 +6,13 @@ import logging import os -from certfuzz.testcase.testcase_linux import LinuxTestcase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.fuzztools.zzuflog import ZzufLog from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.tc_pipeline.tc_pipeline_linux import LinuxTestCasePipeline from certfuzz.helpers.misc import fixup_path +from certfuzz.testcase.testcase_linux import LinuxTestcase logger = logging.getLogger(__name__) @@ -112,7 +112,7 @@ def _construct_testcase(self): with LinuxTestcase(cfg=self.cfg, seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), - program=fixup_path(self.cfg['target']['program']), + program=self.cfg['target']['program'], debugger_timeout=self.cfg['debugger']['runtimeout'], killprocname=self.cfg['target']['killprocname'], backtrace_lines=self.cfg['debugger']['backtracelevels'], From 338008742bbe36b147097cc2ab5441da92aad822 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 15:02:08 -0500 Subject: [PATCH 0780/1169] make runner class configurable on linux/osx --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/runners/zzufrun.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index bc0339f..db4687b 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -187,7 +187,7 @@ def _do_iteration(self, seedfile, range_obj, seednum): uniq_func=self._crash_is_unique, config=self.config, fuzzer_cls=self.fuzzer_cls, - runner_cls=ZzufRunner, + runner_cls=self.runner_cls, ) as iteration: try: iteration() diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index b7acc20..6ffe434 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -75,3 +75,5 @@ def _run(self): if rc != 0: self.saw_crash = True # raise RunnerError('zzuf returncode: {}'.format(rc)) + +_runner_class=ZzufRunner From 9aa9c26217a54ddb7a4e18b01ad43ef416775ca1 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Feb 2016 16:00:16 -0500 Subject: [PATCH 0781/1169] Don't override _set_runner in campaign_linux. Otherwise, BFF won't know what the runner is (zzufrun) --- src/certfuzz/campaign/campaign_linux.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index db4687b..cbd7fde 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -63,7 +63,7 @@ class LinuxCampaign(CampaignBase): def _full_path_original(self, seedfile): # yes, two seedfile mentions are intended - adh - program_basename=os.path.basename(self.program).replace('"', '') + program_basename = os.path.basename(self.program).replace('"', '') return os.path.join(self.work_dir_base, program_basename, seedfile, @@ -140,12 +140,6 @@ def _check_for_script(self): logger.warning("Target application is a shell script.") raise CampaignScriptError() - def _set_runner(self): - ''' - Overrides parent class - ''' - pass - def _set_debugger(self): ''' Overrides parent class From 437121a52642de3fd31204a25bbe82582495ff44 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 15:28:10 -0500 Subject: [PATCH 0782/1169] add check for cert_zzuf (part one, not done yet) --- src/certfuzz/runners/zzufrun.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 6ffe434..54da8b5 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -19,6 +19,12 @@ _zzuf_basename = 'zzuf' _zzuf_loc = None +_use_cert_version_of_zzuf=False + +def check_cert_zzuf(): + #zzuf --help + # if 'null' in result + return True def _find_zzuf(): global _zzuf_loc @@ -52,14 +58,21 @@ def _construct_zzuf_args(self): args = [_zzuf_loc] if self._quiet: args.append('--quiet') + + _opmode='copy' + if _use_cert_version_of_zzuf: + _opmode='null' + args.extend(['--signal', '--ratio=0.0', '--seed=0', '--max-crashes=1', '--max-usertime=5.00', - '--opmode=copy', + '--opmode=%s' % _opmode, '--include=%s' % self.fuzzed_file, ]) + + self._zzuf_args = args def _run(self): From cbaaca12b3b47afd121d71da37e554a87b30014c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 15:30:19 -0500 Subject: [PATCH 0783/1169] add quick test structure --- src/certfuzz/runners/zzufrun.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 54da8b5..2971d96 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -22,9 +22,13 @@ _use_cert_version_of_zzuf=False def check_cert_zzuf(): + result = '' #zzuf --help - # if 'null' in result - return True + + if 'null' in result: + _use_cert_version_of_zzuf=True + + _use_cert_version_of_zzuf=False def _find_zzuf(): global _zzuf_loc From fcc96a101d4514328e2273a82da69968db9350c1 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Feb 2016 17:25:25 -0500 Subject: [PATCH 0784/1169] Use "null" opmode if we have the cert-provide zzuf version. --- src/certfuzz/campaign/campaign_linux.py | 1 + src/certfuzz/runners/zzufrun.py | 51 +++++++++++++++---------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index cbd7fde..8759696 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -84,6 +84,7 @@ def _post_enter(self): if self.config['runoptions']['watchdogtimeout']: self._setup_watchdog() check_ppid() + self.runner_module.check_cert_zzuf() self._cache_app() def _pre_exit(self): diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 2971d96..909126b 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -19,22 +19,35 @@ _zzuf_basename = 'zzuf' _zzuf_loc = None -_use_cert_version_of_zzuf=False +_use_cert_version_of_zzuf = False -def check_cert_zzuf(): - result = '' - #zzuf --help - - if 'null' in result: - _use_cert_version_of_zzuf=True - - _use_cert_version_of_zzuf=False def _find_zzuf(): global _zzuf_loc _zzuf_loc = find_executable(_zzuf_basename) +def _verify_zzuf_installed(): + if _zzuf_loc is None: + _find_zzuf() + # if it's still None, we have a problem + if _zzuf_loc is None: + raise RunnerNotFoundError('Unable to locate {}, $PATH={}'.format(_zzuf_basename, os.environ['PATH'])) + + +def check_cert_zzuf(): + sawnullmode = False + global _use_cert_version_of_zzuf + _verify_zzuf_installed() + result = subprocess.check_output(['zzuf', '-h']) + for line in result.split(os.linesep): + if ' -O, --opmode ' and 'null' in line: + sawnullmode = True + if sawnullmode: + _use_cert_version_of_zzuf = True + else: + _use_cert_version_of_zzuf = False + class ZzufRunner(Runner): def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): Runner.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) @@ -53,20 +66,16 @@ def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): logger.debug('_zzuf_args=%s', self._zzuf_args) def _construct_zzuf_args(self): - if _zzuf_loc is None: - _find_zzuf() - # if it's still None, we have a problem - if _zzuf_loc is None: - raise RunnerNotFoundError('Unable to locate {}, $PATH={}'.format(_zzuf_basename, os.environ['PATH'])) + _verify_zzuf_installed() args = [_zzuf_loc] if self._quiet: args.append('--quiet') - - _opmode='copy' + + _opmode = 'copy' if _use_cert_version_of_zzuf: - _opmode='null' - + _opmode = 'null' + args.extend(['--signal', '--ratio=0.0', '--seed=0', @@ -75,8 +84,8 @@ def _construct_zzuf_args(self): '--opmode=%s' % _opmode, '--include=%s' % self.fuzzed_file, ]) - - + + self._zzuf_args = args def _run(self): @@ -93,4 +102,4 @@ def _run(self): self.saw_crash = True # raise RunnerError('zzuf returncode: {}'.format(rc)) -_runner_class=ZzufRunner +_runner_class = ZzufRunner From 419d34b01f481d794bf5f9a9e2e91301eb107aee Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 3 Feb 2016 10:52:22 -0500 Subject: [PATCH 0785/1169] remove unused imports --- src/certfuzz/campaign/campaign_linux.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 8759696..f3ee471 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -20,9 +20,6 @@ from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.iteration.iteration_linux import LinuxIteration -from certfuzz.fuzzers.bytemut import ByteMutFuzzer -from certfuzz.runners.zzufrun import ZzufRunner -from certfuzz.helpers.misc import fixup_path from certfuzz.fuzztools.command_line_templating import get_command_args_list from certfuzz.fuzzers.errors import FuzzerExhaustedError From 2da189fceb400bae1677ef3d65ab3e07389db309 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 3 Feb 2016 10:53:08 -0500 Subject: [PATCH 0786/1169] refactor check for cert zzuf removes a bug in which we were only looking for the word "null" in any line. --- src/certfuzz/runners/zzufrun.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 909126b..c75fc5d 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -36,17 +36,17 @@ def _verify_zzuf_installed(): def check_cert_zzuf(): - sawnullmode = False global _use_cert_version_of_zzuf + _verify_zzuf_installed() + result = subprocess.check_output(['zzuf', '-h']) + for line in result.split(os.linesep): - if ' -O, --opmode ' and 'null' in line: - sawnullmode = True - if sawnullmode: - _use_cert_version_of_zzuf = True - else: - _use_cert_version_of_zzuf = False + check_for=('--opmode ', 'null') + + if all(x in line for x in check_for): + _use_cert_version_of_zzuf = True class ZzufRunner(Runner): def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): From cf5e6c415dbb0315a0c6a3f66b0a3a9136427ba2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 3 Feb 2016 13:06:52 -0500 Subject: [PATCH 0787/1169] rename check_cert_zzuf method to check_runner and pop it up to the campaign base class Runners without a check_runner method will just log a warning message and proceed. --- src/certfuzz/campaign/campaign_base.py | 9 +++++++++ src/certfuzz/campaign/campaign_linux.py | 1 - src/certfuzz/runners/zzufrun.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 4f1c9e3..a7249a1 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -159,6 +159,7 @@ def __enter__(self): self._setup_workdir() self._set_fuzzer() self._set_runner() + self._check_runner() self._setup_output() self._create_seedfile_set() @@ -255,6 +256,14 @@ def _set_runner(self): self.runner_module = import_module_by_name(self.runner_module_name) self.runner_cls = self.runner_module._runner_class + def _check_runner(self): + # try to run the runner module's check_runner method + try: + self.runner_module.check_runner() + except AttributeError: + # not a big deal if it's not there, just note it and keep going. + logger.warn('Runner module %s has no check_runner method. Skipping runner check.') + @property def _version_file(self): return os.path.join(self.outdir, 'version.txt') diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index f3ee471..70e46ba 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -81,7 +81,6 @@ def _post_enter(self): if self.config['runoptions']['watchdogtimeout']: self._setup_watchdog() check_ppid() - self.runner_module.check_cert_zzuf() self._cache_app() def _pre_exit(self): diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index c75fc5d..5dd54fe 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -35,7 +35,7 @@ def _verify_zzuf_installed(): raise RunnerNotFoundError('Unable to locate {}, $PATH={}'.format(_zzuf_basename, os.environ['PATH'])) -def check_cert_zzuf(): +def check_runner(): global _use_cert_version_of_zzuf _verify_zzuf_installed() From baf81677d169044178863e0d4f0f3124c36e8f4e Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 3 Feb 2016 16:51:54 -0500 Subject: [PATCH 0788/1169] Invoke the target program once with stdout/stderr enabled and wait 10 seconds for the user to confirm that there's no trouble. --- src/certfuzz/campaign/campaign_linux.py | 8 ++++++-- src/certfuzz/fuzztools/subprocess_helper.py | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 70e46ba..1c1b257 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -110,13 +110,17 @@ def _cache_app(self): # Run the program once to cache it into memory fullpathorig = self._full_path_original(sf.path) cmdargs = get_command_args_list(self.config['target']['cmdline_template'], infile=fullpathorig)[1] + logger.info('Invoking %s' % cmdargs) subp.run_with_timer(cmdargs, self.config['runner']['runtimeout'] * 8, self.config['target']['killprocname'], - use_shell=False) + use_shell=False, + seeoutput=True, + ) # Give target time to die - time.sleep(1) + logger.info('Please ensure that the target program has just executed successfully') + time.sleep(10) def _setup_watchdog(self): logger.debug('setup watchdog') diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 016fd4a..5c8bd72 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -42,6 +42,9 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): @return: none ''' output = '' + _seeoutput = False + if options and options.get('seeoutput'): + _seeoutput = True if options and options.get('stdout'): output = open(options['stdout'], 'w') else: @@ -49,7 +52,7 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): errors = '' if options and options.get('stderr'): - errors = open(options['stderr'], 'w') + errors = open(options['stderr'], 'w') else: errors = open(os.devnull, 'w') @@ -62,12 +65,15 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): # remove empty args from the list [Fix for BFF-17] # ['a','','b','c'] -> ['a', 'b', 'c'] args = [arg for arg in args if arg] - + for index, arg in enumerate(args): args[index] = string.replace(args[index], '"', '') try: - p = subprocess.Popen(args, stdout=output, stderr=errors, env=env, shell=use_shell) + if _seeoutput: + p = subprocess.Popen(args, env=env, shell=use_shell) + else: + p = subprocess.Popen(args, stdout=output, stderr=errors, env=env, shell=use_shell) except: print "Failed to run [%s]" % ' '.join(args) sys.exit(-1) @@ -80,7 +86,9 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): t.cancel() # close our stdout and stderr filehandles - [fh.close() for fh in (output, errors)] + + if not _seeoutput: + [fh.close() for fh in (output, errors)] return p From 6fb46aa725acdd434b06ad18f159f0b063d4d234 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 3 Feb 2016 16:53:13 -0500 Subject: [PATCH 0789/1169] Touch gdb output file if it doesn't exist. --- src/certfuzz/debuggers/gdb.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index 7fcea44..696cb3c 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -108,4 +108,7 @@ def go(self): subp.run_with_timer(cmdline, self.timeout, self.killprocname, stdout=os.devnull) self._remove_temp_file() + if not os.path.exists(self.outfile): + # touch it if it doesn't exist + open(self.outfile, 'w').close() return GDBfile(self.outfile, self.exclude_unmapped_frames, self.keep_uniq_faddr) From 33f1a28b786b661b68f74b040a2da22e6697ed77 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 4 Feb 2016 08:24:57 -0500 Subject: [PATCH 0790/1169] Don't allow redirect in cmdline_template BFF-884 --- src/certfuzz/campaign/campaign_linux.py | 12 +++++++++--- src/certfuzz/campaign/errors.py | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 1c1b257..0a129cd 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -11,7 +11,7 @@ import time from certfuzz.campaign.campaign_base import CampaignBase -from certfuzz.campaign.errors import CampaignScriptError +from certfuzz.campaign.errors import CampaignScriptError, CmdlineTemplateError from certfuzz.debuggers import crashwrangler # @UnusedImport from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file @@ -72,10 +72,10 @@ def _full_path_original(self, seedfile): def _pre_enter(self): # give up if prog is a script self._check_for_script() - + self._check_for_redirect() self._start_process_killer() self._set_unbuffered_stdout() - self._check_for_script() + def _post_enter(self): if self.config['runoptions']['watchdogtimeout']: @@ -141,6 +141,12 @@ def _check_for_script(self): logger.warning("Target application is a shell script.") raise CampaignScriptError() + def _check_for_redirect(self): + logger.debug('check for redirect') + if '>' in self.config['target']['cmdline_template'].template: + logger.warning("Redirect (>) present in cmdline_template.") + raise CmdlineTemplateError() + def _set_debugger(self): ''' Overrides parent class diff --git a/src/certfuzz/campaign/errors.py b/src/certfuzz/campaign/errors.py index 0173b26..77b24ad 100644 --- a/src/certfuzz/campaign/errors.py +++ b/src/certfuzz/campaign/errors.py @@ -12,3 +12,6 @@ class CampaignError(CERTFuzzError): class CampaignScriptError(CampaignError): pass + +class CmdlineTemplateError(CampaignError): + pass From de73394f553579f79d81b7c1855ff8d803869a78 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 4 Feb 2016 10:29:31 -0500 Subject: [PATCH 0791/1169] Split cmd and args so that gdb invokes the target correctly. BFF-886 --- src/certfuzz/minimizer/minimizer_base.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index d343608..db495c6 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -380,11 +380,13 @@ def _set_crash_hashes(self): def run_debugger(self, infile, outfile): self.debugger_runs += 1 - cmd_args = get_command_args_list(self.cfg['target']['cmdline_template'],infile)[1] + cmd_args = get_command_args_list(self.cfg['target']['cmdline_template'], infile)[1] + cmd = cmd_args[0] + args = cmd_args[1:] # cmd_args = self.cfg.get_command_args_list(infile) - dbg = self._debugger_cls(cmd_args[0], - cmd_args, + dbg = self._debugger_cls(cmd, + args, outfile, self.debugger_timeout, self.cfg['target']['killprocname'], From e79a3373d2f906be8fde176d3197a4d40b8a54fe Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 4 Feb 2016 14:08:20 -0500 Subject: [PATCH 0792/1169] Stop using DummyCfg object. BFF-888 --- src/certfuzz/minimizer/minimizer_base.py | 22 +++++++++++++++---- .../tc_pipeline/tc_pipeline_windows.py | 17 +------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index db495c6..c3ad23a 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -63,6 +63,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, self.ext = self.crash.fuzzedfile.ext self.logger = None self.log_file_hdlr = None + self.backtracelevels = 5 logger.setLevel(logging.INFO) @@ -187,6 +188,10 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, def __enter__(self): # make sure we can actually minimize + try: + self.backtracelevels = self.cfg['debugger']['backtracelevels'] + except KeyError: + pass if not self._is_crash_to_minimize(): msg = 'Unable to minimize: No crash' self.logger.info(msg) @@ -349,7 +354,7 @@ def _set_crash_hashes(self): if dbg.is_crash: times.append(delta) - current_sig = self.get_signature(dbg, self.cfg['debugger']['backtracelevels']) + current_sig = self.get_signature(dbg, self.backtracelevels) # ditch the temp file if os.path.exists(f): @@ -384,14 +389,23 @@ def run_debugger(self, infile, outfile): cmd = cmd_args[0] args = cmd_args[1:] # cmd_args = self.cfg.get_command_args_list(infile) + try: + killprocname = self.cfg['target']['killprocname'] + except KeyError: + killprocname = cmd + + try: + exclude_unmapped_frames = self.cfg['analyzer']['exclude_unmapped_frames'] + except KeyError: + exclude_unmapped_frames = True dbg = self._debugger_cls(cmd, args, outfile, self.debugger_timeout, - self.cfg['target']['killprocname'], + killprocname, template=self.crash.debugger_template, - exclude_unmapped_frames=self.cfg['analyzer']['exclude_unmapped_frames'], + exclude_unmapped_frames=exclude_unmapped_frames, keep_uniq_faddr=self.keep_uniq_faddr, workingdir=self.tempdir, watchcpu=self.watchcpu @@ -472,7 +486,7 @@ def is_same_crash(self): dbg = self.run_debugger(self.tempfile, f) if dbg.is_crash: - newfuzzed_hash = self.get_signature(dbg, self.cfg['debugger']['backtracelevels']) + newfuzzed_hash = self.get_signature(dbg, self.backtracelevels) else: newfuzzed_hash = None # initialize or increment the counter for this hash diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 6e3b666..102007d 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -48,9 +48,7 @@ def _minimize(self, testcase): logger.info('Minimizing testcase %s', testcase.signature) logger.debug('config = %s', self.cfg) - config = self._create_minimizer_cfg() - - kwargs = {'cfg': config, + kwargs = {'cfg': self.cfg, 'crash': testcase, 'seedfile_as_target': True, 'bitwise': False, @@ -111,16 +109,3 @@ def keep_testcase(self, testcase): return (True, 'heisenbug') else: return (False, 'skip heisenbugs') - - def _create_minimizer_cfg(self): - class DummyCfg(object): - pass - config = DummyCfg() - config.backtracelevels = 5 # doesn't matter what this is, we don't use it - config.debugger_timeout = self.cfg['debugger']['runtimeout'] - config.get_command_args_list = lambda x: get_command_args_list(self.options['cmd_template'], x)[1] - config.program = self.cfg['target']['program'] - config.killprocname = None - config.exclude_unmapped_frames = False - config.watchdogfile = os.devnull - return config From 7ffb3e4145d219b20e15b06f1d9803b5aad465fc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 4 Feb 2016 14:20:43 -0500 Subject: [PATCH 0793/1169] Fix arguments passed to debugger. BFF-889 --- src/certfuzz/debuggers/msec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index c651309..0a9c366 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -75,7 +75,7 @@ def _get_cmdline(self, outfile): cdb_command = 'g;' + cdb_command args.append(cdb_command) args.append(self.program) - args.extend(self.cmd_args[1:]) + args.extend(self.cmd_args) for l in pformat(args).splitlines(): logger.debug('dbg_args: %s', l) return args From 9464674d6d60a0c7dccd9842371f013ee8a9f265 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Feb 2016 14:50:46 -0500 Subject: [PATCH 0794/1169] Make sure rsync of xcat depenencies actually works. BFF-890 --- build/distmods/osx/darwin_build2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/distmods/osx/darwin_build2.py b/build/distmods/osx/darwin_build2.py index 0eb7fa3..8b63821 100644 --- a/build/distmods/osx/darwin_build2.py +++ b/build/distmods/osx/darwin_build2.py @@ -179,8 +179,8 @@ def _convert_sparseimage_to_dmg(self): def _sync_dependencies(self): # Retrieve binary dependecies for building OSX installer # TODO: What if rsync fails? - subprocess.call(['rsync', '-EaxSv', '--delete', self.SHARED_DEPS, self.LOCAL_DEPS]) - subprocess.call(['rsync', '-EaxSv', self.LOCAL_DEPS, self.INSTALLER_BASE]) + subprocess.check_call(['rsync', '-EaxSv', '--delete', self.SHARED_DEPS, self.LOCAL_DEPS]) + subprocess.check_call(['rsync', '-EaxSv', self.LOCAL_DEPS, self.INSTALLER_BASE]) def package(self): self._sync_dependencies() From 1ec07b6f50357c2c48efe81a539c75854110ca9f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Feb 2016 16:24:49 -0500 Subject: [PATCH 0795/1169] Dumb null runner that supports Windows versions newer than XP. BFF-887 --- src/certfuzz/runners/nullrun.py | 17 +++++++++++++++++ src/windows/configs/examples/bff.yaml | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/certfuzz/runners/nullrun.py diff --git a/src/certfuzz/runners/nullrun.py b/src/certfuzz/runners/nullrun.py new file mode 100644 index 0000000..9aa0ee8 --- /dev/null +++ b/src/certfuzz/runners/nullrun.py @@ -0,0 +1,17 @@ +''' +Created on Feb 8, 2016 + +@author: wd +''' + +from certfuzz.runners.runner_base import Runner +from certfuzz.runners.errors import RunnerError + +class NullRunner(Runner): + def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): + Runner.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) + + def _run(self): + self.saw_crash = True + +_runner_class = NullRunner diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 15295a2..d4054f1 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -138,7 +138,7 @@ fuzzer: # ##################################################################### runner: - # runner: null + # runner: nullrun runner: winrun hideoutput: False runtimeout: 5 From 076f45c3f62da62ef1e983d9e2fb2630c76009eb Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Feb 2016 16:25:59 -0500 Subject: [PATCH 0796/1169] Automatically fall back to null runner if not on 32-bit XP. BFF-893 --- src/certfuzz/campaign/campaign_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index efc7793..35f5e67 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -26,7 +26,7 @@ class WindowsCampaign(CampaignBase): def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) - self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker',False) + self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker', False) def __getstate__(self): state = self.__dict__.copy() @@ -80,7 +80,7 @@ def _pre_enter(self): return logger.debug('winrun is not compatible with Windows %s %s. Overriding.', winver, machine) - self.runner_module_name = None + self.runner_module_name = 'certfuzz.runners.nullrun' def _post_enter(self): self._start_buttonclicker() From 8c0044585e55f998fd7db438a8f8c5d5a3d26d05 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Feb 2016 16:36:36 -0500 Subject: [PATCH 0797/1169] Update comments for config change for BFF-887 --- src/windows/configs/examples/bff.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index d4054f1..1fd6afa 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -128,7 +128,7 @@ fuzzer: # runner: # winrun: Use hook.dll to detect exceptions. Investigate exceptions # with a debugger. Only compatible with 32-bit Windows XP and 2003. -# null: Investigate each iteration with the debugger. +# nullrun: Investigate each iteration with the debugger. # hideoutput: Hide stdout of target application # runtimeout: Number of seconds to allow target application to execute # when run with the hook (winrun) From 32f1cc98e539a6b21456be69318027e01e5795ae Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Feb 2016 16:55:44 -0500 Subject: [PATCH 0798/1169] Unit test stub for nullrun --- src/test_certfuzz/runners/test_nullrun.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/test_certfuzz/runners/test_nullrun.py diff --git a/src/test_certfuzz/runners/test_nullrun.py b/src/test_certfuzz/runners/test_nullrun.py new file mode 100644 index 0000000..6e33993 --- /dev/null +++ b/src/test_certfuzz/runners/test_nullrun.py @@ -0,0 +1,21 @@ +''' +Created on Apr 10, 2012 + +@organization: cert.org +''' +import unittest + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 2ab33add81a32964210d4e3fbc05aaf8b09bcb87 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 08:05:45 -0500 Subject: [PATCH 0799/1169] Package updated zzuf source in OSX installer --- build/distmods/osx/installer/BFF_installer.pmdoc/21zzuf.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/21zzuf.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/21zzuf.xml index 843f64f..2eff23b 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/21zzuf.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/21zzuf.xml @@ -1 +1 @@ -com.cert.cc.certBff.zzuf.tar.pkg1.0zzuf.tar.gz/Applications/BFF.app/Contents/Resources/packagesinstallTo.pathinstallFrom.isRelativeTypeparentinstallTo \ No newline at end of file +com.cert.cc.certBff.zzuf.tar.pkg1.0zzuf-master.zip/Applications/BFF.app/Contents/Resources/packagesinstallTo.pathinstallFrom.isRelativeTypeparentinstallTo \ No newline at end of file From 3713b5d06f766005d3110e2f92f31f2a563b2a31 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 08:32:42 -0500 Subject: [PATCH 0800/1169] El Capitan doesn't have libyaml. We'll need to use --without-libyaml on install to avoid this. BFF-896 --- build/distmods/osx/installer/scripts/pyyaml-post.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/distmods/osx/installer/scripts/pyyaml-post.sh b/build/distmods/osx/installer/scripts/pyyaml-post.sh index e4f4fec..2f91c5e 100755 --- a/build/distmods/osx/installer/scripts/pyyaml-post.sh +++ b/build/distmods/osx/installer/scripts/pyyaml-post.sh @@ -7,7 +7,7 @@ export PATH=/Library/Frameworks/Python.framework/Versions/2.7/bin/:$PATH cd $installdir tar xzvf PyYAML-3.10.tar.gz cd PyYAML-3.10 -python setup.py install 2>>/tmp/pyyaml.err +python setup.py install --without-libyaml 2>>/tmp/pyyaml.err exit $? From 99a7c76b078fc5c3e90c9691093f129ce5f315e0 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 08:34:20 -0500 Subject: [PATCH 0801/1169] Don't install python stuff on El Capitan. It has it already. BFF-897 --- .../osx/installer/BFF_installer.pmdoc/index.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index 19095c6..920356a 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -17,15 +17,27 @@ + + + + + + + + From e51a7edf025c90959b4e54553d799eee15ce69d6 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 08:36:02 -0500 Subject: [PATCH 0802/1169] Support the El Capitan CrashWrangler version when it comes out. BFF-898 --- src/certfuzz/debuggers/crashwrangler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index cdccb4f..0451d5d 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -30,6 +30,8 @@ cwapp = 'exc_handler_mavericks' elif re.match('Darwin-14', myplatform): cwapp = 'exc_handler_yosemite' +elif re.match('Darwin-15', myplatform): + cwapp = 'exc_handler_elcapitan' else: cwapp = 'exc_handler' From 52630e17ab6bdd55a88507d34b62363f3cf20c4f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 09:00:40 -0500 Subject: [PATCH 0803/1169] Fix setup.py argument ordering for --without-libyaml BFF-896 --- build/distmods/osx/installer/scripts/pyyaml-post.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/distmods/osx/installer/scripts/pyyaml-post.sh b/build/distmods/osx/installer/scripts/pyyaml-post.sh index 2f91c5e..2ee84bb 100755 --- a/build/distmods/osx/installer/scripts/pyyaml-post.sh +++ b/build/distmods/osx/installer/scripts/pyyaml-post.sh @@ -7,7 +7,7 @@ export PATH=/Library/Frameworks/Python.framework/Versions/2.7/bin/:$PATH cd $installdir tar xzvf PyYAML-3.10.tar.gz cd PyYAML-3.10 -python setup.py install --without-libyaml 2>>/tmp/pyyaml.err +python setup.py --without-libyaml install 2>>/tmp/pyyaml.err exit $? From 85d2e997beff0f675692923675c176934dde6905 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 11:39:57 -0500 Subject: [PATCH 0804/1169] Only minimize when told to. BFF-856 --- src/certfuzz/iteration/iteration_base3.py | 2 +- src/certfuzz/iteration/iteration_windows.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index a6ce82e..f2c0e9e 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -164,7 +164,7 @@ def process_testcases(self): options=self.pipeline_options, outdir=self.outdir, workdirbase=self.working_dir, - minimizable=self.fuzzer.is_minimizable, + minimizable=self.pipeline_options['minimizable'], sf_set=self.sf_set) as pipeline: pipeline.go() diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 3d5fd8d..e525b49 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -59,7 +59,7 @@ def __init__(self, self.debug = debug # TODO: do we use keep_uniq_faddr at all? - self.keep_uniq_faddr = config['runoptions'].get('keep_unique_faddr',False) + self.keep_uniq_faddr = config['runoptions'].get('keep_unique_faddr', False) self.cmd_template = cmd_template @@ -70,12 +70,12 @@ def __init__(self, # runner_cls is not null self.retries = 4 - self.pipeline_options = {'keep_duplicates': self.cfg['runoptions'].get('keep_duplicates',False), - 'keep_heisenbugs': self.cfg['runoptions'].get('keep_heisenbugs',False), + self.pipeline_options = {'keep_duplicates': self.cfg['runoptions'].get('keep_duplicates', False), + 'keep_heisenbugs': self.cfg['runoptions'].get('keep_heisenbugs', False), 'minimizable': False, 'cmd_template': self.cmd_template, 'used_runner': self.runner_cls is not None, - 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize',False) + 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize', False) } def __exit__(self, etype, value, traceback): From 92c368846988956e508d1fc73ea4619d6415a987 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 15:40:25 -0500 Subject: [PATCH 0805/1169] Fix up null runner logic to not be so noisy on non-XP systems. --- src/certfuzz/iteration/iteration_windows.py | 4 ++-- src/certfuzz/runners/nullrun.py | 4 ++++ src/certfuzz/runners/runner_base.py | 3 +++ src/certfuzz/tc_pipeline/tc_pipeline_windows.py | 8 ++++++-- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index e525b49..32dabac 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -63,7 +63,7 @@ def __init__(self, self.cmd_template = cmd_template - if self.runner_cls is None: + if self.runner_cls.is_nullrunner: # null runner_cls case self.retries = 0 else: @@ -74,7 +74,7 @@ def __init__(self, 'keep_heisenbugs': self.cfg['runoptions'].get('keep_heisenbugs', False), 'minimizable': False, 'cmd_template': self.cmd_template, - 'used_runner': self.runner_cls is not None, + 'null_runner': self.runner_cls.is_nullrunner, 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize', False) } diff --git a/src/certfuzz/runners/nullrun.py b/src/certfuzz/runners/nullrun.py index 9aa0ee8..a339f5e 100644 --- a/src/certfuzz/runners/nullrun.py +++ b/src/certfuzz/runners/nullrun.py @@ -7,7 +7,11 @@ from certfuzz.runners.runner_base import Runner from certfuzz.runners.errors import RunnerError + class NullRunner(Runner): + # This isn't a real runner + is_nullrunner = True + def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): Runner.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) diff --git a/src/certfuzz/runners/runner_base.py b/src/certfuzz/runners/runner_base.py index 22a7db3..cee3099 100644 --- a/src/certfuzz/runners/runner_base.py +++ b/src/certfuzz/runners/runner_base.py @@ -7,7 +7,10 @@ logger = logging.getLogger(__name__) + class Runner(object): + is_nullrunner = False + ''' classdocs ''' diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 102007d..9126a71 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -33,7 +33,11 @@ def _verify(self, testcase): keep_it, reason = self.keep_testcase(testcase) if not keep_it: - logger.info('Candidate testcase rejected: %s', reason) + if self.options['null_runner'] and reason == 'not a crash': + # Don't be too chatty about rejecting a null runner crash + pass + else: + logger.info('Candidate testcase rejected: %s', reason) testcase.should_proceed_with_analysis = False return @@ -103,7 +107,7 @@ def keep_testcase(self, testcase): return (True, 'unique') else: return (False, 'skip duplicate %s' % testcase.signature) - elif not self.options['used_runner']: + elif self.options['null_runner']: return (False, 'not a crash') elif self.options['keep_heisenbugs']: return (True, 'heisenbug') From 21b9a868d85e33ebfe460378cc51ebc6b4069652 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 16:49:36 -0500 Subject: [PATCH 0806/1169] Don't bail on the campaign if you can't delete an iteration file. BFF-894 --- src/certfuzz/iteration/iteration_base3.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index f2c0e9e..880cb39 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -89,7 +89,11 @@ def __exit__(self, etype, value, traceback): return handled # clean up - rm_rf(self.working_dir) + try: + rm_rf(self.working_dir) + except WindowsError: + # Windows locks in-use files. No need to kill campaign if we can't delete + pass return handled def _pre_fuzz(self): From a968115338c39aa814d4e510a89ed9d6025cb3ff Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 16:51:12 -0500 Subject: [PATCH 0807/1169] Clean up config formatting and remove "nullrun" option, as it's automatically selected. --- src/windows/configs/examples/bff.yaml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 1fd6afa..cfe2a4b 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -3,7 +3,7 @@ # # id: used for identifying campaign, placement of results # keep_heisenbugs: Keep crashing testcases detected by hook, but -# not when run via the debugger. +# not when run via the debugger. # use_buttonclicker: Spawn program to click buttons ##################################################################### campaign: @@ -16,7 +16,7 @@ campaign: # # program: Path to fuzzing target executable # cmdline_template: Used to specify the command-line invocation of -# the target +# the target ##################################################################### target: program: C:\BFF\imagemagick\convert.exe @@ -47,7 +47,7 @@ target: # # seedfile_dir: Location of seed files (relative to bff.py) # working_dir: Temporary directory used by BFF. Use a ramdisk to -# reduce disk activity +# reduce disk activity # results_dir: Location of fuzzing results (relative to bff.py) ##################################################################### directories: @@ -103,7 +103,7 @@ runoptions: # verify: do not mutate file. Used for verifying crashing testcases # range_list: byte ranges to be fuzzed. One range per line, hex or decimal # fuzz_zip_container: rather than fuzzing zip file contents, fuzz the -# zip container itself +# zip container itself # ##################################################################### fuzzer: @@ -126,19 +126,17 @@ fuzzer: # Runner options # # runner: -# winrun: Use hook.dll to detect exceptions. Investigate exceptions -# with a debugger. Only compatible with 32-bit Windows XP and 2003. -# nullrun: Investigate each iteration with the debugger. +# winrun: Use hook.dll to detect exceptions on 32-bit Windows XP and 2003. +# Fall back to using debugger for each iteration on others. # hideoutput: Hide stdout of target application # runtimeout: Number of seconds to allow target application to execute -# when run with the hook (winrun) +# when run with the hook (winrun) # watchcpu: Kill target process when its CPU usage drops towards zero -# when run with the hook (winrun). (Auto, True, False) +# when run with the hook (winrun). (Auto, True, False) # exceptions: List of exceptions to save # ##################################################################### runner: - # runner: nullrun runner: winrun hideoutput: False runtimeout: 5 @@ -166,9 +164,9 @@ runner: # debugger: # msec: Use the msec !exploitable extension for cdb # runtimeout: Number of seconds to allow target application to execute -# when run via the debugger +# when run via the debugger # watchcpu: Kill target process when its CPU usage drops towards zero -# when run with the debugger (null runner). (Auto, True, False) +# when run with the debugger (null runner). (Auto, True, False) # debugheap: Use the debug heap for the target application # max_handled_exceptions: Maximum number of times to continue exceptions ##################################################################### From 026b2804344a5b13bf3cb8a26255197f13b6f9ce Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Feb 2016 16:53:07 -0500 Subject: [PATCH 0808/1169] Prepare for BFF Windows analyzer support. --- src/certfuzz/analyzers/analyzer_base.py | 7 +++++-- src/certfuzz/tc_pipeline/tc_pipeline_windows.py | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/analyzers/analyzer_base.py b/src/certfuzz/analyzers/analyzer_base.py index 1989052..22925a6 100644 --- a/src/certfuzz/analyzers/analyzer_base.py +++ b/src/certfuzz/analyzers/analyzer_base.py @@ -23,10 +23,13 @@ def __init__(self, cfg, crash, outfile=None, timeout=None, **options): self.cfg = cfg self.crash = crash - self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'],crash.fuzzedfile.path)[1] + self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'], crash.fuzzedfile.path)[1] self.outfile = outfile self.timeout = float(timeout) - self.killprocname = crash.killprocname + try: + self.killprocname = crash.killprocname + except AttributeError: + self.killprocname = self.cmdargs[1] self.options = options self.preserve_stderr = False diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 9126a71..13eecda 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -12,6 +12,8 @@ from certfuzz.minimizer.errors import MinimizerError from certfuzz.reporters.copy_files import CopyFilesReporter from certfuzz.fuzztools.command_line_templating import get_command_args_list +from certfuzz.analyzers import stderr +from certfuzz.analyzers.errors import AnalyzerEmptyOutputError logger = logging.getLogger(__name__) @@ -20,6 +22,7 @@ class WindowsTestCasePipeline(TestCasePipelineBase): def _setup_analyzers(self): pass + #self.analyzer_classes.append(stderr.StdErr) def _pre_verify(self, testcase): # pretty-print the testcase for debugging @@ -82,7 +85,18 @@ def _post_minimize(self, testcase): self.sf_set.add_file(crasherseed_path) def _analyze(self, testcase): - pass + ''' + Loops through all known analyzer_classes for a given testcase + :param testcase: + ''' + + for analyzer_class in self.analyzer_classes: + analyzer_instance = analyzer_class(self.cfg, testcase) + if analyzer_instance: + try: + analyzer_instance.go() + except AnalyzerEmptyOutputError: + logger.warning('Unexpected empty output from analyzer_class. Continuing') def _report(self, testcase): with CopyFilesReporter(testcase, self.tc_dir) as reporter: From 8dcbe4587e0ddc0bf3d50ab5638786d4002cab51 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 07:37:48 -0500 Subject: [PATCH 0809/1169] move exception catch for WindowsError into windows-only code. --- src/certfuzz/iteration/iteration_base3.py | 7 ++----- src/certfuzz/iteration/iteration_windows.py | 7 +++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 880cb39..e589e34 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -89,11 +89,8 @@ def __exit__(self, etype, value, traceback): return handled # clean up - try: - rm_rf(self.working_dir) - except WindowsError: - # Windows locks in-use files. No need to kill campaign if we can't delete - pass + rm_rf(self.working_dir) + return handled def _pre_fuzz(self): diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 32dabac..1f00a96 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -79,8 +79,11 @@ def __init__(self, } def __exit__(self, etype, value, traceback): - handled = IterationBase3.__exit__(self, etype, value, traceback) - + try: + handled = IterationBase3.__exit__(self, etype, value, traceback) + except WindowsError as e: + logger.warning('Caught WindowsError in iteration exit: %s',e) + global IOERROR_COUNT # Reset error count every time we do not have an error From 6fcb12b662a31dec47ae4723363a1af5857e538e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 07:51:42 -0500 Subject: [PATCH 0810/1169] _analyze method in windows wasn't doing anything beyond the base class, so just use that instead. --- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 1 - src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 4 ---- src/certfuzz/tc_pipeline/tc_pipeline_windows.py | 14 -------------- 3 files changed, 19 deletions(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index c5752e7..ed10f87 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -195,7 +195,6 @@ def _post_minimize(self, testcase): def _pre_analyze(self, testcase): pass - @abc.abstractmethod def _analyze(self, testcase): ''' Loops through all known analyzer_classes for a given testcase diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index df52111..aee4faf 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -121,10 +121,6 @@ def _pre_analyze(self, testcase): else: logger.debug('Removed old debug file %s', self.dbg_out_file_orig) - def _analyze(self, testcase): - # we'll just use the implementation in our parent class - TestCasePipelineBase._analyze(self, testcase) - def _post_analyze(self, testcase): if self.options.get('use_valgrind'): logger.info('Annotating callgrind output') diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 13eecda..24fc941 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -84,20 +84,6 @@ def _post_minimize(self, testcase): filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) self.sf_set.add_file(crasherseed_path) - def _analyze(self, testcase): - ''' - Loops through all known analyzer_classes for a given testcase - :param testcase: - ''' - - for analyzer_class in self.analyzer_classes: - analyzer_instance = analyzer_class(self.cfg, testcase) - if analyzer_instance: - try: - analyzer_instance.go() - except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from analyzer_class. Continuing') - def _report(self, testcase): with CopyFilesReporter(testcase, self.tc_dir) as reporter: reporter.go() From eed2ff382b82398225dc2830bf8f2d2a3ca73cd9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 07:56:59 -0500 Subject: [PATCH 0811/1169] _analyze is no longer an abstract method so we don't need it in our mock implementation --- src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py index 2c4800e..9c9a670 100644 --- a/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py +++ b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py @@ -21,9 +21,6 @@ def _minimize(self, testcase): def _verify(self, testcase): pass - def _analyze(self, testcase): - pass - def _report(self, testcase): pass From 4ac5b5e689013bf9fbbf4160db34608c2538b1e2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 08:11:33 -0500 Subject: [PATCH 0812/1169] fix unit test failure since we added is_nullrunner option to the Runner class. --- .../iteration/test_iteration_windows.py | 43 ++++++++++--------- src/test_certfuzz/mocks.py | 3 ++ .../tc_pipeline/test_tc_pipeline_base.py | 2 - 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/test_certfuzz/iteration/test_iteration_windows.py b/src/test_certfuzz/iteration/test_iteration_windows.py index 645cb0e..5bc3fbe 100644 --- a/src/test_certfuzz/iteration/test_iteration_windows.py +++ b/src/test_certfuzz/iteration/test_iteration_windows.py @@ -6,34 +6,37 @@ import unittest from certfuzz.iteration.iteration_windows import WindowsIteration -from test_certfuzz.mocks import MockFuzzer +from test_certfuzz.mocks import MockFuzzer, MockSeedfile, MockRunner +import tempfile +import shutil class Test(unittest.TestCase): def setUp(self): - # args: + self.tmpdir = tempfile.mkdtemp(prefix='test_iteration_windows_') + self.workdirbase = tempfile.mkdtemp(prefix='workdirbase_', dir=self.tmpdir) + self.outdir = tempfile.mkdtemp(prefix='outdir_', dir=self.tmpdir) - -# seedfile=None, -# seednum=None, -# workdirbase=None, -# outdir=None, -# sf_set=None, -# uniq_func=None, -# config=None, -# fuzzer_cls=None, -# runner_cls=None, -# cmd_template=None, -# debug=False, - - args = list('ABCDEFGHILM') - args[6] = {'runoptions': {'keep_unique_faddr': False}} - args[7] = MockFuzzer - self.iteration = WindowsIteration(*args) + _cfg={'runoptions': {'keep_unique_faddr': False}} + + kwargs={'seedfile': MockSeedfile(), + 'seednum':0, + 'workdirbase': self.workdirbase, + 'outdir':self.outdir, + 'sf_set':'a', + 'uniq_func':None, + 'config':_cfg, + 'fuzzer_cls':MockFuzzer, + 'runner_cls':MockRunner, + 'cmd_template':'a', + 'debug':False, + } + + self.iteration = WindowsIteration(**kwargs) def tearDown(self): - pass + shutil.rmtree(self.tmpdir) def testName(self): pass diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 03a48a5..e0c8c36 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -73,6 +73,9 @@ class MockFuzzedFile(Mock): class MockFuzzer(Mock): is_minimizable = False +class MockRunner(Mock): + is_nullrunner=False + class MockTestcase(Mock): signature = 'ABCDEFGHIJK' logger = logging.getLogger('mocktestcaselogger') diff --git a/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py index 9c9a670..a01cf49 100644 --- a/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py +++ b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py @@ -4,10 +4,8 @@ @organization: cert.org ''' import unittest -import certfuzz.tc_pipeline.tc_pipeline_base import tempfile import shutil -import os import certfuzz.tc_pipeline.tc_pipeline_base From ae0bd4634e05ea58e6e6b7a16854606cd46a8c6a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 08:40:14 -0500 Subject: [PATCH 0813/1169] consolidate _post_run into base class and make testcase construction more explicit Move zzuf log handling into the zzuf runner class (+1 squashed commit) Squashed commits: [1027467] make testcase construction a little more explicit instead of hiding in it in _post_run() consolidate _post_run into base class. Move zzuf log handling into the zzuf runner class --- src/certfuzz/iteration/iteration_base3.py | 18 ++++++++++-- src/certfuzz/iteration/iteration_linux.py | 32 --------------------- src/certfuzz/iteration/iteration_windows.py | 17 ----------- src/certfuzz/runners/zzufrun.py | 23 ++++++++++++++- 4 files changed, 38 insertions(+), 52 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index e589e34..150edb0 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -112,14 +112,27 @@ def _pre_run(self): workingdir_base = self.working_dir self.runner = self.runner_cls(self._runner_options, self._runner_cmd_template, fuzzed_file, workingdir_base) - pass - def _run(self): with self.runner: self.runner.run() def _post_run(self): pass + + def construct_testcase(self): + ''' + If the runner saw a crash, construct a test case + and append it to the list of testcases to be analyzed further. + ''' + if not self.runner.saw_crash: + return + + logger.debug('Building testcase object') + self._construct_testcase() + + def _construct_testcase(self): + # should be implemented by child classes + raise NotImplementedError def fuzz(self): ''' @@ -175,4 +188,5 @@ def go(self): logger.debug('go') self.fuzz() self.run() + self.construct_testcase() self.process_testcases() diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 1c26358..2949244 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -8,7 +8,6 @@ from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.ppid_observer import check_ppid -from certfuzz.fuzztools.zzuflog import ZzufLog from certfuzz.iteration.iteration_base3 import IterationBase3 from certfuzz.tc_pipeline.tc_pipeline_linux import LinuxTestCasePipeline from certfuzz.helpers.misc import fixup_path @@ -77,38 +76,7 @@ def _pre_run(self): IterationBase3._pre_run(self) - def _post_run(self): - if not self.runner.saw_crash: - logger.debug('No crash seen') - return - - # we must have seen a crash - # get the results - zzuf_log = ZzufLog(self.runner.zzuf_log_path) - logger.debug("ZzufLog:") - from pprint import pformat - for line in pformat(zzuf_log.__dict__).splitlines(): - logger.debug(line) - - # analysis is required in two cases: - # 1) runner_cls is not defined (self.runner_cls == None) - # 2) runner_cls is defined, and detects crash (runner_cls.saw_crash == True) - # this takes care of case 1 by default - # TODO: does case 1 ever happen? - analysis_needed = True - - # Don't generate cases for killed process or out-of-memory - # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will - # report the exit code in its output log. The exit code is 128 + the signal number. - analysis_needed = zzuf_log.crash_logged() - - if not analysis_needed: - return - - self._construct_testcase() - def _construct_testcase(self): - logger.info('Building testcase object') with LinuxTestcase(cfg=self.cfg, seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 1f00a96..498008e 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -155,24 +155,7 @@ def _pre_run(self): IterationBase3._pre_run(self) - def _post_run(self): - # analysis is required in two cases: - # 1) runner_cls is not defined (self.runner_cls == None) - # 2) runner_cls is defined, and detects crash (runner_cls.saw_crash == True) - # this takes care of case 1 by default - # TODO: does case 1 ever happen? - analysis_needed = True - - if self.runner_cls: - analysis_needed = self.runner.saw_crash - - if not analysis_needed: - return - - self._construct_testcase() - def _construct_testcase(self): - logger.debug('Building testcase object') with WindowsTestcase(cmd_template=self.cmd_template, seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 5dd54fe..97a2f4e 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -12,6 +12,7 @@ from certfuzz.runners.errors import RunnerNotFoundError import shlex from certfuzz.helpers.misc import quoted +from certfuzz.fuzztools.zzuflog import ZzufLog logger = logging.getLogger(__name__) @@ -100,6 +101,26 @@ def _run(self): if rc != 0: self.saw_crash = True -# raise RunnerError('zzuf returncode: {}'.format(rc)) + + def _postrun(self): + if not self.saw_crash: + logger.debug('No crash seen') + return + + # we must have seen a crash + # get the results + zzuf_log = ZzufLog(self.runner.zzuf_log_path) + + # dump zzuflog into our log + logger.debug("ZzufLog:") + from pprint import pformat + for line in pformat(zzuf_log.__dict__).splitlines(): + logger.debug(line) + + # Don't generate cases for killed process or out-of-memory + # In the default mode, zzuf will report a signal. In copy (and exit code) mode, zzuf will + # report the exit code in its output log. The exit code is 128 + the signal number. + self.saw_crash = zzuf_log.crash_logged() + _runner_class = ZzufRunner From c127b7d3038a43df90720fe438970708f3ace21d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 08:45:33 -0500 Subject: [PATCH 0814/1169] consolidate _pre_fuzz into base class --- src/certfuzz/iteration/iteration_base3.py | 1 + src/certfuzz/iteration/iteration_linux.py | 4 ---- src/certfuzz/iteration/iteration_windows.py | 4 ---- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 150edb0..8cb4403 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -94,6 +94,7 @@ def __exit__(self, etype, value, traceback): return handled def _pre_fuzz(self): + self._fuzz_opts = self.cfg['fuzzer'] self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, self._fuzz_opts) def _fuzz(self): diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 2949244..63fd44b 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -63,10 +63,6 @@ def __enter__(self): check_ppid() return self.go - def _pre_fuzz(self): - self._fuzz_opts = self.cfg['fuzzer'] - IterationBase3._pre_fuzz(self) - def _pre_run(self): self._runner_options = self.cfg['runner'] diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 498008e..8402109 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -145,10 +145,6 @@ def _tidy(self): # wipe them out, all of them TmpReaper().clean_tmp() - def _pre_fuzz(self): - self._fuzz_opts = self.cfg['fuzzer'] - IterationBase3._pre_fuzz(self) - def _pre_run(self): self._runner_options = self.cfg['runner'] self._runner_cmd_template = self.cmd_template From f33476806d54160b78fea79ffc7794fb8142ef6f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 09:02:50 -0500 Subject: [PATCH 0815/1169] carve up options in init method --- src/certfuzz/iteration/iteration_base3.py | 6 +++++- src/certfuzz/iteration/iteration_linux.py | 1 - src/certfuzz/iteration/iteration_windows.py | 1 - src/test_certfuzz/iteration/test_iteration_windows.py | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 8cb4403..2464012 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -56,6 +56,11 @@ def __init__(self, self.debug = True + # extract some parts of the config for fuzzer and runner + self._fuzz_opts = self.cfg['fuzzer'] + self._runner_options = self.cfg['runner'] + + @abc.abstractproperty def tcpipeline_cls(self): ''' @@ -94,7 +99,6 @@ def __exit__(self, etype, value, traceback): return handled def _pre_fuzz(self): - self._fuzz_opts = self.cfg['fuzzer'] self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, self._fuzz_opts) def _fuzz(self): diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 63fd44b..9f34695 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -64,7 +64,6 @@ def __enter__(self): return self.go def _pre_run(self): - self._runner_options = self.cfg['runner'] if self.quiet_flag: self._runner_options['hideoutput'] = True diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 8402109..91c564a 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -146,7 +146,6 @@ def _tidy(self): TmpReaper().clean_tmp() def _pre_run(self): - self._runner_options = self.cfg['runner'] self._runner_cmd_template = self.cmd_template IterationBase3._pre_run(self) diff --git a/src/test_certfuzz/iteration/test_iteration_windows.py b/src/test_certfuzz/iteration/test_iteration_windows.py index 5bc3fbe..8a16997 100644 --- a/src/test_certfuzz/iteration/test_iteration_windows.py +++ b/src/test_certfuzz/iteration/test_iteration_windows.py @@ -6,7 +6,7 @@ import unittest from certfuzz.iteration.iteration_windows import WindowsIteration -from test_certfuzz.mocks import MockFuzzer, MockSeedfile, MockRunner +from test_certfuzz.mocks import MockFuzzer, MockSeedfile, MockRunner, MockCfg import tempfile import shutil @@ -18,7 +18,7 @@ def setUp(self): self.workdirbase = tempfile.mkdtemp(prefix='workdirbase_', dir=self.tmpdir) self.outdir = tempfile.mkdtemp(prefix='outdir_', dir=self.tmpdir) - _cfg={'runoptions': {'keep_unique_faddr': False}} + _cfg=MockCfg() kwargs={'seedfile': MockSeedfile(), 'seednum':0, From 047afb181dc34a3492983e5b979c55b48022a4f2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 09:20:09 -0500 Subject: [PATCH 0816/1169] minor unit test tweak trying to eliminate unicode errors when running with nose tests --- .../scoring/multiarmed_bandit/test_multiarmed_bandit_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py b/src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py index ed326d6..1ffdccc 100644 --- a/src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py +++ b/src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py @@ -12,7 +12,7 @@ class Test(unittest.TestCase): def setUp(self): self.mab = MultiArmedBanditBase() - self.keys = 'abcdefghijklmnopqrstuvwxyz' + self.keys = u'abcdefghijklmnopqrstuvwxyz' for arm in self.keys: self.mab.add_item(arm, arm) From 3a9cc7af8054a5b4f1f09bb27e1821b1e2926748 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 15:24:06 -0500 Subject: [PATCH 0817/1169] bug fix from moving zzuf log handling from iteration down to runner class --- src/certfuzz/runners/zzufrun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 97a2f4e..42a5987 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -109,7 +109,7 @@ def _postrun(self): # we must have seen a crash # get the results - zzuf_log = ZzufLog(self.runner.zzuf_log_path) + zzuf_log = ZzufLog(self.zzuf_log_path) # dump zzuflog into our log logger.debug("ZzufLog:") From 280ecf56a75053ec78cb395ea54a3cfe91908b6c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 10 Feb 2016 15:52:09 -0500 Subject: [PATCH 0818/1169] Set value of "handled" variable if WindowsError is caught --- src/certfuzz/iteration/iteration_windows.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 91c564a..917c5ae 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -82,8 +82,9 @@ def __exit__(self, etype, value, traceback): try: handled = IterationBase3.__exit__(self, etype, value, traceback) except WindowsError as e: - logger.warning('Caught WindowsError in iteration exit: %s',e) - + logger.warning('Caught WindowsError in iteration exit: %s', e) + handled = True + global IOERROR_COUNT # Reset error count every time we do not have an error From 89f0443c9c5b85dec8eab113680c963c83c28458 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 10 Feb 2016 15:53:04 -0500 Subject: [PATCH 0819/1169] Hard-code runners in campaigns. Remove references from bff.yaml --- src/certfuzz/campaign/campaign_base.py | 13 ++++++------- src/certfuzz/campaign/campaign_windows.py | 1 + src/linux/conf.d/bff.yaml | 1 - src/windows/configs/examples/bff.yaml | 4 ---- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index a7249a1..1c907d1 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -88,7 +88,7 @@ def __init__(self, config_file, result_dir=None, debug=False): self._read_config_file() self.campaign_id = self.config['campaign']['id'] - + self.current_seed = self.config['runoptions']['first_iteration'] self.seed_interval = self.config['runoptions']['seed_interval'] @@ -100,10 +100,10 @@ def __init__(self, config_file, result_dir=None, debug=False): self.work_dir_base = self.config['directories']['working_dir'] self.program = self.config['target']['program'] self.cmd_template = self.config['target']['cmdline_template'] - - _campaign_id_no_space=re.sub('\s', '_', self.campaign_id) + + _campaign_id_no_space = re.sub('\s', '_', self.campaign_id) _campaign_id_with_underscores = re.sub('\W', '_', self.campaign_id) - + self.outdir = os.path.join(self.outdir_base, _campaign_id_no_space) logger.debug('outdir=%s', self.outdir) @@ -117,14 +117,13 @@ def __init__(self, config_file, result_dir=None, debug=False): self.current_seed = 0 self.fuzzer_module_name = 'certfuzz.fuzzers.{}'.format(self.config['fuzzer']['fuzzer']) - if self.config['runner']['runner']: - self.runner_module_name = 'certfuzz.runners.{}'.format(self.config['runner']['runner']) + self.runner_module_name = 'certfuzz.runners.zzufrun' self.debugger_module_name = 'certfuzz.debuggers.{}'.format(self.config['debugger']['debugger']) def _read_config_file(self): logger.info('Reading config from %s', self.config_file) self.config = load_and_fix_config(self.config_file) - logger.info('Using target program: %s',self.config['target']['program']) + logger.info('Using target program: %s', self.config['target']['program']) @abc.abstractmethod def _pre_enter(self): diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 35f5e67..0d4adda 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -27,6 +27,7 @@ class WindowsCampaign(CampaignBase): def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker', False) + self.runner_module_name = 'certfuzz.runners.winrun' def __getstate__(self): state = self.__dict__.copy() diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index d2703be..3d30650 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -136,7 +136,6 @@ fuzzer: fuzzer: bytemut fuzz_zip_container: False runner: - runner: zzufrun # maximum program execution time (seconds) that BFF will allow: runtimeout: 5 debugger: diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index cfe2a4b..55af5a6 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -125,9 +125,6 @@ fuzzer: ##################################################################### # Runner options # -# runner: -# winrun: Use hook.dll to detect exceptions on 32-bit Windows XP and 2003. -# Fall back to using debugger for each iteration on others. # hideoutput: Hide stdout of target application # runtimeout: Number of seconds to allow target application to execute # when run with the hook (winrun) @@ -137,7 +134,6 @@ fuzzer: # ##################################################################### runner: - runner: winrun hideoutput: False runtimeout: 5 watchcpu: Auto From d455dce6144e569b96b9a06b6457be85bfe0ad54 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 10 Feb 2016 17:35:37 -0500 Subject: [PATCH 0820/1169] New config object structure for tools. BFF-879 --- src/certfuzz/tools/linux/bff_stats.py | 11 ++++++----- src/certfuzz/tools/linux/minimize.py | 18 ++++++++--------- src/certfuzz/tools/linux/minimizer_plot.py | 11 +++++------ src/certfuzz/tools/linux/repro.py | 23 +++++----------------- 4 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/certfuzz/tools/linux/bff_stats.py b/src/certfuzz/tools/linux/bff_stats.py index 5be2658..5792f42 100755 --- a/src/certfuzz/tools/linux/bff_stats.py +++ b/src/certfuzz/tools/linux/bff_stats.py @@ -9,7 +9,7 @@ import os import re import sys -from certfuzz.config.config_linux import LinuxConfig +from certfuzz.config.simple_loader import load_and_fix_config parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) @@ -101,11 +101,12 @@ def main(): cfg_file = os.path.join('conf.d', 'bff.yaml') logger.debug('Using config file: %s', cfg_file) - cfg = LinuxConfig(cfg_file) - with cfg: - pass + cfg = load_and_fix_config(cfg_file) - result_dir = os.path.join(cfg.output_dir, cfg.campaign_id, 'crashers') + _campaign_id = cfg['campaign']['id'] + _campaign_id_no_space = re.sub('\s', '_', _campaign_id) + + result_dir = os.path.join(cfg['directories']['results_dir'], _campaign_id_no_space, 'crashers') logger.debug('Reading results from %s', result_dir) counters = {} diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 54a9a56..ba4c25d 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -72,10 +72,10 @@ def main(): if options.config: cfg_file = os.path.expanduser(options.config) else: - if os.path.isfile("../conf.d/bff.cfg"): + if os.path.isfile("../conf.d/bff.yaml"): cfg_file = "../conf.d/bff.cfg" - elif os.path.isfile("conf.d/bff.cfg"): - cfg_file = "conf.d/bff.cfg" + elif os.path.isfile("conf.d/bff.yaml"): + cfg_file = "conf.d/bff.yaml" else: parser.error('Configuration file (--config) option must be specified.') logger.debug('Config file: %s', cfg_file) @@ -136,14 +136,14 @@ def main(): cfg = load_and_fix_config(cfg_file) - with LinuxTestcase(cfg=cfg, - seedfile=seedfile, - fuzzedfile=fuzzed_file, + with LinuxTestcase(cfg=cfg, + seedfile=seedfile, + fuzzedfile=fuzzed_file, program=cfg['target']['program'], - debugger_timeout=cfg['debugger']['runtimeout'], - killprocname=cfg['target']['killprocname'], + debugger_timeout=cfg['debugger']['runtimeout'], + killprocname=cfg['target']['killprocname'], backtrace_lines=cfg['debugger']['backtracelevels'], - crashers_dir=crashers_dir, + crashers_dir=crashers_dir, workdir_base=None, keep_faddr=options.keep_uniq_faddr) as crash: diff --git a/src/certfuzz/tools/linux/minimizer_plot.py b/src/certfuzz/tools/linux/minimizer_plot.py index 064ae66..9672f38 100755 --- a/src/certfuzz/tools/linux/minimizer_plot.py +++ b/src/certfuzz/tools/linux/minimizer_plot.py @@ -10,7 +10,7 @@ import re import sys -from certfuzz.config.config_linux import LinuxConfig +from certfuzz.config.simple_loader import load_and_fix_config import matplotlib.pyplot as plt @@ -148,11 +148,10 @@ def main(): result_dir = options.dir else: logger.info('Using config file: %s', cfg_file) - cfg = LinuxConfig(cfg_file) - with cfg: - pass - - result_dir = os.path.join(cfg.output_dir, cfg.campaign_id, 'crashers') + cfg = load_and_fix_config(cfg_file) + _campaign_id = cfg['campaign']['id'] + _campaign_id_no_space = re.sub('\s', '_', _campaign_id) + result_dir = os.path.join(cfg['directories']['results_dir'], _campaign_id_no_space, 'crashers') logger.info('Reading results from %s', result_dir) log = None diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index 21d084a..f285179 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -7,7 +7,8 @@ import os import re from subprocess import Popen -from certfuzz.config.config_linux import LinuxConfig +from certfuzz.config.simple_loader import load_and_fix_config +from certfuzz.fuzztools.command_line_templating import get_command_args_list try: from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file @@ -57,7 +58,7 @@ def main(): help='Enable debug messages (overrides --verbose)') parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') - parser.add_option('-c', '--config', default='conf.d/bff.cfg', + parser.add_option('-c', '--config', default='conf.d/bff.yaml', dest='config', help='path to the configuration file to use') parser.add_option('-e', '--edb', dest='use_edb', action='store_true', @@ -101,23 +102,9 @@ def main(): os.path.join(iterationdir, iterationfile)) fullpath_fuzzed_file = iterationpath - config = LinuxConfig(cfg_file) - with config: - pass - - cmd_as_args = config.get_command_list(fullpath_fuzzed_file) - program = cmd_as_args[0] - if not os.path.exists(program): - # edb wants a full path to the target app, so let's find it - for path in os.environ["PATH"].split(":"): - if os.path.exists(os.path.join(path, program)): - program = os.path.join(path, program) - - # Recreate command args list with full path to target - cmd_as_args = [] - cmd_as_args.append(program) - cmd_as_args.extend(config.get_command_args_list(fullpath_fuzzed_file)) + config = load_and_fix_config(cfg_file) + cmd_as_args = get_command_args_list(config['target']['cmdline_template'], fullpath_fuzzed_file)[1] args = [] if options.use_edb and options.debugger: From 0433120cc8246d8cdaedfa0d5aacef10808e9fa6 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 13:21:21 -0500 Subject: [PATCH 0821/1169] New config structure for windows tools\repro --- src/certfuzz/tools/windows/repro.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index 831fc12..a28c8f2 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -10,11 +10,11 @@ from subprocess import Popen from certfuzz import debuggers -from certfuzz.config.config_windows import WindowsConfig, get_command_args_list from certfuzz.debuggers import msec # @UnusedImport from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file - +from certfuzz.config.simple_loader import load_and_fix_config +from certfuzz.fuzztools.command_line_templating import get_command_args_list logger = logging.getLogger() logger.setLevel(logging.WARNING) @@ -37,7 +37,6 @@ def getiterpath(msecfile): def main(): - debuggers.registration.verify_supported_platform() from optparse import OptionParser @@ -83,11 +82,9 @@ def main(): else: parser.error('fuzzedfile must be specified') - with WindowsConfig(cfg_file) as configobj: - config = configobj.config + config = load_and_fix_config(cfg_file) iterationpath = '' - template = string.Template(config['target']['cmdline_template']) if options.filepath: # Recreate same file path as fuzz iteration resultdir = os.path.dirname(fuzzed_file.path) @@ -104,7 +101,7 @@ def main(): os.path.join(iterationdir, iterationfile)) fuzzed_file.path = iterationpath - cmd_as_args = get_command_args_list(template, fuzzed_file.path)[1] + cmd_as_args = get_command_args_list(config['target']['cmdline_template'], fuzzed_file.path)[1] targetdir = os.path.dirname(cmd_as_args[0]) args = [] From 467cc8e795fae77b4e5652d0a834c44b5263a396 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 15:39:23 -0500 Subject: [PATCH 0822/1169] Fix the directory that is being looked at for existing crashers. BFF-819 --- src/certfuzz/testcase/testcase_windows.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index e2c7578..d435e58 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -215,10 +215,9 @@ def _set_heisenbug_properties(self): def _get_output_dir(self, target_base): logger.debug('target_base: %s', target_base) - logger.debug('exp: %s', self.exp) logger.debug('signature: %s', self.signature) - self.target_dir = os.path.join(target_base, self.exp, self.signature) + self.target_dir = os.path.join(target_base, 'crashers', self.signature) if len(self.target_dir) > 170: # Don't make a path too deep. Windows won't support it self.target_dir = self.target_dir[:170] + '__' From c721d54715e8a9852a5d9619ebbf425e84776efd Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 16:50:06 -0500 Subject: [PATCH 0823/1169] Move exception list to winrun module. Nobody is setting those. Also add some more exceptions to treat as crashes. --- src/certfuzz/runners/winrun.py | 61 ++++++++++++++++++++++++--- src/windows/configs/examples/bff.yaml | 16 ------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 58f1742..1ce2439 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -22,6 +22,7 @@ logger = logging.getLogger(__name__) + try: # if we have win32api, use its GetShortPathName from win32api import GetShortPathName # @UnresolvedImport @@ -61,14 +62,62 @@ def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): RunnerBase.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) logger.debug('Initialize Runner') - self.exceptions = [] - self.watchcpu = options.get('watchcpu', False) - try: - self.exceptions = options['exceptions'] - except KeyError: - raise RunnerError('At least one exception code must be specified in runner config.') +# exceptions: +# - 0x80000001 # STATUS_GUARD_PAGE_VIOLATION +# - 0x80000002 # EXCEPTION_DATATYPE_MISALIGNMENT +# - 0x80000005 # STATUS_BUFFER_OVERFLOW +# - 0xC0000005 # STATUS_ACCESS_VIOLATION +# - 0xC0000009 # STATUS_BAD_INITIAL_STACK +# - 0xC000000A # STATUS_BAD_INITIAL_PC +# - 0xC000001D # STATUS_ILLEGAL_INSTRUCTION +# - 0xC0000025 # EXCEPTION_NONCONTINUABLE_EXCEPTION +# - 0xC0000026 # EXCEPTION_INVALID_DISPOSITION +# - 0xC000008C # EXCEPTION_ARRAY_BOUNDS_EXCEEDED +# - 0xC000008D # STATUS_FLOAT_DENORMAL_OPERAND +# - 0xC000008E # EXCEPTION_FLT_DIVIDE_BY_ZERO +# - 0xC000008F # EXCEPTION_FLOAT_INEXACT_RESULT +# - 0xC0000090 # EXCEPTION_FLT_INVALID_OPERATION +# - 0xC0000091 # EXCEPTION_FLT_OVERFLOW +# - 0xC0000092 # EXCEPTION_FLT_STACK_CHECK +# - 0xC0000093 # EXCEPTION_FLT_UNDERFLOW +# - 0xC0000094 # STATUS_INTEGER_DIVIDE_BY_ZERO +# - 0xC0000095 # EXCEPTION_INT_OVERFLOW +# - 0xC0000096 # STATUS_PRIVILEGED_INSTRUCTION +# - 0xC00000FD # STATUS_STACK_OVERFLOW +# - 0xC00002B4 # STATUS_FLOAT_MULTIPLE_FAULTS +# - 0xC00002B5 # STATUS_FLOAT_MULTIPLE_TRAPS +# - 0xC00002C5 # STATUS_DATATYPE_MISALIGNMENT_ERROR +# - 0xC00002C9 # STATUS_REG_NAT_CONSUMPTION + + self.exceptions = [0x80000001, + 0x80000002, + 0x80000005, + 0xC0000005, + 0xC0000009, + 0xC000000A, + 0xC000001D, + 0xC0000025, + 0xC0000026, + 0xC000008C, + 0xC000008D, + 0xC000008E, + 0xC000008F, + 0xC0000090, + 0xC0000091, + 0xC0000092, + 0xC0000093, + 0xC0000094, + 0xC0000095, + 0xC0000096, + 0xC00000FD, + 0xC00002B4, + 0xC00002B5, + 0xC00002C5, + 0xC00002C9, + ] + self.watchcpu = options.get('watchcpu', False) (self.cmd, self.cmdlist) = get_command_args_list(cmd_template, fuzzed_file) logger.debug('Command: %s', self.cmd) diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 55af5a6..fad5d99 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -137,22 +137,6 @@ runner: hideoutput: False runtimeout: 5 watchcpu: Auto - exceptions: - - 0x80000002 # EXCEPTION_DATATYPE_MISALIGNMENT - - 0xC0000005 # STATUS_ACCESS_VIOLATION - - 0xC000001D # STATUS_ILLEGAL_INSTRUCTION - - 0xC0000025 # EXCEPTION_NONCONTINUABLE_EXCEPTION - - 0xC0000026 # EXCEPTION_INVALID_DISPOSITION - - 0xC000008C # EXCEPTION_ARRAY_BOUNDS_EXCEEDED - - 0xC000008E # EXCEPTION_FLT_DIVIDE_BY_ZERO - - 0xC0000090 # EXCEPTION_FLT_INVALID_OPERATION - - 0xC0000091 # EXCEPTION_FLT_OVERFLOW - - 0xC0000092 # EXCEPTION_FLT_STACK_CHECK - - 0xC0000093 # EXCEPTION_FLT_UNDERFLOW - - 0xC0000094 # STATUS_INTEGER_DIVIDE_BY_ZERO - - 0xC0000095 # EXCEPTION_INT_OVERFLOW - - 0xC0000096 # STATUS_PRIVILEGED_INSTRUCTION - - 0xC00000FD # STATUS_STACK_OVERFLOW ##################################################################### # Debugger options From a6fb2ef0e5c72d1b201fded17daeb2dd4137e4f2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 16:50:25 -0500 Subject: [PATCH 0824/1169] clean_windows.py also needs new config structure. --- src/certfuzz/tools/windows/clean_windows.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/tools/windows/clean_windows.py b/src/certfuzz/tools/windows/clean_windows.py index c23b37d..7a0aaf9 100644 --- a/src/certfuzz/tools/windows/clean_windows.py +++ b/src/certfuzz/tools/windows/clean_windows.py @@ -8,6 +8,7 @@ import pprint import tempfile import time +import re defaults = {'config': 'configs/bff.yaml', @@ -26,7 +27,7 @@ def main(): import optparse try: from certfuzz.fuzztools.filetools import delete_contents_of - from certfuzz.config.config_base import ConfigBase + from certfuzz.config.simple_loader import load_and_fix_config except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH import sys @@ -34,7 +35,7 @@ def main(): parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) from certfuzz.fuzztools.filetools import delete_contents_of - from certfuzz.config.config_base import ConfigBase + from certfuzz.config.simple_loader import load_and_fix_config if not os.path.exists(defaults['config']): defaults['config'] = '../configs/bff.yaml' @@ -47,21 +48,23 @@ def main(): parser.add_option('', '--debug', dest='debug', action='store_true', default=defaults['debug']) options, _args = parser.parse_args() - with ConfigBase(options.configfile) as cfgobj: - c = cfgobj.config + cfg_file = options.configfile + config = load_and_fix_config(cfg_file) if options.debug: - pprint.pprint(c) + pprint.pprint(config) dirs = set() if options.nuke: options.remove_results = True - dirs.add(os.path.abspath(c['directories']['working_dir'])) - dirs.add(os.path.join(os.path.abspath(c['directories']['results_dir']), c['campaign']['id'], 'seedfiles')) + dirs.add(os.path.abspath(config['directories']['working_dir'])) + campaign_id = config['campaign']['id'] + campaign_id_no_space = re.sub('\s', '_', campaign_id) + dirs.add(os.path.join(os.path.abspath(config['directories']['results_dir']), campaign_id_no_space, 'seedfiles')) if options.remove_results: - dirs.add(os.path.join(os.path.abspath(c['directories']['results_dir']), c['campaign']['id'],)) + dirs.add(os.path.join(os.path.abspath(config['directories']['results_dir']), campaign_id_no_space,)) # add temp dir(s) if available if tempfile.gettempdir().lower() != os.getcwd().lower(): From f866e30317599849359e21907d4b0c09463cf4b5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 17:15:13 -0500 Subject: [PATCH 0825/1169] More verbose caching of the app on Windows. --- src/certfuzz/campaign/campaign_windows.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 0d4adda..5acfb89 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -7,13 +7,16 @@ import os import platform import sys +import time from threading import Timer + from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.file_handlers.seedfile_set import SeedfileSet from certfuzz.fuzzers.errors import FuzzerExhaustedError from certfuzz.iteration.iteration_windows import WindowsIteration from certfuzz.runners.killableprocess import Popen +from certfuzz.fuzztools.command_line_templating import get_command_args_list logger = logging.getLogger(__name__) @@ -92,10 +95,14 @@ def _pre_exit(self): def _cache_app(self): logger.debug('Caching application %s and determining if we need to watch the CPU...', self.program) + sf = self.seedfile_set.next_item() targetdir = os.path.dirname(self.program) + cmdargs = get_command_args_list(self.config['target']['cmdline_template'], infile=sf.path)[1] + logger.info('Invoking %s' % cmdargs) + # Use overriden Popen that uses a job object to make sure that # child processes are killed - p = Popen(self.program, cwd=targetdir) + p = Popen(cmdargs, cwd=targetdir) runtimeout = self.config['runner']['runtimeout'] logger.debug('...Timer: %f', runtimeout) t = Timer(runtimeout, self.kill, args=[p]) @@ -121,6 +128,9 @@ def _cache_app(self): logger.debug('Disabling debugger CPU monitoring for dynamic timeout') self.config['debugger']['watchcpu'] = False + logger.info('Please ensure that the target program has just executed successfully') + time.sleep(10) + def kill(self, p): # The app didn't complete within the timeout. Assume it's a GUI app logger.debug('This seems to be a GUI application.') From 0231f8a8496da67d07803050da4b503835dd4143 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 17:15:25 -0500 Subject: [PATCH 0826/1169] Re-fix BFF-889 --- src/certfuzz/debuggers/msec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 0a9c366..c651309 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -75,7 +75,7 @@ def _get_cmdline(self, outfile): cdb_command = 'g;' + cdb_command args.append(cdb_command) args.append(self.program) - args.extend(self.cmd_args) + args.extend(self.cmd_args[1:]) for l in pformat(args).splitlines(): logger.debug('dbg_args: %s', l) return args From 4a29e2f186ec0a602d8ac2af5240eb8d02f133da Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 17:48:55 -0500 Subject: [PATCH 0827/1169] Hard-code debugger module name in campaign object. We never set it to anything else, as there is nothing else available. Remove options from config files. --- src/certfuzz/campaign/campaign_base.py | 2 -- src/certfuzz/campaign/campaign_linux.py | 6 ++++++ src/certfuzz/campaign/campaign_windows.py | 1 + src/linux/conf.d/bff.yaml | 1 - src/windows/configs/examples/bff.yaml | 2 -- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 1c907d1..9a427f7 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -117,8 +117,6 @@ def __init__(self, config_file, result_dir=None, debug=False): self.current_seed = 0 self.fuzzer_module_name = 'certfuzz.fuzzers.{}'.format(self.config['fuzzer']['fuzzer']) - self.runner_module_name = 'certfuzz.runners.zzufrun' - self.debugger_module_name = 'certfuzz.debuggers.{}'.format(self.config['debugger']['debugger']) def _read_config_file(self): logger.info('Reading config from %s', self.config_file) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 0a129cd..afd76f0 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -58,6 +58,12 @@ class LinuxCampaign(CampaignBase): Extends CampaignBase to add linux-specific features. ''' + def __init__(self, config_file, result_dir=None, debug=False): + CampaignBase.__init__(self, config_file, result_dir, debug) + self.runner_module_name = 'certfuzz.runners.zzufrun' + self.debugger_module_name = 'certfuzz.debuggers.gdb' + + def _full_path_original(self, seedfile): # yes, two seedfile mentions are intended - adh program_basename = os.path.basename(self.program).replace('"', '') diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 5acfb89..0a08126 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -31,6 +31,7 @@ def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker', False) self.runner_module_name = 'certfuzz.runners.winrun' + self.debugger_module_name = 'certfuzz.debuggers.gdb' def __getstate__(self): state = self.__dict__.copy() diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 3d30650..1fe0ca0 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -139,7 +139,6 @@ runner: # maximum program execution time (seconds) that BFF will allow: runtimeout: 5 debugger: - debugger: gdb # maximum time (seconds) to let the program run to capture debugger and # CERT Triage Tools exploitable output. runtimeout: 60 diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index fad5d99..502adbc 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -142,7 +142,6 @@ runner: # Debugger options # # debugger: -# msec: Use the msec !exploitable extension for cdb # runtimeout: Number of seconds to allow target application to execute # when run via the debugger # watchcpu: Kill target process when its CPU usage drops towards zero @@ -151,7 +150,6 @@ runner: # max_handled_exceptions: Maximum number of times to continue exceptions ##################################################################### debugger: - debugger: msec runtimeout: 10 watchcpu: Auto debugheap: False From 765f4a34b9917686083a31795d7adbc5f2ff311b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 17:49:18 -0500 Subject: [PATCH 0828/1169] Undo cmd_args change until I'm certain what's happening. --- src/certfuzz/debuggers/msec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index c651309..0a9c366 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -75,7 +75,7 @@ def _get_cmdline(self, outfile): cdb_command = 'g;' + cdb_command args.append(cdb_command) args.append(self.program) - args.extend(self.cmd_args[1:]) + args.extend(self.cmd_args) for l in pformat(args).splitlines(): logger.debug('dbg_args: %s', l) return args From 0d24ed0f6fef2b05ddaced9ac79eba730c8fec8a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 18:13:59 -0500 Subject: [PATCH 0829/1169] Minimizer shouldn't strip program off of cmd_args before handing off to the debugger. The debugger object does the stripping itself. --- src/certfuzz/debuggers/msec.py | 2 +- src/certfuzz/minimizer/minimizer_base.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 0a9c366..c651309 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -75,7 +75,7 @@ def _get_cmdline(self, outfile): cdb_command = 'g;' + cdb_command args.append(cdb_command) args.append(self.program) - args.extend(self.cmd_args) + args.extend(self.cmd_args[1:]) for l in pformat(args).splitlines(): logger.debug('dbg_args: %s', l) return args diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index c3ad23a..b09621a 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -387,7 +387,6 @@ def run_debugger(self, infile, outfile): self.debugger_runs += 1 cmd_args = get_command_args_list(self.cfg['target']['cmdline_template'], infile)[1] cmd = cmd_args[0] - args = cmd_args[1:] # cmd_args = self.cfg.get_command_args_list(infile) try: killprocname = self.cfg['target']['killprocname'] @@ -400,7 +399,7 @@ def run_debugger(self, infile, outfile): exclude_unmapped_frames = True dbg = self._debugger_cls(cmd, - args, + cmd_args, outfile, self.debugger_timeout, killprocname, From e30e14bb41cdf7ee55243c1931b892c0ff7d494a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 20:39:09 -0500 Subject: [PATCH 0830/1169] new elcapitan crashwrangler binary. BFF-892 --- .../BFF_installer.pmdoc/30exc-contents.xml | 1 + .../installer/BFF_installer.pmdoc/30exc.xml | 18 ++++++++++++++++++ .../installer/BFF_installer.pmdoc/index.xml | 1 + 3 files changed, 20 insertions(+) create mode 100644 build/distmods/osx/installer/BFF_installer.pmdoc/30exc-contents.xml create mode 100644 build/distmods/osx/installer/BFF_installer.pmdoc/30exc.xml diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/30exc-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/30exc-contents.xml new file mode 100644 index 0000000..69b34ea --- /dev/null +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/30exc-contents.xml @@ -0,0 +1 @@ + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/30exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/30exc.xml new file mode 100644 index 0000000..e313203 --- /dev/null +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/30exc.xml @@ -0,0 +1,18 @@ + + +com.cert.cc.certBff.exc_handler_elcapitan.pkg +1.0 + + + +crashwrangler/binaries/exc_handler_elcapitan +/usr/local/bin + + + + +parent +installFrom.isRelativeType +installTo + + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index 920356a..9e91357 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -65,6 +65,7 @@ + From 067fdd2ee689a1ff21e80c6b7b733ca8b4213e00 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Feb 2016 20:55:10 -0500 Subject: [PATCH 0831/1169] Added reference to new exc_handler_elcapitan.pkg --- build/distmods/osx/installer/BFF_installer.pmdoc/index.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index 9e91357..798f5f0 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -127,6 +127,7 @@ 27pyyaml.xml 28exc.xml 29exc.xml +30exc.xml properties.customizeOption properties.title From 6e8a2da75d139b42c64e3b746e3a600b0e473953 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 09:52:01 -0500 Subject: [PATCH 0832/1169] Cancel timer on debugger object on campaign cancel. Part of fix for BFF-294 --- src/certfuzz/debuggers/debugger_base.py | 6 ++++++ src/certfuzz/debuggers/msec.py | 6 ++++++ src/certfuzz/testcase/testcase_windows.py | 6 +++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/debuggers/debugger_base.py b/src/certfuzz/debuggers/debugger_base.py index 2d620ab..0606e8d 100644 --- a/src/certfuzz/debuggers/debugger_base.py +++ b/src/certfuzz/debuggers/debugger_base.py @@ -90,3 +90,9 @@ def debugger_test(self): to confirm whether the debugger is on the path. ''' raise NotImplementedError + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + pass diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index c651309..f0ec81b 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -19,6 +19,7 @@ logger = logging.getLogger(__name__) +t = None def factory(options): @@ -82,6 +83,7 @@ def _get_cmdline(self, outfile): def run_with_timer(self): # TODO: replace this with subp.run_with_timer() + global t targetdir = os.path.dirname(self.program) exename = os.path.basename(self.program) process_info = {} @@ -159,4 +161,8 @@ def go(self): for l in pformat(parsed.__dict__).splitlines(): logger.debug('parsed: %s', l) return parsed + + def __exit__(self, etype, value, traceback): + t.cancel() + # END MsecDebugger diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index d435e58..b92f605 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -114,15 +114,15 @@ def set_debugger_template(self, *args): def debug_once(self): outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) - debugger = self._debugger_cls(program=self.program, + with self._debugger_cls(program=self.program, cmd_args=self.cmdargs, outfile_base=outfile_base, timeout=self.debugger_timeout, killprocname=None, exception_depth=self.exception_depth, workingdir=self.tempdir, - watchcpu=self.watchcpu) - self.parsed_outputs.append(debugger.go()) + watchcpu=self.watchcpu) as debugger: + self.parsed_outputs.append(debugger.go()) self.reached_secondchance = self.parsed_outputs[self.exception_depth].secondchance From f8c3de9a86486f14985b0bad8026d17fc3db9176 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 09:53:07 -0500 Subject: [PATCH 0833/1169] Use object-level timer, rather than global. BFF-294 --- src/certfuzz/debuggers/msec.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index f0ec81b..f5b25dd 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -19,8 +19,6 @@ logger = logging.getLogger(__name__) -t = None - def factory(options): return MsecDebugger(options) @@ -35,6 +33,7 @@ def __init__(self, program, cmd_args, outfile_base, timeout, killprocname, watch DebuggerBase.__init__(self, program, cmd_args, outfile_base, timeout, killprocname, **options) self.exception_depth = exception_depth self.watchcpu = watchcpu + self.t = None def kill(self, pid, returncode): """kill function for Win32""" @@ -83,7 +82,6 @@ def _get_cmdline(self, outfile): def run_with_timer(self): # TODO: replace this with subp.run_with_timer() - global t targetdir = os.path.dirname(self.program) exename = os.path.basename(self.program) process_info = {} @@ -116,8 +114,8 @@ def run_with_timer(self): return # create a timer that calls kill() when it expires - t = Timer(self.timeout, self.kill, args=[p.pid, 99]) - t.start() + self.t = Timer(self.timeout, self.kill, args=[p.pid, 99]) + self.t.start() if self.watchcpu == True: # This is a race. In some cases, a GUI app could be done before we can even measure it # TODO: Do something about it @@ -144,7 +142,7 @@ def run_with_timer(self): time.sleep(0.2) else: p.wait() - t.cancel() + self.t.cancel() def go(self): """run cdb and process output""" @@ -163,6 +161,8 @@ def go(self): return parsed def __exit__(self, etype, value, traceback): - t.cancel() + if self.t: + logger.debug('Canceling timer...') + self.t.cancel() # END MsecDebugger From 179fcdf9ce830c8c075c5f9e61eb909ab4e8dbbc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 10:02:15 -0500 Subject: [PATCH 0834/1169] Remove debugging iteration log entry --- src/certfuzz/iteration/iteration_windows.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 917c5ae..488d78a 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -125,8 +125,6 @@ def __exit__(self, etype, value, traceback): # log something different if we failed to handle an exception if etype and not handled: logger.warning('WindowsIteration terminating abnormally due to %s: %s', etype.__name__, value) - else: - logger.info('Done with iteration %d', self.seednum) if self.debug and etype and not handled: # don't clean up if we're in debug mode and have an unhandled exception From 1b5e0611741b10b8a902f17155404f3b5eb0af9a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 10:03:40 -0500 Subject: [PATCH 0835/1169] Have zzuf use configured timeout. --- src/certfuzz/runners/zzufrun.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/runners/zzufrun.py b/src/certfuzz/runners/zzufrun.py index 42a5987..3c554bb 100644 --- a/src/certfuzz/runners/zzufrun.py +++ b/src/certfuzz/runners/zzufrun.py @@ -42,10 +42,10 @@ def check_runner(): _verify_zzuf_installed() result = subprocess.check_output(['zzuf', '-h']) - + for line in result.split(os.linesep): - check_for=('--opmode ', 'null') - + check_for = ('--opmode ', 'null') + if all(x in line for x in check_for): _use_cert_version_of_zzuf = True @@ -81,7 +81,7 @@ def _construct_zzuf_args(self): '--ratio=0.0', '--seed=0', '--max-crashes=1', - '--max-usertime=5.00', + '--max-usertime=%s' % self.runtimeout, '--opmode=%s' % _opmode, '--include=%s' % self.fuzzed_file, ]) @@ -96,8 +96,7 @@ def _run(self): with open(self.fuzzed_file, 'rb') as ff, open(self.zzuf_log_path, 'wb') as zo: cmd2run = self._zzuf_args + self._cmd_parts logger.debug('RUN_CMD: {}'.format(' '.join(cmd2run))) - p = subprocess.Popen(cmd2run, cwd=self.workingdir, stdin=ff, stderr=zo) - rc = p.wait() + rc = subprocess.call(cmd2run, cwd=self.workingdir, stdin=ff, stderr=zo) if rc != 0: self.saw_crash = True @@ -110,7 +109,7 @@ def _postrun(self): # we must have seen a crash # get the results zzuf_log = ZzufLog(self.zzuf_log_path) - + # dump zzuflog into our log logger.debug("ZzufLog:") from pprint import pformat From ba60cd8cd453e348bb8056ed7cd23e11eb750e4c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 10:04:14 -0500 Subject: [PATCH 0836/1169] Use lldb on Darwin in tools/repro. BFF-789 --- src/certfuzz/tools/linux/repro.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index f285179..958ed42 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -6,6 +6,7 @@ import logging import os import re +import platform from subprocess import Popen from certfuzz.config.simple_loader import load_and_fix_config from certfuzz.fuzztools.command_line_templating import get_command_args_list @@ -114,13 +115,15 @@ def main(): debugger_app = options.debugger elif options.use_edb: debugger_app = 'edb' + elif platform.system() == 'Darwin': + debugger_app = 'lldb' else: debugger_app = 'gdb' args.append(debugger_app) if options.use_edb: args.append('--run') - else: + elif debugger_app == 'gdb': # Using gdb args.append('--args') args.extend(cmd_as_args) From 136b9363ae50af98e0b30e60e525a39bd59d72d3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 11:25:55 -0500 Subject: [PATCH 0837/1169] Handle signals to allow keyboard interrupt. BFF-823 --- src/certfuzz/campaign/campaign_base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 9a427f7..f8d9701 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -12,6 +12,7 @@ import tempfile import traceback import cPickle as pickle +import signal from certfuzz.campaign.errors import CampaignError from certfuzz.file_handlers.seedfile_set import SeedfileSet @@ -218,7 +219,6 @@ def __exit__(self, etype, value, mytraceback): # handle common errors handled = self._handle_common_errors(etype, value, mytraceback) - if etype and not handled: # call the class-specific error handler handled = self._handle_errors(etype, value, mytraceback) @@ -409,9 +409,14 @@ def _do_iteration(self): Implements a single iteration of the fuzzing process. ''' + def signal_handler(self, signal, frame): + logger.debug('KeyboardInterrupt detected') + raise(KeyboardInterrupt) + def go(self): ''' Starts campaign ''' + signal.signal(signal.SIGINT, self.signal_handler) while self._keep_going(): self._do_interval() From acc02ca717c60811d7bdf914ead11b94164bb077 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 11:58:21 -0500 Subject: [PATCH 0838/1169] Fix up symlinking on OSX installer. BFF-464 --- .../distmods/osx/installer/scripts/symlink.sh | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/build/distmods/osx/installer/scripts/symlink.sh b/build/distmods/osx/installer/scripts/symlink.sh index e6c3858..627633f 100755 --- a/build/distmods/osx/installer/scripts/symlink.sh +++ b/build/distmods/osx/installer/scripts/symlink.sh @@ -2,22 +2,23 @@ installdir="$2" bffdir="$installdir/bff" +homedir=`cd && pwd` -if [ ! -e ~/bff ]; then - ln -s $bffdir ~/bff +if [ ! -e $homedir/bff ]; then + ln -s $bffdir $homedir/bff fi -#if [ ! -e ~/results ]; then -# mkdir ~/results -#fi -#if [ -d ~/bff/results ]; then - rmdir $bffdir/results -#fi + +rmdir $bffdir/results + if [ -L ~/results ]; then - rm ~/results -# mkdir ~/results + rm $homedir/results + mkdir $homedir/results fi -#chmod g+w ~/results -#rmdir ~/bff/results -#ln -s ~/results ~/bff/results + +if [ ! -e $homedir/results ]; then + mkdir $homedir/results +fi + +ln -s $homedir/results $homedir/bff/results exit 0 From 2f4f12b56ba09631c7e00287025e950e00759abd Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 12:29:58 -0500 Subject: [PATCH 0839/1169] Add tag to CW output. BFF-436 --- src/certfuzz/debuggers/crashwrangler.py | 1 + .../debuggers/templates/gdb_complete_nofunction_template.txt | 2 +- src/certfuzz/debuggers/templates/gdb_complete_template.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index 0451d5d..65ae0c3 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -78,6 +78,7 @@ def go(self): # set up the environment for crashwrangler my_env = dict(os.environ) my_env['CW_LOG_PATH'] = self.outfile + my_env['CW_LOG_INFO'] = 'Found_with_CERT_BFF_2.8' my_env['CW_NO_CRASH_REPORTER'] = '1' if re.search('gmalloc', self.outfile): my_env['CW_USE_GMAL'] = '1' diff --git a/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt index 229e67b..693e26b 100644 --- a/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt @@ -4,7 +4,7 @@ set logging on file $PROGRAM run $CMD_ARGS echo \n -echo Crash found with CERT BFF 2.7\n +echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n source ~/bff/CERT_triage_tools/exploitable/exploitable.py diff --git a/src/certfuzz/debuggers/templates/gdb_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_complete_template.txt index d484529..5887b22 100644 --- a/src/certfuzz/debuggers/templates/gdb_complete_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_complete_template.txt @@ -4,7 +4,7 @@ set logging on file $PROGRAM run $CMD_ARGS echo \n -echo Crash found with CERT BFF 2.7\n +echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n source ~/bff/CERT_triage_tools/exploitable/exploitable.py From 7013964654d1ced8966088a672b2fc552c8523f5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 12:30:27 -0500 Subject: [PATCH 0840/1169] Try to get CW zip in installer. BFF-903 --- .../installer/BFF_installer.pmdoc/1152854_crashwrangler.xml | 4 ++-- build/distmods/osx/installer/BFF_installer.pmdoc/index.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml index febe577..e653f67 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/1152854_crashwrangler.xml @@ -1,6 +1,6 @@ - + -com.cert.cc.certBff.52854_crashwrangler.pkg +com.cert.cc.certBff.56876_crashwrangler.pkg 1.0 diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index 798f5f0..ae35923 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -59,7 +59,7 @@ - + From 687a942d75bef8b6461685ee52fafd2cef07ab2b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 12:59:11 -0500 Subject: [PATCH 0841/1169] try rm-rf on results dir. BFF-464 --- build/distmods/osx/installer/scripts/symlink.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/distmods/osx/installer/scripts/symlink.sh b/build/distmods/osx/installer/scripts/symlink.sh index 627633f..521b767 100755 --- a/build/distmods/osx/installer/scripts/symlink.sh +++ b/build/distmods/osx/installer/scripts/symlink.sh @@ -8,7 +8,7 @@ if [ ! -e $homedir/bff ]; then ln -s $bffdir $homedir/bff fi -rmdir $bffdir/results +rm -rf $bffdir/results if [ -L ~/results ]; then rm $homedir/results From b384e0d5d8ed701083ec6f37c6088b1fd8a923f3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 13:02:29 -0500 Subject: [PATCH 0842/1169] allow group write to ~/results --- build/distmods/osx/installer/scripts/symlink.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/build/distmods/osx/installer/scripts/symlink.sh b/build/distmods/osx/installer/scripts/symlink.sh index 521b767..f39c9f7 100755 --- a/build/distmods/osx/installer/scripts/symlink.sh +++ b/build/distmods/osx/installer/scripts/symlink.sh @@ -17,6 +17,7 @@ fi if [ ! -e $homedir/results ]; then mkdir $homedir/results + chmod g+w $homedir/results fi ln -s $homedir/results $homedir/bff/results From 54bf35f6a353ef07f74750f69894d30d99b8fcec Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 13:21:48 -0500 Subject: [PATCH 0843/1169] Log rm and ln stderr in OSX installer. --- build/distmods/osx/installer/scripts/symlink.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build/distmods/osx/installer/scripts/symlink.sh b/build/distmods/osx/installer/scripts/symlink.sh index f39c9f7..49028b3 100755 --- a/build/distmods/osx/installer/scripts/symlink.sh +++ b/build/distmods/osx/installer/scripts/symlink.sh @@ -8,8 +8,6 @@ if [ ! -e $homedir/bff ]; then ln -s $bffdir $homedir/bff fi -rm -rf $bffdir/results - if [ -L ~/results ]; then rm $homedir/results mkdir $homedir/results @@ -20,6 +18,7 @@ if [ ! -e $homedir/results ]; then chmod g+w $homedir/results fi -ln -s $homedir/results $homedir/bff/results +rmdir $homedir/bff/results 2> /tmp/rm.err +ln -s $homedir/results $homedir/bff/results 2> /tmp/ln.err exit 0 From 741b2fac9a20af80ef901d866ccd7eba7718e2dc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 12 Feb 2016 13:42:19 -0500 Subject: [PATCH 0844/1169] More verbose logging of symlink script --- build/distmods/osx/installer/scripts/symlink.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build/distmods/osx/installer/scripts/symlink.sh b/build/distmods/osx/installer/scripts/symlink.sh index 49028b3..55b3e3e 100755 --- a/build/distmods/osx/installer/scripts/symlink.sh +++ b/build/distmods/osx/installer/scripts/symlink.sh @@ -5,20 +5,20 @@ bffdir="$installdir/bff" homedir=`cd && pwd` if [ ! -e $homedir/bff ]; then - ln -s $bffdir $homedir/bff + ln -s $bffdir $homedir/bff 2>> /tmp/symlink.err fi if [ -L ~/results ]; then - rm $homedir/results - mkdir $homedir/results + rm $homedir/results 2>> /tmp/symlink.err + mkdir $homedir/results 2>> /tmp/symlink.err fi if [ ! -e $homedir/results ]; then - mkdir $homedir/results - chmod g+w $homedir/results + mkdir $homedir/results 2>> /tmp/symlink.err + chmod g+w $homedir/results 2>> /tmp/symlink.err fi -rmdir $homedir/bff/results 2> /tmp/rm.err -ln -s $homedir/results $homedir/bff/results 2> /tmp/ln.err +rmdir $homedir/bff/results 2>> /tmp/symlink.err +ln -s $homedir/results $homedir/bff/results 2>> symlink.err exit 0 From 6fe49214bd5eed480d62af081aad214ef156061b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 01:17:56 -0500 Subject: [PATCH 0845/1169] distribute zzuf-master.zip --- .../{zzuf-patched.zip => zzuf-master.zip} | Bin 185892 -> 180637 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename src/linux/{zzuf-patched.zip => zzuf-master.zip} (54%) diff --git a/src/linux/zzuf-patched.zip b/src/linux/zzuf-master.zip similarity index 54% rename from src/linux/zzuf-patched.zip rename to src/linux/zzuf-master.zip index 52f7ff62c0b0ca11b56967321b02fce0ae6c1327..6572e49441a783a3973c3c65da68ee0aadefa560 100644 GIT binary patch delta 64093 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 delta 69267 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 OM`jpIHUoT60{b7`zqic* From bea5d8ccf28a94436b287e2576b3d5720e5dd214 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 01:20:01 -0500 Subject: [PATCH 0846/1169] Make symlinks after installing bff files, not before. BFF-464 --- build/distmods/osx/installer/scripts/chmod.sh | 18 ++++++++++++++++ .../distmods/osx/installer/scripts/symlink.sh | 21 ------------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/build/distmods/osx/installer/scripts/chmod.sh b/build/distmods/osx/installer/scripts/chmod.sh index e8ad75e..1511e8c 100755 --- a/build/distmods/osx/installer/scripts/chmod.sh +++ b/build/distmods/osx/installer/scripts/chmod.sh @@ -3,6 +3,24 @@ installdir="$2" bffdir="$installdir/bff" packagesdir="$2/packages" +homedir=`cd && pwd` + +if [ ! -e $homedir/bff ]; then + ln -s $bffdir $homedir/bff +fi + +if [ -L ~/results ]; then + rm $homedir/results + mkdir $homedir/results +fi + +if [ ! -e $homedir/results ]; then + mkdir $homedir/results + chmod g+w $homedir/results +fi + +rmdir $homedir/bff/results +ln -s $homedir/results $homedir/bff/results launchctl unload -w /System/Library/LaunchDaemons/com.apple.ReportCrash.Root.plist sed "s/^copymode=0/copymode=1/" $bffdir/conf.d/bff.cfg > $bffdir/conf.d/cfg.tmp diff --git a/build/distmods/osx/installer/scripts/symlink.sh b/build/distmods/osx/installer/scripts/symlink.sh index 55b3e3e..c52d3c2 100755 --- a/build/distmods/osx/installer/scripts/symlink.sh +++ b/build/distmods/osx/installer/scripts/symlink.sh @@ -1,24 +1,3 @@ #!/bin/sh -installdir="$2" -bffdir="$installdir/bff" -homedir=`cd && pwd` - -if [ ! -e $homedir/bff ]; then - ln -s $bffdir $homedir/bff 2>> /tmp/symlink.err -fi - -if [ -L ~/results ]; then - rm $homedir/results 2>> /tmp/symlink.err - mkdir $homedir/results 2>> /tmp/symlink.err -fi - -if [ ! -e $homedir/results ]; then - mkdir $homedir/results 2>> /tmp/symlink.err - chmod g+w $homedir/results 2>> /tmp/symlink.err -fi - -rmdir $homedir/bff/results 2>> /tmp/symlink.err -ln -s $homedir/results $homedir/bff/results 2>> symlink.err - exit 0 From 44b22c3d223ae7eaeab1a1cfecc5c7368dc30156 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 14:53:50 -0500 Subject: [PATCH 0847/1169] Get register values from CrashWrangler output. BFF-905 --- .../debuggers/output_parsers/cwfile.py | 79 +++++++++++-------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index 7a301e7..9864ba6 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -13,19 +13,16 @@ logger = logging.getLogger(__name__) -logger.setLevel(logging.WARNING) -# for use with 'info all-registers' in GDB -#registers = ['eip', 'eax', 'ebx', 'ecx', 'edx', 'esp', 'ebp', 'edi', 'esi', -# 'eflags', 'cs', 'ss', 'ds', 'es', 'fs', 'gs', 'st0', 'st1', 'st2', 'st3', -# 'st4', 'st5', 'st6', 'st7', 'fctrl', 'fstat', 'ftag', 'fiseg', 'fioff', -# 'fooff', 'fop'] - -# for use with 'info registers' in GDB registers = ['eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', 'edi', 'eip', 'cs', 'ss', 'ds', 'es', 'fs', 'gs'] +registers64 = ('rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi', 'rbp', + 'rsp', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', 'r14', + 'r15', 'rip', 'rfl', 'cr2') regex = { + 'code_type': re.compile('Code Type:\s+(.+)'), + 'exception_line': re.compile('^exception=.+instruction_address=(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), 'bt_thread': re.compile('^Thread.+'), 'bt_line_basic': re.compile('^\d'), 'bt_line': re.compile('^\d+\s+(.*)$'), @@ -38,7 +35,7 @@ 'exit_code': re.compile('Program exited with code (\d+)'), 'bt_line_from': re.compile(r'\bfrom\b'), 'bt_line_at': re.compile(r'\bat\b'), - 'register': re.compile('(0x[0-9a-zA-Z]+)\s+(.+)$'), + 'register': re.compile('\s\s\s?[0-9a-zA-Z]+:\s(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), } # There are a number of functions that are typically found in crash backtraces, @@ -67,6 +64,8 @@ def __init__(self, f): self.backtrace = [] self.backtrace_without_questionmarks = [] self.registers = {} + # make a copy of registers list we're looking for + self.registers_sought = list(registers) self.registers_hex = {} self.hashable_backtrace = [] self.hashable_backtrace_string = '' @@ -80,7 +79,7 @@ def __init__(self, f): self.is_debugbuild = False self.crashing_thread = False self.pc_in_function = True - self.pc_name = '' + self.pc_name = 'eip' self.keep_uniq_faddr = False self.faddr = None @@ -184,7 +183,7 @@ def _read_file(self): gdb = "" if os.path.exists(self.file): with open(self.file, 'r') as f: - gdb = [s.strip() for s in f.readlines()] + gdb = [s.rstrip() for s in f.readlines()] return gdb def _process_lines(self): @@ -228,6 +227,22 @@ def _look_for_crashing_thread(self, line): elif m: self.crashing_thread = False + def _look_for_64bit(self, line): + ''' + Check for 64-bit process by looking at address of bt frame addresses + ''' + if self.is_64bit: + return + #raw_input('checking exception regex') + m = re.match(regex['code_type'], line) + if m: + code_type = m.group(1) + if 'X86-64' in code_type: + self.is_64bit = True + logger.debug('Target process is 64-bit') + self.pc_name = 'rip' + self.registers_sought = list(registers64) + #TODO: CrashWrangler equivalents of the below def _look_for_corrupt_stack(self, line): if 'corrupt stack' in line: @@ -260,31 +275,33 @@ def _look_for_debug_build(self, line): self.is_debugbuild = True def _look_for_registers(self, line): + ''' + Look for register name/value pairs in CrashWrangler output. + Unlike gdb, CrashWrangler lists more than one register per line + ''' # short-circuit if we're out of registers to look for - if not len(registers): - return - - parts = line.split() - - # short-circuit if line doesn't split - if not len(parts): + if not len(self.registers_sought): return # short-circuit if the first thing in the line isn't a register - if not parts[0] in registers: - return - - r = parts[0] - mystr = ' '.join(parts[1:]) - m = re.match(regex['register'], mystr) - - # short-circuit when no match + m = re.match(regex['register'], line) if not m: return - - self.registers_hex[r] = m.group(1) - self.registers[r] = m.group(2) - # once we've found the register, we don't have to look for it anymore - registers.remove(r) + line = line.lstrip() + regpairs = line.split(' ') + # short-circuit if line doesn't split + if not len(regpairs): + logger.debug('Non-splittable line') + return + # short-circuit if the first thing in the line isn't a register + for regpair in regpairs: + regpairlist = regpair.split(': ') + r = regpairlist[0].strip() + if not r in self.registers_sought: + continue + regval = regpairlist[1] + self.registers_hex[r] = regval + self.registers_sought.remove(r) + logger.debug('Register %s=%s', r, self.registers_hex[r]) def get_crash_signature(self, backtrace_level): ''' From 7e5e5b206db3f92dbc81e01ff26b2ce085f3b9e5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 15:50:44 -0500 Subject: [PATCH 0848/1169] Fix regex for code type detection in CrashWrangler. --- src/certfuzz/debuggers/output_parsers/cwfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index 9864ba6..77ca4af 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -236,7 +236,7 @@ def _look_for_64bit(self, line): #raw_input('checking exception regex') m = re.match(regex['code_type'], line) if m: - code_type = m.group(1) + code_type = m.group(0) if 'X86-64' in code_type: self.is_64bit = True logger.debug('Target process is 64-bit') From ef1a5a5c2fc5eb68a9c724a4c2c5212286337d2b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 16:15:25 -0500 Subject: [PATCH 0849/1169] Don't hard-code bff location in batch.sh --- src/linux/batch.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index 8d907bf..ad9f55d 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -46,7 +46,7 @@ # contract clause at 252.227.7013. ############################################################################## -scriptlocation=~/bff +scriptlocation=`pwd` echo Script location: $scriptlocation/bff.py platform=`uname -a` PINURL=http://software.intel.com/sites/landingpage/pintool/downloads/pin-2.12-58423-gcc.4.4.7-linux.tar.gz @@ -94,7 +94,7 @@ cd $scriptlocation echo "Using python interpreter: $mypython" if [[ -f "$scriptlocation/bff.py" ]]; then - $mypython $scriptlocation/bff.py --config=$scriptlocation/conf.d/bff.yaml "$@" + $mypython $scriptlocation/bff.py "$@" else read -p "Cannot find $scriptlocation/bff.py Please verify script locations." fi From 8929af289f8bc465b00687081e933283da40045b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 17:26:07 -0500 Subject: [PATCH 0850/1169] Get rid of killprocname-related stuff. It's pretty much dead code. Which also removes X requirement for OSX. --- src/certfuzz/analyzers/analyzer_base.py | 9 +++------ src/certfuzz/analyzers/cw_gmalloc.py | 2 +- src/certfuzz/campaign/campaign_linux.py | 11 +--------- src/certfuzz/debuggers/crashwrangler.py | 8 ++++---- src/certfuzz/debuggers/debugger_base.py | 3 +-- src/certfuzz/debuggers/gdb.py | 8 ++++---- src/certfuzz/debuggers/msec.py | 4 ++-- src/certfuzz/fuzztools/subprocess_helper.py | 14 ++++++------- src/certfuzz/iteration/iteration_linux.py | 1 - src/certfuzz/minimizer/minimizer_base.py | 6 ------ src/certfuzz/testcase/testcase_linux.py | 4 +--- src/certfuzz/testcase/testcase_windows.py | 1 - src/certfuzz/tools/linux/minimize.py | 1 - src/certfuzz/tools/windows/minimize.py | 19 +++++++++--------- src/linux/conf.d/bff.yaml | 8 -------- src/linux/welcome.sh | 2 -- src/test_certfuzz/analyzers/test_stderr.py | 9 ++++----- src/test_certfuzz/analyzers/test_valgrind.py | 1 - src/test_certfuzz/debuggers/test_gdb.py | 4 ++-- src/test_certfuzz/mocks.py | 21 ++++++++++---------- 20 files changed, 49 insertions(+), 87 deletions(-) diff --git a/src/certfuzz/analyzers/analyzer_base.py b/src/certfuzz/analyzers/analyzer_base.py index 22925a6..0684c0b 100644 --- a/src/certfuzz/analyzers/analyzer_base.py +++ b/src/certfuzz/analyzers/analyzer_base.py @@ -26,10 +26,7 @@ def __init__(self, cfg, crash, outfile=None, timeout=None, **options): self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'], crash.fuzzedfile.path)[1] self.outfile = outfile self.timeout = float(timeout) - try: - self.killprocname = crash.killprocname - except AttributeError: - self.killprocname = self.cmdargs[1] + self.progname = self.cmdargs[1] self.options = options self.preserve_stderr = False @@ -78,7 +75,7 @@ def go(self): ''' Generates analysis output for into . If analysis process fails to complete before , - attempt to _kill analyzer and . + attempt to _kill analyzer and progname. ''' logger.info('Running %s', self.__class__.__name__) # build the command line in a separate function so we can unit test @@ -92,7 +89,7 @@ def go(self): logger.warning('Skipping analyzer %s: Not found in path.', analyzer) return - subp.run_with_timer(args, self.timeout, self.killprocname, **self.options) + subp.run_with_timer(args, self.timeout, self.progname, **self.options) if not self.missing_output_ok and not os.path.exists(self.outfile): raise AnalyzerOutputMissingError(self.outfile) if not self.empty_output_ok and not os.path.getsize(self.outfile): diff --git a/src/certfuzz/analyzers/cw_gmalloc.py b/src/certfuzz/analyzers/cw_gmalloc.py index 80cc0af..238f9c2 100644 --- a/src/certfuzz/analyzers/cw_gmalloc.py +++ b/src/certfuzz/analyzers/cw_gmalloc.py @@ -44,4 +44,4 @@ def go(self): args = self.cmdargs[1:] from ..debuggers.crashwrangler import CrashWrangler - CrashWrangler(prg, args, self.outfile, self.timeout, self.killprocname).go() + CrashWrangler(prg, args, self.outfile, self.timeout, self.progname).go() diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index afd76f0..f66d380 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -17,7 +17,6 @@ from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools import subprocess_helper as subp from certfuzz.fuzztools.ppid_observer import check_ppid -from certfuzz.fuzztools.process_killer import ProcessKiller from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.iteration.iteration_linux import LinuxIteration from certfuzz.fuzztools.command_line_templating import get_command_args_list @@ -79,7 +78,6 @@ def _pre_enter(self): # give up if prog is a script self._check_for_script() self._check_for_redirect() - self._start_process_killer() self._set_unbuffered_stdout() @@ -102,13 +100,6 @@ def _set_unbuffered_stdout(self): # and 0 as the buffer size (unbuffered) sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) - def _start_process_killer(self): - logger.debug('start process killer') - with ProcessKiller(self.config['target']['killprocname'], - self.config['runoptions']['killproctimeout'] - ) as pk: - self.pk_pid = pk.go() - def _cache_app(self): logger.debug('cache program') sf = self.seedfile_set.next_item() @@ -119,7 +110,7 @@ def _cache_app(self): logger.info('Invoking %s' % cmdargs) subp.run_with_timer(cmdargs, self.config['runner']['runtimeout'] * 8, - self.config['target']['killprocname'], + self.config['target']['program'], use_shell=False, seeoutput=True, ) diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index 65ae0c3..c7ecaa6 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -41,8 +41,8 @@ class CrashWrangler(Debugger): _key = 'cw' _ext = 'cw' - def __init__(self, program, cmd_args, outfile, timeout, killprocname, template=None, exclude_unmapped_frames=True, **options): - Debugger.__init__(self, program, cmd_args, outfile, timeout, killprocname) + def __init__(self, program, cmd_args, outfile, timeout, template=None, exclude_unmapped_frames=True, **options): + Debugger.__init__(self, program, cmd_args, outfile, timeout) def _get_crashwrangler_cmdline(self): if (self.program == cwapp): @@ -69,7 +69,7 @@ def go(self): ''' Generates CrashWrangler output for into . If crashwrangler fails to complete before , - attempt to _kill crashwrangler and . + attempt to _kill crashwrangler and program. ''' # build the command line in a separate function so we can unit test # it without actually running the command @@ -83,7 +83,7 @@ def go(self): if re.search('gmalloc', self.outfile): my_env['CW_USE_GMAL'] = '1' - subp.run_with_timer(args, self.timeout, self.killprocname, env=my_env) + subp.run_with_timer(args, self.timeout, self.program, env=my_env) # We're not guaranteed that CrashWrangler will create an output file: if not os.path.exists(self.outfile): diff --git a/src/certfuzz/debuggers/debugger_base.py b/src/certfuzz/debuggers/debugger_base.py index 0606e8d..c3eea93 100644 --- a/src/certfuzz/debuggers/debugger_base.py +++ b/src/certfuzz/debuggers/debugger_base.py @@ -23,7 +23,7 @@ class Debugger(object): _key = 'debugger' _ext = 'debug' - def __init__(self, program=None, cmd_args=None, outfile_base=None, timeout=None, killprocname=None, **options): + def __init__(self, program=None, cmd_args=None, outfile_base=None, timeout=None, **options): ''' Default initializer for the base Debugger class. ''' @@ -32,7 +32,6 @@ def __init__(self, program=None, cmd_args=None, outfile_base=None, timeout=None, self.cmd_args = cmd_args self.outfile = '.'.join((outfile_base, self._ext)) self.timeout = timeout - self.killprocname = killprocname self.input_file = '' self.debugger_output = None self.result = {} diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index 696cb3c..8bd3f15 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -24,8 +24,8 @@ class GDB(Debugger): _key = 'gdb' _ext = 'gdb' - def __init__(self, program, cmd_args, outfile_base, timeout, killprocname, template=None, exclude_unmapped_frames=True, keep_uniq_faddr=False, **options): - Debugger.__init__(self, program, cmd_args, outfile_base, timeout, killprocname, **options) + def __init__(self, program, cmd_args, outfile_base, timeout, template=None, exclude_unmapped_frames=True, keep_uniq_faddr=False, **options): + Debugger.__init__(self, program, cmd_args, outfile_base, timeout, **options) self.template = template self.exclude_unmapped_frames = exclude_unmapped_frames self.keep_uniq_faddr = keep_uniq_faddr @@ -98,14 +98,14 @@ def go(self): ''' Generates gdb output for into . If gdb fails to complete before , - attempt to _kill gdb and . + attempt to _kill gdb and program. @return: a GDBfile object with the parsed results ''' # build the command line in a separate function so we can unit test # it without actually running the command cmdline = self._get_cmdline() - subp.run_with_timer(cmdline, self.timeout, self.killprocname, stdout=os.devnull) + subp.run_with_timer(cmdline, self.timeout, self.program, stdout=os.devnull) self._remove_temp_file() if not os.path.exists(self.outfile): diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index f5b25dd..44d3fd0 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -29,8 +29,8 @@ class MsecDebugger(DebuggerBase): _key = 'msec' _ext = 'msec' - def __init__(self, program, cmd_args, outfile_base, timeout, killprocname, watchcpu, exception_depth=0, **options): - DebuggerBase.__init__(self, program, cmd_args, outfile_base, timeout, killprocname, **options) + def __init__(self, program, cmd_args, outfile_base, timeout, watchcpu, exception_depth=0, **options): + DebuggerBase.__init__(self, program, cmd_args, outfile_base, timeout, **options) self.exception_depth = exception_depth self.watchcpu = watchcpu self.t = None diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 5c8bd72..9638fee 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -33,11 +33,11 @@ def on_linux(): signal.signal(signal.SIGTTOU, signal.SIG_IGN) -def run_with_timer(args, timeout, killprocname, use_shell=False, **options): +def run_with_timer(args, timeout, progname, use_shell=False, **options): ''' Runs . If it takes longer than we'll kill as well as hunt down any processes named - . If you want to redirect stdout and/or stderr, + . If you want to redirect stdout and/or stderr, use stdout= or stderr= (or both). @return: none ''' @@ -52,7 +52,7 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): errors = '' if options and options.get('stderr'): - errors = open(options['stderr'], 'w') + errors = open(options['stderr'], 'w') else: errors = open(os.devnull, 'w') @@ -80,7 +80,7 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): # Set up timeout timer # Give extra time for the first invocation of the application - t = Timer(timeout, _kill, args=[p, 0x00, killprocname]) + t = Timer(timeout, _kill, args=[p, 0x00, progname]) t.start() p.wait() t.cancel() @@ -92,7 +92,7 @@ def run_with_timer(args, timeout, killprocname, use_shell=False, **options): return p -def _kill(p, returncode, killprocname): #@UnusedVariable +def _kill(p, returncode, progname): #@UnusedVariable if (on_windows()): """_kill function for Win32""" kernel32 = ctypes.windll.kernel32 @@ -101,8 +101,8 @@ def _kill(p, returncode, killprocname): #@UnusedVariable kernel32.CloseHandle(handle) else: ret = p.kill() - if(killprocname): - killall(killprocname, signal.SIGKILL) + if(progname): + killall(progname, signal.SIGKILL) return (0 != ret) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 9f34695..936cb6b 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -77,7 +77,6 @@ def _construct_testcase(self): fuzzedfile=BasicFile(self.fuzzer.output_file_path), program=self.cfg['target']['program'], debugger_timeout=self.cfg['debugger']['runtimeout'], - killprocname=self.cfg['target']['killprocname'], backtrace_lines=self.cfg['debugger']['backtracelevels'], crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index b09621a..3451b0e 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -387,11 +387,6 @@ def run_debugger(self, infile, outfile): self.debugger_runs += 1 cmd_args = get_command_args_list(self.cfg['target']['cmdline_template'], infile)[1] cmd = cmd_args[0] -# cmd_args = self.cfg.get_command_args_list(infile) - try: - killprocname = self.cfg['target']['killprocname'] - except KeyError: - killprocname = cmd try: exclude_unmapped_frames = self.cfg['analyzer']['exclude_unmapped_frames'] @@ -402,7 +397,6 @@ def run_debugger(self, infile, outfile): cmd_args, outfile, self.debugger_timeout, - killprocname, template=self.crash.debugger_template, exclude_unmapped_frames=exclude_unmapped_frames, keep_uniq_faddr=self.keep_uniq_faddr, diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 55be645..1b339ee 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -35,7 +35,7 @@ class LinuxTestcase(Testcase): _debugger_cls = debugger_cls def __init__(self, cfg, seedfile, fuzzedfile, program, - debugger_timeout, killprocname, backtrace_lines, + debugger_timeout, backtrace_lines, crashers_dir, workdir_base, seednum=None, range=None, keep_faddr=False): ''' Constructor @@ -43,7 +43,6 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, Testcase.__init__(self, seedfile, fuzzedfile, debugger_timeout) self.cfg = cfg self.program = program - self.killprocname = killprocname self.backtrace_lines = backtrace_lines self.crash_base_dir = crashers_dir self.seednum = seednum @@ -106,7 +105,6 @@ def get_debug_output(self, outfile_base): self.cmdargs, outfile_base, self.debugger_timeout, - self.killprocname, template=self.debugger_template, exclude_unmapped_frames=self.exclude_unmapped_frames, keep_uniq_faddr=self.keep_uniq_faddr diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index b92f605..7242261 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -118,7 +118,6 @@ def debug_once(self): cmd_args=self.cmdargs, outfile_base=outfile_base, timeout=self.debugger_timeout, - killprocname=None, exception_depth=self.exception_depth, workingdir=self.tempdir, watchcpu=self.watchcpu) as debugger: diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index ba4c25d..bb95410 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -141,7 +141,6 @@ def main(): fuzzedfile=fuzzed_file, program=cfg['target']['program'], debugger_timeout=cfg['debugger']['runtimeout'], - killprocname=cfg['target']['killprocname'], backtrace_lines=cfg['debugger']['backtracelevels'], crashers_dir=crashers_dir, workdir_base=None, diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 91944a8..fc010d4 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -46,7 +46,6 @@ class DummyCfg(object): template = string.Template(cfg['target']['cmdline_template']) config.get_command_args_list = lambda x: get_command_args_list(template, x)[1] config.program = cfg['target']['program'] - config.killprocname = None config.exclude_unmapped_frames = False config.watchdogfile = os.devnull return config @@ -154,16 +153,16 @@ def main(): filename_modifier = '' retries = 0 debugger_class = msec.MsecDebugger - + cmd_as_args = get_command_args_list(cfg['target']['cmdline_template'], fuzzed_file.path)[1] - with WindowsTestcase(cmd_template=cfg['target']['cmdline_template'], - seedfile=seedfile, - fuzzedfile=fuzzed_file, - cmdlist=cmd_as_args, - fuzzer=None, - dbg_opts=cfg['debugger'], - workingdir_base=outdir, - keep_faddr=options.keep_uniq_faddr, + with WindowsTestcase(cmd_template=cfg['target']['cmdline_template'], + seedfile=seedfile, + fuzzedfile=fuzzed_file, + cmdlist=cmd_as_args, + fuzzer=None, + dbg_opts=cfg['debugger'], + workingdir_base=outdir, + keep_faddr=options.keep_uniq_faddr, program=cfg['target']['program'], heisenbug_retries=retries ) as crash: diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 1fe0ca0..f0d33bd 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -19,17 +19,9 @@ campaign: # seed file name. # Use quotes if the target application has spaces in the path # examples: -# Name of process to monitor for hangs. This is done by greping the -# process list, so choose carefully! Usually the same name as "program" -# will suffice, but in cases where the program is started from a script -# you may want to list the actual process name. -# This process name is also used to kill the target process when BFF -# launches the target application from an analyzer, such as gdb or valgrind -# Use quotes if the target application has spaces in its name target: program: ~/convert cmdline_template: $PROGRAM $SEEDFILE /dev/null - killprocname: convert ################################################################ # LOCATIONS FOR FUZZ RUN FILES: diff --git a/src/linux/welcome.sh b/src/linux/welcome.sh index b16ae48..c53652d 100755 --- a/src/linux/welcome.sh +++ b/src/linux/welcome.sh @@ -27,6 +27,4 @@ echo -e "Output directory: " `egrep -m1 '^ results_dir' $currentcfg | sed 's/ if [[ -n "$xterm" ]]; then echo -e "Run ./batch.sh to begin fuzzing.\n" -elif [[ "$platform" =~ "Darwin" ]]; then - echo -e "X is not detected. Please install X before running BFF\nSee: https://support.apple.com/kb/HT5293\n" fi diff --git a/src/test_certfuzz/analyzers/test_stderr.py b/src/test_certfuzz/analyzers/test_stderr.py index 1e82ad0..ba0b006 100644 --- a/src/test_certfuzz/analyzers/test_stderr.py +++ b/src/test_certfuzz/analyzers/test_stderr.py @@ -28,18 +28,17 @@ def setUp(self): cfg = {'runner': {'runtimeout':1}, 'target': {'cmdline_template': string.Template('PROGRAM $SEEDFILE foo')} } - - if sys.platform=='win32': - cfg['target']['cmdline_tempate']=string.Template('c:\\cygwin\\bin\\cat.exe -a foo') + + if sys.platform == 'win32': + cfg['target']['cmdline_tempate'] = string.Template('c:\\cygwin\\bin\\cat.exe -a foo') else: cfg['target']['cmdline_template'] = string.Template('cat -a foo') - + crash = Mock() crash.fuzzedfile = Mock() crash.fuzzedfile.path = f crash.fuzzedfile.dirname = os.path.dirname(f) - crash.killprocname = 'bar' self.se = StdErr(cfg, crash) diff --git a/src/test_certfuzz/analyzers/test_valgrind.py b/src/test_certfuzz/analyzers/test_valgrind.py index c6aaa2c..09e067b 100644 --- a/src/test_certfuzz/analyzers/test_valgrind.py +++ b/src/test_certfuzz/analyzers/test_valgrind.py @@ -24,7 +24,6 @@ def setUp(self): crash.fuzzedfile = Mock() crash.fuzzedfile.path = "foo" crash.fuzzedfile.dirname = 'foodir' - crash.killprocname = 'bar' self.vg = Valgrind(cfg, crash) def tearDown(self): diff --git a/src/test_certfuzz/debuggers/test_gdb.py b/src/test_certfuzz/debuggers/test_gdb.py index 91e26ca..4a8fef6 100644 --- a/src/test_certfuzz/debuggers/test_gdb.py +++ b/src/test_certfuzz/debuggers/test_gdb.py @@ -15,12 +15,12 @@ def delete_file(self, f): self.assertFalse(os.path.exists(f)) def setUp(self): - (program, cmd_args, gdb_file, timeout, killprocname) = tuple('abcde') + (program, cmd_args, gdb_file, timeout) = tuple('abcde') (fd, path) = tempfile.mkstemp() os.close(fd) self.tempfile = path - self.gdb = GDB(program, cmd_args, gdb_file, timeout, killprocname, template=self.tempfile) + self.gdb = GDB(program, cmd_args, gdb_file, timeout, template=self.tempfile) self.gdb._create_input_file() def tearDown(self): diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index e0c8c36..a707963 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -33,7 +33,6 @@ def __init__(self, **kwargs): class MockCrash(MockObj): def __init__(self): self.fuzzedfile = MockFile() - self.killprocname = 'killprocname' class MockFile(MockObj): def __init__(self): @@ -74,7 +73,7 @@ class MockFuzzer(Mock): is_minimizable = False class MockRunner(Mock): - is_nullrunner=False + is_nullrunner = False class MockTestcase(Mock): signature = 'ABCDEFGHIJK' @@ -101,24 +100,24 @@ def go(self): class MockCfg(dict): - def __init__(self,templated=True): - self['debugger']={'runtimeout': 1, + def __init__(self, templated=True): + self['debugger'] = {'runtimeout': 1, 'backtracelevels': 5, 'debugger': 'gdb', } - self['target']={'cmdline_template': '$PROGRAM b c d $SEEDFILE', + self['target'] = {'cmdline_template': '$PROGRAM b c d $SEEDFILE', 'killprocname': 'a', 'program': 'a'} - self['analyzer']={'exclude_unmapped_frames': False, + self['analyzer'] = {'exclude_unmapped_frames': False, 'valgrind_timeout': 1} - self['directories'] ={'seedfile_dir': '', + self['directories'] = {'seedfile_dir': '', 'results_dir': '', 'working_dir': ''} - self['fuzzer']={'fuzzer': 'bytemut'} - self['campaign']={'id': 'xyz'} - self['runoptions']={'first_iteration': 0, + self['fuzzer'] = {'fuzzer': 'bytemut'} + self['campaign'] = {'id': 'xyz'} + self['runoptions'] = {'first_iteration': 0, 'seed_interval': 10} - self['runner']={'runner': 'zzufrun'} + self['runner'] = {'runner': 'zzufrun'} if templated: self['target']['cmdline_template'] = string.Template(self['target']['cmdline_template']) From 814f895f2d1263d1c5157044bd2c369688aa2256 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 17:37:15 -0500 Subject: [PATCH 0851/1169] Fix unit test to reflect missing killprocname option. --- src/test_certfuzz/debuggers/test_gdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test_certfuzz/debuggers/test_gdb.py b/src/test_certfuzz/debuggers/test_gdb.py index 4a8fef6..75b02fa 100644 --- a/src/test_certfuzz/debuggers/test_gdb.py +++ b/src/test_certfuzz/debuggers/test_gdb.py @@ -15,7 +15,7 @@ def delete_file(self, f): self.assertFalse(os.path.exists(f)) def setUp(self): - (program, cmd_args, gdb_file, timeout) = tuple('abcde') + (program, cmd_args, gdb_file, timeout) = tuple('abcd') (fd, path) = tempfile.mkstemp() os.close(fd) self.tempfile = path From 231f1ab64f140604ee1c3f19814f0befe9dca9a1 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 17:48:22 -0500 Subject: [PATCH 0852/1169] Don't hard-code CTT location. BFF-907 --- src/certfuzz/debuggers/gdb.py | 4 +++- .../debuggers/templates/gdb_complete_nofunction_template.txt | 2 +- src/certfuzz/debuggers/templates/gdb_complete_template.txt | 2 +- src/certfuzz/debuggers/templates/gdb_exploitable_template.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index 8bd3f15..37cbc5f 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -56,8 +56,10 @@ def _create_input_file(self): s = Template(template) cmdargs = ' '.join(self.cmd_args) + # Extract the bff directory out of the template location + bffdir = self.template.split('/certfuzz/', 1)[0] new_script = s.safe_substitute(PROGRAM=self.program, CMD_ARGS=cmdargs, - OUTFILE=self.outfile) + OUTFILE=self.outfile, BFFDIR=bffdir) (fd, f) = tempfile.mkstemp(text=True) try: diff --git a/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt index 693e26b..4e2cef1 100644 --- a/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt @@ -7,7 +7,7 @@ echo \n echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n -source ~/bff/CERT_triage_tools/exploitable/exploitable.py +source $BFFDIR/CERT_triage_tools/exploitable/exploitable.py echo \n exploitable echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_complete_template.txt index 5887b22..6abfe42 100644 --- a/src/certfuzz/debuggers/templates/gdb_complete_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_complete_template.txt @@ -7,7 +7,7 @@ echo \n echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n -source ~/bff/CERT_triage_tools/exploitable/exploitable.py +source $BFFDIR/CERT_triage_tools/exploitable/exploitable.py echo \n exploitable echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_exploitable_template.txt b/src/certfuzz/debuggers/templates/gdb_exploitable_template.txt index 3db9440..136cda0 100644 --- a/src/certfuzz/debuggers/templates/gdb_exploitable_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_exploitable_template.txt @@ -3,6 +3,6 @@ set logging file $OUTFILE set logging on file $PROGRAM run $CMD_ARGS -source ~/bff/CERT_triage_tools/exploitable/exploitable.py +source $BFFDIR/CERT_triage_tools/exploitable/exploitable.py exploitable quit \ No newline at end of file From a5740255287455ef3cc0bb4a755318944a928483 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 18:48:44 -0500 Subject: [PATCH 0853/1169] Cleanup of config. Removal of some config options. Fewer absolute paths. --- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/debuggers/gdb.py | 3 +- src/certfuzz/fuzztools/process_killer.py | 52 ------ src/certfuzz/minimizer/minimizer_base.py | 3 +- src/certfuzz/testcase/testcase_linux.py | 2 +- src/linux/conf.d/bff.yaml | 203 +++++++++++++---------- src/windows/configs/examples/bff.yaml | 9 +- 7 files changed, 130 insertions(+), 144 deletions(-) delete mode 100644 src/certfuzz/fuzztools/process_killer.py diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index f66d380..ed883bb 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -122,7 +122,7 @@ def _cache_app(self): def _setup_watchdog(self): logger.debug('setup watchdog') # setup our watchdog file toucher - wdf = self.config['directories']['watchdog_file'] + wdf = '/tmp/bff_watchdog' TWDF.wdf = wdf TWDF.enable() diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index 37cbc5f..d6969dc 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -57,7 +57,8 @@ def _create_input_file(self): cmdargs = ' '.join(self.cmd_args) # Extract the bff directory out of the template location - bffdir = self.template.split('/certfuzz/', 1)[0] + bffdebuggersdir = os.path.dirname(os.path.realpath(__file__)) + bffdir = bffdebuggersdir.replace('/certfuzz/debuggers', '') new_script = s.safe_substitute(PROGRAM=self.program, CMD_ARGS=cmdargs, OUTFILE=self.outfile, BFFDIR=bffdir) diff --git a/src/certfuzz/fuzztools/process_killer.py b/src/certfuzz/fuzztools/process_killer.py deleted file mode 100644 index d4b27b1..0000000 --- a/src/certfuzz/fuzztools/process_killer.py +++ /dev/null @@ -1,52 +0,0 @@ -''' -Created on Oct 25, 2010 - -@organization: cert.org -''' -import logging -import subprocess -import os - -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - - -class ProcessKiller: - def __init__(self, killprocname, killproctimeout): - self.script = os.path.abspath(os.path.expanduser('~/bff/killproc.sh')) - self.killprocname = killprocname - self.killproctimeout = killproctimeout - - # if you don't have xterm... - # template = "bash %s %s %s &" etc - self.template = "xterm -geometry +0-0 -e bash {} {} {} &" - - self.cmdline = None - - def __enter__(self): - self._set_cmdline() - return self - - def __exit__(self, etype, value, traceback): - handled = False - - if etype is subprocess.CalledProcessError: - logger.warning('ProcessKiller startup failed: %s', value) - handled = True - elif etype is None: - logger.debug("Process killer started: %s %s %d", self.script, - self.killprocname, self.killproctimeout) - - return handled - - def _set_cmdline(self): - self.cmdline = self.template.format(self.script, self.killprocname, - self.killproctimeout) - - def go(self): - ''' - Spawns a separate process to kill out of control processes. - ''' - logger.debug('Running [%s]', self.cmdline) - pk_pid = subprocess.Popen(self.cmdline, shell=True).pid - return pk_pid diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 3451b0e..1f5d0b3 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -64,6 +64,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, self.logger = None self.log_file_hdlr = None self.backtracelevels = 5 + self.watchdogfile = '/tmp/bff_watchdog' logger.setLevel(logging.INFO) @@ -625,7 +626,7 @@ def go(self): if self.use_watchdog: # touch the watchdog file so we don't reboot during long minimizations - open(self.cfg['directories']['watchdog_file'], 'w').close() + open(self.watchdogfile, 'w').close() # Fix for BFF-208 if self._time_exceeded(): diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 1b339ee..b7cc811 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -66,7 +66,7 @@ def __exit__(self, etype, value, traceback): def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): dbg_template_name = '%s_%s_template.txt' % (self._debugger_cls._key, option) - self.debugger_template = os.path.join(self.cfg['directories']['debugger_template_dir'], dbg_template_name) + self.debugger_template = os.path.join('certfuzz/debuggers/templates', dbg_template_name) logger.debug('Debugger template set to %s', self.debugger_template) if not os.path.exists(self.debugger_template): raise CrashError('Debugger template does not exist at %s' % self.debugger_template) diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index f0d33bd..361656b 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -5,138 +5,167 @@ # ################################################################ # FUZZ CAMPAIGN SETTINGS +# +# id: +# used for identifying campaign, placement of results ################################################################ campaign: id: convert v5.2.0 + ################################################################ # TARGET APPLICATION INVOCATION OPTIONS: -################################################################ +# +# program: +# Path to fuzzing target executable +# +# cmdline_template: +# Used to specify the command-line invocation of the target # # Command-line arguments will be split using python shlex.split() so # be sure to add quotes where needed # $SEEDFILE will be replaced at runtime with the appropriate # seed file name. # Use quotes if the target application has spaces in the path -# examples: +################################################################ target: program: ~/convert cmdline_template: $PROGRAM $SEEDFILE /dev/null + ################################################################ # LOCATIONS FOR FUZZ RUN FILES: +# # Output files are placed in [outputdir]/[seedfile] -################################################################ -# Location of the fuzzing scripts -# (VMware shared directory if using UbuFuzz VM) - -# The location of the seed files - -# location of debugger templates - -# location of results: +# seedfile_dir: +# Location of seed files (relative to bff.py) +# +# working_dir: +# Temporary directory used by BFF. Use a ramdisk to reduce disk activity +# +# results_dir: +# Location of fuzzing results (relative to bff.py) +# # If results are stored in a shared location, # this directory needs to be unique for each fuzzing machine - -# dirs local to the fuzzing machine -# Local directory for fuzzing run mutated files. - -# Location of file used checked by Linux watchdog to determine -# if fuzzer is still running +################################################################ directories: - seedfile_dir: ~/bff/seedfiles/examples + seedfile_dir: seedfiles/examples working_dir: ~/fuzzing - results_dir: ~/results - debugger_template_dir: ~/bff/certfuzz/debuggers/templates - watchdog_file: /tmp/bff_watchdog + results_dir: results ################################################################ -# VERIFIER PARAMETERS -################################################################ +# FUZZ CAMPAIGN OPTIONS # -# Include backtrace frames that aren't part of a loaded library? -# Set this value to False if you wish to consider unmapped stack -# frames in the crash hashes. This can be useful for target application -# that perform JIT compilation - -# Save cases that cause failed ASSERTs? If set to 1, then __assert_fail termination, -# e.g. via assert(), it will be considered a crash. - -# Use valgrind (and callgrind) -# Note that valgrind can be slow. Disabling this option can improve throughput - -# Use PIN to get call traces for every crash, as opposed to just those -# that result in total stack corruption. -# PIN is even slower than valgrind - -# Obtain minimally-different testcase for each unique crash - -# Minimize to a metasploit string -# Note: this is in addition to minimize to seedfile if minimize_crashers=True -# Also, if minimize_to_string is true, then its minimized result will be used -# for all subsequent analyses (i.e., valgrind, callgrind, etc.) -# Disabled by default due to amount of time that the minimization takes. -# Stand-alone string minimization can be done using tools/minimize.py. - -# Save crashes even when they are unique? If set to True, all -# crashing inputs are saved, even when their backtraces are not -# unique. - -# Recycle crashing testcases as seed files for further fuzzing. -# This can improve the number of unique crash hashes found, however this may -# just demonstrate weaknesses in backtrace-based uniqueness determination -# rather than finding new underlying vulnerabilities. - -analyzer: - exclude_unmapped_frames: True - savefailedasserts: False - use_valgrind: True - use_pin_calltrace: False - # keep_duplicates: False -# maximum time (seconds) to let the program run to capture valgrind output: - valgrind_timeout: 120 - +# first_iteration: +# Iteration number to begin campaign on +# +# seed_interval: +# Number of times to mutate a seed file before moving to the next +# +# minimize: +# Create a file that is minimally-different than the seed file, yet crashes +# with the same hash +# +# minimizer_timeout: +# The maximum amount of time that BFF will spend on a minimization run before +# giving up +# +# recycle_crashers: +# Recycle uniquely-crashing testcases into the pool of available seed files +# to fuzz +# +# watchdogtimeout: +# Number of seconds that if exceeded, the watchdog will restart the fuzzing +# machine. Set to 0 to disable watchdog functionality. +# +################################################################ runoptions: first_iteration: 0 seed_interval: 20 minimize: True -# Minimization can sometimes take a long time, and time spent minimizing -# is time not spent fuzzing. If a minimization run exceeds this time -# (in seconds) the minimization will terminate (keeping whatever progress -# it has made at that point) and return to fuzzing. minimizer_timeout: 3600 minimize_to_string: False # keep_unique_faddr: False keep_duplicates: False recycle_crashers: False - -# maximum time (seconds) that process specified by "killprocname" option -# can run before it's killed via killproc.sh, -# which is used to kill stray processes. Normally, zzuf will kill -# the target process if the above timeout is reached. - killproctimeout: 130 -# Set value for the Linux watchdog timer. If watchdog_file is not touched -# for a period longer than this value (seconds), then the vm is rebooted -# by the watchdog. -# Set to 0 to disable watchdog functionality. watchdogtimeout: 3600 +################################################################ +# FUZZER OPTIONS +# +# fuzzer: +# Mutator to use +# +# fuzz_zip_container: +# rather than fuzzing zip file contents, fuzz the zip container itself +################################################################ fuzzer: fuzzer: bytemut fuzz_zip_container: False + + +################################################################ +# RUNNER OPTIONS +# +# runtimeout: +# maximum program execution time (seconds) that BFF will the target to execute +################################################################ runner: -# maximum program execution time (seconds) that BFF will allow: - runtimeout: 5 + runtimeout: 5 + + +################################################################ +# DEBUGGER OPTIONS +# +# runtimeout: +# Maximum time (seconds) to let the program run to capture debugger and +# CERT Triage Tools exploitable output +# +# backtracelevels: +# Number of backtrace frames to hash for uniqueness +# Increase this number for more crash uniqueness granularity. +# Decrease this number if you think that you are getting too many duplicate +# crashes. +################################################################ debugger: -# maximum time (seconds) to let the program run to capture debugger and -# CERT Triage Tools exploitable output. runtimeout: 60 -# number of backtrace levels to hash for uniqueness. -# Increase this number for more crash uniqueness granularity. -# Decrease this number if you think that you are getting too many -# duplicate crashes. backtracelevels: 5 + +################################################################ +# VERIFIER PARAMETERS +# +# exclude_unmapped_frames: +# Include backtrace frames that aren't part of a loaded library? +# Set this value to False if you wish to consider unmapped stack +# frames in the crash hashes. This can be useful for target application +# that perform JIT compilation +# +# savefailedasserts: +# Save cases that cause failed ASSERTs? If set to 1, then __assert_fail termination, +# e.g. via assert(), it will be considered a crash. +# +# use_valgrind: +# Use valgrind (and callgrind) +# Note that valgrind can be slow. Disabling this option can improve throughput +# +# use_pin_calltrace: +# Use PIN to get call traces for every crash, as opposed to just those +# that result in total stack corruption. +# PIN is even slower than valgrind +# +# valgrind_timeout: +# Number of seconds to allow valgrind to run before giving up on it +################################################################ +analyzer: + exclude_unmapped_frames: True + savefailedasserts: False + use_valgrind: True + use_pin_calltrace: False + # keep_duplicates: False + valgrind_timeout: 120 + diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 502adbc..b1d680b 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -11,6 +11,7 @@ campaign: keep_heisenbugs: False use_buttonclicker: False + ##################################################################### # Fuzz target options: # @@ -21,6 +22,7 @@ campaign: target: program: C:\BFF\imagemagick\convert.exe cmdline_template: $PROGRAM $SEEDFILE NUL + # With the default ImageMagick fuzz run, the above target options # will result in the following invocation of ImageMagick: # C:\BFF\imagemagick\convert.exe NUL @@ -42,6 +44,7 @@ target: # cmdline_template: $PROGRAM -in $SEEDFILE -out c:/some/path/to/file # cmdline_template: $PROGRAM -in $SEEDFILE -out "c:\some path\to file" + ##################################################################### # Directories used by BFF # @@ -52,9 +55,10 @@ target: ##################################################################### directories: seedfile_dir: seedfiles\examples - working_dir: C:\BFF\fuzzdir + working_dir: fuzzdir results_dir: results + ##################################################################### # Fuzz run options # @@ -85,6 +89,7 @@ runoptions: keep_duplicates: False recycle_crashers: False + ##################################################################### # Fuzzer options # @@ -122,6 +127,7 @@ fuzzer: # - [0x1000, 0x100F] fuzz_zip_container: False + ##################################################################### # Runner options # @@ -138,6 +144,7 @@ runner: runtimeout: 5 watchcpu: Auto + ##################################################################### # Debugger options # From e22ecca0d1c65d75d73d1514f0d31a661e916406 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 18:52:44 -0500 Subject: [PATCH 0854/1169] Remove unit test for process killer --- .../fuzztools/test_process_killer.py | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 src/test_certfuzz/fuzztools/test_process_killer.py diff --git a/src/test_certfuzz/fuzztools/test_process_killer.py b/src/test_certfuzz/fuzztools/test_process_killer.py deleted file mode 100644 index 5578083..0000000 --- a/src/test_certfuzz/fuzztools/test_process_killer.py +++ /dev/null @@ -1,28 +0,0 @@ -''' -Created on Apr 8, 2011 - -@organization: cert.org -''' -import unittest -from certfuzz.fuzztools.process_killer import ProcessKiller - - -class Test(unittest.TestCase): - - def setUp(self): - self.pk = ProcessKiller(*tuple('bc')) - - def tearDown(self): - pass - - def test_get_cmdline(self): - self.pk._set_cmdline() - self.assertTrue('b c' in self.pk.cmdline) - - def test_spawn_process_killer(self): - # cannot test directly, see test_get_spawn_process_killer_cmdline() - pass - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From b2f4cc2e28384689af022f9af8a5a7c36ce94dd5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 20:20:27 -0500 Subject: [PATCH 0855/1169] Allow batch.sh to be called from other directories. --- src/linux/batch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index ad9f55d..7a1edb2 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -46,7 +46,7 @@ # contract clause at 252.227.7013. ############################################################################## -scriptlocation=`pwd` +scriptlocation=`echo "$(cd "$(dirname "$0")"; pwd)/"` echo Script location: $scriptlocation/bff.py platform=`uname -a` PINURL=http://software.intel.com/sites/landingpage/pintool/downloads/pin-2.12-58423-gcc.4.4.7-linux.tar.gz From 01e88fe34205c3dbd036e6e9f2cdee9f31ed1d78 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 21:18:19 -0500 Subject: [PATCH 0856/1169] Make sure internal killall function strips path off of the processname to kill. --- src/certfuzz/fuzztools/subprocess_helper.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 9638fee..ed9f60b 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -107,6 +107,12 @@ def _kill(p, returncode, progname): #@UnusedVariable def killall(processname, killsignal): + ''' + Python equivalent of the killall command + @param processname: process name to kill + @param killsignal: signal to send to process + ''' + processname = os.path.basename(processname) assert (processname != ''), "Cannot kill a blank process name" if (on_osx()): os.system('killall -%d %s' % (killsignal, processname)) From 039a49fd06d6ac33c4551c50762e5c78e60ec75b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 15 Feb 2016 23:20:01 -0500 Subject: [PATCH 0857/1169] Only restart watchdog on UbuFuzz or DebianFuzz. BFF-803 --- src/certfuzz/campaign/campaign_linux.py | 38 +++++++++++++++++++------ 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index ed883bb..bee9cd5 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -21,6 +21,7 @@ from certfuzz.iteration.iteration_linux import LinuxIteration from certfuzz.fuzztools.command_line_templating import get_command_args_list from certfuzz.fuzzers.errors import FuzzerExhaustedError +from certfuzz.fuzztools.filetools import read_text_file logger = logging.getLogger(__name__) @@ -121,16 +122,35 @@ def _cache_app(self): def _setup_watchdog(self): logger.debug('setup watchdog') - # setup our watchdog file toucher - wdf = '/tmp/bff_watchdog' + if self._use_watchdog(): + # setup our watchdog file toucher + wdf = '/tmp/bff_watchdog' + + TWDF.wdf = wdf + TWDF.enable() + touch_watchdog_file() + + # set up the watchdog timeout within the VM and restart the daemon + with WatchDog(wdf, self.config['runoptions']['watchdogtimeout']) as watchdog: + watchdog() + + def _check_hostname(self): + hostname = 'System' + try: + hostname = read_text_file('/etc/hostname') + except: + logger.debug('Error determining hostname') + return hostname + + def _use_watchdog(self): + hostname = self._check_hostname() + if 'UbuFuzz' in hostname or 'DebianFuzz' in hostname: + logger.debug('%s is watchdog compatible' % hostname) + return True + else: + logger.debug('%s is not watchdog compatible' % hostname) + return False - TWDF.wdf = wdf - TWDF.enable() - touch_watchdog_file() - - # set up the watchdog timeout within the VM and restart the daemon - with WatchDog(wdf, self.config['runoptions']['watchdogtimeout']) as watchdog: - watchdog() def _check_for_script(self): logger.debug('check for script') From dbda04874eb85811532f9d54e63852507834552c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 16 Feb 2016 08:26:04 -0500 Subject: [PATCH 0858/1169] Strip newline off of hostname. --- src/certfuzz/campaign/campaign_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index bee9cd5..036a27e 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -137,7 +137,7 @@ def _setup_watchdog(self): def _check_hostname(self): hostname = 'System' try: - hostname = read_text_file('/etc/hostname') + hostname = read_text_file('/etc/hostname').rstrip() except: logger.debug('Error determining hostname') return hostname From 87e133b82e7aae651370aba0ab058e3590745fde Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 17 Feb 2016 11:21:31 -0500 Subject: [PATCH 0859/1169] Use process group and kill that, rather than just the direct PID and killall. BFF-904 --- src/certfuzz/fuzztools/subprocess_helper.py | 44 +++------------------ 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index ed9f60b..a9ed587 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -71,9 +71,10 @@ def run_with_timer(args, timeout, progname, use_shell=False, **options): try: if _seeoutput: - p = subprocess.Popen(args, env=env, shell=use_shell) + # os.setsid sets process group + p = subprocess.Popen(args, env=env, shell=use_shell, preexec_fn=os.setsid) else: - p = subprocess.Popen(args, stdout=output, stderr=errors, env=env, shell=use_shell) + p = subprocess.Popen(args, stdout=output, stderr=errors, env=env, shell=use_shell, preexec_fn=os.setsid) except: print "Failed to run [%s]" % ' '.join(args) sys.exit(-1) @@ -100,41 +101,6 @@ def _kill(p, returncode, progname): #@UnusedVariable ret = kernel32.TerminateProcess(handle, returncode) kernel32.CloseHandle(handle) else: - ret = p.kill() - if(progname): - killall(progname, signal.SIGKILL) + # Kill process group + ret = os.killpg(os.getpgid(p.pid), signal.SIGKILL) return (0 != ret) - - -def killall(processname, killsignal): - ''' - Python equivalent of the killall command - @param processname: process name to kill - @param killsignal: signal to send to process - ''' - processname = os.path.basename(processname) - assert (processname != ''), "Cannot kill a blank process name" - if (on_osx()): - os.system('killall -%d %s' % (killsignal, processname)) - else: - for folder in os.listdir("/proc"): - filename = os.path.join("/proc", folder, "cmdline") - - if not os.access(filename, os.R_OK): - # we don't have read access, so skip it - continue - try: - exename = os.path.basename(file(filename).read().split("\x00")[0]) - except IOError: - # just skip it if the filename isn't there anymore - continue - - if exename != processname: - continue - elif (exename.find(processname) == -1): - continue - try: - os.kill(int(folder), killsignal) - except OSError: - # skip it if the process has gone away on its own - continue From f3bba5d1201112baab7da03706b3e4b91f899a94 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 17 Feb 2016 11:21:59 -0500 Subject: [PATCH 0860/1169] Use proper target commandline for minimizer invocation. --- src/certfuzz/minimizer/minimizer_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 1f5d0b3..ecb108b 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -388,6 +388,7 @@ def run_debugger(self, infile, outfile): self.debugger_runs += 1 cmd_args = get_command_args_list(self.cfg['target']['cmdline_template'], infile)[1] cmd = cmd_args[0] + cmd_args = cmd_args[1:] try: exclude_unmapped_frames = self.cfg['analyzer']['exclude_unmapped_frames'] From 2b156dbfae0f54851bd67dae0f46212c6d6f5aa0 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 17 Feb 2016 14:51:48 -0500 Subject: [PATCH 0861/1169] Don't attempt watchdog operations on platforms where it's not used. --- src/certfuzz/campaign/campaign_linux.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 036a27e..647c2ec 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -62,7 +62,19 @@ def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) self.runner_module_name = 'certfuzz.runners.zzufrun' self.debugger_module_name = 'certfuzz.debuggers.gdb' + self.watchdogfile = '/tmp/bff_watchdog' + self._use_watchdog = self._check_watchdog_compat() + def _check_touch_watchdog(self): + if self._use_watchdog: + touch_watchdog_file() + + def _remove_watchdog_file(self): + try: + os.remove(self.watchdogfile) + except OSError: + # No watchdog file to remove + pass def _full_path_original(self, seedfile): # yes, two seedfile mentions are intended - adh @@ -89,7 +101,7 @@ def _post_enter(self): self._cache_app() def _pre_exit(self): - pass + self._remove_watchdog_file() def _set_unbuffered_stdout(self): ''' @@ -122,9 +134,9 @@ def _cache_app(self): def _setup_watchdog(self): logger.debug('setup watchdog') - if self._use_watchdog(): + if self._use_watchdog: # setup our watchdog file toucher - wdf = '/tmp/bff_watchdog' + wdf = self.watchdogfile TWDF.wdf = wdf TWDF.enable() @@ -142,9 +154,9 @@ def _check_hostname(self): logger.debug('Error determining hostname') return hostname - def _use_watchdog(self): - hostname = self._check_hostname() - if 'UbuFuzz' in hostname or 'DebianFuzz' in hostname: + def _check_watchdog_compat(self): + hostname = self._check_hostname().lower() + if 'ubufuzz' in hostname: logger.debug('%s is watchdog compatible' % hostname) return True else: From 2799ae5ed5075bfb0b5b05144a82a455d117d439 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 17 Feb 2016 15:01:24 -0500 Subject: [PATCH 0862/1169] Use TWDF's built-in disable on platforms where it's not used. --- src/certfuzz/campaign/campaign_linux.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 647c2ec..1538fc8 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -65,10 +65,6 @@ def __init__(self, config_file, result_dir=None, debug=False): self.watchdogfile = '/tmp/bff_watchdog' self._use_watchdog = self._check_watchdog_compat() - def _check_touch_watchdog(self): - if self._use_watchdog: - touch_watchdog_file() - def _remove_watchdog_file(self): try: os.remove(self.watchdogfile) @@ -134,17 +130,18 @@ def _cache_app(self): def _setup_watchdog(self): logger.debug('setup watchdog') + # setup our watchdog file toucher + wdf = self.watchdogfile + TWDF.wdf = wdf if self._use_watchdog: - # setup our watchdog file toucher - wdf = self.watchdogfile - - TWDF.wdf = wdf TWDF.enable() touch_watchdog_file() # set up the watchdog timeout within the VM and restart the daemon with WatchDog(wdf, self.config['runoptions']['watchdogtimeout']) as watchdog: watchdog() + else: + TWDF.disable() def _check_hostname(self): hostname = 'System' From 6cd8bb86fdf6c24e88318adf158ab2b0e6c1fc75 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 17 Feb 2016 15:30:09 -0500 Subject: [PATCH 0863/1169] We do seem to need a killall capability. Otherwise there can be stray processes during minimization. --- src/certfuzz/fuzztools/subprocess_helper.py | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index a9ed587..964e318 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -103,4 +103,39 @@ def _kill(p, returncode, progname): #@UnusedVariable else: # Kill process group ret = os.killpg(os.getpgid(p.pid), signal.SIGKILL) + if progname: + killall(progname, signal.SIGKILL) return (0 != ret) + + +def killall(processname, killsignal): + ''' + Python equivalent of the killall command + @param processname: process name to kill + @param killsignal: signal to send to process + ''' + assert (processname != ''), "Cannot kill a blank process name" + if (on_osx()): + os.system('killall -%d %s' % (killsignal, processname)) + else: + for folder in os.listdir("/proc"): + filename = os.path.join("/proc", folder, "cmdline") + + if not os.access(filename, os.R_OK): + # we don't have read access, so skip it + continue + try: + exename = file(filename).read().split("\x00")[0] + except IOError: + # just skip it if the filename isn't there anymore + continue + + if exename != processname: + continue + elif (exename.find(processname) == -1): + continue + try: + os.kill(int(folder), killsignal) + except OSError: + # skip it if the process has gone away on its own + continue From 007081e0fc3bf6f0a45602f99ce5a45dfae81064 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 17 Feb 2016 16:32:14 -0500 Subject: [PATCH 0864/1169] Set KDE_DEBUG=1. BFF-857 --- src/certfuzz/campaign/campaign_linux.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 1538fc8..77291ee 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -88,7 +88,7 @@ def _pre_enter(self): self._check_for_script() self._check_for_redirect() self._set_unbuffered_stdout() - + self._setup_environment() def _post_enter(self): if self.config['runoptions']['watchdogtimeout']: @@ -160,6 +160,8 @@ def _check_watchdog_compat(self): logger.debug('%s is not watchdog compatible' % hostname) return False + def _setup_environment(self): + os.environ['KDE_DEBUG'] = '1' def _check_for_script(self): logger.debug('check for script') @@ -204,7 +206,8 @@ def _save_state(self): pass def _do_iteration(self, seedfile, range_obj, seednum): - # Prevent watchdog from rebooting VM. If /tmp/fuzzing exists and is stale, the machine will reboot + # Prevent watchdog from rebooting VM. + # If /tmp/fuzzing exists and is stale, the machine will reboot touch_watchdog_file() with LinuxIteration(seedfile=seedfile, seednum=seednum, From 398cf0c04a300d8d6b8091b8fd599eea78af8011 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 17 Feb 2016 23:46:37 -0500 Subject: [PATCH 0865/1169] Trim progname off of cmd_args at the iteration level, not the debugger level. --- src/certfuzz/debuggers/msec.py | 2 +- src/certfuzz/testcase/testcase_windows.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 44d3fd0..eeb0cd1 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -75,7 +75,7 @@ def _get_cmdline(self, outfile): cdb_command = 'g;' + cdb_command args.append(cdb_command) args.append(self.program) - args.extend(self.cmd_args[1:]) + args.extend(self.cmd_args) for l in pformat(args).splitlines(): logger.debug('dbg_args: %s', l) return args diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 7242261..5a25c90 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -103,7 +103,7 @@ def update_crash_details(self): fname = self._get_file_basename() outfile_base = os.path.join(self.tempdir, fname) # Regenerate target commandline with new crasher file - self.cmdargs = get_command_args_list(self.cmd_template, outfile_base)[1] + self.cmdargs = get_command_args_list(self.cmd_template, outfile_base)[1][1:] self.debug() self._rename_fuzzed_file() self._rename_dbg_file() @@ -217,9 +217,9 @@ def _get_output_dir(self, target_base): logger.debug('signature: %s', self.signature) self.target_dir = os.path.join(target_base, 'crashers', self.signature) - if len(self.target_dir) > 170: + if len(self.target_dir) > 160: # Don't make a path too deep. Windows won't support it - self.target_dir = self.target_dir[:170] + '__' + self.target_dir = self.target_dir[:160] + '__' logger.debug('target_dir: %s', self.target_dir) return self.target_dir From 79914a54545e6946394deeec4857c2b4f750a622 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 18 Feb 2016 10:43:58 -0500 Subject: [PATCH 0866/1169] Rename exceptions beyond the first. BFF-269 --- src/certfuzz/testcase/testcase_windows.py | 39 ++++++++++++----------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 5a25c90..8095d85 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -241,28 +241,31 @@ def _rename_fuzzed_file(self): self.fuzzedfile = BasicFile(new_fuzzed_file) def _rename_dbg_file(self): - if self.faddr: - faddr_str = '%s' % self.faddr - else: - faddr_str = '' - (path, basename) = os.path.split(self.dbg_file) (basename, dbgext) = os.path.splitext(basename) (root, ext) = os.path.splitext(basename) + for exception_num in range(0, self.exception_depth + 1): + if exception_num > 0: + new_basename = root + ext + '.e%s' % exception_num + dbgext + self.dbg_file = os.path.join(path, new_basename) + if self.faddr: + faddr_str = '%s' % self.parsed_outputs[exception_num].faddr + else: + faddr_str = '' - exp_str = short_exp[self.exp] + exp_str = short_exp[self.parsed_outputs[exception_num].exp] - parts = [root] - if faddr_str: - parts.append(faddr_str) - if exp_str: - parts.append(exp_str) - new_basename = '-'.join(parts) + ext + dbgext + parts = [root] + if faddr_str: + parts.append(faddr_str) + if exp_str: + parts.append(exp_str) + new_basename = '-'.join(parts) + ext + '.e%s' % exception_num + dbgext - new_dbg_file = os.path.join(path, new_basename) + new_dbg_file = os.path.join(path, new_basename) - # best_effort move returns a tuple of booleans indicating (copied, deleted) - # we only care about copied - copied = best_effort_move(self.dbg_file, new_dbg_file)[0] - if copied: - self.dbg_file = new_dbg_file + # best_effort move returns a tuple of booleans indicating (copied, deleted) + # we only care about copied + copied = best_effort_move(self.dbg_file, new_dbg_file)[0] + if copied: + self.dbg_file = new_dbg_file From cfc61c3ab0a732ea18ced987f02e3faf3982421d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 18 Feb 2016 11:39:27 -0500 Subject: [PATCH 0867/1169] Fix order of methods to find testcases based on msec files. Report text has to be read before trying harder to find files. BFF-913 --- src/certfuzz/drillresults/testcasebundle_base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index ba1d8d4..40b32e0 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -29,10 +29,6 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self.dbg_outfile = dbg_outfile self.testcase_file = testcase_file - self._find_testcase_file() - - self._verify_files_exist() - self.crash_hash = crash_hash self.ignore_jit = ignore_jit @@ -41,6 +37,8 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self.regdict = {} self.reporttext = read_text_file(self.dbg_outfile) + self._find_testcase_file() + self._verify_files_exist() # Read in the fuzzed file self.crasherdata = read_bin_file(self.testcase_file) self.current_dir = os.path.dirname(self.dbg_outfile) From d363d382bf6a9e2409e0b0fe01d7be53f2418a00 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 18 Feb 2016 16:21:16 -0500 Subject: [PATCH 0868/1169] Don't rename debugger output files if they don't represent a crash. --- src/certfuzz/testcase/testcase_windows.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 8095d85..e012853 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -241,6 +241,9 @@ def _rename_fuzzed_file(self): self.fuzzedfile = BasicFile(new_fuzzed_file) def _rename_dbg_file(self): + if not self.faddr: + return + (path, basename) = os.path.split(self.dbg_file) (basename, dbgext) = os.path.splitext(basename) (root, ext) = os.path.splitext(basename) @@ -248,11 +251,11 @@ def _rename_dbg_file(self): if exception_num > 0: new_basename = root + ext + '.e%s' % exception_num + dbgext self.dbg_file = os.path.join(path, new_basename) - if self.faddr: - faddr_str = '%s' % self.parsed_outputs[exception_num].faddr - else: - faddr_str = '' + if not self.parsed_outputs[exception_num].is_crash: + return + + faddr_str = '%s' % self.parsed_outputs[exception_num].faddr exp_str = short_exp[self.parsed_outputs[exception_num].exp] parts = [root] From f3b3b389e2f66e9ddab6cbc20071b5204f5c8047 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 19 Feb 2016 08:26:46 -0500 Subject: [PATCH 0869/1169] Include subsequent exceptions for the same fuzzed file in drillresults. BFF-914 --- src/certfuzz/drillresults/result_driller_windows.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index f038c66..5062bd3 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) regex = { - 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]'), + 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]+.+e0.+'), 'msec_report': re.compile('.+.msec$'), } @@ -34,6 +34,7 @@ def _platform_find_testcases(self, crash_hash, files, root): crasherfile = crasherfile.replace('-PEX', '') crasherfile = crasherfile.replace('-PNE', '') crasherfile = crasherfile.replace('-UNK', '') + crasherfile = crasherfile.replace('.e0', '') for current_file in files: # Go through all of the .msec files and parse them if regex['msec_report'].match(current_file): @@ -43,4 +44,12 @@ def _platform_find_testcases(self, crash_hash, files, root): with TestCaseBundle(dbg_file, crasherfile, crash_hash, self.ignore_jit) as tcb: tcb.go() - self.testcase_bundles.append(tcb) + _updated_existing = False + for index, tcbundle in enumerate(self.testcase_bundles): + if tcbundle.crash_hash == crash_hash: + # This is a new exception for the same crash hash + self.testcase_bundles[index].details['exceptions'].update(tcb.details['exceptions']) + _updated_existing = True + if not _updated_existing: + # This is a new crash hash + self.testcase_bundles.append(tcb) From 2422779ce04c14765b3cf5858e23c9478f09e8a2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 19 Feb 2016 15:42:25 -0500 Subject: [PATCH 0870/1169] Only record tries for seedfiles that haven't been removed. BFF-912 --- src/certfuzz/iteration/iteration_base3.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index 2464012..cfb005d 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -95,7 +95,7 @@ def __exit__(self, etype, value, traceback): # clean up rm_rf(self.working_dir) - + return handled def _pre_fuzz(self): @@ -123,7 +123,7 @@ def _run(self): def _post_run(self): pass - + def construct_testcase(self): ''' If the runner saw a crash, construct a test case @@ -131,7 +131,7 @@ def construct_testcase(self): ''' if not self.runner.saw_crash: return - + logger.debug('Building testcase object') self._construct_testcase() @@ -167,9 +167,11 @@ def record_failure(self): self.record_tries() def record_tries(self): - self.sf_set.record_tries(key=self.seedfile.md5, tries=1) - if hasattr(self.r, 'id'): - self.seedfile.rangefinder.record_tries(key=self.r.id, tries=1) + if self.seedfile.md5 in self.sf_set.arms: + # Only record tries for seedfiles that haven't been removed + self.sf_set.record_tries(key=self.seedfile.md5, tries=1) + if hasattr(self.r, 'id'): + self.seedfile.rangefinder.record_tries(key=self.r.id, tries=1) def process_testcases(self): if not len(self.testcases): From 0ade4725b13f9c2886aadb70506a723d52144126 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 19 Feb 2016 15:45:19 -0500 Subject: [PATCH 0871/1169] Stop campaign cleanly when out of seedfiles. BFF-915 --- src/certfuzz/campaign/campaign_base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index f8d9701..e70f5db 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -16,6 +16,7 @@ from certfuzz.campaign.errors import CampaignError from certfuzz.file_handlers.seedfile_set import SeedfileSet +from certfuzz.file_handlers.errors import SeedfileSetError from certfuzz.fuzztools import filetools from certfuzz.runners.errors import RunnerArchitectureError, \ RunnerPlatformVersionError @@ -184,6 +185,9 @@ def _handle_common_errors(self, etype, value, mytraceback): elif etype is RunnerPlatformVersionError: logger.error('Unsupported platform: %s', value) handled = True + elif etype is SeedfileSetError: + logger.error('No seedfiles available') + handled = True return handled def _handle_errors(self, etype, value, mytraceback): @@ -420,3 +424,4 @@ def go(self): signal.signal(signal.SIGINT, self.signal_handler) while self._keep_going(): self._do_interval() + From 37cc8093fe378e661952bb502d0e28c00520ffa0 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 19 Feb 2016 15:45:45 -0500 Subject: [PATCH 0872/1169] Shorter max directory depth to allow for dev work using hgfs. --- src/certfuzz/reporters/copy_files.py | 3 +++ src/certfuzz/testcase/testcase_windows.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/reporters/copy_files.py b/src/certfuzz/reporters/copy_files.py index 780a37a..ba5a133 100644 --- a/src/certfuzz/reporters/copy_files.py +++ b/src/certfuzz/reporters/copy_files.py @@ -30,6 +30,9 @@ def __init__(self, testcase, target_dir): def go(self): dst_dir = os.path.join(self.target_dir, self.testcase.signature) + if len(dst_dir) > 130: + # Don't make a path too deep. Windows won't support it + dst_dir = dst_dir[:130] + '__' # ensure target dir exists already (it might because of crash logging) filetools.mkdir_p(dst_dir) diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index e012853..83e3169 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -217,9 +217,9 @@ def _get_output_dir(self, target_base): logger.debug('signature: %s', self.signature) self.target_dir = os.path.join(target_base, 'crashers', self.signature) - if len(self.target_dir) > 160: + if len(self.target_dir) > 130: # Don't make a path too deep. Windows won't support it - self.target_dir = self.target_dir[:160] + '__' + self.target_dir = self.target_dir[:130] + '__' logger.debug('target_dir: %s', self.target_dir) return self.target_dir From 7ac6809435788011aae06a895cfcfa1b66e97f43 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 19 Feb 2016 16:11:02 -0500 Subject: [PATCH 0873/1169] Cancel timer on KeyboardInterrupt. BFF-916 --- src/certfuzz/fuzztools/subprocess_helper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 964e318..4b5a65f 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -83,7 +83,10 @@ def run_with_timer(args, timeout, progname, use_shell=False, **options): # Give extra time for the first invocation of the application t = Timer(timeout, _kill, args=[p, 0x00, progname]) t.start() - p.wait() + try: + p.wait() + except KeyboardInterrupt: + raise t.cancel() # close our stdout and stderr filehandles From 019f9c5500312e611f24fc674f2880a00b572cf2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 10:41:49 -0500 Subject: [PATCH 0874/1169] Fix unit test for testcasebundle --- src/certfuzz/drillresults/testcasebundle_base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 40b32e0..1c62d0b 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -36,6 +36,9 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self.regdict = {} + if not os.path.exists(self.dbg_outfile): + raise TestCaseBundleError + self.reporttext = read_text_file(self.dbg_outfile) self._find_testcase_file() self._verify_files_exist() From 394e354cf1c658d2d16314cb1b53e2201d522da6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 15:36:50 -0500 Subject: [PATCH 0875/1169] Remove Minimize to String from BFF (+8 squashed commits) It's still available in the standalone minimizer as a command-line option. Squashed commits: [99aeb58] make it run [761da98] remove min2string from configs [a60857c] refactor post_minimize method into base class [d12b9ae] clean up imports [4312068] remove minimize to string [0a3f7a8] use base class _minimize method instead [b4aaa4c] cherry pick [368c883] remove minimize to string --- src/certfuzz/iteration/iteration_base3.py | 1 - src/certfuzz/iteration/iteration_linux.py | 2 - src/certfuzz/iteration/iteration_windows.py | 3 +- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 47 ++++++++++++++++-- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 48 ++----------------- .../tc_pipeline/tc_pipeline_windows.py | 43 ++--------------- src/linux/conf.d/bff.yaml | 1 - src/test_certfuzz/mocks.py | 3 ++ .../tc_pipeline/test_tc_pipeline_base.py | 7 +++ 9 files changed, 61 insertions(+), 94 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index cfb005d..c65016e 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -185,7 +185,6 @@ def process_testcases(self): options=self.pipeline_options, outdir=self.outdir, workdirbase=self.working_dir, - minimizable=self.pipeline_options['minimizable'], sf_set=self.sf_set) as pipeline: pipeline.go() diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 936cb6b..f99096a 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -50,8 +50,6 @@ def __init__(self, self.pipeline_options = {'use_valgrind': self.cfg['analyzer']['use_valgrind'], 'use_pin_calltrace': self.cfg['analyzer']['use_pin_calltrace'], - 'minimize_crashers': self.cfg['runoptions']['minimize'], - 'minimize_to_string': self.cfg['runoptions']['minimize_to_string'], 'uniq_log': os.path.join(self.cfg['directories']['results_dir'], 'uniquelog.txt'), 'local_dir': fixup_path(self.cfg['directories']['working_dir']), 'minimizertimeout': self.cfg['runoptions']['minimizer_timeout'], diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 488d78a..837d76f 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -72,10 +72,9 @@ def __init__(self, self.pipeline_options = {'keep_duplicates': self.cfg['runoptions'].get('keep_duplicates', False), 'keep_heisenbugs': self.cfg['runoptions'].get('keep_heisenbugs', False), - 'minimizable': False, 'cmd_template': self.cmd_template, 'null_runner': self.runner_cls.is_nullrunner, - 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize', False) + 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize', False), } def __exit__(self, etype, value, traceback): diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index ed10f87..8d1a5ec 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -13,6 +13,7 @@ from certfuzz.fuzztools import filetools from certfuzz.helpers.coroutine import coroutine from certfuzz.file_handlers.tmp_reaper import TmpReaper +from certfuzz.minimizer.errors import MinimizerError logger = logging.getLogger(__name__) @@ -26,7 +27,7 @@ class TestCasePipelineBase(object): pipes = ['verify', 'minimize', 'analyze', 'report'] def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, - outdir=None, workdirbase=None, minimizable=None, sf_set=None): + outdir=None, workdirbase=None, sf_set=None): ''' Constructor ''' @@ -37,7 +38,6 @@ def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, self.tc_dir = os.path.join(self.outdir, 'crashers') self.working_dir = workdirbase - self.minimizable = minimizable self.sf_set = sf_set self.tc_candidate_q = Queue.Queue() @@ -62,6 +62,10 @@ def __enter__(self): def __exit__(self, etype, value, traceback): TmpReaper().clean_tmp() + @abc.abstractproperty + def _minimizer_cls(self): + pass + @abc.abstractmethod def _setup_analyzers(self): pass @@ -72,7 +76,7 @@ def _setup_analysis_pipeline(self): logger.debug('Construct analysis pipeline') setup_order = list(self.pipes) - if not self.minimizable: + if not self.options['minimizable']: setup_order.remove('minimize') setup_order.reverse() @@ -179,7 +183,6 @@ def _post_verify(self, testcase): def _pre_minimize(self, testcase): pass - @abc.abstractmethod def _minimize(self, testcase): ''' try to reduce the Hamming Distance between the testcase file and the @@ -188,9 +191,43 @@ def _minimize(self, testcase): :param testcase: the testcase to work on ''' + logger.info('Minimizing testcase %s', testcase.signature) + logger.debug('config = %s', self.cfg) + + if not self.options.get('minimizable'): + # short-circuit if not minimizing + return + + # build arguments for minimizer invocation + kwargs = {'cfg': self.cfg, + 'crash': testcase, + 'seedfile_as_target': False, + 'bitwise': False, + 'confidence': 0.999, + 'tempdir': self.working_dir, + 'maxtime': self.cfg['runoptions']['minimizer_timeout'], + } + + try: + with self._minimizer_cls(**kwargs) as m: + m.go() + for new_tc in m.other_crashes.values(): + self.tc_candidate_q.put(new_tc) + except MinimizerError as e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) + + # calculate the hamming distances for this crash + # between the original seedfile and the minimized fuzzed file + testcase.calculate_hamming_distances() def _post_minimize(self, testcase): - pass + if self.cfg['runoptions']['recycle_crashers']: + logger.debug('Recycling crash as seedfile') + iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext + crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) + filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) + self.sf_set.add_file(crasherseed_path) def _pre_analyze(self, testcase): pass diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index aee4faf..5436f4f 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -17,13 +17,11 @@ from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file from certfuzz.fuzztools import filetools -from certfuzz.minimizer.errors import MinimizerError -from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer +from certfuzz.minimizer.unix_minimizer import UnixMinimizer from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.reporters.copy_files import CopyFilesReporter from certfuzz.reporters.testcase_logger import TestcaseLoggerReporter - logger = logging.getLogger(__name__) @@ -36,6 +34,7 @@ def get_uniq_logger(logfile): class LinuxTestCasePipeline(TestCasePipelineBase): + _minimizer_cls = UnixMinimizer def _setup_analyzers(self): self.analyzer_classes.append(stderr.StdErr) @@ -84,22 +83,8 @@ def _verify(self, testcase): def _post_verify(self, testcase): testcase.get_logger() - def _minimize(self, testcase): - if self.options.get('minimize_crashers'): - touch_watchdog_file() - self._minimize_to_seedfile(testcase) - if self.options.get('minimize_to_string'): - touch_watchdog_file() - self._minimize_to_string(testcase) - - def _post_minimize(self, testcase): - if self.cfg['runoptions']['recycle_crashers']: - logger.debug('Recycling crash as seedfile') - iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext - crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) - filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) - self.sf_set.add_file(crasherseed_path) + def _pre_minimize(self, testcase): + touch_watchdog_file() def _pre_analyze(self, testcase): # get one last debugger output for the newly minimized file @@ -163,28 +148,3 @@ def _post_report(self, testcase): # clean up testcase.delete_files() - def _minimize_to_seedfile(self, testcase): - self._minimize_generic(testcase, sftarget=True, confidence=0.999) - # calculate the hamming distances for this crash - # between the original seedfile and the minimized fuzzed file - testcase.calculate_hamming_distances() - - def _minimize_to_string(self, testcase): - self._minimize_generic(testcase, sftarget=False, confidence=0.9) - - def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): - try: - with Minimizer(cfg=self.cfg, - crash=testcase, - bitwise=False, - seedfile_as_target=sftarget, - confidence=confidence, - tempdir=self.options.get('local_dir'), - maxtime=self.options.get('minimizertimeout'), - ) as m: - m.go() - for new_tc in m.other_crashes.values(): - self.tc_candidate_q.put(new_tc) - except MinimizerError as e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) - m = None diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 24fc941..1617abe 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -6,23 +6,21 @@ import logging import os -from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer +from certfuzz.minimizer.win_minimizer import WindowsMinimizer from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.fuzztools import filetools -from certfuzz.minimizer.errors import MinimizerError from certfuzz.reporters.copy_files import CopyFilesReporter -from certfuzz.fuzztools.command_line_templating import get_command_args_list -from certfuzz.analyzers import stderr -from certfuzz.analyzers.errors import AnalyzerEmptyOutputError logger = logging.getLogger(__name__) class WindowsTestCasePipeline(TestCasePipelineBase): + _minimizer_cls = WindowsMinimizer + def _setup_analyzers(self): pass - #self.analyzer_classes.append(stderr.StdErr) + # self.analyzer_classes.append(stderr.StdErr) def _pre_verify(self, testcase): # pretty-print the testcase for debugging @@ -51,39 +49,6 @@ def _verify(self, testcase): testcase.should_proceed_with_analysis = True self.success = True - def _minimize(self, testcase): - logger.info('Minimizing testcase %s', testcase.signature) - logger.debug('config = %s', self.cfg) - - kwargs = {'cfg': self.cfg, - 'crash': testcase, - 'seedfile_as_target': True, - 'bitwise': False, - 'confidence': 0.999, - 'tempdir': self.working_dir, - 'maxtime': self.cfg['runoptions']['minimizer_timeout'] - } - - try: - with Minimizer(**kwargs) as minimizer: - minimizer.go() - - # minimizer found other crashes, so we should add them - # to our list for subsequent processing - for tc in minimizer.other_crashes.values(): - self.tc_candidate_q.put(tc) - except MinimizerError as e: - logger.error('Caught MinimizerError: {}'.format(e)) - - def _post_minimize(self, testcase): - if self.cfg['runoptions']['recycle_crashers']: - logger.debug('Recycling crash as seedfile') - iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext - crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) - filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) - self.sf_set.add_file(crasherseed_path) - def _report(self, testcase): with CopyFilesReporter(testcase, self.tc_dir) as reporter: reporter.go() diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index 361656b..d1b7248 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -87,7 +87,6 @@ runoptions: seed_interval: 20 minimize: True minimizer_timeout: 3600 - minimize_to_string: False # keep_unique_faddr: False keep_duplicates: False recycle_crashers: False diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index a707963..e4c6e95 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -123,3 +123,6 @@ def __init__(self, templated=True): def MockFixupCfg(): return fixup_config(MockCfg(templated=False)) + +class MockMinimizer(object): + pass \ No newline at end of file diff --git a/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py index a01cf49..47916b8 100644 --- a/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py +++ b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py @@ -7,15 +7,22 @@ import tempfile import shutil import certfuzz.tc_pipeline.tc_pipeline_base +from test_certfuzz.mocks import MockMinimizer class TCPL_Impl(certfuzz.tc_pipeline.tc_pipeline_base.TestCasePipelineBase): + _minimizer_cls = MockMinimizer + def _setup_analyzers(self): pass def _minimize(self, testcase): pass + def _post_minimize(self, testcase): +# certfuzz.tc_pipeline.tc_pipeline_base.TestCasePipelineBase._post_minimize(self, testcase) + pass + def _verify(self, testcase): pass From b5de69c5b47e3288d0be99dcf21095da007b3a6c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 22 Feb 2016 13:57:05 -0500 Subject: [PATCH 0876/1169] fix change from run options to analyzer in config --- src/certfuzz/analyzers/pin_calltrace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/analyzers/pin_calltrace.py b/src/certfuzz/analyzers/pin_calltrace.py index 128e92e..078fb0b 100644 --- a/src/certfuzz/analyzers/pin_calltrace.py +++ b/src/certfuzz/analyzers/pin_calltrace.py @@ -21,7 +21,7 @@ class Pin_calltrace(Analyzer): def __init__(self, cfg, crash): outfile = get_file(crash.fuzzedfile.path) - timeout = cfg['runoptions']['valgrind_timeout'] * 10 + timeout = cfg['analyzer']['valgrind_timeout'] * 10 Analyzer.__init__(self, cfg, crash, outfile, timeout) self.empty_output_ok = True From 18344bd03ace1086fcd79ff00caa0be562c97d3d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 14:39:36 -0500 Subject: [PATCH 0877/1169] Keep track of seedfile try counts. BFF-918 --- src/certfuzz/iteration/iteration_base3.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index c65016e..fb3cc88 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -160,6 +160,7 @@ def run(self): def record_success(self): self.sf_set.record_success(key=self.seedfile.md5) + self.seedfile.tries += 1 if hasattr(self.r, 'id'): self.seedfile.rangefinder.record_success(key=self.r.id) @@ -170,6 +171,7 @@ def record_tries(self): if self.seedfile.md5 in self.sf_set.arms: # Only record tries for seedfiles that haven't been removed self.sf_set.record_tries(key=self.seedfile.md5, tries=1) + self.seedfile.tries += 1 if hasattr(self.r, 'id'): self.seedfile.rangefinder.record_tries(key=self.r.id, tries=1) From a4c6418b51c361d7e52226a03ea8477598923263 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 15:40:00 -0500 Subject: [PATCH 0878/1169] Add mutator options to linux config --- src/linux/conf.d/bff.yaml | 59 +++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/src/linux/conf.d/bff.yaml b/src/linux/conf.d/bff.yaml index d1b7248..af275eb 100644 --- a/src/linux/conf.d/bff.yaml +++ b/src/linux/conf.d/bff.yaml @@ -93,17 +93,64 @@ runoptions: watchdogtimeout: 3600 -################################################################ +##################################################################### # FUZZER OPTIONS # -# fuzzer: -# Mutator to use +# ** Note that only one fuzzer can be selected per campaign ** +# +# bytemut: +# replace bytes with random values +# +# swap: +# swap adjacent bytes +# +# wave: +# cycle through every possible single-byte value, sequentially +# +# drop: +# removes one byte from the file for each position in the file +# +# insert: +# inserts a random byte for each position in the file +# +# truncate: +# truncates bytes from the end of the file +# +# crmut: +# replace carriage return bytes with random values +# +# crlfmut: +# replace carriage return and linefeed bytes with random values +# +# nullmut: +# replace null bytes with random values +# +# verify: +# do not mutate file. Used for verifying crashing testcases +# +# OPTIONS APPLIED TO THE ABOVE MUTATORS: +# +# range_list: +# byte ranges to be fuzzed. One range per line, hex or decimal # -# fuzz_zip_container: +# fuzz_zip_container: # rather than fuzzing zip file contents, fuzz the zip container itself -################################################################ +# +##################################################################### fuzzer: fuzzer: bytemut + # fuzzer: swap + # fuzzer: wave + # fuzzer: drop + # fuzzer: insert + # fuzzer: truncate + # fuzzer: crmut + # fuzzer: crlfmut + # fuzzer: nullmut + # fuzzer: verify + # range_list: + # - [0x0000, 0x0400] + # - [0x1000, 0x100F] fuzz_zip_container: False @@ -114,7 +161,7 @@ fuzzer: # maximum program execution time (seconds) that BFF will the target to execute ################################################################ runner: - runtimeout: 5 + runtimeout: 5 ################################################################ From 7ed137c24b9e70145c2a218ede9e71c6a033a939 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 15:54:17 -0500 Subject: [PATCH 0879/1169] Move tries incrementer into __exit__ --- src/certfuzz/iteration/iteration_base3.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index fb3cc88..c9597fc 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -81,6 +81,9 @@ def __exit__(self, etype, value, traceback): # increment the iteration counter IterationBase3._iteration_counter += 1 + # increment the seedfile try counter + self.seedfile.tries += 1 + if self.success: # score it so we can learn self.record_success() @@ -160,7 +163,6 @@ def run(self): def record_success(self): self.sf_set.record_success(key=self.seedfile.md5) - self.seedfile.tries += 1 if hasattr(self.r, 'id'): self.seedfile.rangefinder.record_success(key=self.r.id) @@ -171,7 +173,6 @@ def record_tries(self): if self.seedfile.md5 in self.sf_set.arms: # Only record tries for seedfiles that haven't been removed self.sf_set.record_tries(key=self.seedfile.md5, tries=1) - self.seedfile.tries += 1 if hasattr(self.r, 'id'): self.seedfile.rangefinder.record_tries(key=self.r.id, tries=1) From 0fae3071e55bd2d2a345461c05b3523ec333744c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 16:05:11 -0500 Subject: [PATCH 0880/1169] conf.d -> configs --- src/certfuzz/bff/linux.py | 2 +- src/certfuzz/tools/linux/bff_stats.py | 2 +- src/certfuzz/tools/linux/minimize.py | 8 +- src/certfuzz/tools/linux/minimizer_plot.py | 2 +- src/certfuzz/tools/linux/repro.py | 2 +- src/certfuzz/tools/windows/minimize.py | 2 +- src/linux/README | 2 +- src/linux/conf.d/unified_config_linux.yml | 66 -------- src/linux/{conf.d => configs}/bff.yaml | 0 src/linux/welcome.sh | 2 +- src/windows/configs/examples/bff.yaml | 172 ++++++++++++++------- 11 files changed, 125 insertions(+), 135 deletions(-) delete mode 100644 src/linux/conf.d/unified_config_linux.yml rename src/linux/{conf.d => configs}/bff.yaml (100%) diff --git a/src/certfuzz/bff/linux.py b/src/certfuzz/bff/linux.py index 43dd63b..88c085c 100644 --- a/src/certfuzz/bff/linux.py +++ b/src/certfuzz/bff/linux.py @@ -17,7 +17,7 @@ def main(): - cfg = os.path.abspath(os.path.join(os.getcwd(), 'conf.d', 'bff.yaml')) + cfg = os.path.abspath(os.path.join(os.getcwd(), 'configs', 'bff.yaml')) with BFF(config_path=cfg, campaign_class=LinuxCampaign) as bff: bff() diff --git a/src/certfuzz/tools/linux/bff_stats.py b/src/certfuzz/tools/linux/bff_stats.py index 5792f42..0a277ee 100755 --- a/src/certfuzz/tools/linux/bff_stats.py +++ b/src/certfuzz/tools/linux/bff_stats.py @@ -98,7 +98,7 @@ def main(): if options.cfgfile: cfg_file = options.cfgfile else: - cfg_file = os.path.join('conf.d', 'bff.yaml') + cfg_file = os.path.join('configs', 'bff.yaml') logger.debug('Using config file: %s', cfg_file) cfg = load_and_fix_config(cfg_file) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index bb95410..1384deb 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -72,10 +72,10 @@ def main(): if options.config: cfg_file = os.path.expanduser(options.config) else: - if os.path.isfile("../conf.d/bff.yaml"): - cfg_file = "../conf.d/bff.cfg" - elif os.path.isfile("conf.d/bff.yaml"): - cfg_file = "conf.d/bff.yaml" + if os.path.isfile("../configs/bff.yaml"): + cfg_file = "../configs/bff.cfg" + elif os.path.isfile("configs/bff.yaml"): + cfg_file = "configs/bff.yaml" else: parser.error('Configuration file (--config) option must be specified.') logger.debug('Config file: %s', cfg_file) diff --git a/src/certfuzz/tools/linux/minimizer_plot.py b/src/certfuzz/tools/linux/minimizer_plot.py index 9672f38..d12dc1e 100755 --- a/src/certfuzz/tools/linux/minimizer_plot.py +++ b/src/certfuzz/tools/linux/minimizer_plot.py @@ -142,7 +142,7 @@ def main(): if options.cfgfile: cfg_file = options.cfgfile else: - cfg_file = os.path.join('conf.d', 'bff.yaml') + cfg_file = os.path.join('configs', 'bff.yaml') if options.dir: result_dir = options.dir diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index 958ed42..0cfb881 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -59,7 +59,7 @@ def main(): help='Enable debug messages (overrides --verbose)') parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') - parser.add_option('-c', '--config', default='conf.d/bff.yaml', + parser.add_option('-c', '--config', default='configs/bff.yaml', dest='config', help='path to the configuration file to use') parser.add_option('-e', '--edb', dest='use_edb', action='store_true', diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index fc010d4..6dd59ac 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -100,7 +100,7 @@ def main(): if options.config: cfg_file = options.config else: - cfg_file = "../conf.d/bff.cfg" + cfg_file = "../configs/bff.cfg" logger.debug('WindowsConfig file: %s', cfg_file) if options.stringmode and options.target: diff --git a/src/linux/README b/src/linux/README index f8ea12d..d3aff3a 100644 --- a/src/linux/README +++ b/src/linux/README @@ -110,7 +110,7 @@ shared folder /mnt/hgfs/fuzz. The default results location is /home/fuzz/results, which in the UbuFuzz vm is a soft link to /home/fuzz/bff/results (thus /mnt/hgfs/fuzz/results). Either of these soft links can be changed if desired, or you can edit -conf.d/bff.cfg to point BFF at a different destination. +configs/bff.cfg to point BFF at a different destination. BFF will copy its configuration to results/bff.cfg, and will log messages of level INFO or higher into results/bff.log. The diff --git a/src/linux/conf.d/unified_config_linux.yml b/src/linux/conf.d/unified_config_linux.yml deleted file mode 100644 index f3eb0a7..0000000 --- a/src/linux/conf.d/unified_config_linux.yml +++ /dev/null @@ -1,66 +0,0 @@ -campaign: - id: default_imagemagick_convert - workdir: ~/fuzzing - results_dir: ~/results - seedfile_dir: ~/bff/seedfiles/examples - use_minimizer: True - recycle_crashers: False -# keep_heisenbugs: False # windows only -# use_buttonclicker: False # windows only -debugger: - timeout: 60 - debugger_cls: gdb - template_dir: ~/bff/certfuzz/debuggers/templates # linux only -# max_handled_exceptions: 6 # windows only -# watchcpu: Auto # windows only -# use_debug_heap: False # windows only -fuzzer: - fuzzer_cls: bytemut - fuzz_zip_container: False - start_iteration: 0 - iteration_interval: 1 - # TODO: can we eliminate copymode? - copymode: False # linux only -killproc: # linux only - killprocname: convert - timeout: 130 -minimizer: - timeout: 3600 - minimize_to_string: False -runner: - timeout: 5 - runner_cls: zzufrun -# windows only - # exceptions: - # - 0x80000002 # EXCEPTION_DATATYPE_MISALIGNMENT - # - 0xC0000005 # STATUS_ACCESS_VIOLATION - # - 0xC000001D # STATUS_ILLEGAL_INSTRUCTION - # - 0xC0000025 # EXCEPTION_NONCONTINUABLE_EXCEPTION - # - 0xC0000026 # EXCEPTION_INVALID_DISPOSITION - # - 0xC000008C # EXCEPTION_ARRAY_BOUNDS_EXCEEDED - # - 0xC000008E # EXCEPTION_FLT_DIVIDE_BY_ZERO - # - 0xC0000090 # EXCEPTION_FLT_INVALID_OPERATION - # - 0xC0000091 # EXCEPTION_FLT_OVERFLOW - # - 0xC0000092 # EXCEPTION_FLT_STACK_CHECK - # - 0xC0000093 # EXCEPTION_FLT_UNDERFLOW - # - 0xC0000094 # STATUS_INTEGER_DIVIDE_BY_ZERO - # - 0xC0000095 # EXCEPTION_INT_OVERFLOW - # - 0xC0000096 # STATUS_PRIVILEGED_INSTRUCTION - # - 0xC00000FD # STATUS_STACK_OVERFLOW - # hideoutput: False - # watchcpu: Auto -target: - program: ~/convert - invocation_template: $PROGRAM $SEEDFILE /dev/null -valgrind: # linux only - timeout: 120 -verifier: - # keep_unique_faddr: True # windows only - keep_failed_asserts: False # linux only - use_pin_calltrace: True # linux only - keep_duplicates: False - backtrace_depth: 5 - use_valgrind: True #linux only -watchdog: - timeout: 3600 - file: /tmp/bff_watchdog \ No newline at end of file diff --git a/src/linux/conf.d/bff.yaml b/src/linux/configs/bff.yaml similarity index 100% rename from src/linux/conf.d/bff.yaml rename to src/linux/configs/bff.yaml diff --git a/src/linux/welcome.sh b/src/linux/welcome.sh index c53652d..fd11fe9 100755 --- a/src/linux/welcome.sh +++ b/src/linux/welcome.sh @@ -17,7 +17,7 @@ if [[ -f ~/fuzzing/bff.log ]]; then echo -e "\n--- Resuming fuzzing campaign ... ---" echo -e "--- Run ./reset_bff.sh to start a new fuzzing campaign ---\n" else - currentcfg=conf.d/bff.yaml + currentcfg=configs/bff.yaml echo "Using configuration file: $currentcfg" fi diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index b1d680b..573798c 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -1,10 +1,19 @@ -##################################################################### -# Campaign options: +################################################################ +# +# This file specifies the options that BFF will use to fuzz +# Comments are specified by the "#" character +# +###################################################################### +# FUZZ CAMPAIGN SETTINGS +# +# id: +# used for identifying campaign, placement of results # -# id: used for identifying campaign, placement of results -# keep_heisenbugs: Keep crashing testcases detected by hook, but -# not when run via the debugger. -# use_buttonclicker: Spawn program to click buttons +# keep_heisenbugs: +# Keep crashing testcases detected by hook, but not when run via the debugger. +# +# use_buttonclicker: +# Spawn program to click buttons ##################################################################### campaign: id: convert v5.5.7 @@ -15,9 +24,11 @@ campaign: ##################################################################### # Fuzz target options: # -# program: Path to fuzzing target executable -# cmdline_template: Used to specify the command-line invocation of -# the target +# program: +# Path to fuzzing target executable +# +# cmdline_template: +# Used to specify the command-line invocation of the target ##################################################################### target: program: C:\BFF\imagemagick\convert.exe @@ -48,10 +59,14 @@ target: ##################################################################### # Directories used by BFF # -# seedfile_dir: Location of seed files (relative to bff.py) -# working_dir: Temporary directory used by BFF. Use a ramdisk to -# reduce disk activity -# results_dir: Location of fuzzing results (relative to bff.py) +# seedfile_dir: +# Location of seed files (relative to bff.py) +# +# working_dir: +# Temporary directory used by BFF. Use a ramdisk to reduce disk activity +# +# results_dir: +# Location of fuzzing results (relative to bff.py) ##################################################################### directories: seedfile_dir: seedfiles\examples @@ -62,23 +77,30 @@ directories: ##################################################################### # Fuzz run options # -# first_iteration: The iteration number to begin with. Defaults to zero -# if not present. -# last_iteration: The iteration when a fuzzing campaign ends. If set -# to zero or not present, the campaign will continue -# until the fuzzer runs out of things to do. -# seed_interval: The number of iterations to perform before selecting -# a new seed file and mutation range. Default is zero -# if not present. -# minimize: Create a file that is minimally-different than the seed -# file, yet crashes with the same hash -# minimizer_timeout: The maximum amount of time that BFF will spend on -# a minimization run before giving up -# keep_unique_faddr: Consider the Exception Faulting Address value as -# part of the crash hash -# keep_duplicates: Keep all duplicate crashing cases -# recycle_crashers: Recycle uniquely-crashing testcases into the pool -# of available seed files to fuzz +# first_iteration: +# The iteration number to begin with. Defaults to zero if not present. +# +# seed_interval: +# The number of iterations to perform before selecting a new seed file and +# mutation range. Default is zero if not present. +# +# minimize: +# Create a file that is minimally-different than the seed file, yet crashes +# with the same hash +# +# minimizer_timeout: +# The maximum amount of time that BFF will spend on a minimization run before +# giving up +# +# keep_unique_faddr: +# Consider the Exception Faulting Address value as part of the crash hash +# +# keep_duplicates: +# Keep all duplicate crashing cases +# +# recycle_crashers: +# Recycle uniquely-crashing testcases into the pool of available seed files +# to fuzz ##################################################################### runoptions: first_iteration: 0 @@ -91,24 +113,47 @@ runoptions: ##################################################################### -# Fuzzer options +# FUZZER OPTIONS # # ** Note that only one fuzzer can be selected per campaign ** # -# fuzzer: -# bytemut: replace bytes with random values -# swap: swap adjacent bytes -# wave: cycle through every possible single-byte value, sequentially -# drop: removes one byte from the file for each position in the file -# insert: inserts a random byte for each position in the file -# truncate: truncates bytes from the end of the file -# crmut: replace carriage return bytes with random values -# crlfmut: replace carriage return and linefeed bytes with random values -# nullmut: replace null bytes with random values -# verify: do not mutate file. Used for verifying crashing testcases -# range_list: byte ranges to be fuzzed. One range per line, hex or decimal -# fuzz_zip_container: rather than fuzzing zip file contents, fuzz the -# zip container itself +# bytemut: +# replace bytes with random values +# +# swap: +# swap adjacent bytes +# +# wave: +# cycle through every possible single-byte value, sequentially +# +# drop: +# removes one byte from the file for each position in the file +# +# insert: +# inserts a random byte for each position in the file +# +# truncate: +# truncates bytes from the end of the file +# +# crmut: +# replace carriage return bytes with random values +# +# crlfmut: +# replace carriage return and linefeed bytes with random values +# +# nullmut: +# replace null bytes with random values +# +# verify: +# do not mutate file. Used for verifying crashing testcases +# +# OPTIONS APPLIED TO THE ABOVE MUTATORS: +# +# range_list: +# byte ranges to be fuzzed. One range per line, hex or decimal +# +# fuzz_zip_container: +# rather than fuzzing zip file contents, fuzz the zip container itself # ##################################################################### fuzzer: @@ -131,12 +176,16 @@ fuzzer: ##################################################################### # Runner options # -# hideoutput: Hide stdout of target application -# runtimeout: Number of seconds to allow target application to execute -# when run with the hook (winrun) -# watchcpu: Kill target process when its CPU usage drops towards zero -# when run with the hook (winrun). (Auto, True, False) -# exceptions: List of exceptions to save +# hideoutput: +# Hide stdout of target application +# +# runtimeout: +# Number of seconds to allow target application to execute when run with +# the hook (winrun) +# +# watchcpu: +# Kill target process when its CPU usage drops towards zero when run with +# the hook (winrun). (Auto, True, False) # ##################################################################### runner: @@ -148,13 +197,20 @@ runner: ##################################################################### # Debugger options # -# debugger: -# runtimeout: Number of seconds to allow target application to execute -# when run via the debugger -# watchcpu: Kill target process when its CPU usage drops towards zero -# when run with the debugger (null runner). (Auto, True, False) -# debugheap: Use the debug heap for the target application -# max_handled_exceptions: Maximum number of times to continue exceptions +# runtimeout: +# Number of seconds to allow target application to execute when run via the +# debugger +# +# watchcpu: +# Kill target process when its CPU usage drops towards zero when run with the +# debugger (null runner). (Auto, True, False) +# +# debugheap: +# Use the debug heap for the target application +# +# max_handled_exceptions: +# Maximum number of times to continue exceptions +# ##################################################################### debugger: runtimeout: 10 From 0a87c530dd4debf866238cdc821f414d9e5584d4 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 16:23:01 -0500 Subject: [PATCH 0881/1169] Remove xterm compatibility. Fix up for dos-formatted bff.yaml --- src/linux/welcome.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/linux/welcome.sh b/src/linux/welcome.sh index fd11fe9..f87ab1c 100755 --- a/src/linux/welcome.sh +++ b/src/linux/welcome.sh @@ -1,6 +1,5 @@ #!/bin/bash -xterm=`which xterm` cd ~/bff platform=`uname -a` if [[ "$platform" =~ "Darwin" ]]; then @@ -12,6 +11,7 @@ clear echo -e "***** Welcome to the CERT BFF! *****\n\n" echo "Working directory: $PWD" +# TODO: This logic needs to change / be fixed if [[ -f ~/fuzzing/bff.log ]]; then currentcfg=~/bff.yaml echo -e "\n--- Resuming fuzzing campaign ... ---" @@ -21,10 +21,8 @@ else echo "Using configuration file: $currentcfg" fi -program=`egrep -m1 '^ program:' $currentcfg | sed 's/^ program://'` +program=`egrep -m1 '^ program:' $currentcfg | sed 's/^ program://' | sed 's/\\r//'` echo "Target commandline: " `egrep -m1 '^ cmdline' $currentcfg | sed 's/^ cmdline_template://' | sed "s|"'$PROGRAM'"|$program|"` -echo -e "Output directory: " `egrep -m1 '^ results_dir' $currentcfg | sed 's/^ results_dir://'` "\n\n" +echo -e "Output directory: " `egrep -m1 '^ results_dir' $currentcfg | sed 's/^ results_dir://' | sed 's/\\r//'` "\n\n" -if [[ -n "$xterm" ]]; then - echo -e "Run ./batch.sh to begin fuzzing.\n" -fi +echo -e "Run ./batch.sh to begin fuzzing.\n" From 66139c2ee056576bb6debbaf1a1ec853caf2554f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 18:10:37 -0500 Subject: [PATCH 0882/1169] yaml formatting --- src/linux/configs/bff.yaml | 40 +++++++++++++++------------ src/windows/configs/examples/bff.yaml | 30 ++++++++++---------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index af275eb..875c00e 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -1,19 +1,20 @@ -################################################################ +############################################################################## # # This file specifies the options that BFF will use to fuzz # Comments are specified by the "#" character # -################################################################ +############################################################################## # FUZZ CAMPAIGN SETTINGS # # id: # used for identifying campaign, placement of results -################################################################ +# +############################################################################## campaign: id: convert v5.2.0 -################################################################ +############################################################################## # TARGET APPLICATION INVOCATION OPTIONS: # # program: @@ -27,13 +28,14 @@ campaign: # $SEEDFILE will be replaced at runtime with the appropriate # seed file name. # Use quotes if the target application has spaces in the path -################################################################ +# +############################################################################## target: program: ~/convert cmdline_template: $PROGRAM $SEEDFILE /dev/null -################################################################ +############################################################################## # LOCATIONS FOR FUZZ RUN FILES: # # Output files are placed in [outputdir]/[seedfile] @@ -48,7 +50,8 @@ target: # # If results are stored in a shared location, # this directory needs to be unique for each fuzzing machine -################################################################ +# +############################################################################## directories: seedfile_dir: seedfiles/examples @@ -56,7 +59,7 @@ directories: results_dir: results -################################################################ +############################################################################## # FUZZ CAMPAIGN OPTIONS # # first_iteration: @@ -81,7 +84,7 @@ directories: # Number of seconds that if exceeded, the watchdog will restart the fuzzing # machine. Set to 0 to disable watchdog functionality. # -################################################################ +############################################################################## runoptions: first_iteration: 0 seed_interval: 20 @@ -93,7 +96,7 @@ runoptions: watchdogtimeout: 3600 -##################################################################### +################################################################################### # FUZZER OPTIONS # # ** Note that only one fuzzer can be selected per campaign ** @@ -136,7 +139,7 @@ runoptions: # fuzz_zip_container: # rather than fuzzing zip file contents, fuzz the zip container itself # -##################################################################### +################################################################################### fuzzer: fuzzer: bytemut # fuzzer: swap @@ -154,17 +157,18 @@ fuzzer: fuzz_zip_container: False -################################################################ +############################################################################## # RUNNER OPTIONS # # runtimeout: # maximum program execution time (seconds) that BFF will the target to execute -################################################################ +# +############################################################################## runner: runtimeout: 5 -################################################################ +############################################################################## # DEBUGGER OPTIONS # # runtimeout: @@ -176,13 +180,14 @@ runner: # Increase this number for more crash uniqueness granularity. # Decrease this number if you think that you are getting too many duplicate # crashes. -################################################################ +# +############################################################################## debugger: runtimeout: 60 backtracelevels: 5 -################################################################ +############################################################################## # VERIFIER PARAMETERS # # exclude_unmapped_frames: @@ -206,7 +211,8 @@ debugger: # # valgrind_timeout: # Number of seconds to allow valgrind to run before giving up on it -################################################################ +# +############################################################################## analyzer: exclude_unmapped_frames: True savefailedasserts: False diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 573798c..024e5ce 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -1,9 +1,9 @@ -################################################################ +############################################################################## # # This file specifies the options that BFF will use to fuzz # Comments are specified by the "#" character # -###################################################################### +############################################################################## # FUZZ CAMPAIGN SETTINGS # # id: @@ -14,14 +14,14 @@ # # use_buttonclicker: # Spawn program to click buttons -##################################################################### +############################################################################## campaign: id: convert v5.5.7 keep_heisenbugs: False use_buttonclicker: False -##################################################################### +############################################################################## # Fuzz target options: # # program: @@ -29,7 +29,7 @@ campaign: # # cmdline_template: # Used to specify the command-line invocation of the target -##################################################################### +############################################################################## target: program: C:\BFF\imagemagick\convert.exe cmdline_template: $PROGRAM $SEEDFILE NUL @@ -56,7 +56,7 @@ target: # cmdline_template: $PROGRAM -in $SEEDFILE -out "c:\some path\to file" -##################################################################### +############################################################################## # Directories used by BFF # # seedfile_dir: @@ -67,14 +67,14 @@ target: # # results_dir: # Location of fuzzing results (relative to bff.py) -##################################################################### +############################################################################## directories: seedfile_dir: seedfiles\examples working_dir: fuzzdir results_dir: results -##################################################################### +############################################################################## # Fuzz run options # # first_iteration: @@ -101,7 +101,7 @@ directories: # recycle_crashers: # Recycle uniquely-crashing testcases into the pool of available seed files # to fuzz -##################################################################### +############################################################################## runoptions: first_iteration: 0 seed_interval: 1 @@ -112,7 +112,7 @@ runoptions: recycle_crashers: False -##################################################################### +############################################################################## # FUZZER OPTIONS # # ** Note that only one fuzzer can be selected per campaign ** @@ -155,7 +155,7 @@ runoptions: # fuzz_zip_container: # rather than fuzzing zip file contents, fuzz the zip container itself # -##################################################################### +############################################################################## fuzzer: fuzzer: bytemut # fuzzer: swap @@ -173,7 +173,7 @@ fuzzer: fuzz_zip_container: False -##################################################################### +############################################################################## # Runner options # # hideoutput: @@ -187,14 +187,14 @@ fuzzer: # Kill target process when its CPU usage drops towards zero when run with # the hook (winrun). (Auto, True, False) # -##################################################################### +############################################################################## runner: hideoutput: False runtimeout: 5 watchcpu: Auto -##################################################################### +############################################################################## # Debugger options # # runtimeout: @@ -211,7 +211,7 @@ runner: # max_handled_exceptions: # Maximum number of times to continue exceptions # -##################################################################### +############################################################################## debugger: runtimeout: 10 watchcpu: Auto From 8697af6a6c8c873fb9761a50beb09f021a853b38 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 18:11:25 -0500 Subject: [PATCH 0883/1169] Get rid of explicit debugger watchcpu option. Just use runner option for everything. --- src/certfuzz/campaign/campaign_windows.py | 4 ++-- src/windows/configs/examples/bff.yaml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 0a08126..2d8d3cf 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -116,7 +116,7 @@ def _cache_app(self): logger.debug('This seems to be a CLI application.') try: runner_watchcpu = str(self.config['runner']['watchcpu']).lower() - debugger_watchcpu = str(self.config['debugger']['watchcpu']).lower() + debugger_watchcpu = runner_watchcpu except KeyError: self.config['runner']['watchcpu'] = 'auto' self.config['debugger']['watchcpu'] = 'auto' @@ -138,7 +138,7 @@ def kill(self, p): self.gui_app = True try: runner_watchcpu = str(self.config['runner']['watchcpu']).lower() - debugger_watchcpu = str(self.config['debugger']['watchcpu']).lower() + debugger_watchcpu = runner_watchcpu except KeyError: self.config['runner']['watchcpu'] = 'auto' self.config['debugger']['watchcpu'] = 'auto' diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 024e5ce..9fa86e1 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -214,7 +214,6 @@ runner: ############################################################################## debugger: runtimeout: 10 - watchcpu: Auto debugheap: False max_handled_exceptions: 6 From ef8d133b5a8b6a32c6871f922bf0f3b666437c13 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 18:18:34 -0500 Subject: [PATCH 0884/1169] Don't have explicit debugger timeout. Just use the runner timeout for both. --- src/certfuzz/analyzers/cw_gmalloc.py | 2 +- src/certfuzz/iteration/iteration_linux.py | 2 +- src/certfuzz/minimizer/minimizer_base.py | 2 +- src/certfuzz/tools/linux/minimize.py | 2 +- src/certfuzz/tools/windows/minimize.py | 2 +- src/linux/configs/bff.yaml | 5 ----- src/test_certfuzz/mocks.py | 5 ++--- src/windows/configs/examples/bff.yaml | 9 --------- 8 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/analyzers/cw_gmalloc.py b/src/certfuzz/analyzers/cw_gmalloc.py index 238f9c2..4478906 100644 --- a/src/certfuzz/analyzers/cw_gmalloc.py +++ b/src/certfuzz/analyzers/cw_gmalloc.py @@ -30,7 +30,7 @@ def __init__(self, cfg, crash): return None outfile = get_file(crash.fuzzedfile.path) - timeout = cfg['debugger']['runtimeout'] + timeout = cfg['runner']['runtimeout'] Analyzer.__init__(self, cfg, crash, outfile, timeout) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index f99096a..80911fa 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -74,7 +74,7 @@ def _construct_testcase(self): seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), program=self.cfg['target']['program'], - debugger_timeout=self.cfg['debugger']['runtimeout'], + debugger_timeout=self.cfg['runner']['runtimeout'], backtrace_lines=self.cfg['debugger']['backtracelevels'], crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index ecb108b..ed4eeb1 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -173,7 +173,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, filetools.copy_file(self.crash.fuzzedfile.path, self.tempfile) # figure out what crash signatures belong to this fuzzedfile - self.debugger_timeout = self.cfg['debugger']['runtimeout'] + self.debugger_timeout = self.cfg['runner']['runtimeout'] self.crash_hashes = [] self.measured_dbg_time = None self._set_crash_hashes() diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 1384deb..19eec6b 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -140,7 +140,7 @@ def main(): seedfile=seedfile, fuzzedfile=fuzzed_file, program=cfg['target']['program'], - debugger_timeout=cfg['debugger']['runtimeout'], + debugger_timeout=cfg['runner']['runtimeout'], backtrace_lines=cfg['debugger']['backtracelevels'], crashers_dir=crashers_dir, workdir_base=None, diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 6dd59ac..e93073a 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -42,7 +42,7 @@ class DummyCfg(object): pass config = DummyCfg() config.backtracelevels = 5 # doesn't matter what this is, we don't use it - config.debugger_timeout = cfg['debugger']['runtimeout'] + config.debugger_timeout = cfg['runner']['runtimeout'] template = string.Template(cfg['target']['cmdline_template']) config.get_command_args_list = lambda x: get_command_args_list(template, x)[1] config.program = cfg['target']['program'] diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index 875c00e..72fea63 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -171,10 +171,6 @@ runner: ############################################################################## # DEBUGGER OPTIONS # -# runtimeout: -# Maximum time (seconds) to let the program run to capture debugger and -# CERT Triage Tools exploitable output -# # backtracelevels: # Number of backtrace frames to hash for uniqueness # Increase this number for more crash uniqueness granularity. @@ -183,7 +179,6 @@ runner: # ############################################################################## debugger: - runtimeout: 60 backtracelevels: 5 diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index e4c6e95..8162e76 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -101,8 +101,7 @@ def go(self): class MockCfg(dict): def __init__(self, templated=True): - self['debugger'] = {'runtimeout': 1, - 'backtracelevels': 5, + self['debugger'] = {'backtracelevels': 5, 'debugger': 'gdb', } self['target'] = {'cmdline_template': '$PROGRAM b c d $SEEDFILE', @@ -125,4 +124,4 @@ def MockFixupCfg(): return fixup_config(MockCfg(templated=False)) class MockMinimizer(object): - pass \ No newline at end of file + pass diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 9fa86e1..f01b9de 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -197,14 +197,6 @@ runner: ############################################################################## # Debugger options # -# runtimeout: -# Number of seconds to allow target application to execute when run via the -# debugger -# -# watchcpu: -# Kill target process when its CPU usage drops towards zero when run with the -# debugger (null runner). (Auto, True, False) -# # debugheap: # Use the debug heap for the target application # @@ -213,7 +205,6 @@ runner: # ############################################################################## debugger: - runtimeout: 10 debugheap: False max_handled_exceptions: 6 From ebaff4a986af30f6a8ebb60c43c9dd34d9740119 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 18:25:48 -0500 Subject: [PATCH 0885/1169] Fix unit test to include 'runtimeout' --- src/test_certfuzz/mocks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 8162e76..50ff7a3 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -116,7 +116,8 @@ def __init__(self, templated=True): self['campaign'] = {'id': 'xyz'} self['runoptions'] = {'first_iteration': 0, 'seed_interval': 10} - self['runner'] = {'runner': 'zzufrun'} + self['runner'] = {'runner': 'zzufrun', + 'runtimeout': 5} if templated: self['target']['cmdline_template'] = string.Template(self['target']['cmdline_template']) From 72721fe49bfe1aaacd800288491bbf9e433d96a1 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 18:38:47 -0500 Subject: [PATCH 0886/1169] Set cfg debugger runtimeout option to the runner runtimeout. --- src/certfuzz/iteration/iteration_windows.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 837d76f..71c3431 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -77,6 +77,9 @@ def __init__(self, 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize', False), } + # Windows testcase object needs a timeout, and we only pass debugger options + self.cfg['debugger']['runtimeout'] = self.cfg['runner']['runtimeout'] + def __exit__(self, etype, value, traceback): try: handled = IterationBase3.__exit__(self, etype, value, traceback) From c72f1e7a24f2b81eaf3c0ed3a5605333dadecd17 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 19:18:14 -0500 Subject: [PATCH 0887/1169] Use proper logger in testcase_base2 --- src/certfuzz/testcase/testcase_base2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py index 48ead05..cb68983 100644 --- a/src/certfuzz/testcase/testcase_base2.py +++ b/src/certfuzz/testcase/testcase_base2.py @@ -77,13 +77,13 @@ def _verify_crash_base_dir(self): def calculate_hamming_distances(self): TestCaseBase.calculate_hamming_distances(self) - self.logger.info("bitwise_hd=%d", self.hd_bits) - self.logger.info("bytewise_hd=%d", self.hd_bytes) + logger.info("bitwise_hd=%d", self.hd_bits) + logger.info("bytewise_hd=%d", self.hd_bytes) def calculate_hamming_distances_a(self): TestCaseBase.calculate_hamming_distances_a(self) - self.logger.info("bitwise_hd=%d", self.hd_bits) - self.logger.info("bytewise_hd=%d", self.hd_bytes) + logger.info("bitwise_hd=%d", self.hd_bits) + logger.info("bytewise_hd=%d", self.hd_bytes) def clean_tmpdir(self): logger.debug('Cleaning up %s', self.tempdir) From 5590ab95a0d4a07541160e1d52b4f398a584a648 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Feb 2016 19:32:53 -0500 Subject: [PATCH 0888/1169] Specify timeout via runner timeout for standalone minimizer. --- src/certfuzz/tools/windows/minimize.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index e93073a..33609e5 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -155,6 +155,8 @@ def main(): debugger_class = msec.MsecDebugger cmd_as_args = get_command_args_list(cfg['target']['cmdline_template'], fuzzed_file.path)[1] + # Use runner timeout, since we now only specify the runner timeout + cfg['debugger']['runtimeout'] = cfg['runner']['runtimeout'] with WindowsTestcase(cmd_template=cfg['target']['cmdline_template'], seedfile=seedfile, fuzzedfile=fuzzed_file, From a81dc704e5a8914cb7fbbe12e0b8458b4a7bd667 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 23 Feb 2016 00:27:14 -0500 Subject: [PATCH 0889/1169] Minimize to seedfile by default, not string. --- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index 8d1a5ec..9bccc1a 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -201,7 +201,7 @@ def _minimize(self, testcase): # build arguments for minimizer invocation kwargs = {'cfg': self.cfg, 'crash': testcase, - 'seedfile_as_target': False, + 'seedfile_as_target': True, 'bitwise': False, 'confidence': 0.999, 'tempdir': self.working_dir, From 06d6f6498efff193e00903e39506af2bdc34c475 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 3 Feb 2016 08:28:30 -0500 Subject: [PATCH 0890/1169] first pass at eradicating crash.foo in favor of testcase.foo --- src/certfuzz/analyzers/analyzer_base.py | 8 +- src/certfuzz/analyzers/callgrind/annotate.py | 8 +- src/certfuzz/analyzers/callgrind/callgrind.py | 6 +- src/certfuzz/analyzers/cw_gmalloc.py | 6 +- src/certfuzz/analyzers/pin_calltrace.py | 6 +- src/certfuzz/analyzers/stderr.py | 6 +- src/certfuzz/analyzers/valgrind.py | 6 +- src/certfuzz/campaign/campaign_base.py | 16 +-- src/certfuzz/campaign/campaign_linux.py | 2 +- src/certfuzz/campaign/campaign_windows.py | 10 +- .../debuggers/output_parsers/calltracefile.py | 2 +- .../debuggers/output_parsers/cwfile.py | 2 +- .../output_parsers/debugger_file_base.py | 2 +- src/certfuzz/minimizer/minimizer_base.py | 120 +++++++++--------- src/certfuzz/minimizer/win_minimizer.py | 12 +- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 2 +- src/certfuzz/tools/linux/minimize.py | 22 ++-- src/certfuzz/tools/windows/minimize.py | 22 ++-- src/test_certfuzz/analyzers/test_stderr.py | 12 +- src/test_certfuzz/analyzers/test_valgrind.py | 10 +- .../campaign/test_campaign_base.py | 20 +-- .../minimizer/test_minimizer_base.py | 4 +- 22 files changed, 152 insertions(+), 152 deletions(-) diff --git a/src/certfuzz/analyzers/analyzer_base.py b/src/certfuzz/analyzers/analyzer_base.py index 0684c0b..9d3d584 100644 --- a/src/certfuzz/analyzers/analyzer_base.py +++ b/src/certfuzz/analyzers/analyzer_base.py @@ -18,19 +18,19 @@ class Analyzer(object): ''' classdocs ''' - def __init__(self, cfg, crash, outfile=None, timeout=None, **options): + def __init__(self, cfg, testcase, outfile=None, timeout=None, **options): logger.debug('Initializing %s', self.__class__.__name__) self.cfg = cfg - self.crash = crash + self.testcase = testcase - self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'], crash.fuzzedfile.path)[1] + self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'], testcase.fuzzedfile.path)[1] self.outfile = outfile self.timeout = float(timeout) self.progname = self.cmdargs[1] self.options = options self.preserve_stderr = False - self.tmpdir = crash.fuzzedfile.dirname + self.tmpdir = testcase.fuzzedfile.dirname # child classes should explicitly set this to True if they need it: self.empty_output_ok = False diff --git a/src/certfuzz/analyzers/callgrind/annotate.py b/src/certfuzz/analyzers/callgrind/annotate.py index 5fa8206..5909b2f 100644 --- a/src/certfuzz/analyzers/callgrind/annotate.py +++ b/src/certfuzz/analyzers/callgrind/annotate.py @@ -36,8 +36,8 @@ def main(): print a.__dict__ -def annotate_callgrind(crash, file_ext='annotated', options=None): - infile = callgrind.get_file(crash.fuzzedfile.path) +def annotate_callgrind(testcase, file_ext='annotated', options=None): + infile = callgrind.get_file(testcase.fuzzedfile.path) if options is None: options = {} @@ -46,11 +46,11 @@ def annotate_callgrind(crash, file_ext='annotated', options=None): CallgrindAnnotate(infile, file_ext, options) -def annotate_callgrind_tree(crash): +def annotate_callgrind_tree(testcase): options = {'tree': 'calling'} file_ext = 'calltree' - annotate_callgrind(crash, file_ext, options) + annotate_callgrind(testcase, file_ext, options) class CallgrindAnnotate(object): diff --git a/src/certfuzz/analyzers/callgrind/callgrind.py b/src/certfuzz/analyzers/callgrind/callgrind.py index 33830ab..a01eb84 100644 --- a/src/certfuzz/analyzers/callgrind/callgrind.py +++ b/src/certfuzz/analyzers/callgrind/callgrind.py @@ -18,11 +18,11 @@ class Callgrind(Analyzer): - def __init__(self, cfg, crash): - outfile = get_file(crash.fuzzedfile.path) + def __init__(self, cfg, testcase): + outfile = get_file(testcase.fuzzedfile.path) timeout = cfg['analyzer']['valgrind_timeout'] - Analyzer.__init__(self, cfg, crash, outfile, timeout) + Analyzer.__init__(self, cfg, testcase, outfile, timeout) self.empty_output_ok = True self.missing_output_ok = True diff --git a/src/certfuzz/analyzers/cw_gmalloc.py b/src/certfuzz/analyzers/cw_gmalloc.py index 4478906..e264798 100644 --- a/src/certfuzz/analyzers/cw_gmalloc.py +++ b/src/certfuzz/analyzers/cw_gmalloc.py @@ -20,7 +20,7 @@ class CrashWranglerGmalloc(Analyzer): classdocs ''' - def __init__(self, cfg, crash): + def __init__(self, cfg, testcase): ''' Constructor ''' @@ -29,10 +29,10 @@ def __init__(self, cfg, crash): elif not os.path.isfile('/usr/lib/libgmalloc.dylib'): return None - outfile = get_file(crash.fuzzedfile.path) + outfile = get_file(testcase.fuzzedfile.path) timeout = cfg['runner']['runtimeout'] - Analyzer.__init__(self, cfg, crash, outfile, timeout) + Analyzer.__init__(self, cfg, testcase, outfile, timeout) def go(self): if not _platform_is_supported: diff --git a/src/certfuzz/analyzers/pin_calltrace.py b/src/certfuzz/analyzers/pin_calltrace.py index 078fb0b..73a730e 100644 --- a/src/certfuzz/analyzers/pin_calltrace.py +++ b/src/certfuzz/analyzers/pin_calltrace.py @@ -19,11 +19,11 @@ class Pin_calltrace(Analyzer): - def __init__(self, cfg, crash): - outfile = get_file(crash.fuzzedfile.path) + def __init__(self, cfg, testcase): + outfile = get_file(testcase.fuzzedfile.path) timeout = cfg['analyzer']['valgrind_timeout'] * 10 - Analyzer.__init__(self, cfg, crash, outfile, timeout) + Analyzer.__init__(self, cfg, testcase, outfile, timeout) self.empty_output_ok = True self.missing_output_ok = True diff --git a/src/certfuzz/analyzers/stderr.py b/src/certfuzz/analyzers/stderr.py index e4313c0..dac47fa 100644 --- a/src/certfuzz/analyzers/stderr.py +++ b/src/certfuzz/analyzers/stderr.py @@ -17,11 +17,11 @@ class StdErr(Analyzer): - def __init__(self, cfg, crash): - outfile = get_file(crash.fuzzedfile.path) + def __init__(self, cfg, testcase): + outfile = get_file(testcase.fuzzedfile.path) timeout = cfg['runner']['runtimeout'] - Analyzer.__init__(self, cfg, crash, outfile, timeout, stderr=outfile) + Analyzer.__init__(self, cfg, testcase, outfile, timeout, stderr=outfile) # need to set the stderr_redirect flag on the base class self.empty_output_ok = True diff --git a/src/certfuzz/analyzers/valgrind.py b/src/certfuzz/analyzers/valgrind.py index 13b0a64..0b9bb91 100644 --- a/src/certfuzz/analyzers/valgrind.py +++ b/src/certfuzz/analyzers/valgrind.py @@ -18,11 +18,11 @@ class Valgrind(Analyzer): - def __init__(self, cfg, crash): - outfile = get_file(crash.fuzzedfile.path) + def __init__(self, cfg, testcase): + outfile = get_file(testcase.fuzzedfile.path) timeout = cfg['analyzer']['valgrind_timeout'] - Analyzer.__init__(self, cfg, crash, outfile, timeout) + Analyzer.__init__(self, cfg, testcase, outfile, timeout) self.empty_output_ok = True self.missing_output_ok = True diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index e70f5db..cca37cd 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -61,7 +61,7 @@ def __init__(self, config_file, result_dir=None, debug=False): self.debug = debug self._version = __version__ - self.crashes_seen = set() + self.testcases_seen = set() self.runner_module_name = None self.runner_module = None @@ -351,19 +351,19 @@ def _save_state(self, cachefile=None): # FIXME # dump_obj_to_file(cachefile, self) - def _crash_is_unique(self, crash_id, exploitability='UNKNOWN'): + def _testcase_is_unique(self, testcase_id, exploitability='UNKNOWN'): ''' - If crash_id represents a new crash, add the crash_id to crashes_seen + If testcase_id represents a new testcase, add the testcase_id to testcases_seen and return True. Otherwise return False. - @param crash_id: the crash_id to look up + @param testcase_id: the testcase_id to look up @param exploitability: not used at this time ''' - if not crash_id in self.crashes_seen: - self.crashes_seen.add(crash_id) - logger.debug("%s did not exist in cache, crash is unique", crash_id) + if not testcase_id in self.testcases_seen: + self.testcases_seen.add(testcase_id) + logger.debug("%s did not exist in cache, testcase is unique", testcase_id) return True - logger.debug('%s was found, not unique', crash_id) + logger.debug('%s was found, not unique', testcase_id) return False def _keep_going(self): diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 77291ee..1bbdb5d 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -214,7 +214,7 @@ def _do_iteration(self, seedfile, range_obj, seednum): workdirbase=self.working_dir, outdir=self.outdir, sf_set=self.seedfile_set, - uniq_func=self._crash_is_unique, + uniq_func=self._testcase_is_unique, config=self.config, fuzzer_cls=self.fuzzer_cls, runner_cls=self.runner_cls, diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 2d8d3cf..827b7e9 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -36,7 +36,7 @@ def __init__(self, config_file, result_dir=None, debug=False): def __getstate__(self): state = self.__dict__.copy() - state['crashes_seen'] = list(state['crashes_seen']) + state['testcases_seen'] = list(state['testcases_seen']) if state['seedfile_set']: state['seedfile_set'] = state['seedfile_set'].__getstate__() @@ -53,7 +53,7 @@ def __getstate__(self): def __setstate__(self, state): # turn the list into a set - state['crashes_seen'] = set(state['crashes_seen']) + state['testcases_seen'] = set(state['testcases_seen']) # reconstitute the seedfile set with SeedfileSet(state['campaign_id'], state['seed_dir_in'], state['seed_dir_local'], @@ -174,7 +174,7 @@ def _do_iteration(self, seedfile, range_obj, seednum): workdirbase=self.working_dir, outdir=self.outdir, sf_set=self.seedfile_set, - uniq_func=self._crash_is_unique, + uniq_func=self._testcase_is_unique, config=self.config, fuzzer_cls=self.fuzzer_cls, runner_cls=self.runner_cls, @@ -190,8 +190,8 @@ def _do_iteration(self, seedfile, range_obj, seednum): self.seedfile_set.remove_file(seedfile) if not seednum % self.status_interval: - logger.info('Iteration: %d Crashes found: %d', self.current_seed, - len(self.crashes_seen)) + logger.info('Iteration: %d testcases found: %d', self.current_seed, + len(self.testcases_seen)) # FIXME # self.seedfile_set.update_csv() logger.info('Seedfile Set Status:') diff --git a/src/certfuzz/debuggers/output_parsers/calltracefile.py b/src/certfuzz/debuggers/output_parsers/calltracefile.py index 52a9521..b80b16d 100644 --- a/src/certfuzz/debuggers/output_parsers/calltracefile.py +++ b/src/certfuzz/debuggers/output_parsers/calltracefile.py @@ -26,7 +26,7 @@ def __init__(self, f): ''' Create a GDB file object from the gdb output file @param lines: The lines of the gdb file - @param is_crash: True if gdb file represents a crash + @param is_crash: True if gdb file represents a testcase @param is_assert_fail: True if gdb file represents an assert_fail @param is_debugbuild: True if gdb file contains source code lines ''' diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index 77ca4af..7409778 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -52,7 +52,7 @@ def __init__(self, f): ''' Create a GDB file object from the gdb output file @param lines: The lines of the gdb file - @param is_crash: True if gdb file represents a crash + @param is_crash: True if gdb file represents a testcase @param is_assert_fail: True if gdb file represents an assert_fail @param is_debugbuild: True if gdb file contains source code lines ''' diff --git a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py index d47868a..577e543 100644 --- a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py +++ b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py @@ -88,7 +88,7 @@ def __init__(self, path, exclude_unmapped_frames=True, keep_uniq_faddr=False): ''' Create a GDB file object from the gdb output file @param lines: The lines of the gdb file - @param is_crash: True if gdb file represents a crash + @param is_crash: True if gdb file represents a testcase @param is_assert_fail: True if gdb file represents an assert_fail @param is_debugbuild: True if gdb file contains source code lines ''' diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index ed4eeb1..ba28c23 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -36,17 +36,17 @@ class Minimizer(object): use_watchdog = False _debugger_cls = None - def __init__(self, cfg=None, crash=None, crash_dst_dir=None, + def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, seedfile_as_target=False, bitwise=False, confidence=0.999, logfile=None, tempdir=None, maxtime=3600, preferx=False, keep_uniq_faddr=False, watchcpu=False): if not cfg: self._raise('Config must be specified') - if not crash: + if not testcase: self._raise('Crasher must be specified') self.cfg = cfg - self.crash = crash + self.testcase = testcase self.seedfile_as_target = seedfile_as_target self.bitwise = bitwise self.preferx = preferx @@ -60,7 +60,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, self.minchar = 'x' self.save_others = True - self.ext = self.crash.fuzzedfile.ext + self.ext = self.testcase.fuzzedfile.ext self.logger = None self.log_file_hdlr = None self.backtracelevels = 5 @@ -69,7 +69,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, logger.setLevel(logging.INFO) self.saved_arcinfo = None - self.is_zipfile = check_zip_file(crash.fuzzedfile.path) + self.is_zipfile = check_zip_file(testcase.fuzzedfile.path) if tempdir and os.path.isdir(tempdir): self.tempdir = tempfile.mkdtemp(prefix='minimizer_', dir=tempdir) @@ -87,19 +87,19 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, self.swap_func = self.bytewise_swap2 if self.seedfile_as_target: - minf = '%s-minimized%s' % (self.crash.fuzzedfile.root, self.crash.fuzzedfile.ext) - minlog = os.path.join(self.crash.fuzzedfile.dirname, 'minimizer_log.txt') - if not os.path.exists(self.crash.seedfile.path): + minf = '%s-minimized%s' % (self.testcase.fuzzedfile.root, self.testcase.fuzzedfile.ext) + minlog = os.path.join(self.testcase.fuzzedfile.dirname, 'minimizer_log.txt') + if not os.path.exists(self.testcase.seedfile.path): self._raise('Seedfile not found at %s' % - self.crash.seedfile.path) + self.testcase.seedfile.path) elif self.preferx: - minf = '%s-min-%s%s' % (self.crash.fuzzedfile.root, self.minchar, self.crash.fuzzedfile.ext) - minlog = os.path.join(self.crash.fuzzedfile.dirname, 'minimizer_%s_log.txt' % self.minchar) + minf = '%s-min-%s%s' % (self.testcase.fuzzedfile.root, self.minchar, self.testcase.fuzzedfile.ext) + minlog = os.path.join(self.testcase.fuzzedfile.dirname, 'minimizer_%s_log.txt' % self.minchar) else: - minf = '%s-min-mtsp%s' % (self.crash.fuzzedfile.root, self.crash.fuzzedfile.ext) - minlog = os.path.join(self.crash.fuzzedfile.dirname, 'minimizer_mtsp_log.txt') + minf = '%s-min-mtsp%s' % (self.testcase.fuzzedfile.root, self.testcase.fuzzedfile.ext) + minlog = os.path.join(self.testcase.fuzzedfile.dirname, 'minimizer_mtsp_log.txt') - self.outputfile = os.path.join(self.crash.fuzzedfile.dirname, minf) + self.outputfile = os.path.join(self.testcase.fuzzedfile.dirname, minf) if logfile: self.minimizer_logfile = logfile @@ -109,7 +109,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, if crash_dst_dir: self.crash_dst = crash_dst_dir else: - self.crash_dst = self.crash.fuzzedfile.dirname + self.crash_dst = self.testcase.fuzzedfile.dirname if not os.path.exists(self.crash_dst): self._raise("%s does not exist" % self.crash_dst) @@ -117,12 +117,12 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, self._raise("%s is not a directory" % self.crash_dst) self._logger_setup() - self.logger.info("Minimizer initializing for %s", self.crash.fuzzedfile.path) + self.logger.info("Minimizer initializing for %s", self.testcase.fuzzedfile.path) - if not os.path.exists(self.crash.fuzzedfile.path): - self._raise("%s does not exist" % self.crash.fuzzedfile.path) + if not os.path.exists(self.testcase.fuzzedfile.path): + self._raise("%s does not exist" % self.testcase.fuzzedfile.path) - self.crash.set_debugger_template('bt_only') + self.testcase.set_debugger_template('bt_only') self.other_crashes = {} self.target_size_guess = 1 @@ -146,7 +146,7 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, # self.saved_arcinfo = {} # self.is_zipfile = False -# self.is_zipfile = self.check_zipfile(self.crash.fuzzedfile.path) +# self.is_zipfile = self.check_zipfile(self.testcase.fuzzedfile.path) self.fuzzed_content = self._read_fuzzed() self.seed = self._read_seed() @@ -159,20 +159,20 @@ def __init__(self, cfg=None, crash=None, crash_dst_dir=None, self.start_distance = self.hd_func(self.seed, self.fuzzed_content) self.min_distance = self.start_distance - # some programs crash differently depending on where the + # some programs testcase differently depending on where the # file is loaded from. So we'll reuse this file name for # everything f = tempfile.NamedTemporaryFile('wb', delete=True, dir=self.tempdir, prefix="minimizer_fuzzed_file_", - suffix=self.crash.fuzzedfile.ext) + suffix=self.testcase.fuzzedfile.ext) # since we set delete=True, f will be deleted on close, # which makes it available to us to reuse f.close() self.tempfile = f.name - filetools.copy_file(self.crash.fuzzedfile.path, self.tempfile) + filetools.copy_file(self.testcase.fuzzedfile.path, self.tempfile) - # figure out what crash signatures belong to this fuzzedfile + # figure out what testcase signatures belong to this fuzzedfile self.debugger_timeout = self.cfg['runner']['runtimeout'] self.crash_hashes = [] self.measured_dbg_time = None @@ -194,15 +194,15 @@ def __enter__(self): except KeyError: pass if not self._is_crash_to_minimize(): - msg = 'Unable to minimize: No crash' + msg = 'Unable to minimize: No testcase' self.logger.info(msg) self._raise(msg) if self._is_already_minimized(): msg = 'Unable to minimize: Already minimized' self.logger.info(msg) self._raise(msg) - if self.crash.debugger_missed_stack_corruption: - msg = 'Unable to minimize: Stack corruption crash, which the debugger missed.' + if self.testcase.debugger_missed_stack_corruption: + msg = 'Unable to minimize: Stack corruption testcase, which the debugger missed.' self.logger.info(msg) self._raise(msg) # start the timer @@ -239,8 +239,8 @@ def _read_fuzzed(self): # store the files in memory if self.is_zipfile: # work with zip file contents, not the container logger.debug('Working with a zip file') - return self._readzip(self.crash.fuzzedfile.path) - return self.crash.fuzzedfile.read() + return self._readzip(self.testcase.fuzzedfile.path) + return self.testcase.fuzzedfile.read() def _read_seed(self): ''' @@ -249,9 +249,9 @@ def _read_seed(self): # we're either going to minimize to the seedfile, the metasploit pattern, or a string of 'x's if self.seedfile_as_target: if self.is_zipfile and self.seedfile_as_target: - return self._readzip(self.crash.seedfile.path) + return self._readzip(self.testcase.seedfile.path) else: - return self.crash.seedfile.read() + return self.testcase.seedfile.read() elif self.preferx: return self.minchar * len(self.fuzzed_content) else: @@ -337,9 +337,9 @@ def _set_crash_hashes(self): max_misses = probability.misses_until_quit(0.95, 0.5) sigs_seen = {} times = [] - # loop until we've found ALL the crash signatures + # loop until we've found ALL the testcase signatures while miss_count < max_misses: - # (sometimes crash sigs change for the same input file) + # (sometimes testcase sigs change for the same input file) (fd, f) = tempfile.mkstemp(prefix='minimizer_set_crash_hashes_', text=True, dir=self.tempdir) os.close(fd) delete_files(f) @@ -369,7 +369,7 @@ def _set_crash_hashes(self): sigs_seen[current_sig] = 1 miss_count = 0 else: - # this crash had no signature, so skip it + # this testcase had no signature, so skip it miss_count += 1 self.crash_hashes = sigs_seen.keys() # calculate average time @@ -399,7 +399,7 @@ def run_debugger(self, infile, outfile): cmd_args, outfile, self.debugger_timeout, - template=self.crash.debugger_template, + template=self.testcase.debugger_template, exclude_unmapped_frames=exclude_unmapped_frames, keep_uniq_faddr=self.keep_uniq_faddr, workingdir=self.tempdir, @@ -409,19 +409,19 @@ def run_debugger(self, infile, outfile): return parsed_debugger_output def _crash_builder(self): - self.logger.debug('Building new crash object.') + self.logger.debug('Building new testcase object.') import copy - # copy our original crash as the basis for the new crash - newcrash = copy.copy(self.crash) + # copy our original testcase as the basis for the new testcase + new_testcase = copy.copy(self.testcase) # get a new dir for the next crasher newcrash_tmpdir = tempfile.mkdtemp(prefix='minimizer_crash_builder_', dir=self.tempdir) # get a new filename for the next crasher - sfx = self.crash.fuzzedfile.ext - if self.crash.seedfile: - pfx = '%s-' % self.crash.seedfile.root + sfx = self.testcase.fuzzedfile.ext + if self.testcase.seedfile: + pfx = '%s-' % self.testcase.seedfile.root else: pfx = 'string-' (fd, f) = tempfile.mkstemp(suffix=sfx, prefix=pfx, dir=newcrash_tmpdir) @@ -434,28 +434,28 @@ def _crash_builder(self): self.logger.debug('\tCopying %s to %s', self.tempfile, outfile) filetools.copy_file(self.tempfile, outfile) - newcrash.fuzzedfile = BasicFile(outfile) - self.logger.debug('\tNew fuzzed_content file: %s %s', newcrash.fuzzedfile.path, newcrash.fuzzedfile.md5) + new_testcase.fuzzedfile = BasicFile(outfile) + self.logger.debug('\tNew fuzzed_content file: %s %s', new_testcase.fuzzedfile.path, new_testcase.fuzzedfile.md5) - # clear out the copied crash signature so that it will be regenerated - newcrash.signature = None + # clear out the copied testcase signature so that it will be regenerated + new_testcase.signature = None - # replace old crash details with new info specific to this crash - self.logger.debug('\tUpdating crash details') - newcrash.update_crash_details() + # replace old testcase details with new info specific to this testcase + self.logger.debug('\tUpdating testcase details') + new_testcase.update_crash_details() # the tempdir we created is no longer needed because update_crash_details creates a fresh one shutil.rmtree(newcrash_tmpdir) if os.path.exists(newcrash_tmpdir): logger.warning("Failed to remove temp dir %s", newcrash_tmpdir) - return newcrash + return new_testcase def get_signature(self, dbg, backtracelevels): signature = dbg.get_crash_signature(backtracelevels) if dbg.total_stack_corruption: # total_stack_corruption. Use pin calltrace to get a backtrace - analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self.crash) + analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self.testcase) try: analyzer_instance.go() except AnalyzerEmptyOutputError: @@ -491,17 +491,17 @@ def is_same_crash(self): # don't do anything with non-crashes pass else: - # the crash is new to this minimization run + # the testcase is new to this minimization run self.crash_sigs_found[newfuzzed_hash] = 1 - self.logger.info('crash=%s signal=%s', newfuzzed_hash, dbg.signal) + self.logger.info('testcase=%s signal=%s', newfuzzed_hash, dbg.signal) if self.save_others and newfuzzed_hash not in self.crash_hashes: - # the crash is not one of the crashes we're looking for + # the testcase is not one of the crashes we're looking for # so add it to the other_crashes dict in case our # caller wants to do something with it newcrash = self._crash_builder() if newcrash.is_crash: - # note that since we're doing this every time we see a crash + # note that since we're doing this every time we see a testcase # that's not in self.crash_hashes, we're also effectively # keeping only the smallest hamming distance version of # newfuzzed_hash as we progress through the minimization process @@ -602,12 +602,12 @@ def _write_file(self): def go(self): # start by copying the fuzzed_content file since as of now it's our best fit - filetools.copy_file(self.crash.fuzzedfile.path, self.outputfile) + filetools.copy_file(self.testcase.fuzzedfile.path, self.outputfile) - # replace the fuzzedfile object in crash with the minimized copy - self.crash.fuzzedfile = BasicFile(self.outputfile) + # replace the fuzzedfile object in testcase with the minimized copy + self.testcase.fuzzedfile = BasicFile(self.outputfile) - self.logger.info('Attempting to minimize crash(es) [%s]', self._crash_hashes_string()) + self.logger.info('Attempting to minimize testcase(es) [%s]', self._crash_hashes_string()) # keep going until either: # a. we find a minimum hd of 1 @@ -705,7 +705,7 @@ def go(self): # 1. copy the tempfile filetools.best_effort_move(self.tempfile, self.outputfile) # 2. replace the fuzzed_content file in the crasher with the current one - self.crash.fuzzedfile = BasicFile(self.outputfile) + self.testcase.fuzzedfile = BasicFile(self.outputfile) # 3. replace the current fuzzed_content with newfuzzed self.fuzzed_content = self.newfuzzed self.min_distance = self.newfuzzed_hd @@ -728,7 +728,7 @@ def go(self): self.consecutive_misses += 1 # Fix for BFF-225 - # There may be some situation that causes crash uniqueness + # There may be some situation that causes testcase uniqueness # hashing to break. (e.g. BFF-224 ). Minimizer should bail # if the number of unique crashes encountered exceeds some # threshold. e.g. 20 maybe? diff --git a/src/certfuzz/minimizer/win_minimizer.py b/src/certfuzz/minimizer/win_minimizer.py index 0396d83..550e48e 100755 --- a/src/certfuzz/minimizer/win_minimizer.py +++ b/src/certfuzz/minimizer/win_minimizer.py @@ -16,15 +16,15 @@ class WindowsMinimizer(MinimizerBase): use_watchdog = False _debugger_cls = MsecDebugger - def __init__(self, cfg=None, crash=None, crash_dst_dir=None, + def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, seedfile_as_target=False, bitwise=False, confidence=0.999, logfile=None, tempdir=None, maxtime=3600, preferx=False, keep_uniq_faddr=False, watchcpu=False): self.saved_arcinfo = None - self.is_zipfile = check_zip_file(crash.fuzzedfile.path) + self.is_zipfile = check_zip_file(testcase.fuzzedfile.path) - MinimizerBase.__init__(self, cfg, crash, crash_dst_dir, + MinimizerBase.__init__(self, cfg, testcase, crash_dst_dir, seedfile_as_target, bitwise, confidence, logfile, tempdir, maxtime, preferx, keep_uniq_faddr, watchcpu) @@ -36,7 +36,7 @@ def get_signature(self, dbg, backtracelevels): self.signature = None else: crash_id_parts = [crash_hash] - if self.crash.keep_uniq_faddr and hasattr(dbg, 'faddr'): + if self.testcase.keep_uniq_faddr and hasattr(dbg, 'faddr'): crash_id_parts.append(dbg.faddr) self.signature = '.'.join(crash_id_parts) return self.signature @@ -48,7 +48,7 @@ def _read_fuzzed(self): # store the files in memory if self.is_zipfile: # work with zip file contents, not the container logger.debug('Working with a zip file') - return self._readzip(self.crash.fuzzedfile.path) + return self._readzip(self.testcase.fuzzedfile.path) # otherwise just call the parent class method return MinimizerBase._read_fuzzed(self) @@ -59,7 +59,7 @@ def _read_seed(self): # we're either going to minimize to the seedfile, the metasploit # pattern, or a string of 'x's if self.is_zipfile and self.seedfile_as_target: - return self._readzip(self.crash.seedfile.path) + return self._readzip(self.testcase.seedfile.path) # otherwise just call the parent class method return MinimizerBase._read_seed(self) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index 9bccc1a..d54a994 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -200,7 +200,7 @@ def _minimize(self, testcase): # build arguments for minimizer invocation kwargs = {'cfg': self.cfg, - 'crash': testcase, + 'testcase': testcase, 'seedfile_as_target': True, 'bitwise': False, 'confidence': 0.999, diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 19eec6b..b45530e 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -48,7 +48,7 @@ def main(): help='Minimize to \'x\' characters instead of Metasploit string pattern') parser.add_option('-f', '--faddr', dest='keep_uniq_faddr', action='store_true', - help='Use exception faulting addresses as part of crash signature') + help='Use exception faulting addresses as part of testcase signature') parser.add_option('-b', '--bitwise', dest='bitwise', action='store_true', help='if set, use bitwise hamming distance. Default is bytewise') parser.add_option('-c', '--confidence', dest='confidence', @@ -144,18 +144,18 @@ def main(): backtrace_lines=cfg['debugger']['backtracelevels'], crashers_dir=crashers_dir, workdir_base=None, - keep_faddr=options.keep_uniq_faddr) as crash: + keep_faddr=options.keep_uniq_faddr) as testcase: - filetools.make_directories(crash.tempdir) - logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) - filetools.copy_file(fuzzed_file.path, crash.tempdir) + filetools.make_directories(testcase.tempdir) + logger.info('Copying %s to %s', fuzzed_file.path, testcase.tempdir) + filetools.copy_file(fuzzed_file.path, testcase.tempdir) - with Minimizer(cfg=cfg, crash=crash, crash_dst_dir=outdir, + with Minimizer(cfg=cfg, testcase=testcase, crash_dst_dir=outdir, seedfile_as_target=min2seed, bitwise=options.bitwise, confidence=confidence, logfile='./min_log.txt', - tempdir=crash.tempdir, + tempdir=testcase.tempdir, maxtime=options.timeout, preferx=options.prefer_x_target, keep_uniq_faddr=options.keep_uniq_faddr) as minimize: @@ -179,14 +179,14 @@ def main(): for idx in minimize.bytemap: logger.debug('Swapping index %d', idx) targetstring[idx] = fuzzed[idx] - filename = ''.join((crash.fuzzedfile.root, filename_modifier, crash.fuzzedfile.ext)) - metasploit_file = os.path.join(crash.tempdir, filename) + filename = ''.join((testcase.fuzzedfile.root, filename_modifier, testcase.fuzzedfile.ext)) + metasploit_file = os.path.join(testcase.tempdir, filename) with open(metasploit_file, 'wb') as f: f.writelines(targetstring) - crash.copy_files(outdir) - crash.clean_tmpdir() + testcase.copy_files(outdir) + testcase.clean_tmpdir() if __name__ == '__main__': main() diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 33609e5..c00d40f 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -75,7 +75,7 @@ def main(): help='Minimize to \'x\' characters instead of Metasploit string pattern') parser.add_option('-f', '--faddr', dest='keep_uniq_faddr', action='store_true', - help='Use exception faulting addresses as part of crash signature') + help='Use exception faulting addresses as part of testcase signature') parser.add_option('-b', '--bitwise', dest='bitwise', action='store_true', help='if set, use bitwise hamming distance. Default is bytewise') parser.add_option('-c', '--confidence', dest='confidence', @@ -89,7 +89,7 @@ def main(): help='Stop minimizing after N seconds (default is 0, never time out).') parser.add_option('-k', '--keepothers', dest='keep_other_crashes', action='store_true', - help='Keep other crash hashes encountered during minimization') + help='Keep other testcase hashes encountered during minimization') (options, args) = parser.parse_args() if options.debug: @@ -167,14 +167,14 @@ def main(): keep_faddr=options.keep_uniq_faddr, program=cfg['target']['program'], heisenbug_retries=retries - ) as crash: - filetools.make_directories(crash.tempdir) - logger.info('Copying %s to %s', fuzzed_file.path, crash.tempdir) - filetools.copy_file(fuzzed_file.path, crash.tempdir) + ) as testcase: + filetools.make_directories(testcase.tempdir) + logger.info('Copying %s to %s', fuzzed_file.path, testcase.tempdir) + filetools.copy_file(fuzzed_file.path, testcase.tempdir) minlog = os.path.join(outdir, 'min_log.txt') - with Minimizer(cfg=cfg, crash=crash, crash_dst_dir=outdir, + with Minimizer(cfg=cfg, testcase=testcase, crash_dst_dir=outdir, seedfile_as_target=min2seed, bitwise=options.bitwise, confidence=confidence, tempdir=outdir, logfile=minlog, maxtime=options.timeout, @@ -199,16 +199,16 @@ def main(): for idx in minimize.bytemap: logger.debug('Swapping index %d', idx) targetstring[idx] = fuzzed[idx] - filename = ''.join((crash.fuzzedfile.root, filename_modifier, crash.fuzzedfile.ext)) - metasploit_file = os.path.join(crash.tempdir, filename) + filename = ''.join((testcase.fuzzedfile.root, filename_modifier, testcase.fuzzedfile.ext)) + metasploit_file = os.path.join(testcase.tempdir, filename) f = open(metasploit_file, 'wb') try: f.writelines(targetstring) finally: f.close() - crash.copy_files(outdir) - crash.clean_tmpdir() + testcase.copy_files(outdir) + testcase.clean_tmpdir() if __name__ == '__main__': diff --git a/src/test_certfuzz/analyzers/test_stderr.py b/src/test_certfuzz/analyzers/test_stderr.py index ba0b006..de149d8 100644 --- a/src/test_certfuzz/analyzers/test_stderr.py +++ b/src/test_certfuzz/analyzers/test_stderr.py @@ -35,12 +35,12 @@ def setUp(self): cfg['target']['cmdline_template'] = string.Template('cat -a foo') - crash = Mock() - crash.fuzzedfile = Mock() - crash.fuzzedfile.path = f - crash.fuzzedfile.dirname = os.path.dirname(f) + testcase = Mock() + testcase.fuzzedfile = Mock() + testcase.fuzzedfile.path = f + testcase.fuzzedfile.dirname = os.path.dirname(f) - self.se = StdErr(cfg, crash) + self.se = StdErr(cfg, testcase) def tearDown(self): if os.path.exists(self.file): @@ -58,5 +58,5 @@ def test_get_stderr(self): self.assertTrue('cat' in contents) if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] + # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/test_certfuzz/analyzers/test_valgrind.py b/src/test_certfuzz/analyzers/test_valgrind.py index 09e067b..e8cf603 100644 --- a/src/test_certfuzz/analyzers/test_valgrind.py +++ b/src/test_certfuzz/analyzers/test_valgrind.py @@ -20,11 +20,11 @@ def delete_file(self, f): def setUp(self): cfg = MockFixupCfg() - crash = Mock() - crash.fuzzedfile = Mock() - crash.fuzzedfile.path = "foo" - crash.fuzzedfile.dirname = 'foodir' - self.vg = Valgrind(cfg, crash) + testcase = Mock() + testcase.fuzzedfile = Mock() + testcase.fuzzedfile.path = "foo" + testcase.fuzzedfile.dirname = 'foodir' + self.vg = Valgrind(cfg, testcase) def tearDown(self): pass diff --git a/src/test_certfuzz/campaign/test_campaign_base.py b/src/test_certfuzz/campaign/test_campaign_base.py index 64b3614..c54c36e 100644 --- a/src/test_certfuzz/campaign/test_campaign_base.py +++ b/src/test_certfuzz/campaign/test_campaign_base.py @@ -121,18 +121,18 @@ def test_cleanup_workdir(self): self.assertFalse(os.path.exists(self.campaign.working_dir)) def test_crash_is_unique(self): - self.assertEqual(0, len(self.campaign.crashes_seen)) - self.assertTrue(self.campaign._crash_is_unique(1)) - self.assertEqual(1, len(self.campaign.crashes_seen)) - self.assertFalse(self.campaign._crash_is_unique(1)) - self.assertEqual(1, len(self.campaign.crashes_seen)) - self.assertTrue(self.campaign._crash_is_unique(2)) - self.assertEqual(2, len(self.campaign.crashes_seen)) - self.assertFalse(self.campaign._crash_is_unique(2)) - self.assertEqual(2, len(self.campaign.crashes_seen)) + self.assertEqual(0, len(self.campaign.testcases_seen)) + self.assertTrue(self.campaign._testcase_is_unique(1)) + self.assertEqual(1, len(self.campaign.testcases_seen)) + self.assertFalse(self.campaign._testcase_is_unique(1)) + self.assertEqual(1, len(self.campaign.testcases_seen)) + self.assertTrue(self.campaign._testcase_is_unique(2)) + self.assertEqual(2, len(self.campaign.testcases_seen)) + self.assertFalse(self.campaign._testcase_is_unique(2)) + self.assertEqual(2, len(self.campaign.testcases_seen)) for x in [1, 2]: - self.assertTrue(x in self.campaign.crashes_seen) + self.assertTrue(x in self.campaign.testcases_seen) def test_keep_going(self): for _x in range(100): diff --git a/src/test_certfuzz/minimizer/test_minimizer_base.py b/src/test_certfuzz/minimizer/test_minimizer_base.py index bbb28ce..aebe3cc 100644 --- a/src/test_certfuzz/minimizer/test_minimizer_base.py +++ b/src/test_certfuzz/minimizer/test_minimizer_base.py @@ -18,7 +18,7 @@ def delete_file(self, f): def setUp(self): self.cfg = MockFixupCfg() - self.crash = MockCrasher() + self.testcase = MockCrasher() Minimizer._debugger_cls = MockDebugger @@ -29,7 +29,7 @@ def setUp(self): os.remove(self.logfile) self.assertFalse(os.path.exists(self.logfile)) - self.m = Minimizer(cfg=self.cfg, crash=self.crash, + self.m = Minimizer(cfg=self.cfg, testcase=self.testcase, crash_dst_dir=self.crash_dst_dir, logfile=self.logfile, tempdir=self.tempdir) From d06b5d6a56bb1a8196df31df124756b1b1f15f61 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 23 Feb 2016 08:34:43 -0500 Subject: [PATCH 0891/1169] rename get_crash_signature to get_testcase_signature --- src/certfuzz/debuggers/output_parsers/abrtfile.py | 2 +- src/certfuzz/debuggers/output_parsers/calltracefile.py | 6 +++--- src/certfuzz/debuggers/output_parsers/cwfile.py | 6 +++--- .../debuggers/output_parsers/debugger_file_base.py | 6 +++--- src/certfuzz/debuggers/output_parsers/konqifile.py | 2 +- src/certfuzz/debuggers/output_parsers/msec_file.py | 2 +- src/certfuzz/minimizer/minimizer_base.py | 4 ++-- src/certfuzz/testcase/testcase_linux.py | 4 ++-- src/certfuzz/tools/linux/debuggerfile.py | 2 +- .../debuggers/output_parsers/test_gdbfile.py | 8 ++++---- src/test_certfuzz/mocks.py | 2 +- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/abrtfile.py b/src/certfuzz/debuggers/output_parsers/abrtfile.py index 5976027..1dd2b8c 100644 --- a/src/certfuzz/debuggers/output_parsers/abrtfile.py +++ b/src/certfuzz/debuggers/output_parsers/abrtfile.py @@ -191,6 +191,6 @@ def _look_for_proc_map(self, line): for f in args: a = ABRTfile(f) - print 'Signature=%s' % a.get_crash_signature(5) + print 'Signature=%s' % a.get_testcase_signature(5) if a.registers_hex.get('eip'): print 'EIP=%s' % a.registers_hex['eip'] diff --git a/src/certfuzz/debuggers/output_parsers/calltracefile.py b/src/certfuzz/debuggers/output_parsers/calltracefile.py index b80b16d..1c30885 100644 --- a/src/certfuzz/debuggers/output_parsers/calltracefile.py +++ b/src/certfuzz/debuggers/output_parsers/calltracefile.py @@ -82,13 +82,13 @@ def _process_lines(self): self.calltrace_line(idx, line) - def get_crash_signature(self, backtrace_level): + def get_testcase_signature(self, backtrace_level): ''' Determines if a crash is unique. Depending on , it may look at a number of source code lines in the gdb backtrace, or simply just the memory location of the crash. ''' - logger.debug('get_crash_signature') + logger.debug('get_testcase_signature') backtrace_string = self._hashable_backtrace_string(backtrace_level) if bool(backtrace_string): return hashlib.md5(backtrace_string).hexdigest() @@ -108,4 +108,4 @@ def get_crash_signature(self, backtrace_level): for path in args: g = Calltracefile(path) - print g.get_crash_signature(50) + print g.get_testcase_signature(50) diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index 7409778..3a86d70 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -303,13 +303,13 @@ def _look_for_registers(self, line): self.registers_sought.remove(r) logger.debug('Register %s=%s', r, self.registers_hex[r]) - def get_crash_signature(self, backtrace_level): + def get_testcase_signature(self, backtrace_level): ''' Determines if a crash is unique. Depending on , it may look at a number of source code lines in the gdb backtrace, or simply just the memory location of the crash. ''' - logger.debug('get_crash_signature') + logger.debug('get_testcase_signature') backtrace_string = self._hashable_backtrace_string(backtrace_level) if bool(backtrace_string): return hashlib.md5(backtrace_string).hexdigest() @@ -329,4 +329,4 @@ def get_crash_signature(self, backtrace_level): for path in args: g = CWfile(path) - print g.get_crash_signature(5) + print g.get_testcase_signature(5) diff --git a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py index 577e543..f4e7d62 100644 --- a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py +++ b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py @@ -539,13 +539,13 @@ def _look_for_registers(self, line): self.registers_sought.remove(r) logger.debug('Register %s=%s', r, self.registers_hex[r]) - def get_crash_signature(self, backtrace_level): + def get_testcase_signature(self, backtrace_level): ''' Determines if a crash is unique. Depending on , it may look at a number of source code lines in the gdb backtrace, or simply just the memory location of the crash. ''' - logger.debug('get_crash_signature') + logger.debug('get_testcase_signature') backtrace_string = self._hashable_backtrace_string(backtrace_level) if bool(backtrace_string): @@ -596,7 +596,7 @@ def _analyze_file(filepath, include_bt=False): bthash = 'no_backtrace_available' if bt: - sig = bt.get_crash_signature(5) + sig = bt.get_testcase_signature(5) if bt.total_stack_corruption: sig = "total_stack_corruption" if not sig: diff --git a/src/certfuzz/debuggers/output_parsers/konqifile.py b/src/certfuzz/debuggers/output_parsers/konqifile.py index 236f263..3b7849f 100644 --- a/src/certfuzz/debuggers/output_parsers/konqifile.py +++ b/src/certfuzz/debuggers/output_parsers/konqifile.py @@ -208,6 +208,6 @@ def _look_for_proc_map(self, line): for f in args: k = Konqifile(f) - print 'Signature=%s' % k.get_crash_signature(5) + print 'Signature=%s' % k.get_testcase_signature(5) if k.registers_hex.get('eip'): print 'EIP=%s' % k.registers_hex['eip'] diff --git a/src/certfuzz/debuggers/output_parsers/msec_file.py b/src/certfuzz/debuggers/output_parsers/msec_file.py index 598dd1b..f1bdf23 100644 --- a/src/certfuzz/debuggers/output_parsers/msec_file.py +++ b/src/certfuzz/debuggers/output_parsers/msec_file.py @@ -52,7 +52,7 @@ def _process_backtrace(self): def _hashable_backtrace(self): pass - def get_crash_signature(self, backtrace_level): + def get_testcase_signature(self, backtrace_level): return self.crash_hash def _find_exploitability(self, line): diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index ba28c23..41fb543 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -452,7 +452,7 @@ def _crash_builder(self): return new_testcase def get_signature(self, dbg, backtracelevels): - signature = dbg.get_crash_signature(backtracelevels) + signature = dbg.get_testcase_signature(backtracelevels) if dbg.total_stack_corruption: # total_stack_corruption. Use pin calltrace to get a backtrace analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self.testcase) @@ -462,7 +462,7 @@ def get_signature(self, dbg, backtracelevels): logger.warning('Unexpected empty output from analyzer. Continuing') if os.path.exists(analyzer_instance.outfile): calltrace = Calltracefile(analyzer_instance.outfile) - pinsignature = calltrace.get_crash_signature(backtracelevels * 10) + pinsignature = calltrace.get_testcase_signature(backtracelevels * 10) if pinsignature: signature = pinsignature return signature diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index b7cc811..dcc59f4 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -135,7 +135,7 @@ def get_signature(self): @raise CrasherHasNoSignatureError: if it's a valid crash, but we don't get a signature ''' if not self.signature: - self.signature = self.dbg.get_crash_signature(self.backtrace_lines) + self.signature = self.dbg.get_testcase_signature(self.backtrace_lines) if self.signature: logger.debug("Testcase signature is %s", self.signature) else: @@ -150,7 +150,7 @@ def get_signature(self): return self.signature calltrace = Calltracefile(analyzer_instance.outfile) - pinsignature = calltrace.get_crash_signature(self.backtrace_lines * 10) + pinsignature = calltrace.get_testcase_signature(self.backtrace_lines * 10) if pinsignature: self.signature = pinsignature return self.signature diff --git a/src/certfuzz/tools/linux/debuggerfile.py b/src/certfuzz/tools/linux/debuggerfile.py index 51b8a91..e055894 100755 --- a/src/certfuzz/tools/linux/debuggerfile.py +++ b/src/certfuzz/tools/linux/debuggerfile.py @@ -47,7 +47,7 @@ def main(): parser.error('No file suffix found, but \'.gdb\' or \'.cw\' expected') else: parser.error('Unknown file suffix \'%s\' found, but \'.gdb\' or \'.cw\' expected' % debugger) - print 'Signature=%s' % g.get_crash_signature(5) + print 'Signature=%s' % g.get_testcase_signature(5) if g.registers_hex.get(g.pc_name): print 'PC=%s' % g.registers_hex[g.pc_name] diff --git a/src/test_certfuzz/debuggers/output_parsers/test_gdbfile.py b/src/test_certfuzz/debuggers/output_parsers/test_gdbfile.py index c336e7c..baa6107 100644 --- a/src/test_certfuzz/debuggers/output_parsers/test_gdbfile.py +++ b/src/test_certfuzz/debuggers/output_parsers/test_gdbfile.py @@ -111,7 +111,7 @@ def test_hashable_backtrace_string(self): self.assertEqual(gdbf._hashable_backtrace_string(2), '0x11111111 foo.c:80') self.assertEqual(gdbf._hashable_backtrace_string(3), '0x11111111 foo.c:80 0x33333333') - def test_get_crash_signature(self): + def test_get_testcase_signature(self): gdbf = GDBfile(self.file) self.assertFalse(gdbf._hashable_backtrace()) @@ -121,9 +121,9 @@ def test_get_crash_signature(self): gdbf._process_lines() gdbf._hashable_backtrace() - self.assertEqual(gdbf.get_crash_signature(1), hashlib.md5('0x11111111').hexdigest()) - self.assertEqual(gdbf.get_crash_signature(2), hashlib.md5('0x11111111 foo.c:80').hexdigest()) - self.assertEqual(gdbf.get_crash_signature(3), hashlib.md5('0x11111111 foo.c:80 0x33333333').hexdigest()) + self.assertEqual(gdbf.get_testcase_signature(1), hashlib.md5('0x11111111').hexdigest()) + self.assertEqual(gdbf.get_testcase_signature(2), hashlib.md5('0x11111111 foo.c:80').hexdigest()) + self.assertEqual(gdbf.get_testcase_signature(3), hashlib.md5('0x11111111 foo.c:80 0x33333333').hexdigest()) def test_backtrace(self): (fd, f) = tempfile.mkstemp(text=True) diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 50ff7a3..497ef5e 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -88,7 +88,7 @@ class MockDbgOut(Mock): is_crash = False total_stack_corruption = False - def get_crash_signature(self, *dummyargs): + def get_testcase_signature(self, *dummyargs): return 'AAAAA' class MockDebugger(Mock): From 14553b2786d950fabadeed7d2c624e8c0b285912 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 23 Feb 2016 09:35:42 -0500 Subject: [PATCH 0892/1169] replace some perl-looking dictionary usage with python sets. --- src/certfuzz/minimizer/minimizer_base.py | 214 +++++++++--------- .../minimizer/test_minimizer_base.py | 6 - 2 files changed, 105 insertions(+), 115 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 41fb543..0b8f8cb 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -24,6 +24,7 @@ from certfuzz.analyzers.errors import AnalyzerEmptyOutputError from certfuzz.debuggers.output_parsers.calltracefile import Calltracefile from certfuzz.fuzztools.command_line_templating import get_command_args_list +import copy logger = logging.getLogger(__name__) @@ -139,7 +140,7 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, self.newfuzzed = '' self.crash_sigs_found = {} - self.files_tried = {} + self.files_tried = set() self.files_tried_at_hd = {} self.files_tried_singlebyte_at_hd = {} self.bytemap = [] @@ -236,10 +237,12 @@ def _read_fuzzed(self): ''' returns the contents of the fuzzed_content file ''' + # store the files in memory if self.is_zipfile: # work with zip file contents, not the container logger.debug('Working with a zip file') return self._readzip(self.testcase.fuzzedfile.path) + return self.testcase.fuzzedfile.read() def _read_seed(self): @@ -329,58 +332,61 @@ def _logger_setup(self): self.logger.addHandler(self.log_file_hdlr) def _set_crash_hashes(self): - if not self.crash_hashes: - miss_count = 0 - # we want to keep going until we are 0.95 confident that - # if there are any other crashers they have a probability - # less than 0.5 - max_misses = probability.misses_until_quit(0.95, 0.5) - sigs_seen = {} - times = [] - # loop until we've found ALL the testcase signatures - while miss_count < max_misses: - # (sometimes testcase sigs change for the same input file) - (fd, f) = tempfile.mkstemp(prefix='minimizer_set_crash_hashes_', text=True, dir=self.tempdir) - os.close(fd) - delete_files(f) + if self.crash_hashes: + # shortcut if it's already set + return self.crash_hashes + + miss_count = 0 + # we want to keep going until we are 0.95 confident that + # if there are any other crashers they have a probability + # less than 0.5 + max_misses = probability.misses_until_quit(0.95, 0.5) + sigs_set = set() + times = [] + # loop until we've found ALL the testcase signatures + while miss_count < max_misses: + # (sometimes testcase sigs change for the same input file) + (fd, f) = tempfile.mkstemp(prefix='minimizer_set_crash_hashes_', text=True, dir=self.tempdir) + os.close(fd) + delete_files(f) - # run debugger - start = time.time() + # run debugger + start = time.time() - dbg = self.run_debugger(self.tempfile, f) + dbg = self.run_debugger(self.tempfile, f) - # remember the elapsed time for later - end = time.time() - delta = end - start - if dbg.is_crash: - times.append(delta) + # remember the elapsed time for later + end = time.time() + delta = end - start - current_sig = self.get_signature(dbg, self.backtracelevels) + if dbg.is_crash: + times.append(delta) - # ditch the temp file - if os.path.exists(f): - delete_files(f) + current_sig = self.get_signature(dbg, self.backtracelevels) - if current_sig: - if sigs_seen.get(current_sig): - sigs_seen[current_sig] += 1 - miss_count += 1 - else: - sigs_seen[current_sig] = 1 - miss_count = 0 - else: - # this testcase had no signature, so skip it + # ditch the temp file + if os.path.exists(f): + delete_files(f) + + if current_sig: + if current_sig in sigs_set: miss_count += 1 - self.crash_hashes = sigs_seen.keys() - # calculate average time - # get stdev - avg_time = numpy.average(times) - stdev_time = numpy.std(times) - # set debugger timeout to 0.95 confidence - # TODO: What if the VM becomes slower. - # We may give up on crashes before they happen. - zscore = 1.645 - self.measured_dbg_time = avg_time + (zscore * stdev_time) + else: + sigs_set.add(current_sig) + miss_count = 0 + else: + # this testcase had no signature, so skip it + miss_count += 1 + self.crash_hashes = list(sigs_set) + # calculate average time + # get stdev + avg_time = numpy.average(times) + stdev_time = numpy.std(times) + # set debugger timeout to 0.95 confidence + # TODO: What if the VM becomes slower. + # We may give up on crashes before they happen. + zscore = 1.645 + self.measured_dbg_time = avg_time + (zscore * stdev_time) return self.crash_hashes @@ -390,10 +396,7 @@ def run_debugger(self, infile, outfile): cmd = cmd_args[0] cmd_args = cmd_args[1:] - try: - exclude_unmapped_frames = self.cfg['analyzer']['exclude_unmapped_frames'] - except KeyError: - exclude_unmapped_frames = True + exclude_unmapped_frames = self.cfg['analyzer'].get('exclude_unmapped_frames', True) dbg = self._debugger_cls(cmd, cmd_args, @@ -406,11 +409,11 @@ def run_debugger(self, infile, outfile): watchcpu=self.watchcpu ) parsed_debugger_output = dbg.go() + return parsed_debugger_output def _crash_builder(self): self.logger.debug('Building new testcase object.') - import copy # copy our original testcase as the basis for the new testcase new_testcase = copy.copy(self.testcase) @@ -424,6 +427,7 @@ def _crash_builder(self): pfx = '%s-' % self.testcase.seedfile.root else: pfx = 'string-' + (fd, f) = tempfile.mkstemp(suffix=sfx, prefix=pfx, dir=newcrash_tmpdir) os.close(fd) delete_files(f) @@ -451,20 +455,27 @@ def _crash_builder(self): return new_testcase + + def _get_pin_signature(self, backtracelevels): + # total_stack_corruption. Use pin calltrace to get a backtrace + analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self.testcase) + try: + analyzer_instance.go() + except AnalyzerEmptyOutputError: + logger.warning('Unexpected empty output from analyzer. Continuing') + if os.path.exists(analyzer_instance.outfile): + calltrace = Calltracefile(analyzer_instance.outfile) + pinsignature = calltrace.get_testcase_signature(backtracelevels * 10) + if pinsignature: + signature = pinsignature + return signature + def get_signature(self, dbg, backtracelevels): signature = dbg.get_testcase_signature(backtracelevels) + if dbg.total_stack_corruption: - # total_stack_corruption. Use pin calltrace to get a backtrace - analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self.testcase) - try: - analyzer_instance.go() - except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from analyzer. Continuing') - if os.path.exists(analyzer_instance.outfile): - calltrace = Calltracefile(analyzer_instance.outfile) - pinsignature = calltrace.get_testcase_signature(backtracelevels * 10) - if pinsignature: - signature = pinsignature + signature = self._get_pin_signature(backtracelevels) + return signature def is_same_crash(self): @@ -540,17 +551,6 @@ def set_n_misses(self): self.n_misses_allowed = p.how_many_misses_until_quit(self.confidence_level) return True - def have_we_seen_this_file_before(self): - # is this a new file? - if not self.files_tried.get(self.newfuzzed_md5): - # totally new file - self.files_tried[self.newfuzzed_md5] = 1 - return False - - # it's a repeat - self.files_tried[self.newfuzzed_md5] += 1 - return True - def print_intermediate_log(self): if not self.newfuzzed_hd: self.logger.debug('self.newfuzzed_hd not set. Default to self.min_distance') @@ -645,48 +645,41 @@ def go(self): self.total_tries += 1 - is_repeat = self.have_we_seen_this_file_before() - # have we been at this level before? if not self.files_tried_at_hd.get(self.min_distance): - # we've reached a new minimum - self.files_tried_at_hd[self.min_distance] = {} - self.files_tried_singlebyte_at_hd[self.min_distance] = {} - - # have we seen this file at this level before? - if not self.files_tried_at_hd[self.min_distance].get(self.newfuzzed_md5): - # this is a new file so we'll try it - self.files_tried_at_hd[self.min_distance][self.newfuzzed_md5] = 1 - if self.newfuzzed_hd == (self.min_distance - 1): - self.files_tried_singlebyte_at_hd[self.min_distance][self.newfuzzed_md5] = 1 - else: - # this is a repeat at this level - self.files_tried_at_hd[self.min_distance][self.newfuzzed_md5] += 1 - if self.newfuzzed_hd == (self.min_distance - 1): - self.files_tried_singlebyte_at_hd[self.min_distance][self.newfuzzed_md5] += 1 - - # have we exhausted all the possible files with smaller hd? - possible_files = (2 ** self.min_distance) - 2 - seen_files = len(self.files_tried_at_hd[self.min_distance]) - # maybe we're done? - if seen_files == possible_files: - # we've exhaustively searched everything with hd < self.min_distance - self.logger.info('Exhaustively searched all files shorter than %d', self.min_distance) - self.min_found = True - break + # we've reached a new minimum, so create new sets + self.files_tried_at_hd[self.min_distance] = set() + self.files_tried_singlebyte_at_hd[self.min_distance] = set() + + # have we exhausted all the possible files with smaller hd? + possible_files = (2 ** self.min_distance) - 2 + seen_files = len(self.files_tried_at_hd[self.min_distance]) + + # maybe we're done? + if seen_files == possible_files: + # we've exhaustively searched everything with hd < self.min_distance + self.logger.info('Exhaustively searched all files shorter than %d', self.min_distance) + self.min_found = True + break - # have we exhausted all files that are 1 byte smaller hd? - possible_singlebyte_diff_files = self.min_distance - singlebyte_diff_files_seen = len(self.files_tried_singlebyte_at_hd[self.min_distance]) - # maybe we're done? - if singlebyte_diff_files_seen == possible_singlebyte_diff_files: - self.logger.info('We have tried all %d files that are one byte closer than the current minimum', self.min_distance) - self.min_found = True - break + # have we exhausted all files that are 1 byte smaller hd? + possible_singlebyte_diff_files = self.min_distance + singlebyte_diff_files_seen = len(self.files_tried_singlebyte_at_hd[self.min_distance]) + + # maybe we're done? + if singlebyte_diff_files_seen == possible_singlebyte_diff_files: + self.logger.info('We have tried all %d files that are one byte closer than the current minimum', self.min_distance) + self.min_found = True + break + + # remember this file for next time around + self.files_tried_at_hd[self.min_distance].add(self.newfuzzed_md5) + if self.newfuzzed_hd == (self.min_distance - 1): + self.files_tried_singlebyte_at_hd[self.min_distance].add(self.newfuzzed_md5) self.print_intermediate_log() - if is_repeat: + if self.newfuzzed_md5 in self.files_tried: # we've already seen this attempt, so skip ahead to the next one # but still count it as a miss since our math assumes we're putting # the marbles back in the jar after each draw @@ -694,6 +687,9 @@ def go(self): self.total_misses += 1 continue + # we didn't skip ahead, so it must have been new. Remember it now + self.files_tried.add(self.newfuzzed_md5) + # we have a better match, write it to a file if not len(self.newfuzzed): raise MinimizerError('New fuzzed_content content is empty.') diff --git a/src/test_certfuzz/minimizer/test_minimizer_base.py b/src/test_certfuzz/minimizer/test_minimizer_base.py index aebe3cc..97734b4 100644 --- a/src/test_certfuzz/minimizer/test_minimizer_base.py +++ b/src/test_certfuzz/minimizer/test_minimizer_base.py @@ -39,12 +39,6 @@ def tearDown(self): def test_go(self): pass - def test_have_we_seen_this_file_before(self): - self.m.newfuzzed_md5 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - - self.assertFalse(self.m.have_we_seen_this_file_before()) - self.assertTrue(self.m.have_we_seen_this_file_before()) - def test_is_same_crash(self): pass From 72e55a246f5a06de505c09491e6ed7cfa1fc131e Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 23 Feb 2016 12:43:10 -0500 Subject: [PATCH 0893/1169] Add 'analyzer' element to config dictionary if not present. BFF-921 --- src/certfuzz/config/simple_loader.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/config/simple_loader.py b/src/certfuzz/config/simple_loader.py index 019fdf1..35a69ed 100644 --- a/src/certfuzz/config/simple_loader.py +++ b/src/certfuzz/config/simple_loader.py @@ -24,7 +24,7 @@ def load_config(yaml_file): # yaml.load returns None if the file is empty. We need to raise an error if cfg is None: - raise(ConfigError,'Config file was empty') + raise(ConfigError, 'Config file was empty') # add the file timestamp so we can tell if it changes later cfg['config_timestamp'] = os.path.getmtime(yaml_file) @@ -40,17 +40,19 @@ def fixup_config(cfg): cfgdict = deepcopy(cfg) # fix target program path cfgdict['target']['program'] = fixup_path(cfgdict['target']['program']) - + quoted_prg = quoted(cfgdict['target']['program']) quoted_sf = quoted('$SEEDFILE') t = Template(cfgdict['target']['cmdline_template']) intermediate_t = t.safe_substitute(PROGRAM=quoted_prg, SEEDFILE=quoted_sf) cfgdict['target']['cmdline_template'] = Template(intermediate_t) - for k,v in cfgdict['directories'].iteritems(): + for k, v in cfgdict['directories'].iteritems(): cfgdict['directories'][k] = fixup_path(v) + if 'analyzer' not in cfgdict: cfgdict['analyzer'] = {} + return cfgdict def load_and_fix_config(yaml_file): - return fixup_config(load_config(yaml_file)) \ No newline at end of file + return fixup_config(load_config(yaml_file)) From c19c016c25590314e4fd0b80e29198e3e5653a52 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 23 Feb 2016 10:32:09 -0500 Subject: [PATCH 0894/1169] start calling it a testcase instead of a crash (+1 squashed commit) Squashed commits: [ce3fa0a] don't use reserved 'file' keyword --- src/certfuzz/testcase/testcase_base.py | 8 ++++++++ src/certfuzz/testcase/testcase_base2.py | 14 ++------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index d731547..abb8b29 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -68,6 +68,10 @@ def calculate_hamming_distances(self): # one of the files wasn't defined logger.warning('Cannot find either sf_path or minimized file to calculate Hamming Distances') + self.logger.info("bitwise_hd=%d", self.hd_bits) + self.logger.info("bytewise_hd=%d", self.hd_bytes) + + def calculate_hamming_distances_a(self): with open(self.fuzzedfile.path, 'rb') as fd: fuzzed = fd.read() @@ -75,4 +79,8 @@ def calculate_hamming_distances_a(self): a_string = 'x' * len(fuzzed) self.hd_bits = hamming.bitwise_hd(a_string, fuzzed) + self.logger.info("bitwise_hd=%d", self.hd_bits) + self.hd_bytes = hamming.bytewise_hd(a_string, fuzzed) + self.logger.info("bytewise_hd=%d", self.hd_bytes) + diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py index cb68983..3792e63 100644 --- a/src/certfuzz/testcase/testcase_base2.py +++ b/src/certfuzz/testcase/testcase_base2.py @@ -75,16 +75,6 @@ def _set_attr_from_dbg(self, attrname): def _verify_crash_base_dir(self): raise NotImplementedError - def calculate_hamming_distances(self): - TestCaseBase.calculate_hamming_distances(self) - logger.info("bitwise_hd=%d", self.hd_bits) - logger.info("bytewise_hd=%d", self.hd_bytes) - - def calculate_hamming_distances_a(self): - TestCaseBase.calculate_hamming_distances_a(self) - logger.info("bitwise_hd=%d", self.hd_bits) - logger.info("bytewise_hd=%d", self.hd_bytes) - def clean_tmpdir(self): logger.debug('Cleaning up %s', self.tempdir) if os.path.exists(self.tempdir): @@ -110,8 +100,8 @@ def copy_files_to_temp(self): def copy_files(self, outdir): crash_files = os.listdir(self.tempdir) - for file in crash_files: - filepath = os.path.join(self.tempdir, file) + for f in crash_files: + filepath = os.path.join(self.tempdir, f) if os.path.isfile(filepath): filetools.copy_file(filepath, outdir) From ca42bc9e55a569809567ea2ad7404278e6060251 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 2 Feb 2016 15:01:00 -0500 Subject: [PATCH 0895/1169] add drill results analyzer and unit tests --- src/certfuzz/analyzers/drillresults.py | 87 +++++++++++++++++++ .../analyzers/test_drillresults.py | 33 +++++++ 2 files changed, 120 insertions(+) create mode 100644 src/certfuzz/analyzers/drillresults.py create mode 100644 src/test_certfuzz/analyzers/test_drillresults.py diff --git a/src/certfuzz/analyzers/drillresults.py b/src/certfuzz/analyzers/drillresults.py new file mode 100644 index 0000000..254212e --- /dev/null +++ b/src/certfuzz/analyzers/drillresults.py @@ -0,0 +1,87 @@ +''' +Created on Jan 29, 2016 + +@author: adh +''' +import logging +from certfuzz.analyzers.analyzer_base import Analyzer +from certfuzz.drillresults.testcasebundle_base import TestCaseBundle +from certfuzz.drillresults.errors import TestCaseBundleError + +logger=logging.getLogger(__name__) + +OUTFILE_EXT = "drillresults" +get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) + + +class DrillResults(Analyzer): + ''' + Drills a bit deeper into results to see how exploitable a testcase might be. + ''' + + def __init__(self, cfg, testcase): + ''' + Constructor + ''' + self.cfg = cfg + self.testcase = testcase + + self.outfile = get_file(testcase.fuzzedfile.path) + self.output_lines = [] + + + def _process_tcb(self, tcb): + details = tcb.details + score = tcb.score + crash_key = tcb.crash_hash + + output_lines = self.output_lines + + output_lines.append('%s - Exploitability rank: %s' % (crash_key, score)) + output_lines.append('Fuzzed file: %s' % details['fuzzedfile']) + for exception in details['exceptions']: + shortdesc = details['exceptions'][exception]['shortdesc'] + eiftext = '' + efa = '0x' + details['exceptions'][exception]['efa'] + if details['exceptions'][exception]['EIF']: + eiftext = " *** Byte pattern is in fuzzed file! ***" + output_lines.append('exception %s: %s accessing %s %s' % (exception, shortdesc, efa, eiftext)) + if details['exceptions'][exception]['instructionline']: + output_lines.append(details['exceptions'][exception]['instructionline']) + module = details['exceptions'][exception]['pcmodule'] + if module == 'unloaded': + if not self.ignore_jit: + output_lines.append('Instruction pointer is not in a loaded module!') + else: + output_lines.append('Code executing in: %s' % module) + + def _write_outfile(self): + with open(self.outfile,'wb') as f: + f.write('\n'.join(self.output_lines)) + + def go(self): + logger.info('Drill Results PLACEHOLDER') + +# return + # put a list of files in testcasedir + + # turn testcase into tescase_bundle + with TestCaseBundle(dbg_file=dbg_file, + testcase_file=self.testcase.fuzzed_file.path, + crash_hash=self.testcase.signature, + ignore_jit=False) as tcb: + try: + tcb.go() + except TestCaseBundleError as e: + logger.warning('Skipping drillresults on testcase %s: %s', self.testcase.signature, e) + return + + self._process_tcb(tcb) + self._write_outfile() + # if score < max_score do something (more interesting) + # if score > max_score do something else (less interesting) + +# +# output_blob=tcb.details +# output_blob['score']=tcb.score + \ No newline at end of file diff --git a/src/test_certfuzz/analyzers/test_drillresults.py b/src/test_certfuzz/analyzers/test_drillresults.py new file mode 100644 index 0000000..cad6597 --- /dev/null +++ b/src/test_certfuzz/analyzers/test_drillresults.py @@ -0,0 +1,33 @@ +''' +Created on Jan 29, 2016 + +@author: adh +''' +import unittest +from certfuzz.analyzers import drillresults +from test_certfuzz.mocks import MockCfg, MockTestcase + +class Test(unittest.TestCase): + + + def setUp(self): + cfg = MockCfg() + testcase=MockTestcase() + self.dra = drillresults.DrillResults(cfg,testcase) + pass + + + def tearDown(self): + pass + + + def testInit(self): + self.assertTrue('debugger' in self.dra.cfg) + + def test_go(self): + self.dra.go() + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file From 9d731f0e8ca11c1e10c4719db1ed04bef48cdecf Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 10 Feb 2016 15:34:44 -0500 Subject: [PATCH 0896/1169] add drill results analyzer on both windows and linux (+2 squashed commits) Squashed commits: [9187fac] generalize tcb handling [77924eb] remove unused imports generalize tcb handling add drill results analyzer on both windows and linux --- src/certfuzz/analyzers/drillresults.py | 39 ++++--- src/certfuzz/debuggers/debugger_base.py | 6 +- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 51 ++++++++- .../tc_pipeline/tc_pipeline_windows.py | 44 +++++++- src/certfuzz/testcase/testcase_linux.py | 4 + .../analyzers/test_drillresults.py | 106 +++++++++++++++++- src/test_certfuzz/mocks.py | 5 + 7 files changed, 226 insertions(+), 29 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults.py b/src/certfuzz/analyzers/drillresults.py index 254212e..52d1c3f 100644 --- a/src/certfuzz/analyzers/drillresults.py +++ b/src/certfuzz/analyzers/drillresults.py @@ -7,17 +7,20 @@ from certfuzz.analyzers.analyzer_base import Analyzer from certfuzz.drillresults.testcasebundle_base import TestCaseBundle from certfuzz.drillresults.errors import TestCaseBundleError +from certfuzz.drillresults.testcasebundle_linux import LinuxTestCaseBundle +from certfuzz.drillresults.testcasebundle_windows import WindowsTestCaseBundle logger=logging.getLogger(__name__) OUTFILE_EXT = "drillresults" -get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) +get_file = lambda x: '{}.{}'.format(x, OUTFILE_EXT) class DrillResults(Analyzer): ''' Drills a bit deeper into results to see how exploitable a testcase might be. ''' + _tcb_cls = TestCaseBundle def __init__(self, cfg, testcase): ''' @@ -26,7 +29,7 @@ def __init__(self, cfg, testcase): self.cfg = cfg self.testcase = testcase - self.outfile = get_file(testcase.fuzzedfile.path) + self.outfile = get_file(self.testcase.fuzzedfile.path) self.output_lines = [] @@ -35,10 +38,11 @@ def _process_tcb(self, tcb): score = tcb.score crash_key = tcb.crash_hash - output_lines = self.output_lines + output_lines = [] output_lines.append('%s - Exploitability rank: %s' % (crash_key, score)) output_lines.append('Fuzzed file: %s' % details['fuzzedfile']) + for exception in details['exceptions']: shortdesc = details['exceptions'][exception]['shortdesc'] eiftext = '' @@ -54,22 +58,26 @@ def _process_tcb(self, tcb): output_lines.append('Instruction pointer is not in a loaded module!') else: output_lines.append('Code executing in: %s' % module) + + self.output_lines = output_lines def _write_outfile(self): with open(self.outfile,'wb') as f: f.write('\n'.join(self.output_lines)) + + @property + def dbg_file(self): + return '{}.{}'.format(self.testcase.fuzzedfile.path,self.testcase.debugger_extension) def go(self): logger.info('Drill Results PLACEHOLDER') -# return - # put a list of files in testcasedir - + # turn testcase into tescase_bundle - with TestCaseBundle(dbg_file=dbg_file, - testcase_file=self.testcase.fuzzed_file.path, - crash_hash=self.testcase.signature, - ignore_jit=False) as tcb: + with self._tcb_cls(dbg_outfile=self.dbg_file, + testcase_file=self.testcase.fuzzedfile.path, + crash_hash=self.testcase.signature, + ignore_jit=False) as tcb: try: tcb.go() except TestCaseBundleError as e: @@ -81,7 +89,10 @@ def go(self): # if score < max_score do something (more interesting) # if score > max_score do something else (less interesting) -# -# output_blob=tcb.details -# output_blob['score']=tcb.score - \ No newline at end of file + +class LinuxDrillResults(DrillResults): + _tcb_cls = LinuxTestCaseBundle + +class WindowsDrillResults(DrillResults): + _tcb_cls = WindowsTestCaseBundle + \ No newline at end of file diff --git a/src/certfuzz/debuggers/debugger_base.py b/src/certfuzz/debuggers/debugger_base.py index c3eea93..03c422f 100644 --- a/src/certfuzz/debuggers/debugger_base.py +++ b/src/certfuzz/debuggers/debugger_base.py @@ -30,7 +30,7 @@ def __init__(self, program=None, cmd_args=None, outfile_base=None, timeout=None, logger.debug('Initialize Debugger') self.program = program self.cmd_args = cmd_args - self.outfile = '.'.join((outfile_base, self._ext)) + self.outfile = '.'.join((outfile_base, self.extension)) self.timeout = timeout self.input_file = '' self.debugger_output = None @@ -95,3 +95,7 @@ def __enter__(self): def __exit__(self, etype, value, traceback): pass + + @property + def extension(self): + return self._ext \ No newline at end of file diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index 5436f4f..81443b7 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -17,11 +17,14 @@ from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file from certfuzz.fuzztools import filetools -from certfuzz.minimizer.unix_minimizer import UnixMinimizer +from certfuzz.minimizer.errors import MinimizerError +from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.reporters.copy_files import CopyFilesReporter from certfuzz.reporters.testcase_logger import TestcaseLoggerReporter +from certfuzz.analyzers import drillresults + logger = logging.getLogger(__name__) @@ -34,7 +37,6 @@ def get_uniq_logger(logfile): class LinuxTestCasePipeline(TestCasePipelineBase): - _minimizer_cls = UnixMinimizer def _setup_analyzers(self): self.analyzer_classes.append(stderr.StdErr) @@ -46,6 +48,8 @@ def _setup_analyzers(self): if self.options.get('use_pin_calltrace'): self.analyzer_classes.append(pin_calltrace.Pin_calltrace) + + self.analyzer_classes.append(drillresults.LinuxDrillResults) def _verify(self, testcase): ''' @@ -83,8 +87,22 @@ def _verify(self, testcase): def _post_verify(self, testcase): testcase.get_logger() - def _pre_minimize(self, testcase): - touch_watchdog_file() + def _minimize(self, testcase): + if self.options.get('minimize_crashers'): + touch_watchdog_file() + self._minimize_to_seedfile(testcase) + if self.options.get('minimize_to_string'): + touch_watchdog_file() + self._minimize_to_string(testcase) + + def _post_minimize(self, testcase): + if self.cfg['runoptions']['recycle_crashers']: + logger.debug('Recycling crash as seedfile') + iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext + crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) + filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) + self.sf_set.add_file(crasherseed_path) def _pre_analyze(self, testcase): # get one last debugger output for the newly minimized file @@ -148,3 +166,28 @@ def _post_report(self, testcase): # clean up testcase.delete_files() + def _minimize_to_seedfile(self, testcase): + self._minimize_generic(testcase, sftarget=True, confidence=0.999) + # calculate the hamming distances for this crash + # between the original seedfile and the minimized fuzzed file + testcase.calculate_hamming_distances() + + def _minimize_to_string(self, testcase): + self._minimize_generic(testcase, sftarget=False, confidence=0.9) + + def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): + try: + with Minimizer(cfg=self.cfg, + crash=testcase, + bitwise=False, + seedfile_as_target=sftarget, + confidence=confidence, + tempdir=self.options.get('local_dir'), + maxtime=self.options.get('minimizertimeout'), + ) as m: + m.go() + for new_tc in m.other_crashes.values(): + self.tc_candidate_q.put(new_tc) + except MinimizerError as e: + logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) + m = None diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 1617abe..8d22a9d 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -6,21 +6,22 @@ import logging import os -from certfuzz.minimizer.win_minimizer import WindowsMinimizer +from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.fuzztools import filetools +from certfuzz.minimizer.errors import MinimizerError from certfuzz.reporters.copy_files import CopyFilesReporter +from certfuzz.analyzers import stderr +from certfuzz.analyzers import drillresults logger = logging.getLogger(__name__) class WindowsTestCasePipeline(TestCasePipelineBase): - _minimizer_cls = WindowsMinimizer - def _setup_analyzers(self): - pass - # self.analyzer_classes.append(stderr.StdErr) + self.analyzer_classes.append(stderr.StdErr) + self.analyzer_classes.append(drillresults.WindowsDrillResults) def _pre_verify(self, testcase): # pretty-print the testcase for debugging @@ -49,6 +50,39 @@ def _verify(self, testcase): testcase.should_proceed_with_analysis = True self.success = True + def _minimize(self, testcase): + logger.info('Minimizing testcase %s', testcase.signature) + logger.debug('config = %s', self.cfg) + + kwargs = {'cfg': self.cfg, + 'crash': testcase, + 'seedfile_as_target': True, + 'bitwise': False, + 'confidence': 0.999, + 'tempdir': self.working_dir, + 'maxtime': self.cfg['runoptions']['minimizer_timeout'] + } + + try: + with Minimizer(**kwargs) as minimizer: + minimizer.go() + + # minimizer found other crashes, so we should add them + # to our list for subsequent processing + for tc in minimizer.other_crashes.values(): + self.tc_candidate_q.put(tc) + except MinimizerError as e: + logger.error('Caught MinimizerError: {}'.format(e)) + + def _post_minimize(self, testcase): + if self.cfg['runoptions']['recycle_crashers']: + logger.debug('Recycling crash as seedfile') + iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] + crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext + crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) + filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) + self.sf_set.add_file(crasherseed_path) + def _report(self, testcase): with CopyFilesReporter(testcase, self.tc_dir) as reporter: reporter.go() diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index dcc59f4..cd49f02 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -59,6 +59,10 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, self.pc = None self.result_dir = None + @property + def debugger_extension(self): + return self._debugger_cls.extension + def __exit__(self, etype, value, traceback): pass # self.clean_tmpdir() diff --git a/src/test_certfuzz/analyzers/test_drillresults.py b/src/test_certfuzz/analyzers/test_drillresults.py index cad6597..71a3e84 100644 --- a/src/test_certfuzz/analyzers/test_drillresults.py +++ b/src/test_certfuzz/analyzers/test_drillresults.py @@ -5,19 +5,57 @@ ''' import unittest from certfuzz.analyzers import drillresults -from test_certfuzz.mocks import MockCfg, MockTestcase +from test_certfuzz.mocks import MockCfg, MockTestcase, MockFuzzedFile +import tempfile +import shutil +import os + +go_count = 0 +process_count = 0 + +class MockTcb(object): + details = {'fuzzedfile': 'foo', + 'exceptions': {},} + score = 15 + crash_hash = '0123456789abcdef' + + def __init__(self,*args,**kwargs): + self.go_count=0 + + def __enter__(self): + return self + + def __exit__(self,*args): + pass + + def go(self): + global go_count + go_count += 1 + +def _inc_proc_count(*args): + global process_count + + process_count += 1 class Test(unittest.TestCase): def setUp(self): - cfg = MockCfg() - testcase=MockTestcase() - self.dra = drillresults.DrillResults(cfg,testcase) - pass + global go_count + go_count = 0 + + global process_count + process_count = 0 + + self.cfg = MockCfg() + self.tc=MockTestcase() + self.dra = drillresults.LinuxDrillResults(self.cfg,self.tc) + + self.tmpdir = tempfile.mkdtemp() def tearDown(self): + shutil.rmtree(self.tmpdir) pass @@ -25,8 +63,66 @@ def testInit(self): self.assertTrue('debugger' in self.dra.cfg) def test_go(self): + fd,ff = tempfile.mkstemp(prefix='fuzzed-', dir=self.tmpdir) + os.close(fd) + + dbgf = os.path.join(self.tmpdir,'{}.{}'.format(ff,self.dra.testcase.debugger_extension)) + # touch the file + open(dbgf,'w').close() + + self.dra.testcase.fuzzedfile = MockFuzzedFile(ff) + self.dra._tcb_cls = MockTcb + self.dra._process_tcb = _inc_proc_count + + fd,of=tempfile.mkstemp(dir=self.tmpdir) + os.close(fd) + self.dra.outfile = of + + self.assertEqual(0,go_count) + self.assertEqual(0,process_count) self.dra.go() + self.assertEqual(1,go_count) + self.assertEqual(1,process_count) + + def test_dbg_file(self): + self.assertTrue(self.dra.dbg_file.endswith(self.tc.debugger_extension)) + self.assertTrue(self.dra.dbg_file.startswith(self.tc.fuzzedfile.path)) + + def test_process_tcb(self): + tcb = MockTcb() + + self.assertEqual(0,len(self.dra.output_lines)) + self.dra._process_tcb(tcb) + # should be 2 lines if tcb is uninteresting + self.assertEqual(2,len(self.dra.output_lines)) + + self.assertTrue(MockTcb.crash_hash in self.dra.output_lines[0]) + self.assertTrue(str(MockTcb.score) in self.dra.output_lines[0]) + self.assertTrue(MockTcb.details['fuzzedfile'] in self.dra.output_lines[1]) + + def test_write_outfile(self): + fd,f = tempfile.mkstemp(suffix='-fuzzed', prefix='test-', dir=self.tmpdir) + os.close(fd) + + self.dra.output_lines = ['a','b','c'] + self.dra.outfile = f + + os.remove(f) + self.assertFalse(os.path.exists(f)) + self.dra._write_outfile() + self.assertTrue(os.path.exists(f)) + + with open(f,'rb') as fp: + contents=fp.read() + self.assertTrue('a' in contents) + self.assertTrue('b' in contents) + self.assertTrue('c' in contents) + def test_getfile(self): + x = 'asdfghjklqwertyuiop' + self.assertTrue(drillresults.get_file(x).startswith(x)) + self.assertTrue(drillresults.get_file(x).endswith(drillresults.OUTFILE_EXT)) + if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 497ef5e..3f02ea9 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -69,6 +69,10 @@ def read(self): class MockFuzzedFile(Mock): path = u'foo' + def __init__(self,path=None): + if path is not None: + self.path = path + class MockFuzzer(Mock): is_minimizable = False @@ -83,6 +87,7 @@ class MockTestcase(Mock): range = MockRange() fuzzedfile = MockFuzzedFile() pc = u'dummyPCstring' + debugger_extension='abcdefg' class MockDbgOut(Mock): is_crash = False From eb5ffe24f6795760d6ba4c527d2dbc906925ecba Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 23 Feb 2016 10:09:32 -0500 Subject: [PATCH 0897/1169] minimizer stuff was refactored into base class --- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 47 ++----------------- .../tc_pipeline/tc_pipeline_windows.py | 38 ++------------- 2 files changed, 7 insertions(+), 78 deletions(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index 81443b7..5947139 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -17,8 +17,7 @@ from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateMissingInputFileError from certfuzz.file_handlers.watchdog_file import touch_watchdog_file from certfuzz.fuzztools import filetools -from certfuzz.minimizer.errors import MinimizerError -from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer +from certfuzz.minimizer.unix_minimizer import UnixMinimizer from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.reporters.copy_files import CopyFilesReporter from certfuzz.reporters.testcase_logger import TestcaseLoggerReporter @@ -37,6 +36,7 @@ def get_uniq_logger(logfile): class LinuxTestCasePipeline(TestCasePipelineBase): + _minimizer_cls = UnixMinimizer def _setup_analyzers(self): self.analyzer_classes.append(stderr.StdErr) @@ -87,22 +87,8 @@ def _verify(self, testcase): def _post_verify(self, testcase): testcase.get_logger() - def _minimize(self, testcase): - if self.options.get('minimize_crashers'): - touch_watchdog_file() - self._minimize_to_seedfile(testcase) - if self.options.get('minimize_to_string'): - touch_watchdog_file() - self._minimize_to_string(testcase) - - def _post_minimize(self, testcase): - if self.cfg['runoptions']['recycle_crashers']: - logger.debug('Recycling crash as seedfile') - iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext - crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) - filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) - self.sf_set.add_file(crasherseed_path) + def _pre_minimize(self, testcase): + touch_watchdog_file() def _pre_analyze(self, testcase): # get one last debugger output for the newly minimized file @@ -166,28 +152,3 @@ def _post_report(self, testcase): # clean up testcase.delete_files() - def _minimize_to_seedfile(self, testcase): - self._minimize_generic(testcase, sftarget=True, confidence=0.999) - # calculate the hamming distances for this crash - # between the original seedfile and the minimized fuzzed file - testcase.calculate_hamming_distances() - - def _minimize_to_string(self, testcase): - self._minimize_generic(testcase, sftarget=False, confidence=0.9) - - def _minimize_generic(self, testcase, sftarget=True, confidence=0.999): - try: - with Minimizer(cfg=self.cfg, - crash=testcase, - bitwise=False, - seedfile_as_target=sftarget, - confidence=confidence, - tempdir=self.options.get('local_dir'), - maxtime=self.options.get('minimizertimeout'), - ) as m: - m.go() - for new_tc in m.other_crashes.values(): - self.tc_candidate_q.put(new_tc) - except MinimizerError as e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) - m = None diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 8d22a9d..9667b2b 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -6,10 +6,9 @@ import logging import os -from certfuzz.minimizer.win_minimizer import WindowsMinimizer as Minimizer +from certfuzz.minimizer.win_minimizer import WindowsMinimizer from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.fuzztools import filetools -from certfuzz.minimizer.errors import MinimizerError from certfuzz.reporters.copy_files import CopyFilesReporter from certfuzz.analyzers import stderr @@ -19,6 +18,8 @@ class WindowsTestCasePipeline(TestCasePipelineBase): + _minimizer_cls = WindowsMinimizer + def _setup_analyzers(self): self.analyzer_classes.append(stderr.StdErr) self.analyzer_classes.append(drillresults.WindowsDrillResults) @@ -50,39 +51,6 @@ def _verify(self, testcase): testcase.should_proceed_with_analysis = True self.success = True - def _minimize(self, testcase): - logger.info('Minimizing testcase %s', testcase.signature) - logger.debug('config = %s', self.cfg) - - kwargs = {'cfg': self.cfg, - 'crash': testcase, - 'seedfile_as_target': True, - 'bitwise': False, - 'confidence': 0.999, - 'tempdir': self.working_dir, - 'maxtime': self.cfg['runoptions']['minimizer_timeout'] - } - - try: - with Minimizer(**kwargs) as minimizer: - minimizer.go() - - # minimizer found other crashes, so we should add them - # to our list for subsequent processing - for tc in minimizer.other_crashes.values(): - self.tc_candidate_q.put(tc) - except MinimizerError as e: - logger.error('Caught MinimizerError: {}'.format(e)) - - def _post_minimize(self, testcase): - if self.cfg['runoptions']['recycle_crashers']: - logger.debug('Recycling crash as seedfile') - iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext - crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) - filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) - self.sf_set.add_file(crasherseed_path) - def _report(self, testcase): with CopyFilesReporter(testcase, self.tc_dir) as reporter: reporter.go() From dd8451b0cf76ee4f1718e8891aa9a7caa67a1b85 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 24 Feb 2016 08:52:35 -0500 Subject: [PATCH 0898/1169] add drill results analyzer & clean up other analyzer imports (+1 squashed commit) Squashed commits: [5c25537] clean up error message (+1 squashed commit) Squashed commits: [41e1a2e] make it work (+1 squashed commit) Squashed commits: [44c91d7] if an exception doesn't have an efa note the error but continue scoring (+1 squashed commit) Squashed commits: [b7f15df] more key error fixes --- src/certfuzz/analyzers/drillresults.py | 6 +- src/certfuzz/debuggers/debugger_base.py | 2 +- .../drillresults/testcasebundle_base.py | 37 +++++++-- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 27 +++--- .../tc_pipeline/tc_pipeline_windows.py | 10 +-- .../analyzers/test_drillresults.py | 83 +++++++++---------- src/test_certfuzz/mocks.py | 5 +- 7 files changed, 91 insertions(+), 79 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults.py b/src/certfuzz/analyzers/drillresults.py index 52d1c3f..24a83ba 100644 --- a/src/certfuzz/analyzers/drillresults.py +++ b/src/certfuzz/analyzers/drillresults.py @@ -64,17 +64,13 @@ def _process_tcb(self, tcb): def _write_outfile(self): with open(self.outfile,'wb') as f: f.write('\n'.join(self.output_lines)) - - @property - def dbg_file(self): - return '{}.{}'.format(self.testcase.fuzzedfile.path,self.testcase.debugger_extension) def go(self): logger.info('Drill Results PLACEHOLDER') # turn testcase into tescase_bundle - with self._tcb_cls(dbg_outfile=self.dbg_file, + with self._tcb_cls(dbg_outfile=self.testcase.dbg_file, testcase_file=self.testcase.fuzzedfile.path, crash_hash=self.testcase.signature, ignore_jit=False) as tcb: diff --git a/src/certfuzz/debuggers/debugger_base.py b/src/certfuzz/debuggers/debugger_base.py index 03c422f..d175655 100644 --- a/src/certfuzz/debuggers/debugger_base.py +++ b/src/certfuzz/debuggers/debugger_base.py @@ -30,7 +30,7 @@ def __init__(self, program=None, cmd_args=None, outfile_base=None, timeout=None, logger.debug('Initialize Debugger') self.program = program self.cmd_args = cmd_args - self.outfile = '.'.join((outfile_base, self.extension)) + self.outfile = '.'.join((outfile_base, self._ext)) self.timeout = timeout self.input_file = '' self.debugger_output = None diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/drillresults/testcasebundle_base.py index 1c62d0b..918b04d 100644 --- a/src/certfuzz/drillresults/testcasebundle_base.py +++ b/src/certfuzz/drillresults/testcasebundle_base.py @@ -37,7 +37,7 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self.regdict = {} if not os.path.exists(self.dbg_outfile): - raise TestCaseBundleError + raise TestCaseBundleError('Debugger file not found: {}'.format(self.dbg_outfile)) self.reporttext = read_text_file(self.dbg_outfile) self._find_testcase_file() @@ -150,7 +150,7 @@ def _parse_testcase(self): return if not instraddr: - #raise TestCaseBundleError('No instraddr address means no crash') + # raise TestCaseBundleError('No instraddr address means no crash') return faultaddr, instraddr = self._64bit_addr_fixup(faultaddr, instraddr) @@ -297,15 +297,14 @@ def _score_interesting(self): exceptions = self.details['exceptions'] for exception in exceptions.itervalues(): - module = exception['pcmodule'] + module, efa, eif = self._get_efa_mod_eif(exception) + if module == 'unloaded' and not self.ignore_jit: # EIP is not in a loaded module scores.append(20) if exception['shortdesc'] in self.re_set: - efa = '0x' + exception['efa'] - - if exception['EIF']: + if eif: # The faulting address pattern is in the fuzzed file if '0x000000' in efa: # Faulting address is near null @@ -325,13 +324,33 @@ def _score_interesting(self): return scores + + def _get_efa_mod_eif(self, exception): + try: + efa = '0x' + exception['efa'] + except KeyError: + logger.error('Exception has no value set for efa.') + efa = '' + try: + module = exception['pcmodule'] + except KeyError: + logger.error('Exception has no value set for pcmodule') + module = '' + try: + eif = exception['EIF'] + except KeyError: + logger.error('Exception has no value set for EIF') + eif = False + + return module, efa, eif + def _score_less_interesting(self): scores = [] exceptions = self.details['exceptions'] for exception in exceptions.itervalues(): - efa = '0x' + exception['efa'] - module = exception['pcmodule'] + module, efa, eif = self._get_efa_mod_eif(exception) + if module == 'unloaded' and not self.ignore_jit: scores.append(20) elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): @@ -340,7 +359,7 @@ def _score_less_interesting(self): elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: # non-continued potential stack buffer overflow scores.append(40) - elif exception['EIF']: + elif eif: # The faulting address pattern is in the fuzzed file if '0x000000' in efa: # Faulting address is near null diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index 5947139..53bb4c9 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -6,11 +6,6 @@ import logging import os -from certfuzz.analyzers import cw_gmalloc -from certfuzz.analyzers import pin_calltrace -from certfuzz.analyzers import stderr -from certfuzz.analyzers import valgrind -from certfuzz.analyzers.callgrind import callgrind from certfuzz.analyzers.callgrind.annotate import annotate_callgrind from certfuzz.analyzers.callgrind.annotate import annotate_callgrind_tree from certfuzz.analyzers.callgrind.errors import CallgrindAnnotateEmptyOutputFileError @@ -21,8 +16,12 @@ from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.reporters.copy_files import CopyFilesReporter from certfuzz.reporters.testcase_logger import TestcaseLoggerReporter - -from certfuzz.analyzers import drillresults +from certfuzz.analyzers.drillresults import LinuxDrillResults +from certfuzz.analyzers.pin_calltrace import Pin_calltrace +from certfuzz.analyzers.callgrind.callgrind import Callgrind +from certfuzz.analyzers.valgrind import Valgrind +from certfuzz.analyzers.cw_gmalloc import CrashWranglerGmalloc +from certfuzz.analyzers.stderr import StdErr logger = logging.getLogger(__name__) @@ -39,17 +38,17 @@ class LinuxTestCasePipeline(TestCasePipelineBase): _minimizer_cls = UnixMinimizer def _setup_analyzers(self): - self.analyzer_classes.append(stderr.StdErr) - self.analyzer_classes.append(cw_gmalloc.CrashWranglerGmalloc) + self.analyzer_classes.append(StdErr) + self.analyzer_classes.append(CrashWranglerGmalloc) if self.options.get('use_valgrind'): - self.analyzer_classes.append(valgrind.Valgrind) - self.analyzer_classes.append(callgrind.Callgrind) + self.analyzer_classes.append(Valgrind) + self.analyzer_classes.append(Callgrind) if self.options.get('use_pin_calltrace'): - self.analyzer_classes.append(pin_calltrace.Pin_calltrace) - - self.analyzer_classes.append(drillresults.LinuxDrillResults) + self.analyzer_classes.append(Pin_calltrace) + + self.analyzer_classes.append(LinuxDrillResults) def _verify(self, testcase): ''' diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 9667b2b..e9a396c 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -8,11 +8,11 @@ from certfuzz.minimizer.win_minimizer import WindowsMinimizer from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase -from certfuzz.fuzztools import filetools from certfuzz.reporters.copy_files import CopyFilesReporter -from certfuzz.analyzers import stderr +from certfuzz.analyzers.stderr import StdErr +from certfuzz.analyzers.drillresults import WindowsDrillResults + -from certfuzz.analyzers import drillresults logger = logging.getLogger(__name__) @@ -21,8 +21,8 @@ class WindowsTestCasePipeline(TestCasePipelineBase): _minimizer_cls = WindowsMinimizer def _setup_analyzers(self): - self.analyzer_classes.append(stderr.StdErr) - self.analyzer_classes.append(drillresults.WindowsDrillResults) + self.analyzer_classes.append(StdErr) + self.analyzer_classes.append(WindowsDrillResults) def _pre_verify(self, testcase): # pretty-print the testcase for debugging diff --git a/src/test_certfuzz/analyzers/test_drillresults.py b/src/test_certfuzz/analyzers/test_drillresults.py index 71a3e84..da4427e 100644 --- a/src/test_certfuzz/analyzers/test_drillresults.py +++ b/src/test_certfuzz/analyzers/test_drillresults.py @@ -15,26 +15,26 @@ class MockTcb(object): details = {'fuzzedfile': 'foo', - 'exceptions': {},} + 'exceptions': {}, } score = 15 crash_hash = '0123456789abcdef' - - def __init__(self,*args,**kwargs): - self.go_count=0 - + + def __init__(self, *args, **kwargs): + self.go_count = 0 + def __enter__(self): return self - - def __exit__(self,*args): + + def __exit__(self, *args): pass - + def go(self): global go_count go_count += 1 - + def _inc_proc_count(*args): global process_count - + process_count += 1 class Test(unittest.TestCase): @@ -43,14 +43,14 @@ class Test(unittest.TestCase): def setUp(self): global go_count go_count = 0 - + global process_count process_count = 0 - + self.cfg = MockCfg() - self.tc=MockTestcase() - self.dra = drillresults.LinuxDrillResults(self.cfg,self.tc) - + self.tc = MockTestcase() + self.dra = drillresults.LinuxDrillResults(self.cfg, self.tc) + self.tmpdir = tempfile.mkdtemp() @@ -61,59 +61,56 @@ def tearDown(self): def testInit(self): self.assertTrue('debugger' in self.dra.cfg) - + def test_go(self): - fd,ff = tempfile.mkstemp(prefix='fuzzed-', dir=self.tmpdir) + fd, ff = tempfile.mkstemp(prefix='fuzzed-', dir=self.tmpdir) os.close(fd) - dbgf = os.path.join(self.tmpdir,'{}.{}'.format(ff,self.dra.testcase.debugger_extension)) + dbgf = os.path.join(self.tmpdir, '{}.{}'.format(ff, self.dra.testcase.debugger_extension)) # touch the file - open(dbgf,'w').close() + open(dbgf, 'w').close() - self.dra.testcase.fuzzedfile = MockFuzzedFile(ff) + self.dra.testcase.dbg_file = dbgf + self.dra.testcase.fuzzedfile = MockFuzzedFile(ff) self.dra._tcb_cls = MockTcb self.dra._process_tcb = _inc_proc_count - - fd,of=tempfile.mkstemp(dir=self.tmpdir) + + fd, of = tempfile.mkstemp(dir=self.tmpdir) os.close(fd) self.dra.outfile = of - - self.assertEqual(0,go_count) - self.assertEqual(0,process_count) + + self.assertEqual(0, go_count) + self.assertEqual(0, process_count) self.dra.go() - self.assertEqual(1,go_count) - self.assertEqual(1,process_count) - - def test_dbg_file(self): - self.assertTrue(self.dra.dbg_file.endswith(self.tc.debugger_extension)) - self.assertTrue(self.dra.dbg_file.startswith(self.tc.fuzzedfile.path)) + self.assertEqual(1, go_count) + self.assertEqual(1, process_count) def test_process_tcb(self): tcb = MockTcb() - self.assertEqual(0,len(self.dra.output_lines)) + self.assertEqual(0, len(self.dra.output_lines)) self.dra._process_tcb(tcb) # should be 2 lines if tcb is uninteresting - self.assertEqual(2,len(self.dra.output_lines)) + self.assertEqual(2, len(self.dra.output_lines)) self.assertTrue(MockTcb.crash_hash in self.dra.output_lines[0]) self.assertTrue(str(MockTcb.score) in self.dra.output_lines[0]) self.assertTrue(MockTcb.details['fuzzedfile'] in self.dra.output_lines[1]) def test_write_outfile(self): - fd,f = tempfile.mkstemp(suffix='-fuzzed', prefix='test-', dir=self.tmpdir) + fd, f = tempfile.mkstemp(suffix='-fuzzed', prefix='test-', dir=self.tmpdir) os.close(fd) - - self.dra.output_lines = ['a','b','c'] + + self.dra.output_lines = ['a', 'b', 'c'] self.dra.outfile = f - + os.remove(f) self.assertFalse(os.path.exists(f)) self.dra._write_outfile() self.assertTrue(os.path.exists(f)) - - with open(f,'rb') as fp: - contents=fp.read() + + with open(f, 'rb') as fp: + contents = fp.read() self.assertTrue('a' in contents) self.assertTrue('b' in contents) self.assertTrue('c' in contents) @@ -122,8 +119,8 @@ def test_getfile(self): x = 'asdfghjklqwertyuiop' self.assertTrue(drillresults.get_file(x).startswith(x)) self.assertTrue(drillresults.get_file(x).endswith(drillresults.OUTFILE_EXT)) - + if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() \ No newline at end of file + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 3f02ea9..21f3cd6 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -69,7 +69,7 @@ def read(self): class MockFuzzedFile(Mock): path = u'foo' - def __init__(self,path=None): + def __init__(self, path=None): if path is not None: self.path = path @@ -87,7 +87,8 @@ class MockTestcase(Mock): range = MockRange() fuzzedfile = MockFuzzedFile() pc = u'dummyPCstring' - debugger_extension='abcdefg' + debugger_extension = 'abcdefg' + dbg_outfile = 'xyz' class MockDbgOut(Mock): is_crash = False From 65585ad2c591e3f4d8664f74197eb12b885a5b68 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 23 Feb 2016 11:24:55 -0500 Subject: [PATCH 0899/1169] rearrange drillresults into an analyzer package similar to callgrind (+1 squashed commit) Squashed commits: [72ab4ac] move testcase bundle stuff into analyzers sub package --- .../analyzers/drillresults/__init__.py | 1 + .../{ => drillresults}/drillresults.py | 35 ++++++++++--------- .../drillresults/testcasebundle_base.py | 0 .../drillresults/testcasebundle_linux.py | 2 +- .../drillresults/testcasebundle_windows.py | 2 +- .../drillresults/result_driller_linux.py | 2 +- .../drillresults/result_driller_windows.py | 2 +- src/certfuzz/testcase/testcase_base2.py | 2 +- src/certfuzz/testcase/testcase_linux.py | 7 ++-- .../analyzers/drillresults/__init__.py | 0 .../{ => drillresults}/test_drillresults.py | 2 +- .../drillresults/test_drillresults_pkg.py | 33 +++++++++++++++++ .../drillresults/test_testcasebundle_base.py | 2 +- .../drillresults/test_testcasebundle_linux.py | 2 +- .../test_testcasebundle_windows.py | 2 +- 15 files changed, 63 insertions(+), 31 deletions(-) create mode 100644 src/certfuzz/analyzers/drillresults/__init__.py rename src/certfuzz/analyzers/{ => drillresults}/drillresults.py (88%) rename src/certfuzz/{ => analyzers}/drillresults/testcasebundle_base.py (100%) rename src/certfuzz/{ => analyzers}/drillresults/testcasebundle_linux.py (97%) rename src/certfuzz/{ => analyzers}/drillresults/testcasebundle_windows.py (98%) create mode 100644 src/test_certfuzz/analyzers/drillresults/__init__.py rename src/test_certfuzz/analyzers/{ => drillresults}/test_drillresults.py (98%) create mode 100644 src/test_certfuzz/analyzers/drillresults/test_drillresults_pkg.py rename src/test_certfuzz/{ => analyzers}/drillresults/test_testcasebundle_base.py (99%) rename src/test_certfuzz/{ => analyzers}/drillresults/test_testcasebundle_linux.py (82%) rename src/test_certfuzz/{ => analyzers}/drillresults/test_testcasebundle_windows.py (82%) diff --git a/src/certfuzz/analyzers/drillresults/__init__.py b/src/certfuzz/analyzers/drillresults/__init__.py new file mode 100644 index 0000000..386040a --- /dev/null +++ b/src/certfuzz/analyzers/drillresults/__init__.py @@ -0,0 +1 @@ +from .drillresults import * \ No newline at end of file diff --git a/src/certfuzz/analyzers/drillresults.py b/src/certfuzz/analyzers/drillresults/drillresults.py similarity index 88% rename from src/certfuzz/analyzers/drillresults.py rename to src/certfuzz/analyzers/drillresults/drillresults.py index 24a83ba..52a7698 100644 --- a/src/certfuzz/analyzers/drillresults.py +++ b/src/certfuzz/analyzers/drillresults/drillresults.py @@ -5,12 +5,12 @@ ''' import logging from certfuzz.analyzers.analyzer_base import Analyzer -from certfuzz.drillresults.testcasebundle_base import TestCaseBundle +from certfuzz.analyzers.drillresults.testcasebundle_base import TestCaseBundle from certfuzz.drillresults.errors import TestCaseBundleError -from certfuzz.drillresults.testcasebundle_linux import LinuxTestCaseBundle -from certfuzz.drillresults.testcasebundle_windows import WindowsTestCaseBundle +from certfuzz.analyzers.drillresults.testcasebundle_linux import LinuxTestCaseBundle +from certfuzz.analyzers.drillresults.testcasebundle_windows import WindowsTestCaseBundle -logger=logging.getLogger(__name__) +logger = logging.getLogger(__name__) OUTFILE_EXT = "drillresults" get_file = lambda x: '{}.{}'.format(x, OUTFILE_EXT) @@ -28,21 +28,21 @@ def __init__(self, cfg, testcase): ''' self.cfg = cfg self.testcase = testcase - + self.outfile = get_file(self.testcase.fuzzedfile.path) self.output_lines = [] - - + + def _process_tcb(self, tcb): details = tcb.details score = tcb.score crash_key = tcb.crash_hash - + output_lines = [] - + output_lines.append('%s - Exploitability rank: %s' % (crash_key, score)) output_lines.append('Fuzzed file: %s' % details['fuzzedfile']) - + for exception in details['exceptions']: shortdesc = details['exceptions'][exception]['shortdesc'] eiftext = '' @@ -58,17 +58,19 @@ def _process_tcb(self, tcb): output_lines.append('Instruction pointer is not in a loaded module!') else: output_lines.append('Code executing in: %s' % module) - + self.output_lines = output_lines def _write_outfile(self): - with open(self.outfile,'wb') as f: + with open(self.outfile, 'wb') as f: f.write('\n'.join(self.output_lines)) def go(self): logger.info('Drill Results PLACEHOLDER') - + + + # turn testcase into tescase_bundle with self._tcb_cls(dbg_outfile=self.testcase.dbg_file, testcase_file=self.testcase.fuzzedfile.path, @@ -79,16 +81,15 @@ def go(self): except TestCaseBundleError as e: logger.warning('Skipping drillresults on testcase %s: %s', self.testcase.signature, e) return - + self._process_tcb(tcb) self._write_outfile() # if score < max_score do something (more interesting) # if score > max_score do something else (less interesting) - + class LinuxDrillResults(DrillResults): _tcb_cls = LinuxTestCaseBundle - + class WindowsDrillResults(DrillResults): _tcb_cls = WindowsTestCaseBundle - \ No newline at end of file diff --git a/src/certfuzz/drillresults/testcasebundle_base.py b/src/certfuzz/analyzers/drillresults/testcasebundle_base.py similarity index 100% rename from src/certfuzz/drillresults/testcasebundle_base.py rename to src/certfuzz/analyzers/drillresults/testcasebundle_base.py diff --git a/src/certfuzz/drillresults/testcasebundle_linux.py b/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py similarity index 97% rename from src/certfuzz/drillresults/testcasebundle_linux.py rename to src/certfuzz/analyzers/drillresults/testcasebundle_linux.py index 31fb327..2e2b644 100644 --- a/src/certfuzz/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py @@ -7,7 +7,7 @@ import re from certfuzz.drillresults.common import carve -from certfuzz.drillresults.testcasebundle_base import TestCaseBundle +from certfuzz.analyzers.drillresults.testcasebundle_base import TestCaseBundle logger = logging.getLogger(__name__) diff --git a/src/certfuzz/drillresults/testcasebundle_windows.py b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py similarity index 98% rename from src/certfuzz/drillresults/testcasebundle_windows.py rename to src/certfuzz/analyzers/drillresults/testcasebundle_windows.py index a05d04b..bf0ab60 100644 --- a/src/certfuzz/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py @@ -8,7 +8,7 @@ import re from certfuzz.drillresults.common import carve -from certfuzz.drillresults.testcasebundle_base import TestCaseBundle +from certfuzz.analyzers.drillresults.testcasebundle_base import TestCaseBundle logger = logging.getLogger(__name__) diff --git a/src/certfuzz/drillresults/result_driller_linux.py b/src/certfuzz/drillresults/result_driller_linux.py index 96b49e2..e4238d3 100755 --- a/src/certfuzz/drillresults/result_driller_linux.py +++ b/src/certfuzz/drillresults/result_driller_linux.py @@ -5,7 +5,7 @@ import logging import os -from certfuzz.drillresults.testcasebundle_linux import LinuxTestCaseBundle as TestCaseBundle +from certfuzz.analyzers.drillresults.testcasebundle_linux import LinuxTestCaseBundle as TestCaseBundle from certfuzz.drillresults.result_driller_base import ResultDriller logger = logging.getLogger(__name__) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index 5062bd3..566a58e 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -5,7 +5,7 @@ import os import re -from certfuzz.drillresults.testcasebundle_windows import WindowsTestCaseBundle as TestCaseBundle +from certfuzz.analyzers.drillresults.testcasebundle_windows import WindowsTestCaseBundle as TestCaseBundle from certfuzz.drillresults.result_driller_base import ResultDriller diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py index 3792e63..152680a 100644 --- a/src/certfuzz/testcase/testcase_base2.py +++ b/src/certfuzz/testcase/testcase_base2.py @@ -35,7 +35,7 @@ def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): # set some defaults # Not a crash until we're sure self.is_crash = False - self.debugger_file = None + self.dbg_file = None self.is_unique = False self.should_proceed_with_analysis = False self.is_corrupt_stack = False diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index cd49f02..e687326 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -52,17 +52,12 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, self.keep_uniq_faddr = keep_faddr self.cmdargs = None -# self.debugger_file = None self.is_crash = False self.signature = None self.faddr = None self.pc = None self.result_dir = None - @property - def debugger_extension(self): - return self._debugger_cls.extension - def __exit__(self, etype, value, traceback): pass # self.clean_tmpdir() @@ -114,6 +109,8 @@ def get_debug_output(self, outfile_base): keep_uniq_faddr=self.keep_uniq_faddr ) self.dbg = debugger_obj.go() + self.dbg_file = self.dbg.file + def confirm_crash(self): # get debugger output diff --git a/src/test_certfuzz/analyzers/drillresults/__init__.py b/src/test_certfuzz/analyzers/drillresults/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/test_certfuzz/analyzers/test_drillresults.py b/src/test_certfuzz/analyzers/drillresults/test_drillresults.py similarity index 98% rename from src/test_certfuzz/analyzers/test_drillresults.py rename to src/test_certfuzz/analyzers/drillresults/test_drillresults.py index da4427e..e9acfbe 100644 --- a/src/test_certfuzz/analyzers/test_drillresults.py +++ b/src/test_certfuzz/analyzers/drillresults/test_drillresults.py @@ -4,7 +4,7 @@ @author: adh ''' import unittest -from certfuzz.analyzers import drillresults +from certfuzz.analyzers.drillresults import drillresults from test_certfuzz.mocks import MockCfg, MockTestcase, MockFuzzedFile import tempfile import shutil diff --git a/src/test_certfuzz/analyzers/drillresults/test_drillresults_pkg.py b/src/test_certfuzz/analyzers/drillresults/test_drillresults_pkg.py new file mode 100644 index 0000000..f58c709 --- /dev/null +++ b/src/test_certfuzz/analyzers/drillresults/test_drillresults_pkg.py @@ -0,0 +1,33 @@ +''' +Created on Feb 24, 2016 + +@author: adh +''' +import unittest + + +class Test(unittest.TestCase): + + + def setUp(self): + pass + + + def tearDown(self): + pass + + + def test_confirm_api(self): + import certfuzz.analyzers.drillresults as dr + + # we basically just need to make sure that the platform-specific + # drillresults classes are imported in to the package namespace + from certfuzz.analyzers.drillresults.drillresults import LinuxDrillResults, WindowsDrillResults, DrillResults + + for cls in (LinuxDrillResults, WindowsDrillResults, DrillResults): + self.assertTrue(cls.__name__ in dr.__dict__) + + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/test_certfuzz/drillresults/test_testcasebundle_base.py b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_base.py similarity index 99% rename from src/test_certfuzz/drillresults/test_testcasebundle_base.py rename to src/test_certfuzz/analyzers/drillresults/test_testcasebundle_base.py index f3b268f..3beabde 100644 --- a/src/test_certfuzz/drillresults/test_testcasebundle_base.py +++ b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_base.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz.drillresults import testcasebundle_base +from certfuzz.analyzers.drillresults import testcasebundle_base import tempfile import shutil import os diff --git a/src/test_certfuzz/drillresults/test_testcasebundle_linux.py b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_linux.py similarity index 82% rename from src/test_certfuzz/drillresults/test_testcasebundle_linux.py rename to src/test_certfuzz/analyzers/drillresults/test_testcasebundle_linux.py index 092c3fd..3136b4c 100644 --- a/src/test_certfuzz/drillresults/test_testcasebundle_linux.py +++ b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_linux.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz.drillresults import testcasebundle_linux +from certfuzz.analyzers.drillresults import testcasebundle_linux class Test(unittest.TestCase): diff --git a/src/test_certfuzz/drillresults/test_testcasebundle_windows.py b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_windows.py similarity index 82% rename from src/test_certfuzz/drillresults/test_testcasebundle_windows.py rename to src/test_certfuzz/analyzers/drillresults/test_testcasebundle_windows.py index a2b06ad..dedd1b3 100644 --- a/src/test_certfuzz/drillresults/test_testcasebundle_windows.py +++ b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_windows.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -from certfuzz.drillresults import testcasebundle_windows +from certfuzz.analyzers.drillresults import testcasebundle_windows class Test(unittest.TestCase): From 63d02aadb24ce48779d6b631230ff59309a640e7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 09:06:16 -0500 Subject: [PATCH 0900/1169] drop superfluous log message --- src/certfuzz/analyzers/drillresults/drillresults.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/drillresults.py b/src/certfuzz/analyzers/drillresults/drillresults.py index 52a7698..6c0dc07 100644 --- a/src/certfuzz/analyzers/drillresults/drillresults.py +++ b/src/certfuzz/analyzers/drillresults/drillresults.py @@ -66,11 +66,6 @@ def _write_outfile(self): f.write('\n'.join(self.output_lines)) def go(self): - logger.info('Drill Results PLACEHOLDER') - - - - # turn testcase into tescase_bundle with self._tcb_cls(dbg_outfile=self.testcase.dbg_file, testcase_file=self.testcase.fuzzedfile.path, From e02428b9f7372b2df04ba34902135779afc90955 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 09:07:14 -0500 Subject: [PATCH 0901/1169] refactor testcase-specific logging so it's consistent across windows and linux --- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 2 +- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 2 -- src/certfuzz/testcase/testcase_base.py | 24 +++++++++++++++++++ src/certfuzz/testcase/testcase_base2.py | 1 - src/certfuzz/testcase/testcase_linux.py | 18 -------------- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index d54a994..e1d87e3 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -178,7 +178,7 @@ def _verify(self, testcase): pass def _post_verify(self, testcase): - pass + testcase.get_logger() def _pre_minimize(self, testcase): pass diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index 53bb4c9..eb864f9 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -83,8 +83,6 @@ def _verify(self, testcase): logger.info('Testcase signature %s was already seen, skipping further analysis', tc.signature) else: logger.debug('not a crash, continuing') - def _post_verify(self, testcase): - testcase.get_logger() def _pre_minimize(self, testcase): touch_watchdog_file() diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index abb8b29..4d6e1f1 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -10,6 +10,8 @@ from certfuzz.fuzztools import hamming from certfuzz.fuzztools.filetools import check_zip_file from pprint import pformat +import os +from certfuzz.testcase.errors import CrashError logger = logging.getLogger(__name__) @@ -33,6 +35,9 @@ def __init__(self, seedfile, fuzzedfile, workdir_base=None): self.working_dir = None self.is_zipfile = False + # this will get overridden by calls to get_logger + self.logger = logger + def __enter__(self): self._setup_workdir() self.calculate_hamming_distances() @@ -84,3 +89,22 @@ def calculate_hamming_distances_a(self): self.hd_bytes = hamming.bytewise_hd(a_string, fuzzed) self.logger.info("bytewise_hd=%d", self.hd_bytes) + def get_logger(self): + ''' + sets self.logger to a logger specific to this crash + ''' + self.logger = logging.getLogger(self.signature) + if len(self.logger.handlers) == 0: + if not os.path.exists(self.result_dir): + logger.error('Result path not found: %s', self.result_dir) + raise CrashError('Result path not found: {}'.format(self.result_dir)) + logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) + logfile = '%s.log' % self.signature + logger.debug('logfile=%s', logfile) + logpath = os.path.join(self.result_dir, logfile) + logger.debug('logpath=%s', logpath) + hdlr = logging.FileHandler(logpath) + self.logger.addHandler(hdlr) + + return self.logger + diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py index 152680a..7b37221 100644 --- a/src/certfuzz/testcase/testcase_base2.py +++ b/src/certfuzz/testcase/testcase_base2.py @@ -41,7 +41,6 @@ def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): self.is_corrupt_stack = False self.copy_fuzzedfile = True self.pc = None - self.logger = None self.result_dir = None self.debugger_missed_stack_corruption = False self.total_stack_corruption = False diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index e687326..e53eb73 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -170,21 +170,3 @@ def get_result_dir(self): return self.result_dir - def get_logger(self): - ''' - sets self.logger to a logger specific to this crash - ''' - self.logger = logging.getLogger(self.signature) - if len(self.logger.handlers) == 0: - if not os.path.exists(self.result_dir): - logger.error('Result path not found: %s', self.result_dir) - raise CrashError('Result path not found: {}'.format(self.result_dir)) - logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) - logfile = '%s.log' % self.signature - logger.debug('logfile=%s', logfile) - logpath = os.path.join(self.result_dir, logfile) - logger.debug('logpath=%s', logpath) - hdlr = logging.FileHandler(logpath) - self.logger.addHandler(hdlr) - - return self.logger From c538f5c283fd2c1fa49b7718722163e1eabfe252 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 11:10:37 -0500 Subject: [PATCH 0902/1169] remove error throwing from testcasebase2...parent class now has this method filled in --- src/certfuzz/testcase/testcase_base2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py index 7b37221..a31a21d 100644 --- a/src/certfuzz/testcase/testcase_base2.py +++ b/src/certfuzz/testcase/testcase_base2.py @@ -118,9 +118,6 @@ def delete_files(self): def get_debug_output(self, f): raise NotImplementedError - def get_logger(self): - raise NotImplementedError - def get_result_dir(self): raise NotImplementedError From 3c82e88e13a40d93fbd2ac30bd1814023ca0f3bc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 11:40:35 -0500 Subject: [PATCH 0903/1169] add stub get_logger to unit test --- src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py index 47916b8..1e06af6 100644 --- a/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py +++ b/src/test_certfuzz/tc_pipeline/test_tc_pipeline_base.py @@ -58,6 +58,8 @@ def func(tc): class MockTestCase(object): should_proceed_with_analysis = True + def get_logger(self): + pass tcpl._verify = func tcpl._minimize = func From 49c4fc324e9bf8121025a05be0ac68e90f7538df Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 26 Feb 2016 15:12:15 -0500 Subject: [PATCH 0904/1169] BFF writes output to the tempdir before everything is done. BFF-927 --- src/certfuzz/testcase/testcase_base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 4d6e1f1..6d318de 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -95,13 +95,13 @@ def get_logger(self): ''' self.logger = logging.getLogger(self.signature) if len(self.logger.handlers) == 0: - if not os.path.exists(self.result_dir): - logger.error('Result path not found: %s', self.result_dir) - raise CrashError('Result path not found: {}'.format(self.result_dir)) - logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) + if not os.path.exists(self.tempdir): + logger.error('Working path not found: %s', self.tempdir) + raise CrashError('Working path not found: {}'.format(self.tempdir)) + logger.debug('tempdir=%s sig=%s', self.tempdir, self.signature) logfile = '%s.log' % self.signature logger.debug('logfile=%s', logfile) - logpath = os.path.join(self.result_dir, logfile) + logpath = os.path.join(self.tempdir, logfile) logger.debug('logpath=%s', logpath) hdlr = logging.FileHandler(logpath) self.logger.addHandler(hdlr) From 8d6b0b6e53da76e83d5fa6c811f40651ec09afe2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 26 Feb 2016 17:04:43 -0500 Subject: [PATCH 0905/1169] Re-apply clobbered patch that disabled TWDF on Windows. --- src/certfuzz/campaign/campaign_windows.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 827b7e9..0ef5d30 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -17,6 +17,7 @@ from certfuzz.iteration.iteration_windows import WindowsIteration from certfuzz.runners.killableprocess import Popen from certfuzz.fuzztools.command_line_templating import get_command_args_list +from certfuzz.file_handlers.watchdog_file import TWDF logger = logging.getLogger(__name__) @@ -32,6 +33,7 @@ def __init__(self, config_file, result_dir=None, debug=False): self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker', False) self.runner_module_name = 'certfuzz.runners.winrun' self.debugger_module_name = 'certfuzz.debuggers.gdb' + TWDF.disable() def __getstate__(self): state = self.__dict__.copy() From d884b694c85ec25676b82669d443e15066c5aeac Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 26 Feb 2016 17:08:37 -0500 Subject: [PATCH 0906/1169] Don't run stderr analyzer on Windows. You'll fill up the session with crash dialogs. --- src/certfuzz/tc_pipeline/tc_pipeline_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index e9a396c..0e47bc1 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -21,7 +21,7 @@ class WindowsTestCasePipeline(TestCasePipelineBase): _minimizer_cls = WindowsMinimizer def _setup_analyzers(self): - self.analyzer_classes.append(StdErr) + #self.analyzer_classes.append(StdErr) self.analyzer_classes.append(WindowsDrillResults) def _pre_verify(self, testcase): From b6e432c8d0d517b8a9b47b9c76faeb8f7de41fd5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 26 Feb 2016 17:12:38 -0500 Subject: [PATCH 0907/1169] Workaround missing self.ignore_jit variable. TODO: actually fix --- src/certfuzz/analyzers/drillresults/drillresults.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/analyzers/drillresults/drillresults.py b/src/certfuzz/analyzers/drillresults/drillresults.py index 6c0dc07..c4e24b0 100644 --- a/src/certfuzz/analyzers/drillresults/drillresults.py +++ b/src/certfuzz/analyzers/drillresults/drillresults.py @@ -32,6 +32,9 @@ def __init__(self, cfg, testcase): self.outfile = get_file(self.testcase.fuzzedfile.path) self.output_lines = [] + # TODO: This should be dynamic, no? + self.ignore_jit = False + def _process_tcb(self, tcb): details = tcb.details From a9796f33e3defe2842fd39e4149705f27463910e Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 26 Feb 2016 18:01:11 -0500 Subject: [PATCH 0908/1169] Don't enable crash-specific logging. Workaround for BFF-928, BFF-926 --- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index e1d87e3..4418324 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -178,7 +178,9 @@ def _verify(self, testcase): pass def _post_verify(self, testcase): - testcase.get_logger() + # TODO: This isn't writing data where we want it. BFF-928, BFF-926 + # testcase.get_logger() + pass def _pre_minimize(self, testcase): pass From b0f3ae0f8fe0d8e472e092ae53662144de177c78 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sat, 27 Feb 2016 13:13:28 -0500 Subject: [PATCH 0909/1169] Fix Windows installer to use underscore. --- build/distmods/windows/nsis/nsis_footer.txt | 2 +- build/distmods/windows/nsis/nsis_header.txt | 2 +- build/distmods/windows/nsis/nsis_mid.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/distmods/windows/nsis/nsis_footer.txt b/build/distmods/windows/nsis/nsis_footer.txt index e9ce9eb..4e0febb 100644 --- a/build/distmods/windows/nsis/nsis_footer.txt +++ b/build/distmods/windows/nsis/nsis_footer.txt @@ -7,7 +7,7 @@ Delete "$INSTDIR\CERT website.url" RmDir "$INSTDIR\fuzzdir" RmDir "$INSTDIR\symbols" RmDir "$INSTDIR\configs" -RmDir "$INSTDIR\results\convert v5.5.7" +RmDir "$INSTDIR\results\convert_v5.5.7" RmDir "$INSTDIR\results" RmDir "$INSTDIR" diff --git a/build/distmods/windows/nsis/nsis_header.txt b/build/distmods/windows/nsis/nsis_header.txt index 2515ec7..5e044a6 100644 --- a/build/distmods/windows/nsis/nsis_header.txt +++ b/build/distmods/windows/nsis/nsis_header.txt @@ -121,7 +121,7 @@ FunctionEnd Function LaunchBFF ${If} $FUZZ_IMAGEMAGICK == "Y" #CopyFiles "$INSTDIR\seedfiles\examples\*.*" "$INSTDIR\seedfiles" - ExecShell "open" "results\convert v5.5.7\" + ExecShell "open" "results\convert_v5.5.7\" sleep 2000 BringToFront Exec '"cmd.exe" /k @echo off & set PATH=%PATH%;$DEBUGDIR& @echo on & cd $INSTDIR & bff.py' diff --git a/build/distmods/windows/nsis/nsis_mid.txt b/build/distmods/windows/nsis/nsis_mid.txt index 3dd61ed..c50b1a1 100644 --- a/build/distmods/windows/nsis/nsis_mid.txt +++ b/build/distmods/windows/nsis/nsis_mid.txt @@ -325,7 +325,7 @@ WriteRegStr HKLM "SOFTWARE\ImageMagick\5.5.7\Q:16" ConfigurePath "$INSTDIR\image WriteRegStr HKLM "SOFTWARE\ImageMagick\5.5.7\Q:16" LibPath "$INSTDIR\imagemagick" #${If} $FUZZ_IMAGEMAGICK == "Y" - CreateDirectory "$INSTDIR\results\convert v5.5.7" + CreateDirectory "$INSTDIR\results\convert_v5.5.7" #${EndIf} SectionEnd From b818c284143d79e38925506da57a2465d224321a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 31 Mar 2016 15:00:39 -0400 Subject: [PATCH 0910/1169] keep_duplicates moved. BFF-931 --- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index eb864f9..9011d01 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -70,7 +70,7 @@ def _verify(self, testcase): # crash_dir_found = filetools.find_or_create_dir(tc.result_dir) - keep_all = self.cfg['analyzer'].get('keep_duplicates', False) + keep_all = self.cfg['runoptions'].get('keep_duplicates', False) tc.should_proceed_with_analysis = keep_all or (is_new_to_campaign and not crash_dir_found) From 3efb4a956ed029e04b452156649d14bce0ef30a5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 4 Apr 2016 14:38:23 -0400 Subject: [PATCH 0911/1169] Simplify wording in yaml config --- src/windows/configs/examples/bff.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index f01b9de..40a0ebb 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -57,16 +57,16 @@ target: ############################################################################## -# Directories used by BFF +# Directories used by BFF (all relative to bff.py) # # seedfile_dir: -# Location of seed files (relative to bff.py) +# Location of seed files # # working_dir: # Temporary directory used by BFF. Use a ramdisk to reduce disk activity # # results_dir: -# Location of fuzzing results (relative to bff.py) +# Location of fuzzing results ############################################################################## directories: seedfile_dir: seedfiles\examples From fa126d035420ebb61170631f90afc1e1bf8f4966 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 12 Apr 2016 10:44:36 -0400 Subject: [PATCH 0912/1169] keep_heisenbugs moved from runoptions -> campaign. BFF-932 --- src/certfuzz/iteration/iteration_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 71c3431..a7b5f8f 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -71,7 +71,7 @@ def __init__(self, self.retries = 4 self.pipeline_options = {'keep_duplicates': self.cfg['runoptions'].get('keep_duplicates', False), - 'keep_heisenbugs': self.cfg['runoptions'].get('keep_heisenbugs', False), + 'keep_heisenbugs': self.cfg['campaign'].get('keep_heisenbugs', False), 'cmd_template': self.cmd_template, 'null_runner': self.runner_cls.is_nullrunner, 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize', False), From ddcac3b93cc77e43be7fddeabdf069a7c4a9c993 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 12 Apr 2016 10:46:03 -0400 Subject: [PATCH 0913/1169] Remove unified_windows_config.yml --- .../examples/unified_config_windows.yml | 65 ------------------- 1 file changed, 65 deletions(-) delete mode 100644 src/windows/configs/examples/unified_config_windows.yml diff --git a/src/windows/configs/examples/unified_config_windows.yml b/src/windows/configs/examples/unified_config_windows.yml deleted file mode 100644 index 9db19f8..0000000 --- a/src/windows/configs/examples/unified_config_windows.yml +++ /dev/null @@ -1,65 +0,0 @@ -campaign: - id: default_imagemagick_convert - workdir: ~/fuzzing - results_dir: ~/results - seedfile_dir: ~/bff/seedfiles/examples - use_minimizer: True - recycle_crashers: False - keep_heisenbugs: False # windows only - use_buttonclicker: False # windows only -debugger: - timeout: 60 - debugger_cls: gdb - # template_dir: ~/bff/certfuzz/debuggers/templates # linux only - max_handled_exceptions: 6 # windows only - watchcpu: Auto # windows only - use_debug_heap: False # windows only -fuzzer: - fuzzer_cls: bytemut - fuzz_zip_container: False - start_iteration: 0 - iteration_interval: 1 - # # TODO: can we eliminate copymode? - # copymode: False # linux only -# killproc: # linux only -# killprocname: convert -# timeout: 130 -minimizer: - timeout: 3600 - minimize_to_string: False -runner: - timeout: 5 - runner_cls: zzufrun - exceptions: - - 0x80000002 # EXCEPTION_DATATYPE_MISALIGNMENT - - 0xC0000005 # STATUS_ACCESS_VIOLATION - - 0xC000001D # STATUS_ILLEGAL_INSTRUCTION - - 0xC0000025 # EXCEPTION_NONCONTINUABLE_EXCEPTION - - 0xC0000026 # EXCEPTION_INVALID_DISPOSITION - - 0xC000008C # EXCEPTION_ARRAY_BOUNDS_EXCEEDED - - 0xC000008E # EXCEPTION_FLT_DIVIDE_BY_ZERO - - 0xC0000090 # EXCEPTION_FLT_INVALID_OPERATION - - 0xC0000091 # EXCEPTION_FLT_OVERFLOW - - 0xC0000092 # EXCEPTION_FLT_STACK_CHECK - - 0xC0000093 # EXCEPTION_FLT_UNDERFLOW - - 0xC0000094 # STATUS_INTEGER_DIVIDE_BY_ZERO - - 0xC0000095 # EXCEPTION_INT_OVERFLOW - - 0xC0000096 # STATUS_PRIVILEGED_INSTRUCTION - - 0xC00000FD # STATUS_STACK_OVERFLOW - hideoutput: False - watchcpu: Auto -target: - program: ~/convert - invocation_template: $PROGRAM $SEEDFILE /dev/null -# valgrind: # linux only -# timeout: 120 -verifier: - keep_unique_faddr: True # windows only - # keep_failed_asserts: False # linux only - # use_pin_calltrace: True # linux only - keep_duplicates: False - backtrace_depth: 5 - # use_valgrind: True #linux only -# watchdog: -# timeout: 3600 -# file: /tmp/bff_watchdog \ No newline at end of file From 51f9d8d09d4e4ea44fc363f309dcdb38f9909e3f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 12 Apr 2016 10:47:20 -0400 Subject: [PATCH 0914/1169] Minor refactor of msec CPU watching. Fix logic for killing target app and waiting for debugger. BFF-933 --- src/certfuzz/debuggers/msec.py | 72 +++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index eeb0cd1..29de8ec 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -33,6 +33,8 @@ def __init__(self, program, cmd_args, outfile_base, timeout, watchcpu, exception DebuggerBase.__init__(self, program, cmd_args, outfile_base, timeout, **options) self.exception_depth = exception_depth self.watchcpu = watchcpu + if watchcpu: + self.wmiInterface = wmi.WMI() self.t = None def kill(self, pid, returncode): @@ -80,38 +82,48 @@ def _get_cmdline(self, outfile): logger.debug('dbg_args: %s', l) return args + + def _find_debug_target(self, exename): + pid = None + retrycount = 0 + foundpid = False + + if self.watchcpu == True: + + while retrycount < 5 and not foundpid: + for process in self.wmiInterface.Win32_Process(name=exename): + # TODO: What if there's more than one? + pid = process.ProcessID + logger.debug('Found %s PID: %s', exename, pid) + foundpid = True + + if not foundpid: + logger.debug('%s not seen yet. Retrying...', exename) + retrycount += 1 + time.sleep(0.1) + + if not pid: + logger.debug('Cannot find %s child process!', exename) + return pid + def run_with_timer(self): # TODO: replace this with subp.run_with_timer() targetdir = os.path.dirname(self.program) exename = os.path.basename(self.program) process_info = {} - _id = None + child_pid = None done = False started = False - wmiInterface = None - retrycount = 0 - foundpid = False args = self._get_cmdline(self.outfile) p = Popen(args, stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'), universal_newlines=True) - if self.watchcpu == True: - wmiInterface = wmi.WMI() - while retrycount < 5 and not foundpid: - for process in wmiInterface.Win32_Process(name=exename): - # TODO: What if there's more than one? - _id = process.ProcessID - logger.debug('Found %s PID: %s', exename, _id) - foundpid = True - if not foundpid: - logger.debug('%s not seen yet. Retrying...', exename) - retrycount += 1 - time.sleep(0.1) - if not _id: - logger.debug('Cannot find %s child process! Bailing.', exename) - self.kill(p.pid, 99) - return + child_pid = self._find_debug_target(exename) + if child_pid is None: + logger.debug('Bailing on debugger iteration') + self.kill(p.pid, 99) + return # create a timer that calls kill() when it expires self.t = Timer(self.timeout, self.kill, args=[p.pid, 99]) @@ -119,21 +131,27 @@ def run_with_timer(self): if self.watchcpu == True: # This is a race. In some cases, a GUI app could be done before we can even measure it # TODO: Do something about it - while p.poll() is None and not done and _id: - for proc in wmiInterface.Win32_PerfRawData_PerfProc_Process(IDProcess=_id): + while p.poll() is None and not done and child_pid: + for proc in self.wmiInterface.Win32_PerfRawData_PerfProc_Process(IDProcess=child_pid): n1, d1 = long(proc.PercentProcessorTime), long(proc.Timestamp_Sys100NS) - n0, d0 = process_info.get(_id, (0, 0)) + n0, d0 = process_info.get(child_pid, (0, 0)) try: percent_processor_time = (float(n1 - n0) / float(d1 - d0)) * 100.0 except ZeroDivisionError: percent_processor_time = 0.0 - process_info[_id] = (n1, d1) - logger.debug('Process %s CPU usage: %s', _id, percent_processor_time) + process_info[child_pid] = (n1, d1) + logger.debug('Process %s CPU usage: %s', child_pid, percent_processor_time) if percent_processor_time < 0.01: if started: - logger.debug('killing %s due to CPU inactivity', p.pid) + logger.debug('killing %s due to CPU inactivity', child_pid) done = True - self.kill(p.pid, 99) + self.kill(child_pid, 99) + child_pid = self._find_debug_target(exename) + if child_pid is not None: + # cdb launched the target app, but it wasn't killed + # This indicates that the target experienced an exception (crash) + # We will wait for cdb to do its thing + p.wait() else: # Detected CPU usage. Now look for it to drop near zero started = True From 5a908210e6b99b46a8383e79ff0de3ecce7d9990 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 12 Apr 2016 11:35:41 -0400 Subject: [PATCH 0915/1169] Only look for child app if we're monitoring the CPU. Refinement of BFF-933 fix. --- src/certfuzz/debuggers/msec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 29de8ec..892cae1 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -120,7 +120,7 @@ def run_with_timer(self): universal_newlines=True) child_pid = self._find_debug_target(exename) - if child_pid is None: + if child_pid is None and self.watchcpu == True: logger.debug('Bailing on debugger iteration') self.kill(p.pid, 99) return From 4da582296a80716d17f25dc3338ce1482baec151 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 12 Apr 2016 16:33:59 -0400 Subject: [PATCH 0916/1169] Tighten up timing of CPU watching option --- src/certfuzz/debuggers/msec.py | 19 ++++++++++--------- src/certfuzz/runners/winrun.py | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 892cae1..7ce6e3a 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -83,23 +83,23 @@ def _get_cmdline(self, outfile): return args - def _find_debug_target(self, exename): + def _find_debug_target(self, exename, trycount=5): pid = None - retrycount = 0 + attempts = 0 foundpid = False if self.watchcpu == True: - while retrycount < 5 and not foundpid: + while attempts < trycount and not foundpid: for process in self.wmiInterface.Win32_Process(name=exename): # TODO: What if there's more than one? pid = process.ProcessID logger.debug('Found %s PID: %s', exename, pid) foundpid = True - if not foundpid: + attempts += 1 + if not foundpid and attempts < trycount: logger.debug('%s not seen yet. Retrying...', exename) - retrycount += 1 time.sleep(0.1) if not pid: @@ -119,7 +119,7 @@ def run_with_timer(self): p = Popen(args, stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'), universal_newlines=True) - child_pid = self._find_debug_target(exename) + child_pid = self._find_debug_target(exename, trycount=5) if child_pid is None and self.watchcpu == True: logger.debug('Bailing on debugger iteration') self.kill(p.pid, 99) @@ -141,14 +141,15 @@ def run_with_timer(self): percent_processor_time = 0.0 process_info[child_pid] = (n1, d1) logger.debug('Process %s CPU usage: %s', child_pid, percent_processor_time) - if percent_processor_time < 0.01: + if percent_processor_time < 0.0000000001: if started: logger.debug('killing %s due to CPU inactivity', child_pid) done = True self.kill(child_pid, 99) - child_pid = self._find_debug_target(exename) + # Look once to see if the child process is there still + child_pid = self._find_debug_target(exename, trycount=1) if child_pid is not None: - # cdb launched the target app, but it wasn't killed + # cdb launched the target app, but the child wasn't killed # This indicates that the target experienced an exception (crash) # We will wait for cdb to do its thing p.wait() diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index 1ce2439..e254f2e 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -283,7 +283,7 @@ def _run(self): percent_processor_time = 0.0 process_info[id] = (n1, d1) logger.debug('Process %s CPU usage: %s', id, percent_processor_time) - if percent_processor_time < 0.01: + if percent_processor_time < 0.0000000001: if started: logger.debug('killing %s due to CPU inactivity', id) done = True From fd6e70e615e146a3b1d9e79dc4f2cd17a00f5b83 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 13 Apr 2016 16:32:00 -0400 Subject: [PATCH 0917/1169] Use absolute path to target files with minimizer. IE needs this. --- src/certfuzz/minimizer/minimizer_base.py | 2 +- src/certfuzz/tools/linux/minimize.py | 1 + src/certfuzz/tools/windows/minimize.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 0b8f8cb..84f888b 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -195,7 +195,7 @@ def __enter__(self): except KeyError: pass if not self._is_crash_to_minimize(): - msg = 'Unable to minimize: No testcase' + msg = 'Unable to minimize: No crash' self.logger.info(msg) self._raise(msg) if self._is_already_minimized(): diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index b45530e..f5363b3 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -110,6 +110,7 @@ def main(): outdir = options.outdir else: outdir = "./minimizer_out" + outdir = os.path.abspath(outdir) if not os.path.exists(outdir): filetools.make_directories(outdir) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index c00d40f..b16927c 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -133,6 +133,7 @@ def main(): outdir = options.outdir else: outdir = 'minimizer_out' + outdir = os.path.abspath(outdir) filetools.make_directories(outdir) From 1354d1a6f165c3f659c511482f58e547a29dda6e Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 13 Apr 2016 17:18:47 -0400 Subject: [PATCH 0918/1169] Fix regression introduced with 58cb36c60a96 (BFF-826) that caused improper zip file mutation. BFF-935 --- src/certfuzz/fuzzers/fuzzer_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/fuzzers/fuzzer_base.py b/src/certfuzz/fuzzers/fuzzer_base.py index a1158ce..8ff16d1 100644 --- a/src/certfuzz/fuzzers/fuzzer_base.py +++ b/src/certfuzz/fuzzers/fuzzer_base.py @@ -206,7 +206,7 @@ def _prefuzz(self): # save compress type self.saved_arcinfo[i] = (len(self.zipinput), len(data), tempzip.getinfo(i).compress_type) - self.input += data + self.zipinput += data tempzip.close() inmemseed.close() # Zip processing went fine, so use the zip contents as self.input to fuzzer From 4e0926aaad40c61cfe128cb21aee39c6a6d89417 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 15 Apr 2016 11:31:25 -0400 Subject: [PATCH 0919/1169] When the minimizer gets close (10 bytes) from the target, exhaustively test the remaining byte locations. BFF-934 --- src/certfuzz/minimizer/minimizer_base.py | 226 +++++++++++++++-------- 1 file changed, 153 insertions(+), 73 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 84f888b..cf716b6 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -58,6 +58,7 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, self.start_time = 0.0 self.keep_uniq_faddr = keep_uniq_faddr self.watchcpu = watchcpu + self.exhaustivesearch_threshold = 10 self.minchar = 'x' self.save_others = True @@ -88,17 +89,23 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, self.swap_func = self.bytewise_swap2 if self.seedfile_as_target: - minf = '%s-minimized%s' % (self.testcase.fuzzedfile.root, self.testcase.fuzzedfile.ext) - minlog = os.path.join(self.testcase.fuzzedfile.dirname, 'minimizer_log.txt') + minf = '%s-minimized%s' % (self.testcase.fuzzedfile.root, + self.testcase.fuzzedfile.ext) + minlog = os.path.join( + self.testcase.fuzzedfile.dirname, 'minimizer_log.txt') if not os.path.exists(self.testcase.seedfile.path): self._raise('Seedfile not found at %s' % - self.testcase.seedfile.path) + self.testcase.seedfile.path) elif self.preferx: - minf = '%s-min-%s%s' % (self.testcase.fuzzedfile.root, self.minchar, self.testcase.fuzzedfile.ext) - minlog = os.path.join(self.testcase.fuzzedfile.dirname, 'minimizer_%s_log.txt' % self.minchar) + minf = '%s-min-%s%s' % (self.testcase.fuzzedfile.root, + self.minchar, self.testcase.fuzzedfile.ext) + minlog = os.path.join( + self.testcase.fuzzedfile.dirname, 'minimizer_%s_log.txt' % self.minchar) else: - minf = '%s-min-mtsp%s' % (self.testcase.fuzzedfile.root, self.testcase.fuzzedfile.ext) - minlog = os.path.join(self.testcase.fuzzedfile.dirname, 'minimizer_mtsp_log.txt') + minf = '%s-min-mtsp%s' % (self.testcase.fuzzedfile.root, + self.testcase.fuzzedfile.ext) + minlog = os.path.join( + self.testcase.fuzzedfile.dirname, 'minimizer_mtsp_log.txt') self.outputfile = os.path.join(self.testcase.fuzzedfile.dirname, minf) @@ -118,7 +125,8 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, self._raise("%s is not a directory" % self.crash_dst) self._logger_setup() - self.logger.info("Minimizer initializing for %s", self.testcase.fuzzedfile.path) + self.logger.info( + "Minimizer initializing for %s", self.testcase.fuzzedfile.path) if not os.path.exists(self.testcase.fuzzedfile.path): self._raise("%s does not exist" % self.testcase.fuzzedfile.path) @@ -133,6 +141,7 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, self.discard_chance = 0 self.debugger_runs = 0 self.min_found = False + self.try_exhaustive = False self.confidence_level = confidence self.newfuzzed_hd = 0 self.newfuzzed_md5 = None @@ -154,7 +163,8 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, # none of this will work if the files are of different size if len(self.seed) != len(self.fuzzed_content): - self._raise('Minimizer requires seed and fuzzed_content to have the same length. %d != %d' % (len(self.seed), len(self.fuzzed_content))) + self._raise('Minimizer requires seed and fuzzed_content to have the same length. %d != %d' % ( + len(self.seed), len(self.fuzzed_content))) # initialize the hamming distance self.start_distance = self.hd_func(self.seed, self.fuzzed_content) @@ -182,11 +192,14 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, # set the debugger_timeout to the lesser of what we've measured # in getting the crash_hashes or what it already was if self.measured_dbg_time: - self.debugger_timeout = min(self.debugger_timeout, self.measured_dbg_time) + self.debugger_timeout = min( + self.debugger_timeout, self.measured_dbg_time) - self.logger.info('\tusing debugger timeout: %0.5f', self.debugger_timeout) + self.logger.info( + '\tusing debugger timeout: %0.5f', self.debugger_timeout) self.logger.info('\tconfidence level: %0.5f', self.confidence_level) - self.logger.info('\tstarting Hamming Distance is %d', self.start_distance) + self.logger.info( + '\tstarting Hamming Distance is %d', self.start_distance) def __enter__(self): # make sure we can actually minimize @@ -249,12 +262,13 @@ def _read_seed(self): ''' returns the contents of the seed file ''' - # we're either going to minimize to the seedfile, the metasploit pattern, or a string of 'x's + # we're either going to minimize to the seedfile, the metasploit pattern, + # or a string of 'x's if self.seedfile_as_target: - if self.is_zipfile and self.seedfile_as_target: - return self._readzip(self.testcase.seedfile.path) - else: - return self.testcase.seedfile.read() + if self.is_zipfile and self.seedfile_as_target: + return self._readzip(self.testcase.seedfile.path) + else: + return self.testcase.seedfile.read() elif self.preferx: return self.minchar * len(self.fuzzed_content) else: @@ -280,7 +294,7 @@ def _readzip(self, filepath): # probably unnecessary since it's the content that matters self.saved_arcinfo[i] = (len(unzippedbytes), len(data), - tempzip.getinfo(i).compress_type) + tempzip.getinfo(i).compress_type) unzippedbytes += data tempzip.close() return unzippedbytes @@ -315,9 +329,11 @@ def _writezip(self): # Python zipfile only supports compression types 0 and 8 compressiontype = info[2] else: - logger.warning('Compression type %s is not supported. Overriding', info[2]) + logger.warning( + 'Compression type %s is not supported. Overriding', info[2]) compressiontype = 8 - tempzip.writestr(name, str(filedata[info[0]:info[0] + info[1]]), compress_type=compressiontype) + tempzip.writestr( + name, str(filedata[info[0]:info[0] + info[1]]), compress_type=compressiontype) tempzip.close() def _logger_setup(self): @@ -326,7 +342,8 @@ def _logger_setup(self): if not os.path.exists(dirname): self._raise('Directory should already exist: %s' % dirname) if os.path.exists(self.minimizer_logfile): - self._raise('Log file must not already exist: %s' % self.minimizer_logfile) + self._raise('Log file must not already exist: %s' % + self.minimizer_logfile) self.logger = logging.getLogger(__name__) self.log_file_hdlr = logging.FileHandler(self.minimizer_logfile) self.logger.addHandler(self.log_file_hdlr) @@ -346,7 +363,8 @@ def _set_crash_hashes(self): # loop until we've found ALL the testcase signatures while miss_count < max_misses: # (sometimes testcase sigs change for the same input file) - (fd, f) = tempfile.mkstemp(prefix='minimizer_set_crash_hashes_', text=True, dir=self.tempdir) + (fd, f) = tempfile.mkstemp( + prefix='minimizer_set_crash_hashes_', text=True, dir=self.tempdir) os.close(fd) delete_files(f) @@ -392,22 +410,24 @@ def _set_crash_hashes(self): def run_debugger(self, infile, outfile): self.debugger_runs += 1 - cmd_args = get_command_args_list(self.cfg['target']['cmdline_template'], infile)[1] + cmd_args = get_command_args_list( + self.cfg['target']['cmdline_template'], infile)[1] cmd = cmd_args[0] cmd_args = cmd_args[1:] - exclude_unmapped_frames = self.cfg['analyzer'].get('exclude_unmapped_frames', True) + exclude_unmapped_frames = self.cfg['analyzer'].get( + 'exclude_unmapped_frames', True) dbg = self._debugger_cls(cmd, - cmd_args, - outfile, - self.debugger_timeout, - template=self.testcase.debugger_template, - exclude_unmapped_frames=exclude_unmapped_frames, - keep_uniq_faddr=self.keep_uniq_faddr, - workingdir=self.tempdir, - watchcpu=self.watchcpu - ) + cmd_args, + outfile, + self.debugger_timeout, + template=self.testcase.debugger_template, + exclude_unmapped_frames=exclude_unmapped_frames, + keep_uniq_faddr=self.keep_uniq_faddr, + workingdir=self.tempdir, + watchcpu=self.watchcpu + ) parsed_debugger_output = dbg.go() return parsed_debugger_output @@ -419,7 +439,8 @@ def _crash_builder(self): new_testcase = copy.copy(self.testcase) # get a new dir for the next crasher - newcrash_tmpdir = tempfile.mkdtemp(prefix='minimizer_crash_builder_', dir=self.tempdir) + newcrash_tmpdir = tempfile.mkdtemp( + prefix='minimizer_crash_builder_', dir=self.tempdir) # get a new filename for the next crasher sfx = self.testcase.fuzzedfile.ext @@ -439,33 +460,37 @@ def _crash_builder(self): filetools.copy_file(self.tempfile, outfile) new_testcase.fuzzedfile = BasicFile(outfile) - self.logger.debug('\tNew fuzzed_content file: %s %s', new_testcase.fuzzedfile.path, new_testcase.fuzzedfile.md5) + self.logger.debug('\tNew fuzzed_content file: %s %s', + new_testcase.fuzzedfile.path, new_testcase.fuzzedfile.md5) - # clear out the copied testcase signature so that it will be regenerated + # clear out the copied testcase signature so that it will be + # regenerated new_testcase.signature = None # replace old testcase details with new info specific to this testcase self.logger.debug('\tUpdating testcase details') new_testcase.update_crash_details() - # the tempdir we created is no longer needed because update_crash_details creates a fresh one + # the tempdir we created is no longer needed because + # update_crash_details creates a fresh one shutil.rmtree(newcrash_tmpdir) if os.path.exists(newcrash_tmpdir): logger.warning("Failed to remove temp dir %s", newcrash_tmpdir) return new_testcase - def _get_pin_signature(self, backtracelevels): # total_stack_corruption. Use pin calltrace to get a backtrace - analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self.testcase) + analyzer_instance = pin_calltrace.Pin_calltrace( + self.cfg, self.testcase) try: analyzer_instance.go() except AnalyzerEmptyOutputError: logger.warning('Unexpected empty output from analyzer. Continuing') if os.path.exists(analyzer_instance.outfile): calltrace = Calltracefile(analyzer_instance.outfile) - pinsignature = calltrace.get_testcase_signature(backtracelevels * 10) + pinsignature = calltrace.get_testcase_signature( + backtracelevels * 10) if pinsignature: signature = pinsignature return signature @@ -481,7 +506,8 @@ def get_signature(self, dbg, backtracelevels): def is_same_crash(self): # get debugger output filename - (fd, f) = tempfile.mkstemp(dir=self.tempdir, prefix="minimizer_is_same_crash_") + (fd, f) = tempfile.mkstemp( + dir=self.tempdir, prefix="minimizer_is_same_crash_") os.close(fd) if os.path.exists(f): delete_files(f) @@ -504,7 +530,8 @@ def is_same_crash(self): else: # the testcase is new to this minimization run self.crash_sigs_found[newfuzzed_hash] = 1 - self.logger.info('testcase=%s signal=%s', newfuzzed_hash, dbg.signal) + self.logger.info( + 'testcase=%s signal=%s', newfuzzed_hash, dbg.signal) if self.save_others and newfuzzed_hash not in self.crash_hashes: # the testcase is not one of the crashes we're looking for @@ -515,7 +542,8 @@ def is_same_crash(self): # note that since we're doing this every time we see a testcase # that's not in self.crash_hashes, we're also effectively # keeping only the smallest hamming distance version of - # newfuzzed_hash as we progress through the minimization process + # newfuzzed_hash as we progress through the minimization + # process self.other_crashes[newfuzzed_hash] = newcrash # ditch the temp file @@ -533,7 +561,8 @@ def set_discard_chance(self): if new_dc >= min_dc: return False - # if we're changing the discard chance, reset the consecutive miss count + # if we're changing the discard chance, reset the consecutive miss + # count if not self.discard_chance == new_dc: self.consecutive_misses = 0 self.discard_chance = new_dc @@ -546,20 +575,24 @@ def set_n_misses(self): return False keep_chance = 1.0 - self.discard_chance - p = probability.FuzzRun(self.min_distance, self.target_size_guess, keep_chance) + p = probability.FuzzRun( + self.min_distance, self.target_size_guess, keep_chance) - self.n_misses_allowed = p.how_many_misses_until_quit(self.confidence_level) + self.n_misses_allowed = p.how_many_misses_until_quit( + self.confidence_level) return True def print_intermediate_log(self): if not self.newfuzzed_hd: - self.logger.debug('self.newfuzzed_hd not set. Default to self.min_distance') + self.logger.debug( + 'self.newfuzzed_hd not set. Default to self.min_distance') new_hd = self.min_distance else: new_hd = self.newfuzzed_hd if not self.n_misses_allowed: - self.logger.debug('self.n_misses_allowed not set. Default to self.consecutive_misses') + self.logger.debug( + 'self.n_misses_allowed not set. Default to self.consecutive_misses') n_misses_allowed = self.consecutive_misses else: n_misses_allowed = self.n_misses_allowed @@ -570,8 +603,10 @@ def print_intermediate_log(self): parts.append('target_guess=%d' % self.target_size_guess) parts.append('curr=%d' % new_hd) parts.append('chance=%0.5f' % self.discard_chance) - parts.append('miss=%d/%d' % (self.consecutive_misses, n_misses_allowed)) - parts.append('total_misses=%d/%d' % (self.total_misses, self.total_tries)) + parts.append('miss=%d/%d' % + (self.consecutive_misses, n_misses_allowed)) + parts.append('total_misses=%d/%d' % + (self.total_misses, self.total_tries)) parts.append('u_crashes=%d' % len(self.crash_sigs_found.items())) logstring = ' '.join(parts) self.logger.info(logstring) @@ -600,21 +635,28 @@ def _write_file(self): else: write_file(''.join(self.newfuzzed), self.tempfile) + def _set_bytemap(self): + if self.fuzzed_content: + self.bytemap = hamming.bytemap(self.seed, self.fuzzed_content) + def go(self): - # start by copying the fuzzed_content file since as of now it's our best fit + # start by copying the fuzzed_content file since as of now it's our + # best fit filetools.copy_file(self.testcase.fuzzedfile.path, self.outputfile) # replace the fuzzedfile object in testcase with the minimized copy self.testcase.fuzzedfile = BasicFile(self.outputfile) - self.logger.info('Attempting to minimize testcase(es) [%s]', self._crash_hashes_string()) + self.logger.info( + 'Attempting to minimize testcase(es) [%s]', self._crash_hashes_string()) # keep going until either: # a. we find a minimum hd of 1 # b. we run out of discard_chances # c. our discard_chance * minimum hd is less than one (we won't discard anything) - # d. we've exhaustively searched all the possible files with hd less than self.min_distance - while not self.min_found: + # d. we've exhaustively searched all the possible files with hd less + # than self.min_distance + while not self.min_found and not self.try_exhaustive: if not self.set_discard_chance(): break @@ -626,12 +668,14 @@ def go(self): while self.consecutive_misses <= self.n_misses_allowed: if self.use_watchdog: - # touch the watchdog file so we don't reboot during long minimizations + # touch the watchdog file so we don't reboot during long + # minimizations open(self.watchdogfile, 'w').close() # Fix for BFF-208 if self._time_exceeded(): - logger.info('Max time for minimization exceeded, ending minimizer early.') + logger.info( + 'Max time for minimization exceeded, ending minimizer early.') self.min_found = True break @@ -649,7 +693,8 @@ def go(self): if not self.files_tried_at_hd.get(self.min_distance): # we've reached a new minimum, so create new sets self.files_tried_at_hd[self.min_distance] = set() - self.files_tried_singlebyte_at_hd[self.min_distance] = set() + self.files_tried_singlebyte_at_hd[ + self.min_distance] = set() # have we exhausted all the possible files with smaller hd? possible_files = (2 ** self.min_distance) - 2 @@ -657,25 +702,31 @@ def go(self): # maybe we're done? if seen_files == possible_files: - # we've exhaustively searched everything with hd < self.min_distance - self.logger.info('Exhaustively searched all files shorter than %d', self.min_distance) + # we've exhaustively searched everything with hd < + # self.min_distance + self.logger.info( + 'Exhaustively searched all files shorter than %d', self.min_distance) self.min_found = True break # have we exhausted all files that are 1 byte smaller hd? possible_singlebyte_diff_files = self.min_distance - singlebyte_diff_files_seen = len(self.files_tried_singlebyte_at_hd[self.min_distance]) + singlebyte_diff_files_seen = len( + self.files_tried_singlebyte_at_hd[self.min_distance]) # maybe we're done? if singlebyte_diff_files_seen == possible_singlebyte_diff_files: - self.logger.info('We have tried all %d files that are one byte closer than the current minimum', self.min_distance) + self.logger.info( + 'We have tried all %d files that are one byte closer than the current minimum', self.min_distance) self.min_found = True break # remember this file for next time around - self.files_tried_at_hd[self.min_distance].add(self.newfuzzed_md5) + self.files_tried_at_hd[ + self.min_distance].add(self.newfuzzed_md5) if self.newfuzzed_hd == (self.min_distance - 1): - self.files_tried_singlebyte_at_hd[self.min_distance].add(self.newfuzzed_md5) + self.files_tried_singlebyte_at_hd[ + self.min_distance].add(self.newfuzzed_md5) self.print_intermediate_log() @@ -687,12 +738,14 @@ def go(self): self.total_misses += 1 continue - # we didn't skip ahead, so it must have been new. Remember it now + # we didn't skip ahead, so it must have been new. Remember it + # now self.files_tried.add(self.newfuzzed_md5) # we have a better match, write it to a file if not len(self.newfuzzed): - raise MinimizerError('New fuzzed_content content is empty.') + raise MinimizerError( + 'New fuzzed_content content is empty.') self._write_file() @@ -700,7 +753,8 @@ def go(self): # record the result # 1. copy the tempfile filetools.best_effort_move(self.tempfile, self.outputfile) - # 2. replace the fuzzed_content file in the crasher with the current one + # 2. replace the fuzzed_content file in the crasher with + # the current one self.testcase.fuzzedfile = BasicFile(self.outputfile) # 3. replace the current fuzzed_content with newfuzzed self.fuzzed_content = self.newfuzzed @@ -711,6 +765,12 @@ def go(self): if self.min_distance == 1: # we are done self.min_found = True + elif self.newfuzzed_hd <= self.exhaustivesearch_threshold: + self._set_bytemap() + logger.info( + 'Exhaustively checking remaining %s bytes' % self.newfuzzed_hd) + self.try_exhaustive = True + break else: # set up for next iteration self.consecutive_misses = 0 @@ -739,14 +799,28 @@ def go(self): # so increment it by 1 self.target_size_guess += 1 - self.print_intermediate_log() + if self.try_exhaustive: + for offset in list(self.bytemap): + logger.debug('Verifying byte location: %s' % hex(offset)) + self.revert_byte(offset) + self._write_file() + if self.is_same_crash(): + logger.debug( + 'Fuzzed byte at offset %s is not relevant' % hex(offset)) + filetools.best_effort_move(self.tempfile, self.outputfile) + self.testcase.fuzzedfile = BasicFile(self.outputfile) + self.fuzzed_content = self.newfuzzed + self.bytemap.remove(offset) - self.logger.info('We were looking for [%s] ...', self._crash_hashes_string()) + self.logger.info( + 'We were looking for [%s] ...', self._crash_hashes_string()) for (md5, count) in self.crash_sigs_found.items(): self.logger.info('\t...and found %s\t%d times', md5, count) - if self.fuzzed_content: - self.bytemap = hamming.bytemap(self.seed, self.fuzzed_content) - self.logger.info('Bytemap: %s', self.bytemap) + if self.bytemap: + hex_bytemap = [] + for offset in self.bytemap: + hex_bytemap.append(hex(offset)) + self.logger.info('Bytemap: %s', hex_bytemap) def get_mask(self): mask = 0 @@ -766,13 +840,18 @@ def swap_bytes(self): # or that we didn't drop any bytes at all # so keep trying until both are true while not (0 < newfuzzed_hd < self.min_distance): - newfuzzed, newfuzzed_hd = self.swap_func(self.seed, self.fuzzed_content) + newfuzzed, newfuzzed_hd = self.swap_func( + self.seed, self.fuzzed_content) # we know our hd is > 0 and < what it was when we started self.newfuzzed = newfuzzed self.newfuzzed_hd = newfuzzed_hd self.newfuzzed_md5 = hashlib.md5(''.join(self.newfuzzed)).hexdigest() + def revert_byte(self, offset): + self.newfuzzed[offset] = self.seed[offset] + self.newfuzzed_hd -= 1 + def bytewise_swap2(self, seed, fuzzed): swapped = [] hd = 0 @@ -791,7 +870,8 @@ def bytewise_swap2(self, seed, fuzzed): append(a) return swapped, hd # Note that the above implementation is actually faster overall than the list - # comprehension below since we're catching the hamming distance at the same time. + # comprehension below since we're catching the hamming distance at the + # same time. def _mask(self): mask = 0 From 6000cb2eb3106a4343b18bf3c6ec918055cb9066 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 18 Apr 2016 16:57:53 -0400 Subject: [PATCH 0920/1169] Open .drillresults output file in text mode rather than binary to get universal newline support. BFF-938 --- .../analyzers/drillresults/drillresults.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/drillresults.py b/src/certfuzz/analyzers/drillresults/drillresults.py index c4e24b0..7c4f34a 100644 --- a/src/certfuzz/analyzers/drillresults/drillresults.py +++ b/src/certfuzz/analyzers/drillresults/drillresults.py @@ -35,7 +35,6 @@ def __init__(self, cfg, testcase): # TODO: This should be dynamic, no? self.ignore_jit = False - def _process_tcb(self, tcb): details = tcb.details score = tcb.score @@ -43,7 +42,8 @@ def _process_tcb(self, tcb): output_lines = [] - output_lines.append('%s - Exploitability rank: %s' % (crash_key, score)) + output_lines.append( + '%s - Exploitability rank: %s' % (crash_key, score)) output_lines.append('Fuzzed file: %s' % details['fuzzedfile']) for exception in details['exceptions']: @@ -52,20 +52,23 @@ def _process_tcb(self, tcb): efa = '0x' + details['exceptions'][exception]['efa'] if details['exceptions'][exception]['EIF']: eiftext = " *** Byte pattern is in fuzzed file! ***" - output_lines.append('exception %s: %s accessing %s %s' % (exception, shortdesc, efa, eiftext)) + output_lines.append( + 'exception %s: %s accessing %s %s' % (exception, shortdesc, efa, eiftext)) if details['exceptions'][exception]['instructionline']: - output_lines.append(details['exceptions'][exception]['instructionline']) + output_lines.append( + details['exceptions'][exception]['instructionline']) module = details['exceptions'][exception]['pcmodule'] if module == 'unloaded': if not self.ignore_jit: - output_lines.append('Instruction pointer is not in a loaded module!') + output_lines.append( + 'Instruction pointer is not in a loaded module!') else: output_lines.append('Code executing in: %s' % module) self.output_lines = output_lines def _write_outfile(self): - with open(self.outfile, 'wb') as f: + with open(self.outfile, 'w') as f: f.write('\n'.join(self.output_lines)) def go(self): @@ -77,7 +80,8 @@ def go(self): try: tcb.go() except TestCaseBundleError as e: - logger.warning('Skipping drillresults on testcase %s: %s', self.testcase.signature, e) + logger.warning( + 'Skipping drillresults on testcase %s: %s', self.testcase.signature, e) return self._process_tcb(tcb) @@ -89,5 +93,6 @@ def go(self): class LinuxDrillResults(DrillResults): _tcb_cls = LinuxTestCaseBundle + class WindowsDrillResults(DrillResults): _tcb_cls = WindowsTestCaseBundle From e8fd5d05697bcc211e4e465fae0dd2f84b6bd0ef Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 22 Apr 2016 14:37:54 -0400 Subject: [PATCH 0921/1169] standalone minimizer on Linux should have option to keep other crashers. BFF-939 --- src/certfuzz/testcase/testcase_linux.py | 45 ++++++++++++++----------- src/certfuzz/tools/linux/minimize.py | 42 +++++++++++++---------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index e53eb73..9897f2a 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -47,11 +47,13 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, self.crash_base_dir = crashers_dir self.seednum = seednum self.range = range - self.exclude_unmapped_frames = cfg['analyzer']['exclude_unmapped_frames'] + self.exclude_unmapped_frames = cfg[ + 'analyzer']['exclude_unmapped_frames'] self.set_debugger_template('bt_only') self.keep_uniq_faddr = keep_faddr self.cmdargs = None + self.workdir_base = workdir_base self.is_crash = False self.signature = None self.faddr = None @@ -64,18 +66,21 @@ def __exit__(self, etype, value, traceback): def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): - dbg_template_name = '%s_%s_template.txt' % (self._debugger_cls._key, option) - self.debugger_template = os.path.join('certfuzz/debuggers/templates', dbg_template_name) + dbg_template_name = '%s_%s_template.txt' % ( + self._debugger_cls._key, option) + self.debugger_template = os.path.join( + 'certfuzz/debuggers/templates', dbg_template_name) logger.debug('Debugger template set to %s', self.debugger_template) if not os.path.exists(self.debugger_template): - raise CrashError('Debugger template does not exist at %s' % self.debugger_template) + raise CrashError( + 'Debugger template does not exist at %s' % self.debugger_template) def update_crash_details(self): Testcase.update_crash_details(self) cmdlist = get_command_args_list(self.cfg['target']['cmdline_template'], - infile=self.fuzzedfile.path, - posix=True)[1] + infile=self.fuzzedfile.path, + posix=True)[1] self.cmdargs = cmdlist[1:] self.is_crash = self.confirm_crash() @@ -101,17 +106,16 @@ def get_debug_output(self, outfile_base): logger.debug('Debugger template: %s outfile_base: %s', self.debugger_template, outfile_base) debugger_obj = self._debugger_cls(self.program, - self.cmdargs, - outfile_base, - self.debugger_timeout, - template=self.debugger_template, - exclude_unmapped_frames=self.exclude_unmapped_frames, - keep_uniq_faddr=self.keep_uniq_faddr - ) + self.cmdargs, + outfile_base, + self.debugger_timeout, + template=self.debugger_template, + exclude_unmapped_frames=self.exclude_unmapped_frames, + keep_uniq_faddr=self.keep_uniq_faddr + ) self.dbg = debugger_obj.go() self.dbg_file = self.dbg.file - def confirm_crash(self): # get debugger output self.get_debug_output(self.fuzzedfile.path) @@ -119,7 +123,8 @@ def confirm_crash(self): if not self.dbg: raise CrashError('Debug object not found') - logger.debug('is_crash: %s is_assert_fail: %s', self.dbg.is_crash, self.dbg.is_assert_fail) + logger.debug('is_crash: %s is_assert_fail: %s', + self.dbg.is_crash, self.dbg.is_assert_fail) if self.cfg['analyzer']['savefailedasserts']: return self.dbg.is_crash else: @@ -136,7 +141,8 @@ def get_signature(self): @raise CrasherHasNoSignatureError: if it's a valid crash, but we don't get a signature ''' if not self.signature: - self.signature = self.dbg.get_testcase_signature(self.backtrace_lines) + self.signature = self.dbg.get_testcase_signature( + self.backtrace_lines) if self.signature: logger.debug("Testcase signature is %s", self.signature) else: @@ -147,11 +153,13 @@ def get_signature(self): try: analyzer_instance.go() except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from pin. Cannot determine call trace.') + logger.warning( + 'Unexpected empty output from pin. Cannot determine call trace.') return self.signature calltrace = Calltracefile(analyzer_instance.outfile) - pinsignature = calltrace.get_testcase_signature(self.backtrace_lines * 10) + pinsignature = calltrace.get_testcase_signature( + self.backtrace_lines * 10) if pinsignature: self.signature = pinsignature return self.signature @@ -169,4 +177,3 @@ def get_result_dir(self): self.result_dir = os.path.join(self.crash_base_dir, self.signature) return self.result_dir - diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index f5363b3..f46d443 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -61,6 +61,9 @@ def main(): parser.add_option('', '--timeout', dest='timeout', metavar='N', type='int', default=0, help='Stop minimizing after N seconds (default is 0, never time out).') + parser.add_option('-k', '--keepothers', dest='keep_other_crashes', + action='store_true', + help='Keep other testcase hashes encountered during minimization') (options, args) = parser.parse_args() @@ -77,11 +80,13 @@ def main(): elif os.path.isfile("configs/bff.yaml"): cfg_file = "configs/bff.yaml" else: - parser.error('Configuration file (--config) option must be specified.') + parser.error( + 'Configuration file (--config) option must be specified.') logger.debug('Config file: %s', cfg_file) if options.stringmode and options.target: - parser.error('Options --stringmode and --target are mutually exclusive.') + parser.error( + 'Options --stringmode and --target are mutually exclusive.') # Set some default options. Fast and loose if in string mode # More precise with minimize to seedfile @@ -116,7 +121,8 @@ def main(): filetools.make_directories(outdir) if not os.path.isdir(outdir): - parser.error('--outdir must either already be a dir or not exist: %s' % outdir) + parser.error( + '--outdir must either already be a dir or not exist: %s' % outdir) if len(args) and os.path.exists(args[0]): fuzzed_file = BasicFile(args[0]) @@ -124,7 +130,6 @@ def main(): else: parser.error('fuzzedfile must be specified') - if options.target: seedfile = BasicFile(options.target) else: @@ -144,7 +149,7 @@ def main(): debugger_timeout=cfg['runner']['runtimeout'], backtrace_lines=cfg['debugger']['backtracelevels'], crashers_dir=crashers_dir, - workdir_base=None, + workdir_base=outdir, keep_faddr=options.keep_uniq_faddr) as testcase: filetools.make_directories(testcase.tempdir) @@ -152,15 +157,15 @@ def main(): filetools.copy_file(fuzzed_file.path, testcase.tempdir) with Minimizer(cfg=cfg, testcase=testcase, crash_dst_dir=outdir, - seedfile_as_target=min2seed, - bitwise=options.bitwise, - confidence=confidence, - logfile='./min_log.txt', - tempdir=testcase.tempdir, - maxtime=options.timeout, - preferx=options.prefer_x_target, - keep_uniq_faddr=options.keep_uniq_faddr) as minimize: - minimize.save_others = False + seedfile_as_target=min2seed, + bitwise=options.bitwise, + confidence=confidence, + logfile='./min_log.txt', + tempdir=testcase.tempdir, + maxtime=options.timeout, + preferx=options.prefer_x_target, + keep_uniq_faddr=options.keep_uniq_faddr) as minimize: + minimize.save_others = options.keep_other_crashes minimize.target_size_guess = int(options.initial_target_size) minimize.go() @@ -168,11 +173,13 @@ def main(): logger.debug('x character substitution') length = len(minimize.fuzzed_content) if options.prefer_x_target: - # We minimized to 'x', so we attempt to get metasploit as a freebie + # We minimized to 'x', so we attempt to get metasploit as a + # freebie targetstring = list(text.metasploit_pattern_orig(length)) filename_modifier = '-mtsp' else: - # We minimized to metasploit, so we attempt to get 'x' as a freebie + # We minimized to metasploit, so we attempt to get 'x' as a + # freebie targetstring = list('x' * length) filename_modifier = '-x' @@ -180,7 +187,8 @@ def main(): for idx in minimize.bytemap: logger.debug('Swapping index %d', idx) targetstring[idx] = fuzzed[idx] - filename = ''.join((testcase.fuzzedfile.root, filename_modifier, testcase.fuzzedfile.ext)) + filename = ''.join( + (testcase.fuzzedfile.root, filename_modifier, testcase.fuzzedfile.ext)) metasploit_file = os.path.join(testcase.tempdir, filename) with open(metasploit_file, 'wb') as f: From 0ca7cb29942add7538ae607d26d8d4af98bafa40 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 22 Apr 2016 14:38:08 -0400 Subject: [PATCH 0922/1169] Misc cleanup. --- src/certfuzz/runners/winrun.py | 129 +++++++++++++++------------------ src/linux/configs/bff.yaml | 3 +- 2 files changed, 62 insertions(+), 70 deletions(-) diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index e254f2e..f0caace 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -5,11 +5,13 @@ from certfuzz.fuzztools.command_line_templating import get_command_args_list if not platform.version().startswith('5.'): - raise RunnerPlatformVersionError('Incompatible OS: winrun only works on Windows XP and 2003') + raise RunnerPlatformVersionError( + 'Incompatible OS: winrun only works on Windows XP and 2003') from killableprocess import Popen from threading import Timer -from _winreg import OpenKey, SetValueEx, HKEY_LOCAL_MACHINE, REG_SZ, KEY_ALL_ACCESS, QueryValueEx # @UnresolvedImport +# @UnresolvedImport +from _winreg import OpenKey, SetValueEx, HKEY_LOCAL_MACHINE, REG_SZ, KEY_ALL_ACCESS, QueryValueEx import ctypes import os import logging @@ -30,7 +32,8 @@ # we don't have win32api, try ctypes def GetShortPathName(longname): buf = ctypes.create_unicode_buffer(512) - if ctypes.windll.kernel32.GetShortPathNameW(longname, buf, ctypes.sizeof(buf)): # @UndefinedVariable + # @UndefinedVariable + if ctypes.windll.kernel32.GetShortPathNameW(longname, buf, ctypes.sizeof(buf)): return buf.value else: # but don't panic if we can't do that either @@ -58,67 +61,43 @@ def kill(p): class WinRunner(RunnerBase): + def __init__(self, options, cmd_template, fuzzed_file, workingdir_base): - RunnerBase.__init__(self, options, cmd_template, fuzzed_file, workingdir_base) + RunnerBase.__init__( + self, options, cmd_template, fuzzed_file, workingdir_base) logger.debug('Initialize Runner') -# exceptions: -# - 0x80000001 # STATUS_GUARD_PAGE_VIOLATION -# - 0x80000002 # EXCEPTION_DATATYPE_MISALIGNMENT -# - 0x80000005 # STATUS_BUFFER_OVERFLOW -# - 0xC0000005 # STATUS_ACCESS_VIOLATION -# - 0xC0000009 # STATUS_BAD_INITIAL_STACK -# - 0xC000000A # STATUS_BAD_INITIAL_PC -# - 0xC000001D # STATUS_ILLEGAL_INSTRUCTION -# - 0xC0000025 # EXCEPTION_NONCONTINUABLE_EXCEPTION -# - 0xC0000026 # EXCEPTION_INVALID_DISPOSITION -# - 0xC000008C # EXCEPTION_ARRAY_BOUNDS_EXCEEDED -# - 0xC000008D # STATUS_FLOAT_DENORMAL_OPERAND -# - 0xC000008E # EXCEPTION_FLT_DIVIDE_BY_ZERO -# - 0xC000008F # EXCEPTION_FLOAT_INEXACT_RESULT -# - 0xC0000090 # EXCEPTION_FLT_INVALID_OPERATION -# - 0xC0000091 # EXCEPTION_FLT_OVERFLOW -# - 0xC0000092 # EXCEPTION_FLT_STACK_CHECK -# - 0xC0000093 # EXCEPTION_FLT_UNDERFLOW -# - 0xC0000094 # STATUS_INTEGER_DIVIDE_BY_ZERO -# - 0xC0000095 # EXCEPTION_INT_OVERFLOW -# - 0xC0000096 # STATUS_PRIVILEGED_INSTRUCTION -# - 0xC00000FD # STATUS_STACK_OVERFLOW -# - 0xC00002B4 # STATUS_FLOAT_MULTIPLE_FAULTS -# - 0xC00002B5 # STATUS_FLOAT_MULTIPLE_TRAPS -# - 0xC00002C5 # STATUS_DATATYPE_MISALIGNMENT_ERROR -# - 0xC00002C9 # STATUS_REG_NAT_CONSUMPTION - - self.exceptions = [0x80000001, - 0x80000002, - 0x80000005, - 0xC0000005, - 0xC0000009, - 0xC000000A, - 0xC000001D, - 0xC0000025, - 0xC0000026, - 0xC000008C, - 0xC000008D, - 0xC000008E, - 0xC000008F, - 0xC0000090, - 0xC0000091, - 0xC0000092, - 0xC0000093, - 0xC0000094, - 0xC0000095, - 0xC0000096, - 0xC00000FD, - 0xC00002B4, - 0xC00002B5, - 0xC00002C5, - 0xC00002C9, + self.exceptions = [0x80000001, # STATUS_GUARD_PAGE_VIOLATION + 0x80000002, # EXCEPTION_DATATYPE_MISALIGNMENT + 0x80000005, # STATUS_BUFFER_OVERFLOW + 0xC0000005, # STATUS_ACCESS_VIOLATION + 0xC0000009, # STATUS_BAD_INITIAL_STACK + 0xC000000A, # STATUS_BAD_INITIAL_PC + 0xC000001D, # STATUS_ILLEGAL_INSTRUCTION + 0xC0000025, # EXCEPTION_NONCONTINUABLE_EXCEPTION + 0xC0000026, # EXCEPTION_INVALID_DISPOSITION + 0xC000008C, # EXCEPTION_ARRAY_BOUNDS_EXCEEDED + 0xC000008D, # STATUS_FLOAT_DENORMAL_OPERAND + 0xC000008E, # EXCEPTION_FLT_DIVIDE_BY_ZERO + 0xC000008F, # EXCEPTION_FLOAT_INEXACT_RESULT + 0xC0000090, # EXCEPTION_FLT_INVALID_OPERATION + 0xC0000091, # EXCEPTION_FLT_OVERFLOW + 0xC0000092, # EXCEPTION_FLT_STACK_CHECK + 0xC0000093, # EXCEPTION_FLT_UNDERFLOW + 0xC0000094, # EXCEPTION_INT_OVERFLOW + 0xC0000095, # EXCEPTION_INT_OVERFLOW + 0xC0000096, # STATUS_PRIVILEGED_INSTRUCTION + 0xC00000FD, # STATUS_STACK_OVERFLOW + 0xC00002B4, # STATUS_FLOAT_MULTIPLE_FAULTS + 0xC00002B5, # STATUS_FLOAT_MULTIPLE_TRAPS + 0xC00002C5, # STATUS_DATATYPE_MISALIGNMENT_ERROR + 0xC00002C9, # STATUS_REG_NAT_CONSUMPTION ] self.watchcpu = options.get('watchcpu', False) - (self.cmd, self.cmdlist) = get_command_args_list(cmd_template, fuzzed_file) + (self.cmd, self.cmdlist) = get_command_args_list( + cmd_template, fuzzed_file) logger.debug('Command: %s', self.cmd) find_or_create_dir(self.workingdir) @@ -159,13 +138,15 @@ def __enter__(self): # assume hook dll is at ../hooks/winxp/Release/hook.dll relative to # the location of this module my_path = os.path.dirname(__file__) - relative_path_to_hook_dll = os.path.join(my_path, '..', "hooks", "winxp", "Release", "hook.dll") + relative_path_to_hook_dll = os.path.join( + my_path, '..', "hooks", "winxp", "Release", "hook.dll") rval = GetShortPathName(os.path.abspath(relative_path_to_hook_dll)) try: _set_reg_value(hive, branch, rname, rval) except OSError, e: - logger.error('Unable to set registry: %s\%s\%s=%s', hive, branch, rname, rval) + logger.error( + 'Unable to set registry: %s\%s\%s=%s', hive, branch, rname, rval) raise RunnerRegistryError(e) # register jit debugger (or lack thereof) @@ -175,7 +156,8 @@ def __enter__(self): python_path = sys.executable if not python_path: python_path = 'c:\python27\python.exe' - logger.warning('No path to python exec in sys.executable, using default of %s', python_path) + logger.warning( + 'No path to python exec in sys.executable, using default of %s', python_path) # Find our preferred debugger module dbg_path = dbg.__file__ # dbg_path = 'calc.exe' @@ -187,7 +169,8 @@ def __enter__(self): try: _set_reg_value(hive, branch, rname, rval) except OSError: - logger.error('Unable to set registry: %s\%s\%s=%s', hive, branch, rname, rval) + logger.error( + 'Unable to set registry: %s\%s\%s=%s', hive, branch, rname, rval) raise RunnerRegistryError(e) # enable auto debugger invocation @@ -199,7 +182,8 @@ def __enter__(self): try: _set_reg_value(hive, branch, rname, rval) except OSError: - logger.error('Unable to set registry: %s\%s\%s=%s', hive, branch, rname, rval) + logger.error( + 'Unable to set registry: %s\%s\%s=%s', hive, branch, rname, rval) raise RunnerRegistryError(e) # check cdb path @@ -219,7 +203,8 @@ def __exit__(self, etype, value, traceback): try: _set_reg_value(hive, branch, rname, rval) except OSError: - logger.warning('Unable to set registry: %s\%s\%s=%s', hive, branch, rname, rval) + logger.warning( + 'Unable to set registry: %s\%s\%s=%s', hive, branch, rname, rval) def _verify_architecture(self, expected_bits=None): ''' @@ -255,7 +240,8 @@ def _run(self): # set timeout(s) # run program if self.hideoutput: - p = Popen(self.cmdlist, cwd=targetdir, stdout=open(os.devnull), stderr=open(os.devnull)) + p = Popen(self.cmdlist, cwd=targetdir, stdout=open( + os.devnull), stderr=open(os.devnull)) else: p = Popen(self.cmdlist, cwd=targetdir) @@ -275,17 +261,21 @@ def _run(self): # TODO: Do something about it while p.poll() is None and not done and id: for proc in wmiInterface.Win32_PerfRawData_PerfProc_Process(IDProcess=id): - n1, d1 = long(proc.PercentProcessorTime), long(proc.Timestamp_Sys100NS) + n1, d1 = long(proc.PercentProcessorTime), long( + proc.Timestamp_Sys100NS) n0, d0 = process_info.get(id, (0, 0)) try: - percent_processor_time = (float(n1 - n0) / float(d1 - d0)) * 100.0 + percent_processor_time = ( + float(n1 - n0) / float(d1 - d0)) * 100.0 except ZeroDivisionError: percent_processor_time = 0.0 process_info[id] = (n1, d1) - logger.debug('Process %s CPU usage: %s', id, percent_processor_time) + logger.debug( + 'Process %s CPU usage: %s', id, percent_processor_time) if percent_processor_time < 0.0000000001: if started: - logger.debug('killing %s due to CPU inactivity', id) + logger.debug( + 'killing %s due to CPU inactivity', id) done = True kill(p) else: @@ -301,7 +291,8 @@ def _run(self): t.cancel() self.returncode = ctypes.c_uint(p.returncode).value - logger.debug('...Returncode: raw=%s cast=%s', p.returncode, self.returncode) + logger.debug( + '...Returncode: raw=%s cast=%s', p.returncode, self.returncode) logger.debug('...Exceptions: %s', self.exceptions) if self.returncode in self.exceptions: self.saw_crash = True diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index 72fea63..89236a4 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -18,7 +18,7 @@ campaign: # TARGET APPLICATION INVOCATION OPTIONS: # # program: -# Path to fuzzing target executable +# Full path to fuzzing target executable # # cmdline_template: # Used to specify the command-line invocation of the target @@ -28,6 +28,7 @@ campaign: # $SEEDFILE will be replaced at runtime with the appropriate # seed file name. # Use quotes if the target application has spaces in the path +# Shell redirection (>) is not allowed # ############################################################################## target: From 607068be5c2f97ff798bfd91169687df33c3d642 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 22 Apr 2016 14:51:05 -0400 Subject: [PATCH 0923/1169] Use same min_log.txt location as Windows. --- src/certfuzz/tools/linux/minimize.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index f46d443..5ff88c6 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -156,11 +156,13 @@ def main(): logger.info('Copying %s to %s', fuzzed_file.path, testcase.tempdir) filetools.copy_file(fuzzed_file.path, testcase.tempdir) + minlog = os.path.join(outdir, 'min_log.txt') + with Minimizer(cfg=cfg, testcase=testcase, crash_dst_dir=outdir, seedfile_as_target=min2seed, bitwise=options.bitwise, confidence=confidence, - logfile='./min_log.txt', + logfile=minlog, tempdir=testcase.tempdir, maxtime=options.timeout, preferx=options.prefer_x_target, From f2d224a772227ec3dd85cb8ec152d319cd97638f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 25 Apr 2016 13:59:03 -0400 Subject: [PATCH 0924/1169] zipdiff moved from "windows" to "common" --- src/windows/tools/zipdiff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/windows/tools/zipdiff.py b/src/windows/tools/zipdiff.py index 56e40f8..495b69e 100644 --- a/src/windows/tools/zipdiff.py +++ b/src/windows/tools/zipdiff.py @@ -6,13 +6,13 @@ import os import sys try: - from certfuzz.tools.windows.zipdiff import main + from certfuzz.tools.common.zipdiff import main except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) - from certfuzz.tools.windows.zipdiff import main + from certfuzz.tools.common.zipdiff import main if __name__ == '__main__': main() From ebb74a35bbdd30c178e31b404c9a1cd0633536b2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 25 Apr 2016 14:16:57 -0400 Subject: [PATCH 0925/1169] move zipdiff.py to common directory --- build/devmods/build_base.py | 20 +++++++++++------ build/distmods/build_base2.py | 24 ++++++++++++-------- src/{windows => }/tools/zipdiff.py | 36 +++++++++++++++--------------- 3 files changed, 46 insertions(+), 34 deletions(-) rename src/{windows => }/tools/zipdiff.py (96%) diff --git a/build/devmods/build_base.py b/build/devmods/build_base.py index 90e002b..6d4b95b 100644 --- a/build/devmods/build_base.py +++ b/build/devmods/build_base.py @@ -13,7 +13,7 @@ class Build(object): - _common_dirs = ['certfuzz', 'seedfiles'] + _common_dirs = ['certfuzz', 'seedfiles', 'tools'] _blacklist = ['.svn'] _name = None _platform = None @@ -30,12 +30,15 @@ def __init__(self, name=None, platform=None): self.platform = self._platform self.my_path = os.path.abspath(os.path.dirname(__file__)) - self.src_path = os.path.abspath(os.path.join(self.my_path, '../../src')) - self.dev_builds_path = os.path.abspath(os.path.join(self.src_path, '..', 'dev_builds')) + self.src_path = os.path.abspath( + os.path.join(self.my_path, '../../src')) + self.dev_builds_path = os.path.abspath( + os.path.join(self.src_path, '..', 'dev_builds')) target_shortname = '{}-{}'.format(self.name, self.platform) - self.target_path = os.path.abspath(os.path.join(self.dev_builds_path, target_shortname)) + self.target_path = os.path.abspath( + os.path.join(self.dev_builds_path, target_shortname)) self.platform_path = os.path.join(self.src_path, self.platform) self.license_md_path = os.path.join(self.src_path, '..', 'LICENSE.md') self.license_txt_path = os.path.join(self.target_path, 'COPYING.txt') @@ -74,10 +77,12 @@ def _convert_md_files(self): def _create_target_path(self): # create base build path if it doesn't already exist if not os.path.exists(self.target_path): - logger.info('Build dir does not exist, creating %s', self.target_path) + logger.info( + 'Build dir does not exist, creating %s', self.target_path) os.makedirs(self.target_path) else: - logger.info('Build dir %s already exists, proceeding', self.target_path) + logger.info( + 'Build dir %s already exists, proceeding', self.target_path) # base build path exists assert os.path.isdir(self.target_path) @@ -115,7 +120,8 @@ def _create_results_dir(self): logger.info('Result path does not exist, creating %s', result_path) os.makedirs(result_path) else: - logger.info('Result path %s already exists, proceeding', result_path) + logger.info( + 'Result path %s already exists, proceeding', result_path) def _clean_up(self, path, remove_blacklist=True): logger.debug("Cleaning up %s", path) diff --git a/build/distmods/build_base2.py b/build/distmods/build_base2.py index 11211cb..00091b4 100644 --- a/build/distmods/build_base2.py +++ b/build/distmods/build_base2.py @@ -32,7 +32,7 @@ def _zipdir(path, zip_): class Build(object): _blacklist = [] - _common_dirs = ['certfuzz', 'seedfiles'] + _common_dirs = ['certfuzz', 'seedfiles', 'tools'] _license_file = 'COPYING.txt' def __init__(self, platform=None, distpath=None, srcpath=None): @@ -44,7 +44,8 @@ def __init__(self, platform=None, distpath=None, srcpath=None): self.base_path = os.path.abspath(distpath) if srcpath == None: - self.src_path = os.path.abspath(os.path.join(self.base_path, '../../src')) + self.src_path = os.path.abspath( + os.path.join(self.base_path, '../../src')) else: self.src_path = os.path.abspath(srcpath) @@ -56,11 +57,13 @@ def __init__(self, platform=None, distpath=None, srcpath=None): self.zipfile = '{}.zip'.format(self._filename_pfx) self.target = os.path.join(self.base_path, self.zipfile) self.license_md_path = os.path.join(self.src_path, '..', 'LICENSE.md') - self.license_txt_path = os.path.join(self.platform_path, self._license_file) + self.license_txt_path = os.path.join( + self.platform_path, self._license_file) def __enter__(self): logger.debug('Entering Build context') - self.tmp_dir = tempfile.mkdtemp(prefix='bff_build_{}_'.format(self.platform)) + self.tmp_dir = tempfile.mkdtemp( + prefix='bff_build_{}_'.format(self.platform)) logger.debug('Temp dir is %s', self.tmp_dir) self.build_dir = os.path.join(self.tmp_dir, 'bff') os.mkdir(self.build_dir) @@ -138,7 +141,8 @@ def _move_to_target(self, tmpzip): logger.debug('moving {} to {}'.format(tmpzip, self.target)) shutil.move(tmpzip, self.target) _perm = 0644 - logger.debug('setting {:04o} permissions on {}'.format(_perm, self.target)) + logger.debug( + 'setting {:04o} permissions on {}'.format(_perm, self.target)) os.chmod(self.target, _perm) def _create_zip(self): @@ -155,11 +159,13 @@ def _copy_platform(self): if os.path.isdir(self.platform_path): platform_path = self.platform_path else: - logger.info('No platform-specific info found at %s', self.platform_path) + logger.info( + 'No platform-specific info found at %s', self.platform_path) platform_path = os.path.join(self.src_path, 'linux') logger.info('Defaulting to %s', platform_path) # Set license text path since we're overriding it above - self.license_txt_path = os.path.join(platform_path, self._license_file) + self.license_txt_path = os.path.join( + platform_path, self._license_file) logger.info('Converting markdown files') self._convert_md_files() @@ -182,7 +188,6 @@ def _copy_platform(self): else: logger.warning("Not sure what to do with %s", f_src) - def _copy_common_dirs(self): # copy other dirs for d in self._common_dirs: @@ -197,7 +202,8 @@ def _create_results_dir(self): logger.info('Result path does not exist, creating %s', result_path) os.makedirs(result_path) else: - logger.info('Result path %s already exists, proceeding', result_path) + logger.info( + 'Result path %s already exists, proceeding', result_path) def _clean_up(self, path, remove_blacklist=True): for f in os.listdir(path): diff --git a/src/windows/tools/zipdiff.py b/src/tools/zipdiff.py similarity index 96% rename from src/windows/tools/zipdiff.py rename to src/tools/zipdiff.py index 495b69e..ab583de 100644 --- a/src/windows/tools/zipdiff.py +++ b/src/tools/zipdiff.py @@ -1,18 +1,18 @@ -''' -Created on Jul 10, 2013 - -@organization: cert.org -''' -import os -import sys -try: - from certfuzz.tools.common.zipdiff import main -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.tools.common.zipdiff import main - -if __name__ == '__main__': - main() +''' +Created on Jul 10, 2013 + +@organization: cert.org +''' +import os +import sys +try: + from certfuzz.tools.common.zipdiff import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.common.zipdiff import main + +if __name__ == '__main__': + main() From 0bafe5f05c888d668d1c18d73c05682709df0a12 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 25 Apr 2016 14:24:32 -0400 Subject: [PATCH 0926/1169] Add shebang and use unix line delimiters for zipdiff.py --- src/tools/zipdiff.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools/zipdiff.py b/src/tools/zipdiff.py index ab583de..b9c8b7b 100644 --- a/src/tools/zipdiff.py +++ b/src/tools/zipdiff.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python ''' Created on Jul 10, 2013 From 892c716b235adfd81ec7ea63ed6f2404111da86c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 26 Apr 2016 10:52:05 -0400 Subject: [PATCH 0927/1169] Fix arguments to _safe_createzip. BFF-940 --- src/certfuzz/minimizer/win_minimizer.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/minimizer/win_minimizer.py b/src/certfuzz/minimizer/win_minimizer.py index 550e48e..a88b9e7 100755 --- a/src/certfuzz/minimizer/win_minimizer.py +++ b/src/certfuzz/minimizer/win_minimizer.py @@ -83,7 +83,7 @@ def _readzip(self, filepath): # probably unnecessary since it's the content that matters self.saved_arcinfo[i] = (len(unzippedbytes), len(data), - tempzip.getinfo(i).compress_type) + tempzip.getinfo(i).compress_type) unzippedbytes += data tempzip.close() return unzippedbytes @@ -106,7 +106,7 @@ def _writezip(self): filepath = self.tempfile logger.debug('Creating zip with mutated contents.') - tempzip = self._safe_createzip(filepath, 'w') + tempzip = self._safe_createzip(filepath) ''' reconstruct archived files, using the same compression scheme as @@ -118,9 +118,11 @@ def _writezip(self): # Python zipfile only supports compression types 0 and 8 compressiontype = info[2] else: - logger.warning('Compression type %s is not supported. Overriding', info[2]) + logger.warning( + 'Compression type %s is not supported. Overriding', info[2]) compressiontype = 8 - tempzip.writestr(name, str(filedata[info[0]:info[0] + info[1]]), compress_type=compressiontype) + tempzip.writestr( + name, str(filedata[info[0]:info[0] + info[1]]), compress_type=compressiontype) tempzip.close() def _write_file(self): From 72c4fdc74f60e9655d220ddbf36493a69efa2fa5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 27 Apr 2016 09:20:06 -0400 Subject: [PATCH 0928/1169] Loaded modules' text sections don't always begin at 0x0. BFF-930 --- .../drillresults/testcasebundle_linux.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py b/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py index 2e2b644..a88e5d4 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py @@ -16,18 +16,21 @@ RE_BT_ADDR = re.compile(r'(0x[0-9a-fA-F]+)\s+.+$') RE_CURRENT_INSTR = re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)') RE_FRAME_0 = re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+') -RE_MAPPED_FRAME = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)') -RE_VDSO = re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(\[vdso\])') +RE_MAPPED_FRAME = re.compile( + r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x[0-9a-fA-F]+)?\s+(/.+)') +RE_VDSO = re.compile( + r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(\[vdso\])') RE_RETURN_ADDR = re.compile(r'^#1\s.(0x[0-9a-fA-F]+)\s') + class LinuxTestCaseBundle(TestCaseBundle): really_exploitable = [ - 'SegFaultOnPc', - 'BranchAv', - 'StackCodeExection', - 'BadInstruction', - 'ReturnAv', - ] + 'SegFaultOnPc', + 'BranchAv', + 'StackCodeExection', + 'BadInstruction', + 'ReturnAv', + ] def _get_classification(self): self.classification = carve(self.reporttext, "Classification: ", "\n") @@ -64,7 +67,8 @@ def _look_for_loaded_module(self, instraddr, line): begin_address = int(n.group(1).replace('`', ''), 16) end_address = int(n.group(2).replace('`', ''), 16) module_name = n.group(4) - logger.debug('%x %x %s %x', begin_address, end_address, module_name, instraddr) + logger.debug( + '%x %x %s %x', begin_address, end_address, module_name, instraddr) if begin_address < instraddr < end_address: logger.debug('Matched: %x in %x %x %s', instraddr, begin_address, end_address, module_name) From e72b021d8a1ea860c4c0fb7e688e54a675d2078b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 28 Apr 2016 11:14:41 -0400 Subject: [PATCH 0929/1169] Touch watchdog file during minimization. BFF-941 --- src/certfuzz/minimizer/unix_minimizer.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/minimizer/unix_minimizer.py b/src/certfuzz/minimizer/unix_minimizer.py index 3ddd033..70eb994 100644 --- a/src/certfuzz/minimizer/unix_minimizer.py +++ b/src/certfuzz/minimizer/unix_minimizer.py @@ -20,24 +20,21 @@ class UnixMinimizer(MinimizerBase): def __enter__(self): # touch the watchdogfile try: - open(self.cfg.watchdogfile, 'w').close() + open(self.watchdogfile, 'w').close() except (OSError, IOError) as e: # it's okay if we can't, but we should note it self.logger.warning('Unable to touch watchdog file %s: %s', - self.cfg.watchdogfile, e) - except AttributeError: - # self.cfg doesn't have a watchdogfile - self.logger.info('Config has no watchdogfile attribute, skipping') + self.watchdogfile, e) return MinimizerBase.__enter__(self) def __exit__(self, etype, value, traceback): try: - os.remove(self.cfg.watchdogfile) + os.remove(self.watchdogfile) except (OSError, IOError) as e: # it's okay if we can't, but we should note it self.logger.warning('Unable to remove watchdog file %s: %s', - self.cfg.watchdogfile, e) + self.watchdogfile, e) except AttributeError: # self.cfg doesn't have a watchdogfile # We probably already logged this fact in __enter__ so we can From 903b38bff162dd7b038d576bf90f1444af5b236e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 4 May 2016 07:53:52 -0400 Subject: [PATCH 0930/1169] bump license copyright date --- LICENSE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 6f1500d..8fd941e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,7 +3,7 @@ subject to the following terms: # LICENSE # -Copyright © 2014 Carnegie Mellon University. All Rights Reserved. +Copyright © 2016 Carnegie Mellon University. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: 3. Products derived from this software may not include "Carnegie Mellon University," "SEI" and/or "Software Engineering Institute" in the name of such derived product, nor shall "Carnegie Mellon University," "SEI" and/or "Software Engineering Institute" be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact permission@sei.cmu.edu. # ACKNOWLEDGMENTS AND DISCLAIMERS: # -Copyright © 2014 Carnegie Mellon University +Copyright © 2016 Carnegie Mellon University This material is based upon work funded and supported by the Department of Homeland Security under Contract No. FA8721-05-C-0003 with Carnegie Mellon From e25116c2ba59a6f7421343b91beaf643bfc00c13 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 9 May 2016 09:33:06 -0400 Subject: [PATCH 0931/1169] Use PIN version compatible with Ubuntu 16.04. Use HTTPS as well. --- src/linux/batch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index 7a1edb2..ca3e7e8 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -49,7 +49,7 @@ scriptlocation=`echo "$(cd "$(dirname "$0")"; pwd)/"` echo Script location: $scriptlocation/bff.py platform=`uname -a` -PINURL=http://software.intel.com/sites/landingpage/pintool/downloads/pin-2.12-58423-gcc.4.4.7-linux.tar.gz +PINURL=https://software.intel.com/sites/landingpage/pintool/downloads/pin-3.0-76991-gcc-linux.tar.gz if [[ "$platform" =~ "Darwin Kernel Version 11" ]]; then mypython="/Library/Frameworks/Python.framework/Versions/2.7/bin/python" else From 428e121d27521e35aa117f853f2f019997d62506 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 9 May 2016 11:23:15 -0400 Subject: [PATCH 0932/1169] Fix version checking to support things like "7.11", which is indeed larger (newer) than "7.2" --- .../exploitable/exploitable.py | 489 +++++++++--------- 1 file changed, 254 insertions(+), 235 deletions(-) diff --git a/src/linux/CERT_triage_tools/exploitable/exploitable.py b/src/linux/CERT_triage_tools/exploitable/exploitable.py index 471a2a7..1378466 100644 --- a/src/linux/CERT_triage_tools/exploitable/exploitable.py +++ b/src/linux/CERT_triage_tools/exploitable/exploitable.py @@ -1,235 +1,254 @@ -### BEGIN LICENSE ### -### Use of the triage tools and related source code is subject to the terms -### of the license below. -### -### ------------------------------------------------------------------------ -### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. -### ------------------------------------------------------------------------ -### Redistribution and use in source and binary forms, with or without -### modification, are permitted provided that the following conditions are -### met: -### -### 1. Redistributions of source code must retain the above copyright -### notice, this list of conditions and the following acknowledgments -### and disclaimers. -### -### 2. Redistributions in binary form must reproduce the above copyright -### notice, this list of conditions and the following disclaimer in the -### documentation and/or other materials provided with the distribution. -### -### 3. The names "Department of Homeland Security," "Carnegie Mellon -### University," "CERT" and/or "Software Engineering Institute" shall -### not be used to endorse or promote products derived from this software -### without prior written permission. For written permission, please -### contact permission@sei.cmu.edu. -### -### 4. Products derived from this software may not be called "CERT" nor -### may "CERT" appear in their names without prior written permission of -### permission@sei.cmu.edu. -### -### 5. Redistributions of any form whatsoever must retain the following -### acknowledgment: -### -### "This product includes software developed by CERT with funding -### and support from the Department of Homeland Security under -### Contract No. FA 8721-05-C-0003." -### -### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND -### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER -### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING -### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE -### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, -### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND -### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, -### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND -### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. -### END LICENSE ### -### -### Portions Copyright 2013, BlackBerry Ltd. All Rights Reserved. -### - -# Jonathan Foote -# jmfoote@loyola.edu - -''' -Implements GDB entry point for GDB 'exploitable' command - -example usage: - (gdb) source exploitable.py - (gdb) exploitable - -''' -try: - import gdb -except ImportError as e: - raise ImportError("This script must be run in GDB: ", str(e)) - -import argparse -import os -import re -import sys -import warnings - -version = "1.32" - -# Code below contains a workaround for a bug in GDB's Python API: -# os.path.abspath returns an incorrect string when this script is sourced -# from a path containing "~" -abspath = os.path.abspath(__file__) -pos = abspath.find("/~/") -if pos != -1: - abspath = abspath[pos+1:] -abspath = os.path.expanduser(abspath) -sys.path.append(os.path.dirname(abspath)) - -import lib.classifier as classifier -import lib.arch as arch -from lib.tools import print_machine_string - -def check_version(): - ''' - Checks to see if current operating environment has been tested with - this command. If not, a warning is generated. - - If the current operation environment is not Linux, an error is generated. - ''' - - # check GDB ver - if gdb_ver() < "7.2": - warnings.warn("GDB v{} may not support required Python API".format(gdb_ver())) - -_re_gdb_version = re.compile(r"\d+\.\d+") -def gdb_ver(): - ''' - Gets the GDB version number as a string. - ''' - gdbstr = gdb.execute("show version", False, True).splitlines()[0] - version = _re_gdb_version.search(gdbstr) - if version is None: - warnings.warn("Error while parsing gdb version string: {}".format(gdbstr)) - return version.group() - -class NiceArgParserExit(RuntimeError): - pass - -class NiceArgParser(argparse.ArgumentParser): - def exit(self, *args): - raise NiceArgParserExit(*args) - -class ExploitableCommand(gdb.Command): - ''' - A GDB Command that determines how exploitable the current state of the - Inferior (the program being debugged) is. Either prints the result to - GDB's STDOUT or pickles the result to a file. - - This command is designed to be run just after the Inferior stops on - a signal, before any commands that might change the underlying state - of GDB have been issued. WARNING: This command may change the underlying - state of GDB (ex: changing the disassembler flavor). - - Type -h for options. - ''' - - _cmdstr = "exploitable" - _re_gdb_pc = re.compile(r"^=>.*$") - def __init__(self): - ''' - Specifies the command string that invokes this Command from the GDB - shell. See GDB Python API documentation for details - ''' - gdb.Command.__init__(self, self._cmdstr, gdb.COMMAND_OBSCURE) - - def print_disassembly(self): - ''' - Attempts to print some disassembled instructions from the function - containing $pc to GDB's STDOUT. If GDB is unable to print the - disassembled instructions, an error message will be printed. - - If GDB's version is less than 7.3, the entire disassembled function - will be printed (if possible). Otherwise only a subset of the - function will be printed. - - This behavior is due to a bug in the Python API of earlier versions - of GDB. In these versions, the results of the 'disassemble' command - are always printed directly to GDB's STDOUT rather than optionally - being suppressed and passed as a return value via the Python API. - ''' - if gdb_ver() < "7.3": - try: - gdb.execute("disas $pc", False, True) - except RuntimeError as e: - warnings.warn(str(e)) - return - - try: - disas = gdb.execute("disas $pc", False, True).splitlines() - except RuntimeError as e: - warnings.warn(str(e)) - return - - pos = 0 - for line in disas: - if self._re_gdb_pc.match(line): - break - pos += 1 - print("\n".join(disas[max(pos-5, 0):pos+5])) - - def invoke(self, argstr, from_tty): - ''' - Called when this Command is invoked from GDB. Prints classification of - Inferior to GDB's STDOUT. - - Note that sys.stdout is automatically redirected to GDB's STDOUT. - See GDB Python API documentation for details - ''' - check_version() - - op = NiceArgParser(prog=self._cmdstr, description=self.__doc__) - op.add_argument("-v", "--verbose", action="store_true", - help="print analysis info from the Inferior") - op.add_argument("-m", "--machine", action="store_true", - help="Print output in a machine parsable format") - op.add_argument("-p", "--pkl-file", type=argparse.FileType("wb"), - help="pickle exploitability classification object and store to PKL_FILE") - op.add_argument("-a", "--asan-log", type=argparse.FileType(), - help="Symbolize and analyze AddressSanitizer output (assumes " - "executable is loaded) (WARNING: untested).") - op.add_argument("-b", "--backtrace-limit", type=int, - help="Limit number of stack frames in backtrace to supplied value. " - "0 means no limit.", default=1000) - - try: - args = op.parse_args(gdb.string_to_argv(argstr)) - except NiceArgParserExit: - return - - import logging - try: - target = arch.getTarget(args.asan_log, args.backtrace_limit) - c = classifier.Classifier().getClassification(target) - except Exception as e: - logging.exception(e) - raise e - - if args.pkl_file: - import pickle as pickle - pickle.dump(c, args.pkl_file, 2) - return - - if args.verbose: - print("'exploitable' version {}".format(version)) - print(" ".join([str(i) for i in os.uname()])) - print("Signal si_signo: {} Signal si_addr: {}".format(target.si_signo(), target.si_addr())) - print("Nearby code:") - self.print_disassembly() - print("Stack trace:") - print(str(target.backtrace())) - print("Faulting frame: {}".format(target.faulting_frame())) - - if args.machine: - print_machine_string(c, target) - else: - gdb.write(str(c)) - gdb.flush() - -ExploitableCommand() +### BEGIN LICENSE ### +# Use of the triage tools and related source code is subject to the terms +# of the license below. +### +# ------------------------------------------------------------------------ +# Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved. +# ------------------------------------------------------------------------ +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +### +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following acknowledgments +# and disclaimers. +### +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +### +# 3. The names "Department of Homeland Security," "Carnegie Mellon +# University," "CERT" and/or "Software Engineering Institute" shall +# not be used to endorse or promote products derived from this software +# without prior written permission. For written permission, please +# contact permission@sei.cmu.edu. +### +# 4. Products derived from this software may not be called "CERT" nor +# may "CERT" appear in their names without prior written permission of +# permission@sei.cmu.edu. +### +# 5. Redistributions of any form whatsoever must retain the following +# acknowledgment: +### +# "This product includes software developed by CERT with funding +# and support from the Department of Homeland Security under +# Contract No. FA 8721-05-C-0003." +### +# THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER +# EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +# EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, +# CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND +# RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION, +# RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND +# COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS. +### END LICENSE ### +### +# Portions Copyright 2013, BlackBerry Ltd. All Rights Reserved. +### + +# Jonathan Foote +# jmfoote@loyola.edu + +''' +Implements GDB entry point for GDB 'exploitable' command + +example usage: + (gdb) source exploitable.py + (gdb) exploitable + +''' +try: + import gdb +except ImportError as e: + raise ImportError("This script must be run in GDB: ", str(e)) + +import argparse +import os +import re +import sys +import warnings + +version = "1.32" + +# Code below contains a workaround for a bug in GDB's Python API: +# os.path.abspath returns an incorrect string when this script is sourced +# from a path containing "~" +abspath = os.path.abspath(__file__) +pos = abspath.find("/~/") +if pos != -1: + abspath = abspath[pos + 1:] +abspath = os.path.expanduser(abspath) +sys.path.append(os.path.dirname(abspath)) + +import lib.classifier as classifier +import lib.arch as arch +from lib.tools import print_machine_string + + +def check_version(): + ''' + Checks to see if current operating environment has been tested with + this command. If not, a warning is generated. + + If the current operation environment is not Linux, an error is generated. + ''' + + unsupported_gdb = False + # check GDB ver + gdb_version = gdb_ver() + gdb_major = int(gdb_version.split(".")[0]) + gdb_minor = int(gdb_version.split(".")[1]) + if (gdb_major < 7): + unsupported_gdb = True + if (gdb_major == 7 and gdb_minor < 2): + unsupported_gdb = True + if unsupported_gdb: + warnings.warn( + "GDB v{} may not support required Python API".format(gdb_ver())) + +_re_gdb_version = re.compile(r"\d+\.\d+") + + +def gdb_ver(): + ''' + Gets the GDB version number as a string. + ''' + gdbstr = gdb.execute("show version", False, True).splitlines()[0] + version = _re_gdb_version.search(gdbstr) + if version is None: + warnings.warn( + "Error while parsing gdb version string: {}".format(gdbstr)) + return version.group() + + +class NiceArgParserExit(RuntimeError): + pass + + +class NiceArgParser(argparse.ArgumentParser): + + def exit(self, *args): + raise NiceArgParserExit(*args) + + +class ExploitableCommand(gdb.Command): + ''' + A GDB Command that determines how exploitable the current state of the + Inferior (the program being debugged) is. Either prints the result to + GDB's STDOUT or pickles the result to a file. + + This command is designed to be run just after the Inferior stops on + a signal, before any commands that might change the underlying state + of GDB have been issued. WARNING: This command may change the underlying + state of GDB (ex: changing the disassembler flavor). + + Type -h for options. + ''' + + _cmdstr = "exploitable" + _re_gdb_pc = re.compile(r"^=>.*$") + + def __init__(self): + ''' + Specifies the command string that invokes this Command from the GDB + shell. See GDB Python API documentation for details + ''' + gdb.Command.__init__(self, self._cmdstr, gdb.COMMAND_OBSCURE) + + def print_disassembly(self): + ''' + Attempts to print some disassembled instructions from the function + containing $pc to GDB's STDOUT. If GDB is unable to print the + disassembled instructions, an error message will be printed. + + If GDB's version is less than 7.3, the entire disassembled function + will be printed (if possible). Otherwise only a subset of the + function will be printed. + + This behavior is due to a bug in the Python API of earlier versions + of GDB. In these versions, the results of the 'disassemble' command + are always printed directly to GDB's STDOUT rather than optionally + being suppressed and passed as a return value via the Python API. + ''' + if gdb_ver() < "7.3": + try: + gdb.execute("disas $pc", False, True) + except RuntimeError as e: + warnings.warn(str(e)) + return + + try: + disas = gdb.execute("disas $pc", False, True).splitlines() + except RuntimeError as e: + warnings.warn(str(e)) + return + + pos = 0 + for line in disas: + if self._re_gdb_pc.match(line): + break + pos += 1 + print("\n".join(disas[max(pos - 5, 0):pos + 5])) + + def invoke(self, argstr, from_tty): + ''' + Called when this Command is invoked from GDB. Prints classification of + Inferior to GDB's STDOUT. + + Note that sys.stdout is automatically redirected to GDB's STDOUT. + See GDB Python API documentation for details + ''' + check_version() + + op = NiceArgParser(prog=self._cmdstr, description=self.__doc__) + op.add_argument("-v", "--verbose", action="store_true", + help="print analysis info from the Inferior") + op.add_argument("-m", "--machine", action="store_true", + help="Print output in a machine parsable format") + op.add_argument("-p", "--pkl-file", type=argparse.FileType("wb"), + help="pickle exploitability classification object and store to PKL_FILE") + op.add_argument("-a", "--asan-log", type=argparse.FileType(), + help="Symbolize and analyze AddressSanitizer output (assumes " + "executable is loaded) (WARNING: untested).") + op.add_argument("-b", "--backtrace-limit", type=int, + help="Limit number of stack frames in backtrace to supplied value. " + "0 means no limit.", default=1000) + + try: + args = op.parse_args(gdb.string_to_argv(argstr)) + except NiceArgParserExit: + return + + import logging + try: + target = arch.getTarget(args.asan_log, args.backtrace_limit) + c = classifier.Classifier().getClassification(target) + except Exception as e: + logging.exception(e) + raise e + + if args.pkl_file: + import pickle as pickle + pickle.dump(c, args.pkl_file, 2) + return + + if args.verbose: + print("'exploitable' version {}".format(version)) + print(" ".join([str(i) for i in os.uname()])) + print("Signal si_signo: {} Signal si_addr: {}".format( + target.si_signo(), target.si_addr())) + print("Nearby code:") + self.print_disassembly() + print("Stack trace:") + print(str(target.backtrace())) + print("Faulting frame: {}".format(target.faulting_frame())) + + if args.machine: + print_machine_string(c, target) + else: + gdb.write(str(c)) + gdb.flush() + +ExploitableCommand() From 9af48448596f89bc06201bbb6c543f04e4c7ad6d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 9 May 2016 12:40:11 -0400 Subject: [PATCH 0933/1169] Tweak logic of version checking. --- src/linux/CERT_triage_tools/exploitable/exploitable.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/linux/CERT_triage_tools/exploitable/exploitable.py b/src/linux/CERT_triage_tools/exploitable/exploitable.py index 1378466..c581ffa 100644 --- a/src/linux/CERT_triage_tools/exploitable/exploitable.py +++ b/src/linux/CERT_triage_tools/exploitable/exploitable.py @@ -102,8 +102,9 @@ def check_version(): gdb_minor = int(gdb_version.split(".")[1]) if (gdb_major < 7): unsupported_gdb = True - if (gdb_major == 7 and gdb_minor < 2): + elif (gdb_major == 7 and gdb_minor < 2): unsupported_gdb = True + if unsupported_gdb: warnings.warn( "GDB v{} may not support required Python API".format(gdb_ver())) From f96b9d33cbff0bac8b2ceba1b5c9a107b2f94688 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 24 May 2016 09:08:09 -0400 Subject: [PATCH 0934/1169] "testcases" -> "crashes" --- src/certfuzz/campaign/campaign_windows.py | 35 +++++++++++++++-------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 0ef5d30..539abda 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -30,7 +30,8 @@ class WindowsCampaign(CampaignBase): def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) - self.use_buttonclicker = self.config['campaign'].get('use_buttonclicker', False) + self.use_buttonclicker = self.config[ + 'campaign'].get('use_buttonclicker', False) self.runner_module_name = 'certfuzz.runners.winrun' self.debugger_module_name = 'certfuzz.debuggers.gdb' TWDF.disable() @@ -86,7 +87,8 @@ def _pre_enter(self): if not hook_incompatible: return - logger.debug('winrun is not compatible with Windows %s %s. Overriding.', winver, machine) + logger.debug( + 'winrun is not compatible with Windows %s %s. Overriding.', winver, machine) self.runner_module_name = 'certfuzz.runners.nullrun' def _post_enter(self): @@ -97,10 +99,12 @@ def _pre_exit(self): self._stop_buttonclicker() def _cache_app(self): - logger.debug('Caching application %s and determining if we need to watch the CPU...', self.program) + logger.debug( + 'Caching application %s and determining if we need to watch the CPU...', self.program) sf = self.seedfile_set.next_item() targetdir = os.path.dirname(self.program) - cmdargs = get_command_args_list(self.config['target']['cmdline_template'], infile=sf.path)[1] + cmdargs = get_command_args_list( + self.config['target']['cmdline_template'], infile=sf.path)[1] logger.info('Invoking %s' % cmdargs) # Use overriden Popen that uses a job object to make sure that @@ -128,10 +132,12 @@ def _cache_app(self): logger.debug('Disabling runner CPU monitoring for dynamic timeout') self.config['runner']['watchcpu'] = False if debugger_watchcpu == 'auto': - logger.debug('Disabling debugger CPU monitoring for dynamic timeout') + logger.debug( + 'Disabling debugger CPU monitoring for dynamic timeout') self.config['debugger']['watchcpu'] = False - logger.info('Please ensure that the target program has just executed successfully') + logger.info( + 'Please ensure that the target program has just executed successfully') time.sleep(10) def kill(self, p): @@ -149,18 +155,22 @@ def kill(self, p): if runner_watchcpu == 'auto': logger.debug('Enabling runner CPU monitoring for dynamic timeout') self.config['runner']['watchcpu'] = True - logger.debug('kill runner watchcpu: %s', self.config['runner']['watchcpu']) + logger.debug( + 'kill runner watchcpu: %s', self.config['runner']['watchcpu']) if debugger_watchcpu == 'auto': - logger.debug('Enabling debugger CPU monitoring for dynamic timeout') + logger.debug( + 'Enabling debugger CPU monitoring for dynamic timeout') self.config['debugger']['watchcpu'] = True - logger.debug('kill debugger watchcpu: %s', self.config['debugger']['watchcpu']) + logger.debug( + 'kill debugger watchcpu: %s', self.config['debugger']['watchcpu']) logger.debug('kill %s', p) p.kill() def _start_buttonclicker(self): if self.use_buttonclicker: rootpath = os.path.dirname(sys.argv[0]) - buttonclicker = os.path.join(rootpath, 'buttonclicker', 'buttonclicker.exe') + buttonclicker = os.path.join( + rootpath, 'buttonclicker', 'buttonclicker.exe') os.startfile(buttonclicker) # @UndefinedVariable def _stop_buttonclicker(self): @@ -188,11 +198,12 @@ def _do_iteration(self, seedfile, range_obj, seednum): except FuzzerExhaustedError: # Some fuzzers run out of things to do. They should # raise a FuzzerExhaustedError when that happens. - logger.info('Done with %s, removing from set', seedfile.basename) + logger.info( + 'Done with %s, removing from set', seedfile.basename) self.seedfile_set.remove_file(seedfile) if not seednum % self.status_interval: - logger.info('Iteration: %d testcases found: %d', self.current_seed, + logger.info('Iteration: %d crashes found: %d', self.current_seed, len(self.testcases_seen)) # FIXME # self.seedfile_set.update_csv() From 7015795b87c98823b5b2a73704c2c91b4c40b8a6 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 24 May 2016 09:09:40 -0400 Subject: [PATCH 0935/1169] More "really exploitable" categorizations. BFF-943 --- src/certfuzz/analyzers/drillresults/testcasebundle_linux.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py b/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py index a88e5d4..6645213 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py @@ -26,10 +26,13 @@ class LinuxTestCaseBundle(TestCaseBundle): really_exploitable = [ 'SegFaultOnPc', + 'SegFaultOnPcNearNull', 'BranchAv', + 'BranchAvNearNull', 'StackCodeExection', 'BadInstruction', 'ReturnAv', + 'BadInstruction', ] def _get_classification(self): From 15e301c812bf428b0069be4982f3abb46ab3bc59 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 24 May 2016 13:30:36 -0400 Subject: [PATCH 0936/1169] Remove FIXME logging. BFF-945 exists. --- src/certfuzz/campaign/campaign_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 539abda..ad9dbde 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -207,8 +207,8 @@ def _do_iteration(self, seedfile, range_obj, seednum): len(self.testcases_seen)) # FIXME # self.seedfile_set.update_csv() - logger.info('Seedfile Set Status:') - logger.info('FIXME') + # logger.info('Seedfile Set Status:') + # logger.info('FIXME') # for k, score, successes, tries, p in self.seedfile_set.status(): # logger.info('%s %0.6f %d %d %0.6f', k, score, successes, # tries, p) From 6722bae77517ab93df341c934109a6cab10f3199 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 25 May 2016 16:36:00 -0400 Subject: [PATCH 0937/1169] No heisenbugs w/o the winrun hook. BFF-946 --- src/certfuzz/iteration/iteration_windows.py | 45 ++++++++----- src/certfuzz/testcase/testcase_windows.py | 75 ++++++++++++--------- 2 files changed, 72 insertions(+), 48 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index a7b5f8f..004d4f8 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -59,7 +59,8 @@ def __init__(self, self.debug = debug # TODO: do we use keep_uniq_faddr at all? - self.keep_uniq_faddr = config['runoptions'].get('keep_unique_faddr', False) + self.keep_uniq_faddr = config['runoptions'].get( + 'keep_unique_faddr', False) self.cmd_template = cmd_template @@ -77,7 +78,8 @@ def __init__(self, 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize', False), } - # Windows testcase object needs a timeout, and we only pass debugger options + # Windows testcase object needs a timeout, and we only pass debugger + # options self.cfg['debugger']['runtimeout'] = self.cfg['runner']['runtimeout'] def __exit__(self, etype, value, traceback): @@ -111,25 +113,30 @@ def __exit__(self, etype, value, traceback): logger.warning('Failed to debug, Skipping seed %d', self.seednum) handled = True elif etype is RunnerRegistryError: - logger.warning('Runner cannot set registry entries. Consider null runner in config?') + logger.warning( + 'Runner cannot set registry entries. Consider null runner in config?') # this is fatal, pass it up handled = False elif etype is IOError: IOERROR_COUNT += 1 if IOERROR_COUNT > MAX_IOERRORS: # something is probably wrong, we should crash - logger.critical('Too many IOErrors (%d in a row): %s', IOERROR_COUNT + 1, value) + logger.critical( + 'Too many IOErrors (%d in a row): %s', IOERROR_COUNT + 1, value) else: # we can keep going for a bit - logger.error('Intercepted IOError, will try to continue: %s', value) + logger.error( + 'Intercepted IOError, will try to continue: %s', value) handled = True # log something different if we failed to handle an exception if etype and not handled: - logger.warning('WindowsIteration terminating abnormally due to %s: %s', etype.__name__, value) + logger.warning( + 'WindowsIteration terminating abnormally due to %s: %s', etype.__name__, value) if self.debug and etype and not handled: - # don't clean up if we're in debug mode and have an unhandled exception + # don't clean up if we're in debug mode and have an unhandled + # exception logger.debug('Skipping cleanup since we are in debug mode.') else: self._tidy() @@ -153,16 +160,20 @@ def _pre_run(self): def _construct_testcase(self): with WindowsTestcase(cmd_template=self.cmd_template, - seedfile=self.seedfile, - fuzzedfile=BasicFile(self.fuzzer.output_file_path), - cmdlist=get_command_args_list(self.cmd_template, self.fuzzer.output_file_path)[1], - fuzzer=self.fuzzer, - dbg_opts=self.cfg['debugger'], - workingdir_base=self.working_dir, - keep_faddr=self.cfg['runoptions']['keep_unique_faddr'], - program=self.cfg['target']['program'], - heisenbug_retries=self.retries, - copy_fuzzedfile=self.fuzzer.fuzzed_changes_input) as testcase: + seedfile=self.seedfile, + fuzzedfile=BasicFile( + self.fuzzer.output_file_path), + cmdlist=get_command_args_list( + self.cmd_template, self.fuzzer.output_file_path)[1], + fuzzer=self.fuzzer, + dbg_opts=self.cfg['debugger'], + workingdir_base=self.working_dir, + keep_faddr=self.cfg['runoptions'][ + 'keep_unique_faddr'], + program=self.cfg['target']['program'], + heisenbug_retries=self.retries, + copy_fuzzedfile=self.fuzzer.fuzzed_changes_input, + is_nullrunner=self.runner_cls.is_nullrunner) as testcase: # put it on the list for the analysis pipeline self.testcases.append(testcase) diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 83e3169..32dba53 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -22,20 +22,20 @@ def logerror(func, path, excinfo): logger.warning('%s failed to remove %s: %s', func, path, excinfo) short_exp = { - 'UNKNOWN': 'UNK', - 'PROBABLY_NOT_EXPLOITABLE': 'PNE', - 'PROBABLY_EXPLOITABLE': 'PEX', - 'EXPLOITABLE': 'EXP', - 'HEISENBUG': 'HSB', - } + 'UNKNOWN': 'UNK', + 'PROBABLY_NOT_EXPLOITABLE': 'PNE', + 'PROBABLY_EXPLOITABLE': 'PEX', + 'EXPLOITABLE': 'EXP', + 'HEISENBUG': 'HSB', +} exp_rank = { - 'EXPLOITABLE': 1, - 'PROBABLY_EXPLOITABLE': 2, - 'UNKNOWN': 3, - 'PROBABLY_NOT_EXPLOITABLE': 4, - 'HEISENBUG': 5, - } + 'EXPLOITABLE': 1, + 'PROBABLY_EXPLOITABLE': 2, + 'UNKNOWN': 3, + 'PROBABLY_NOT_EXPLOITABLE': 4, + 'HEISENBUG': 5, +} class WindowsTestcase(Testcase): @@ -45,7 +45,7 @@ class WindowsTestcase(Testcase): # TODO: do we still need fuzzer as an arg? def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, dbg_opts, workingdir_base, keep_faddr, program, - heisenbug_retries=4, copy_fuzzedfile=True): + heisenbug_retries=4, copy_fuzzedfile=True, is_nullrunner=False): dbg_timeout = dbg_opts['runtimeout'] @@ -66,7 +66,8 @@ def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, self.dbg_file = '' self.cmd_template = cmd_template try: - self.max_handled_exceptions = self.dbg_opts['max_handled_exceptions'] + self.max_handled_exceptions = self.dbg_opts[ + 'max_handled_exceptions'] except KeyError: self.max_handled_exceptions = 6 try: @@ -78,6 +79,7 @@ def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, self.parsed_outputs = [] self.max_depth = heisenbug_retries + self.is_nullrunner = is_nullrunner def _get_file_basename(self): ''' @@ -103,7 +105,8 @@ def update_crash_details(self): fname = self._get_file_basename() outfile_base = os.path.join(self.tempdir, fname) # Regenerate target commandline with new crasher file - self.cmdargs = get_command_args_list(self.cmd_template, outfile_base)[1][1:] + self.cmdargs = get_command_args_list( + self.cmd_template, outfile_base)[1][1:] self.debug() self._rename_fuzzed_file() self._rename_dbg_file() @@ -115,15 +118,16 @@ def debug_once(self): outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) with self._debugger_cls(program=self.program, - cmd_args=self.cmdargs, - outfile_base=outfile_base, - timeout=self.debugger_timeout, - exception_depth=self.exception_depth, - workingdir=self.tempdir, - watchcpu=self.watchcpu) as debugger: + cmd_args=self.cmdargs, + outfile_base=outfile_base, + timeout=self.debugger_timeout, + exception_depth=self.exception_depth, + workingdir=self.tempdir, + watchcpu=self.watchcpu) as debugger: self.parsed_outputs.append(debugger.go()) - self.reached_secondchance = self.parsed_outputs[self.exception_depth].secondchance + self.reached_secondchance = self.parsed_outputs[ + self.exception_depth].secondchance if self.reached_secondchance and self.exception_depth > 0: # No need to process second-chance exception @@ -139,8 +143,10 @@ def debug_once(self): elif exp_rank[current_exception_exp] < exp_rank[self.exp]: self.exp = current_exception_exp - current_exception_hash = self.parsed_outputs[self.exception_depth].crash_hash - current_exception_faddr = self.parsed_outputs[self.exception_depth].faddr + current_exception_hash = self.parsed_outputs[ + self.exception_depth].crash_hash + current_exception_faddr = self.parsed_outputs[ + self.exception_depth].faddr if current_exception_hash: # We have a hash for the current exception if self.exception_depth == 0: @@ -148,7 +154,8 @@ def debug_once(self): self.crash_hash = current_exception_hash elif self.crash_hash: # Append to the exception hash chain - self.crash_hash = self.crash_hash + '_' + current_exception_hash + self.crash_hash = self.crash_hash + \ + '_' + current_exception_hash if self.keep_uniq_faddr and current_exception_faddr: self.crash_hash += '.' + current_exception_faddr @@ -186,13 +193,15 @@ def debug(self, tries_remaining=None): break # get the signature now that we've got all of the exceptions self.get_signature() - else: + elif not self.is_nullrunner: + # With the Windows XP hook, there's the concept of a heisenbug. # if we are still a heisenbug after debugging # we might need to try again # or give up if we've tried enough already if tries_remaining: # keep diving - logger.debug("possible heisenbug (%d tries left)", tries_remaining) + logger.debug( + "possible heisenbug (%d tries left)", tries_remaining) self.debug(tries_remaining - 1) else: # we're at the bottom @@ -207,7 +216,8 @@ def _set_heisenbug_properties(self): # for whatever reason we couldn't get the real content, # and since we're just generating a string here # any string will do - logger.warning('Unable to get md5 of %s, using random string for heisenbug signature: %s', self.fuzzedfile.path, e) + logger.warning( + 'Unable to get md5 of %s, using random string for heisenbug signature: %s', self.fuzzedfile.path, e) fuzzed_content = random_str(64) self.is_heisenbug = True self.signature = hashlib.md5(fuzzed_content).hexdigest() @@ -228,9 +238,11 @@ def _rename_fuzzed_file(self): return logger.debug('Attempting to rename %s', self.fuzzedfile.path) - new_basename = '%s-%s%s' % (self.fuzzedfile.root, self.faddr, self.fuzzedfile.ext) + new_basename = '%s-%s%s' % (self.fuzzedfile.root, + self.faddr, self.fuzzedfile.ext) new_fuzzed_file = os.path.join(self.fuzzedfile.dirname, new_basename) - logger.debug('renaming %s -> %s', self.fuzzedfile.path, new_fuzzed_file) + logger.debug( + 'renaming %s -> %s', self.fuzzedfile.path, new_fuzzed_file) # best_effort move returns a tuple of booleans indicating (copied, deleted) # we only care about copied @@ -263,7 +275,8 @@ def _rename_dbg_file(self): parts.append(faddr_str) if exp_str: parts.append(exp_str) - new_basename = '-'.join(parts) + ext + '.e%s' % exception_num + dbgext + new_basename = '-'.join(parts) + ext + \ + '.e%s' % exception_num + dbgext new_dbg_file = os.path.join(path, new_basename) From 91230fd3385789fd2bec4d42a6e922b01a453b03 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 26 May 2016 15:16:27 -0400 Subject: [PATCH 0938/1169] If we've reaped tmp, we better touch the watchdog file. BFF-947 --- src/certfuzz/file_handlers/tmp_reaper.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certfuzz/file_handlers/tmp_reaper.py b/src/certfuzz/file_handlers/tmp_reaper.py index 83c475e..7b01305 100644 --- a/src/certfuzz/file_handlers/tmp_reaper.py +++ b/src/certfuzz/file_handlers/tmp_reaper.py @@ -10,6 +10,7 @@ import tempfile from certfuzz.fuzztools.filetools import delete_contents_of +from certfuzz.file_handlers.watchdog_file import touch_watchdog_file logger = logging.getLogger(__name__) @@ -19,6 +20,7 @@ class TmpReaper(object): ''' classdocs ''' + def __init__(self): ''' Constructor @@ -66,3 +68,7 @@ def clean_tmp_unix(self, extras=[]): # we don't mind these exceptions as they're usually indicative # of a file that got deleted before we could do the same continue + # We've just cleaned tmp, which is the default watchdog file location + # If BFF dies before the watchdog file is recreated, UbuFuzz won't + # notice + touch_watchdog_file() From 1f09eed2a7f300a35f792601045af092a3c56641 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 13 Jun 2016 14:26:42 -0400 Subject: [PATCH 0939/1169] Next OSX version: Sierra --- src/certfuzz/debuggers/crashwrangler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index c7ecaa6..660b639 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -32,6 +32,8 @@ cwapp = 'exc_handler_yosemite' elif re.match('Darwin-15', myplatform): cwapp = 'exc_handler_elcapitan' +elif re.match('Darwin-16', myplatform): + cwapp = 'exc_handler_sierra' else: cwapp = 'exc_handler' From 541c8221c57144b70faef65a0eaeb795413135b2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 15 Jun 2016 13:52:46 -0400 Subject: [PATCH 0940/1169] It's OK if process disappears by the time you get to killpg. --- src/certfuzz/fuzztools/subprocess_helper.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 4b5a65f..18a19f9 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -29,7 +29,8 @@ def on_linux(): import ctypes if on_linux(): - #gdb can cause SIGTTOU to get sent to python. We don't want python to stop. + # gdb can cause SIGTTOU to get sent to python. We don't want python to + # stop. signal.signal(signal.SIGTTOU, signal.SIG_IGN) @@ -72,9 +73,11 @@ def run_with_timer(args, timeout, progname, use_shell=False, **options): try: if _seeoutput: # os.setsid sets process group - p = subprocess.Popen(args, env=env, shell=use_shell, preexec_fn=os.setsid) + p = subprocess.Popen( + args, env=env, shell=use_shell, preexec_fn=os.setsid) else: - p = subprocess.Popen(args, stdout=output, stderr=errors, env=env, shell=use_shell, preexec_fn=os.setsid) + p = subprocess.Popen( + args, stdout=output, stderr=errors, env=env, shell=use_shell, preexec_fn=os.setsid) except: print "Failed to run [%s]" % ' '.join(args) sys.exit(-1) @@ -96,7 +99,7 @@ def run_with_timer(args, timeout, progname, use_shell=False, **options): return p -def _kill(p, returncode, progname): #@UnusedVariable +def _kill(p, returncode, progname): # @UnusedVariable if (on_windows()): """_kill function for Win32""" kernel32 = ctypes.windll.kernel32 @@ -105,7 +108,11 @@ def _kill(p, returncode, progname): #@UnusedVariable kernel32.CloseHandle(handle) else: # Kill process group - ret = os.killpg(os.getpgid(p.pid), signal.SIGKILL) + try: + ret = os.killpg(os.getpgid(p.pid), signal.SIGKILL) + except OSError: + # Process could be dead by now + pass if progname: killall(progname, signal.SIGKILL) return (0 != ret) From b297230cb6544306ee479f4f4fb6a385ebac00e4 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 15 Jun 2016 13:59:11 -0400 Subject: [PATCH 0941/1169] Set ret if the killpg fails. --- src/certfuzz/fuzztools/subprocess_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 18a19f9..8beb2f5 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -112,7 +112,7 @@ def _kill(p, returncode, progname): # @UnusedVariable ret = os.killpg(os.getpgid(p.pid), signal.SIGKILL) except OSError: # Process could be dead by now - pass + ret = 1 if progname: killall(progname, signal.SIGKILL) return (0 != ret) From 000f5e346cdd92ec78fcfced010f4e9cdce7eb6e Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 16 Jun 2016 14:02:46 -0400 Subject: [PATCH 0942/1169] Fix quickstats.sh count on OSX --- src/linux/quickstats.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/linux/quickstats.sh b/src/linux/quickstats.sh index 5b6c642..7be8006 100755 --- a/src/linux/quickstats.sh +++ b/src/linux/quickstats.sh @@ -4,8 +4,7 @@ platform=`uname -a` echo "Exploitability Summary for campaign thus far:" if [[ "$platform" =~ "Darwin" ]]; then exploitable=`find -L ~/results -name '*.cw' | xargs grep is_exploitable=y | awk -Fcrashers/ '{print $2}' | awk -F/ '{print $1}' | sort | uniq | wc -l` - total=`find -L ~/results -name '*.cw' | wc -l` - let total-=1 + total=`find -L ~/results -name '*.cw' | grep -v gmalloc | wc -l` not_exploitable=$(expr $total - $exploitable) echo $exploitable Exploitable echo $not_exploitable Unknown From 5a25c41644c0cede74513f9172ca90a101732af8 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 16 Jun 2016 14:46:22 -0400 Subject: [PATCH 0943/1169] hide killall stderr on OSX --- src/certfuzz/fuzztools/subprocess_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index 8beb2f5..deea8f2 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -126,7 +126,7 @@ def killall(processname, killsignal): ''' assert (processname != ''), "Cannot kill a blank process name" if (on_osx()): - os.system('killall -%d %s' % (killsignal, processname)) + os.system('killall -%d %s 2> /dev/null' % (killsignal, processname)) else: for folder in os.listdir("/proc"): filename = os.path.join("/proc", folder, "cmdline") From 07aa880af04110de12e24076419c15b2ba0fd0e4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 6 Jun 2016 15:34:11 -0400 Subject: [PATCH 0944/1169] pep8 cleanup --- src/certfuzz/campaign/campaign_linux.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 1bbdb5d..67f65e8 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -36,7 +36,8 @@ def check_program_file_type(string, program): Runs the system "file" command on self.program @return: True if appears in the output. ''' - file_loc = subprocess.Popen("which %s" % program, stdout=subprocess.PIPE, shell=True).stdout.read().strip() + file_loc = subprocess.Popen( + "which %s" % program, stdout=subprocess.PIPE, shell=True).stdout.read().strip() # maybe it's not on the path, but it still exists if not file_loc: if os.path.exists(program): @@ -47,12 +48,14 @@ def check_program_file_type(string, program): return False # get the 'file' results - ftype = subprocess.Popen("file -b -L %s" % file_loc, stdout=subprocess.PIPE, shell=True).stdout.read() + ftype = subprocess.Popen( + "file -b -L %s" % file_loc, stdout=subprocess.PIPE, shell=True).stdout.read() if string in ftype: return True else: return False + class LinuxCampaign(CampaignBase): ''' Extends CampaignBase to add linux-specific features. @@ -81,7 +84,8 @@ def _full_path_original(self, seedfile): seedfile) # def _get_command_list(self, seedfile): -# return [re.sub(SEEDFILE_REPLACE_STRING, seedfile, item) for item in self.cmd_list] +# return [re.sub(SEEDFILE_REPLACE_STRING, seedfile, item) for item in +# self.cmd_list] def _pre_enter(self): # give up if prog is a script @@ -115,7 +119,8 @@ def _cache_app(self): # Run the program once to cache it into memory fullpathorig = self._full_path_original(sf.path) - cmdargs = get_command_args_list(self.config['target']['cmdline_template'], infile=fullpathorig)[1] + cmdargs = get_command_args_list( + self.config['target']['cmdline_template'], infile=fullpathorig)[1] logger.info('Invoking %s' % cmdargs) subp.run_with_timer(cmdargs, self.config['runner']['runtimeout'] * 8, @@ -125,7 +130,8 @@ def _cache_app(self): ) # Give target time to die - logger.info('Please ensure that the target program has just executed successfully') + logger.info( + 'Please ensure that the target program has just executed successfully') time.sleep(10) def _setup_watchdog(self): @@ -224,5 +230,6 @@ def _do_iteration(self, seedfile, range_obj, seednum): except FuzzerExhaustedError: # Some fuzzers run out of things to do. They should # raise a FuzzerExhaustedError when that happens. - logger.info('Done with %s, removing from set', seedfile.basename) + logger.info( + 'Done with %s, removing from set', seedfile.basename) self.seedfile_set.remove_file(seedfile) From f1d8647fb27125703eaf314f2dfd15ec98ee8475 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 6 Jun 2016 15:35:42 -0400 Subject: [PATCH 0945/1169] refactor watchdog init --- src/certfuzz/campaign/campaign_linux.py | 50 +++++---------------- src/certfuzz/file_handlers/watchdog_file.py | 44 +++++++++++++++++- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 67f65e8..fb0fc3e 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -21,7 +21,6 @@ from certfuzz.iteration.iteration_linux import LinuxIteration from certfuzz.fuzztools.command_line_templating import get_command_args_list from certfuzz.fuzzers.errors import FuzzerExhaustedError -from certfuzz.fuzztools.filetools import read_text_file logger = logging.getLogger(__name__) @@ -65,15 +64,6 @@ def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) self.runner_module_name = 'certfuzz.runners.zzufrun' self.debugger_module_name = 'certfuzz.debuggers.gdb' - self.watchdogfile = '/tmp/bff_watchdog' - self._use_watchdog = self._check_watchdog_compat() - - def _remove_watchdog_file(self): - try: - os.remove(self.watchdogfile) - except OSError: - # No watchdog file to remove - pass def _full_path_original(self, seedfile): # yes, two seedfile mentions are intended - adh @@ -101,7 +91,7 @@ def _post_enter(self): self._cache_app() def _pre_exit(self): - self._remove_watchdog_file() + TWDF.remove_wdf() def _set_unbuffered_stdout(self): ''' @@ -135,36 +125,18 @@ def _cache_app(self): time.sleep(10) def _setup_watchdog(self): + # short circuit if we're not using the watchdog + if not TWDF.use_watchdog: + logger.debug('skipping watchdog setup') + return + logger.debug('setup watchdog') # setup our watchdog file toucher - wdf = self.watchdogfile - TWDF.wdf = wdf - if self._use_watchdog: - TWDF.enable() - touch_watchdog_file() - - # set up the watchdog timeout within the VM and restart the daemon - with WatchDog(wdf, self.config['runoptions']['watchdogtimeout']) as watchdog: - watchdog() - else: - TWDF.disable() - - def _check_hostname(self): - hostname = 'System' - try: - hostname = read_text_file('/etc/hostname').rstrip() - except: - logger.debug('Error determining hostname') - return hostname - - def _check_watchdog_compat(self): - hostname = self._check_hostname().lower() - if 'ubufuzz' in hostname: - logger.debug('%s is watchdog compatible' % hostname) - return True - else: - logger.debug('%s is not watchdog compatible' % hostname) - return False + touch_watchdog_file() + + # set up the watchdog timeout within the VM and restart the daemon + with WatchDog(TWDF.wdf, self.config['runoptions']['watchdogtimeout']) as watchdog: + watchdog() def _setup_environment(self): os.environ['KDE_DEBUG'] = '1' diff --git a/src/certfuzz/file_handlers/watchdog_file.py b/src/certfuzz/file_handlers/watchdog_file.py index 0eb3c81..70e899d 100644 --- a/src/certfuzz/file_handlers/watchdog_file.py +++ b/src/certfuzz/file_handlers/watchdog_file.py @@ -4,14 +4,24 @@ @author: adh ''' import os +import logging +from certfuzz.fuzztools.filetools import read_text_file + +logger = logging.getLogger(__name__) + +_wdf = '/tmp/bff_watchdog' class Twdf(object): def __init__(self): - self.func = None - self.wdf = None + # disable watchdog by default + self.func = self._noop + + self.wdf = _wdf self.remote_d = None + self.use_watchdog = False + self._enable_iff_compat() def enable(self): self.func = self._twdf @@ -25,6 +35,36 @@ def _noop(self, *_args, **_kwargs): def _twdf(self): open(self.wdf, 'w').close() + def remove_wdf(self): + try: + os.remove(self.wdf) + except OSError: + # No watchdog file to remove + pass + + def _enable_iff_compat(self): + ''' + enables watchdog if we're running on ubufuzz + ''' + hostname = self._check_hostname() + if 'ubufuzz' in hostname: + logger.debug('%s is watchdog compatible' % hostname) + self.use_watchdog = True + self.enable() + + logger.debug('%s is not watchdog compatible' % hostname) + self.disable() + + def _check_hostname(self): + hostname = 'System' + try: + hostname = read_text_file('/etc/hostname').rstrip() + except: + logger.debug('Error determining hostname') + + return hostname.lower() + + TWDF = Twdf() From d8a09674d25ad19df8f4fb9e6de245ddb3efb8c3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 09:39:09 -0500 Subject: [PATCH 0946/1169] more iteration merging BFF-759 Squashed commits: [d9a81e8] refactor cmd_template handling, pep8 cleanup (+6 squashed commits) Squashed commits: [035a1a1] add comment, pep8 cleanup [ce3d6bc] move shared pipeline options to base class [e693329] replace quiet flag in linux iteration with property in parent class [5c505fa] pep8 cleanup [c1628e6] small refactor of iteration exit [fa5b948] move some exception handling stuff into base class --- src/certfuzz/campaign/campaign_windows.py | 1 - src/certfuzz/fuzztools/ppid_observer.py | 4 +- src/certfuzz/iteration/iteration_base3.py | 66 +++++++++++++-- src/certfuzz/iteration/iteration_linux.py | 45 ++++------ src/certfuzz/iteration/iteration_windows.py | 92 ++++----------------- 5 files changed, 98 insertions(+), 110 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index ad9dbde..5fc8910 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -190,7 +190,6 @@ def _do_iteration(self, seedfile, range_obj, seednum): config=self.config, fuzzer_cls=self.fuzzer_cls, runner_cls=self.runner_cls, - cmd_template=self.cmd_template, debug=self.debug, ) as iteration: try: diff --git a/src/certfuzz/fuzztools/ppid_observer.py b/src/certfuzz/fuzztools/ppid_observer.py index a518528..b131a34 100644 --- a/src/certfuzz/fuzztools/ppid_observer.py +++ b/src/certfuzz/fuzztools/ppid_observer.py @@ -6,6 +6,7 @@ import logging import os +# Added as fix for BFF-434 logger = logging.getLogger(__name__) @@ -18,5 +19,6 @@ def check_ppid(): current_ppid = os.getppid() if current_ppid != PPID: - logger.warning('Parent process ID changed from %d to %d', PPID, current_ppid) + logger.warning( + 'Parent process ID changed from %d to %d', PPID, current_ppid) PPID = current_ppid diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base3.py index c9597fc..0219135 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base3.py @@ -7,9 +7,17 @@ import tempfile import abc from certfuzz.fuzztools.filetools import rm_rf +from certfuzz.fuzzers.errors import FuzzerExhaustedError, \ + FuzzerInputMatchesOutputError, FuzzerError +from certfuzz.minimizer.errors import MinimizerError +from certfuzz.debuggers.output_parsers.errors import DebuggerFileError +from certfuzz.runners.errors import RunnerRegistryError logger = logging.getLogger(__name__) +IOERROR_COUNT = 0 +MAX_IOERRORS = 5 + class IterationBase3(object): __metaclass__ = abc.ABCMeta @@ -40,7 +48,8 @@ def __init__(self, self.r = None - self.pipeline_options = {} + self.pipeline_options = {'minimizable': self.fuzzer_cls.is_minimizable and self.cfg[ + 'runoptions'].get('minimize', False), } if uniq_func is None: self.uniq_func = lambda _tc_id: True @@ -60,7 +69,6 @@ def __init__(self, self._fuzz_opts = self.cfg['fuzzer'] self._runner_options = self.cfg['runner'] - @abc.abstractproperty def tcpipeline_cls(self): ''' @@ -90,6 +98,46 @@ def __exit__(self, etype, value, traceback): else: self.record_failure() + global IOERROR_COUNT + + # Reset error count every time we do not have an error + if not etype: + IOERROR_COUNT = 0 + + # check for exceptions we want to handle + if etype is FuzzerExhaustedError: + # let Fuzzer Exhaustion filter up to the campaign level + handled = False + elif etype is FuzzerInputMatchesOutputError: + # Non-fuzzing happens sometimes, just log and move on + logger.debug('Skipping seed %d, fuzzed == input', self.seednum) + handled = True + elif etype is FuzzerError: + logger.warning('Failed to fuzz, Skipping seed %d.', self.seednum) + handled = True + elif etype is MinimizerError: + logger.warning('Failed to minimize %d, Continuing.', self.seednum) + handled = True + elif etype is DebuggerFileError: + logger.warning('Failed to debug, Skipping seed %d', self.seednum) + handled = True + elif etype is RunnerRegistryError: + logger.warning( + 'Runner cannot set registry entries. Consider null runner in config?') + # this is fatal, pass it up + handled = False + elif etype is IOError: + IOERROR_COUNT += 1 + if IOERROR_COUNT > MAX_IOERRORS: + # something is probably wrong, we should crash + logger.critical( + 'Too many IOErrors (%d in a row): %s', IOERROR_COUNT + 1, value) + else: + # we can keep going for a bit + logger.error( + 'Intercepted IOError, will try to continue: %s', value) + handled = True + if self.debug and etype: # leave it behind if we're in debug mode # and there's a problem @@ -101,8 +149,13 @@ def __exit__(self, etype, value, traceback): return handled + @property + def quiet_flag(self): + return self._iteration_counter < 2 + def _pre_fuzz(self): - self.fuzzer = self.fuzzer_cls(self.seedfile, self.working_dir, self.seednum, self._fuzz_opts) + self.fuzzer = self.fuzzer_cls( + self.seedfile, self.working_dir, self.seednum, self._fuzz_opts) def _fuzz(self): with self.fuzzer: @@ -118,7 +171,10 @@ def _post_fuzz(self): def _pre_run(self): fuzzed_file = self.fuzzer.output_file_path workingdir_base = self.working_dir - self.runner = self.runner_cls(self._runner_options, self._runner_cmd_template, fuzzed_file, workingdir_base) + self._runner_options['hideoutput'] = self.quiet_flag + self.cmd_template = self.cfg['target']['cmdline_template'] + self.runner = self.runner_cls( + self._runner_options, self.cmd_template, fuzzed_file, workingdir_base) def _run(self): with self.runner: @@ -129,7 +185,7 @@ def _post_run(self): def construct_testcase(self): ''' - If the runner saw a crash, construct a test case + If the runner saw a crash, construct a test case and append it to the list of testcases to be analyzed further. ''' if not self.runner.saw_crash: diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 80911fa..5eb0d84 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -44,41 +44,30 @@ def __init__(self, runner_cls=runner_cls, ) - self.quiet_flag = self._iteration_counter < 2 - self.testcase_base_dir = os.path.join(self.outdir, 'crashers') - self.pipeline_options = {'use_valgrind': self.cfg['analyzer']['use_valgrind'], - 'use_pin_calltrace': self.cfg['analyzer']['use_pin_calltrace'], - 'uniq_log': os.path.join(self.cfg['directories']['results_dir'], 'uniquelog.txt'), - 'local_dir': fixup_path(self.cfg['directories']['working_dir']), - 'minimizertimeout': self.cfg['runoptions']['minimizer_timeout'], - 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions']['minimize'], - } + self.pipeline_options.update({'use_valgrind': self.cfg['analyzer']['use_valgrind'], + 'use_pin_calltrace': self.cfg['analyzer']['use_pin_calltrace'], + 'uniq_log': os.path.join(self.cfg['directories']['results_dir'], 'uniquelog.txt'), + 'local_dir': fixup_path(self.cfg['directories']['working_dir']), + 'minimizertimeout': self.cfg['runoptions']['minimizer_timeout'], + }) def __enter__(self): - IterationBase3.__enter__(self) check_ppid() - return self.go - - def _pre_run(self): - - if self.quiet_flag: - self._runner_options['hideoutput'] = True - self._runner_cmd_template = self.cfg['target']['cmdline_template'] - - IterationBase3._pre_run(self) + return IterationBase3.__enter__(self) def _construct_testcase(self): with LinuxTestcase(cfg=self.cfg, - seedfile=self.seedfile, - fuzzedfile=BasicFile(self.fuzzer.output_file_path), - program=self.cfg['target']['program'], - debugger_timeout=self.cfg['runner']['runtimeout'], - backtrace_lines=self.cfg['debugger']['backtracelevels'], - crashers_dir=self.testcase_base_dir, - workdir_base=self.working_dir, - seednum=self.seednum, - range=self.r) as testcase: + seedfile=self.seedfile, + fuzzedfile=BasicFile(self.fuzzer.output_file_path), + program=self.cfg['target']['program'], + debugger_timeout=self.cfg['runner']['runtimeout'], + backtrace_lines=self.cfg[ + 'debugger']['backtracelevels'], + crashers_dir=self.testcase_base_dir, + workdir_base=self.working_dir, + seednum=self.seednum, + range=self.r) as testcase: # put it on the list for the analysis pipeline self.testcases.append(testcase) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 004d4f8..386c08d 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -8,16 +8,10 @@ import os from certfuzz.testcase.testcase_windows import WindowsTestcase -from certfuzz.debuggers.output_parsers.errors import DebuggerFileError from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.file_handlers.tmp_reaper import TmpReaper -from certfuzz.fuzzers.errors import FuzzerError -from certfuzz.fuzzers.errors import FuzzerExhaustedError -from certfuzz.fuzzers.errors import FuzzerInputMatchesOutputError -from certfuzz.minimizer.errors import MinimizerError from certfuzz.fuzztools.filetools import delete_files_or_dirs from certfuzz.iteration.iteration_base3 import IterationBase3 -from certfuzz.runners.errors import RunnerRegistryError from certfuzz.tc_pipeline.tc_pipeline_windows import WindowsTestCasePipeline from certfuzz.fuzztools.command_line_templating import get_command_args_list @@ -25,9 +19,6 @@ # from certfuzz.iteration.iteration_base import IterationBase2 logger = logging.getLogger(__name__) -IOERROR_COUNT = 0 -MAX_IOERRORS = 5 - class WindowsIteration(IterationBase3): tcpipeline_cls = WindowsTestCasePipeline @@ -42,7 +33,6 @@ def __init__(self, config=None, fuzzer_cls=None, runner_cls=None, - cmd_template=None, debug=False, ): IterationBase3.__init__(self, @@ -62,8 +52,6 @@ def __init__(self, self.keep_uniq_faddr = config['runoptions'].get( 'keep_unique_faddr', False) - self.cmd_template = cmd_template - if self.runner_cls.is_nullrunner: # null runner_cls case self.retries = 0 @@ -71,12 +59,11 @@ def __init__(self, # runner_cls is not null self.retries = 4 - self.pipeline_options = {'keep_duplicates': self.cfg['runoptions'].get('keep_duplicates', False), - 'keep_heisenbugs': self.cfg['campaign'].get('keep_heisenbugs', False), - 'cmd_template': self.cmd_template, - 'null_runner': self.runner_cls.is_nullrunner, - 'minimizable': self.fuzzer_cls.is_minimizable and self.cfg['runoptions'].get('minimize', False), - } + self.pipeline_options.update({'keep_duplicates': self.cfg['runoptions'].get('keep_duplicates', False), + 'keep_heisenbugs': self.cfg['campaign'].get('keep_heisenbugs', False), + 'cmd_template': self.cfg['target']['cmdline_template'], + 'null_runner': self.runner_cls.is_nullrunner, + }) # Windows testcase object needs a timeout, and we only pass debugger # options @@ -89,58 +76,19 @@ def __exit__(self, etype, value, traceback): logger.warning('Caught WindowsError in iteration exit: %s', e) handled = True - global IOERROR_COUNT - - # Reset error count every time we do not have an error - if not etype: - IOERROR_COUNT = 0 - - # check for exceptions we want to handle - if etype is FuzzerExhaustedError: - # let Fuzzer Exhaustion filter up to the campaign level - handled = False - elif etype is FuzzerInputMatchesOutputError: - # Non-fuzzing happens sometimes, just log and move on - logger.debug('Skipping seed %d, fuzzed == input', self.seednum) - handled = True - elif etype is FuzzerError: - logger.warning('Failed to fuzz, Skipping seed %d.', self.seednum) - handled = True - elif etype is MinimizerError: - logger.warning('Failed to minimize %d, Continuing.', self.seednum) - handled = True - elif etype is DebuggerFileError: - logger.warning('Failed to debug, Skipping seed %d', self.seednum) - handled = True - elif etype is RunnerRegistryError: - logger.warning( - 'Runner cannot set registry entries. Consider null runner in config?') - # this is fatal, pass it up - handled = False - elif etype is IOError: - IOERROR_COUNT += 1 - if IOERROR_COUNT > MAX_IOERRORS: - # something is probably wrong, we should crash - logger.critical( - 'Too many IOErrors (%d in a row): %s', IOERROR_COUNT + 1, value) - else: - # we can keep going for a bit - logger.error( - 'Intercepted IOError, will try to continue: %s', value) - handled = True - - # log something different if we failed to handle an exception if etype and not handled: logger.warning( - 'WindowsIteration terminating abnormally due to %s: %s', etype.__name__, value) - - if self.debug and etype and not handled: - # don't clean up if we're in debug mode and have an unhandled - # exception - logger.debug('Skipping cleanup since we are in debug mode.') - else: - self._tidy() - + 'WindowsIteration terminating abnormally due to %s: %s', + etype.__name__, + value + ) + if self.debug: + # don't clean up if we're in debug mode and have an unhandled + # exception + logger.debug('Skipping cleanup since we are in debug mode.') + return handled + + self._tidy() return handled def _tidy(self): @@ -153,11 +101,6 @@ def _tidy(self): # wipe them out, all of them TmpReaper().clean_tmp() - def _pre_run(self): - self._runner_cmd_template = self.cmd_template - - IterationBase3._pre_run(self) - def _construct_testcase(self): with WindowsTestcase(cmd_template=self.cmd_template, seedfile=self.seedfile, @@ -172,8 +115,7 @@ def _construct_testcase(self): 'keep_unique_faddr'], program=self.cfg['target']['program'], heisenbug_retries=self.retries, - copy_fuzzedfile=self.fuzzer.fuzzed_changes_input, - is_nullrunner=self.runner_cls.is_nullrunner) as testcase: + copy_fuzzedfile=self.fuzzer.fuzzed_changes_input) as testcase: # put it on the list for the analysis pipeline self.testcases.append(testcase) From 414a1170c415dc0431ec19a50cddf30bb3a0b7fa Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 20 Jun 2016 10:41:30 -0400 Subject: [PATCH 0947/1169] fix unit test --- .../iteration/test_iteration_windows.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/test_certfuzz/iteration/test_iteration_windows.py b/src/test_certfuzz/iteration/test_iteration_windows.py index 8a16997..7735a3b 100644 --- a/src/test_certfuzz/iteration/test_iteration_windows.py +++ b/src/test_certfuzz/iteration/test_iteration_windows.py @@ -15,23 +15,23 @@ class Test(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp(prefix='test_iteration_windows_') - self.workdirbase = tempfile.mkdtemp(prefix='workdirbase_', dir=self.tmpdir) + self.workdirbase = tempfile.mkdtemp( + prefix='workdirbase_', dir=self.tmpdir) self.outdir = tempfile.mkdtemp(prefix='outdir_', dir=self.tmpdir) - - _cfg=MockCfg() - - kwargs={'seedfile': MockSeedfile(), - 'seednum':0, - 'workdirbase': self.workdirbase, - 'outdir':self.outdir, - 'sf_set':'a', - 'uniq_func':None, - 'config':_cfg, - 'fuzzer_cls':MockFuzzer, - 'runner_cls':MockRunner, - 'cmd_template':'a', - 'debug':False, - } + + _cfg = MockCfg() + + kwargs = {'seedfile': MockSeedfile(), + 'seednum': 0, + 'workdirbase': self.workdirbase, + 'outdir': self.outdir, + 'sf_set': 'a', + 'uniq_func': None, + 'config': _cfg, + 'fuzzer_cls': MockFuzzer, + 'runner_cls': MockRunner, + 'debug': False, + } self.iteration = WindowsIteration(**kwargs) From d2cadeb1dcef9a6e9b64129b3725a02c1d7e18f9 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 21 Jun 2016 13:34:02 -0400 Subject: [PATCH 0948/1169] pep8 cleanup --- src/certfuzz/debuggers/msec.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 7ce6e3a..007d7a5 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -30,7 +30,8 @@ class MsecDebugger(DebuggerBase): _ext = 'msec' def __init__(self, program, cmd_args, outfile_base, timeout, watchcpu, exception_depth=0, **options): - DebuggerBase.__init__(self, program, cmd_args, outfile_base, timeout, **options) + DebuggerBase.__init__( + self, program, cmd_args, outfile_base, timeout, **options) self.exception_depth = exception_depth self.watchcpu = watchcpu if watchcpu: @@ -82,7 +83,6 @@ def _get_cmdline(self, outfile): logger.debug('dbg_args: %s', l) return args - def _find_debug_target(self, exename, trycount=5): pid = None attempts = 0 @@ -117,7 +117,7 @@ def run_with_timer(self): args = self._get_cmdline(self.outfile) p = Popen(args, stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'), - universal_newlines=True) + universal_newlines=True) child_pid = self._find_debug_target(exename, trycount=5) if child_pid is None and self.watchcpu == True: @@ -133,21 +133,27 @@ def run_with_timer(self): # TODO: Do something about it while p.poll() is None and not done and child_pid: for proc in self.wmiInterface.Win32_PerfRawData_PerfProc_Process(IDProcess=child_pid): - n1, d1 = long(proc.PercentProcessorTime), long(proc.Timestamp_Sys100NS) + n1, d1 = long(proc.PercentProcessorTime), long( + proc.Timestamp_Sys100NS) n0, d0 = process_info.get(child_pid, (0, 0)) try: - percent_processor_time = (float(n1 - n0) / float(d1 - d0)) * 100.0 + percent_processor_time = ( + float(n1 - n0) / float(d1 - d0)) * 100.0 except ZeroDivisionError: percent_processor_time = 0.0 process_info[child_pid] = (n1, d1) - logger.debug('Process %s CPU usage: %s', child_pid, percent_processor_time) + logger.debug( + 'Process %s CPU usage: %s', child_pid, percent_processor_time) if percent_processor_time < 0.0000000001: if started: - logger.debug('killing %s due to CPU inactivity', child_pid) + logger.debug( + 'killing %s due to CPU inactivity', child_pid) done = True self.kill(child_pid, 99) - # Look once to see if the child process is there still - child_pid = self._find_debug_target(exename, trycount=1) + # Look once to see if the child process is there + # still + child_pid = self._find_debug_target( + exename, trycount=1) if child_pid is not None: # cdb launched the target app, but the child wasn't killed # This indicates that the target experienced an exception (crash) @@ -165,9 +171,11 @@ def run_with_timer(self): def go(self): """run cdb and process output""" - # For exceptions beyond the first one, put the handled exception number in the name + # For exceptions beyond the first one, put the handled exception number + # in the name if self.exception_depth > 0: - self.outfile = os.path.splitext(self.outfile)[0] + '.e' + str(self.exception_depth) + os.path.splitext(self.outfile)[1] + self.outfile = os.path.splitext(self.outfile)[ + 0] + '.e' + str(self.exception_depth) + os.path.splitext(self.outfile)[1] self.run_with_timer() if not os.path.exists(self.outfile): # touch it if it doesn't exist From d104bf2012f5d6f4d522ec2af4185fea34f0cc83 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 21 Jun 2016 13:59:38 -0400 Subject: [PATCH 0949/1169] Don't attempt to kill the process that cdb spawned. If that child process spawns a child of its own, that can leave zombies. BFF-949 --- src/certfuzz/debuggers/msec.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 007d7a5..27ea4b8 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -37,6 +37,7 @@ def __init__(self, program, cmd_args, outfile_base, timeout, watchcpu, exception if watchcpu: self.wmiInterface = wmi.WMI() self.t = None + self.savedpid = None def kill(self, pid, returncode): """kill function for Win32""" @@ -88,7 +89,7 @@ def _find_debug_target(self, exename, trycount=5): attempts = 0 foundpid = False - if self.watchcpu == True: + if self.watchcpu is True: while attempts < trycount and not foundpid: for process in self.wmiInterface.Win32_Process(name=exename): @@ -108,7 +109,6 @@ def _find_debug_target(self, exename, trycount=5): def run_with_timer(self): # TODO: replace this with subp.run_with_timer() - targetdir = os.path.dirname(self.program) exename = os.path.basename(self.program) process_info = {} child_pid = None @@ -118,17 +118,18 @@ def run_with_timer(self): args = self._get_cmdline(self.outfile) p = Popen(args, stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'), universal_newlines=True) + self.savedpid = p.pid child_pid = self._find_debug_target(exename, trycount=5) - if child_pid is None and self.watchcpu == True: + if child_pid is None and self.watchcpu is True: logger.debug('Bailing on debugger iteration') - self.kill(p.pid, 99) + self.kill(self.savedpid, 99) return # create a timer that calls kill() when it expires - self.t = Timer(self.timeout, self.kill, args=[p.pid, 99]) + self.t = Timer(self.timeout, self.kill, args=[self.savedpid, 99]) self.t.start() - if self.watchcpu == True: + if self.watchcpu is True: # This is a race. In some cases, a GUI app could be done before we can even measure it # TODO: Do something about it while p.poll() is None and not done and child_pid: @@ -147,18 +148,9 @@ def run_with_timer(self): if percent_processor_time < 0.0000000001: if started: logger.debug( - 'killing %s due to CPU inactivity', child_pid) + 'killing cdb session for %s due to CPU inactivity', child_pid) done = True - self.kill(child_pid, 99) - # Look once to see if the child process is there - # still - child_pid = self._find_debug_target( - exename, trycount=1) - if child_pid is not None: - # cdb launched the target app, but the child wasn't killed - # This indicates that the target experienced an exception (crash) - # We will wait for cdb to do its thing - p.wait() + self.kill(self.savedpid, 99) else: # Detected CPU usage. Now look for it to drop near zero started = True From 0119862f625fc9763af66ea0efcaaaac631a9f5c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 21 Jun 2016 15:29:25 -0400 Subject: [PATCH 0950/1169] Windows config file cleanup --- src/windows/configs/examples/bff.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 40a0ebb..f02d9e4 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -180,12 +180,11 @@ fuzzer: # Hide stdout of target application # # runtimeout: -# Number of seconds to allow target application to execute when run with -# the hook (winrun) +# Number of seconds to allow target application to execute # # watchcpu: -# Kill target process when its CPU usage drops towards zero when run with -# the hook (winrun). (Auto, True, False) +# Kill target process when its CPU usage drops towards zero +# (Auto, True, False) # ############################################################################## runner: From 799e585383410146ef24618036e2dd84dfecb4fe Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 21 Jun 2016 15:43:13 -0400 Subject: [PATCH 0951/1169] Explain hook a little better in windows yaml file --- src/windows/configs/examples/bff.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index f02d9e4..93073b6 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -10,7 +10,8 @@ # used for identifying campaign, placement of results # # keep_heisenbugs: -# Keep crashing testcases detected by hook, but not when run via the debugger. +# Keep crashing testcases detected by the Windows XP hook, but not when run +# via the debugger. This option is ignored on non-XP platforms. # # use_buttonclicker: # Spawn program to click buttons From af9ff74c60972330dc99d21eddd10a310fea0db0 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 21 Jun 2016 16:33:04 -0400 Subject: [PATCH 0952/1169] Set bytemap of minimization even if you didn't get to exhaustive (e.g. string minimization). BFF-948 --- src/certfuzz/minimizer/minimizer_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index cf716b6..8fce7a8 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -812,6 +812,8 @@ def go(self): self.fuzzed_content = self.newfuzzed self.bytemap.remove(offset) + # We're done minimizing. Set the bytemap (kept bytes) + self._set_bytemap() self.logger.info( 'We were looking for [%s] ...', self._crash_hashes_string()) for (md5, count) in self.crash_sigs_found.items(): From 689955aa89391ef378518691521df8ddc1674467 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 14:21:41 -0400 Subject: [PATCH 0953/1169] eliminate redundant "is True" from if-then logic --- src/certfuzz/debuggers/msec.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 27ea4b8..46af9e6 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -89,7 +89,7 @@ def _find_debug_target(self, exename, trycount=5): attempts = 0 foundpid = False - if self.watchcpu is True: + if self.watchcpu: while attempts < trycount and not foundpid: for process in self.wmiInterface.Win32_Process(name=exename): @@ -121,7 +121,7 @@ def run_with_timer(self): self.savedpid = p.pid child_pid = self._find_debug_target(exename, trycount=5) - if child_pid is None and self.watchcpu is True: + if child_pid is None and self.watchcpu: logger.debug('Bailing on debugger iteration') self.kill(self.savedpid, 99) return @@ -129,7 +129,7 @@ def run_with_timer(self): # create a timer that calls kill() when it expires self.t = Timer(self.timeout, self.kill, args=[self.savedpid, 99]) self.t.start() - if self.watchcpu is True: + if self.watchcpu: # This is a race. In some cases, a GUI app could be done before we can even measure it # TODO: Do something about it while p.poll() is None and not done and child_pid: From d4566f999062a73368452bd01a306492d118b4a1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 11:39:38 -0500 Subject: [PATCH 0954/1169] use TestcaseError instead of CrashError --- src/certfuzz/testcase/errors.py | 5 --- src/certfuzz/testcase/testcase_base2.py | 2 +- src/certfuzz/testcase/testcase_linux.py | 54 +++++++++++-------------- 3 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/certfuzz/testcase/errors.py b/src/certfuzz/testcase/errors.py index e85f0d7..78511f6 100644 --- a/src/certfuzz/testcase/errors.py +++ b/src/certfuzz/testcase/errors.py @@ -8,8 +8,3 @@ class TestCaseError(CERTFuzzError): pass - - -class CrashError(TestCaseError): - pass - diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py index a31a21d..2dc6576 100644 --- a/src/certfuzz/testcase/testcase_base2.py +++ b/src/certfuzz/testcase/testcase_base2.py @@ -7,10 +7,10 @@ import os import tempfile -from certfuzz.testcase.errors import CrashError from certfuzz.testcase.testcase_base import TestCaseBase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools import filetools +from certfuzz.testcase.errors import TestCaseError logger = logging.getLogger(__name__) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 9897f2a..203b36c 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -6,9 +6,10 @@ import logging import os -from certfuzz.testcase.testcase_base2 import Testcase, CrashError +from certfuzz.testcase.testcase_base2 import Testcase from certfuzz.fuzztools import hostinfo, filetools from certfuzz.fuzztools.command_line_templating import get_command_args_list +from certfuzz.testcase.errors import TestCaseError try: from certfuzz.analyzers import pin_calltrace @@ -47,13 +48,11 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, self.crash_base_dir = crashers_dir self.seednum = seednum self.range = range - self.exclude_unmapped_frames = cfg[ - 'analyzer']['exclude_unmapped_frames'] + self.exclude_unmapped_frames = cfg['analyzer']['exclude_unmapped_frames'] self.set_debugger_template('bt_only') self.keep_uniq_faddr = keep_faddr self.cmdargs = None - self.workdir_base = workdir_base self.is_crash = False self.signature = None self.faddr = None @@ -66,21 +65,18 @@ def __exit__(self, etype, value, traceback): def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): - dbg_template_name = '%s_%s_template.txt' % ( - self._debugger_cls._key, option) - self.debugger_template = os.path.join( - 'certfuzz/debuggers/templates', dbg_template_name) + dbg_template_name = '%s_%s_template.txt' % (self._debugger_cls._key, option) + self.debugger_template = os.path.join('certfuzz/debuggers/templates', dbg_template_name) logger.debug('Debugger template set to %s', self.debugger_template) if not os.path.exists(self.debugger_template): - raise CrashError( - 'Debugger template does not exist at %s' % self.debugger_template) + raise TestCaseError('Debugger template does not exist at %s' % self.debugger_template) def update_crash_details(self): Testcase.update_crash_details(self) cmdlist = get_command_args_list(self.cfg['target']['cmdline_template'], - infile=self.fuzzedfile.path, - posix=True)[1] + infile=self.fuzzedfile.path, + posix=True)[1] self.cmdargs = cmdlist[1:] self.is_crash = self.confirm_crash() @@ -106,25 +102,25 @@ def get_debug_output(self, outfile_base): logger.debug('Debugger template: %s outfile_base: %s', self.debugger_template, outfile_base) debugger_obj = self._debugger_cls(self.program, - self.cmdargs, - outfile_base, - self.debugger_timeout, - template=self.debugger_template, - exclude_unmapped_frames=self.exclude_unmapped_frames, - keep_uniq_faddr=self.keep_uniq_faddr - ) + self.cmdargs, + outfile_base, + self.debugger_timeout, + template=self.debugger_template, + exclude_unmapped_frames=self.exclude_unmapped_frames, + keep_uniq_faddr=self.keep_uniq_faddr + ) self.dbg = debugger_obj.go() self.dbg_file = self.dbg.file + def confirm_crash(self): # get debugger output self.get_debug_output(self.fuzzedfile.path) if not self.dbg: - raise CrashError('Debug object not found') + raise TestCaseError('Debug object not found') - logger.debug('is_crash: %s is_assert_fail: %s', - self.dbg.is_crash, self.dbg.is_assert_fail) + logger.debug('is_crash: %s is_assert_fail: %s', self.dbg.is_crash, self.dbg.is_assert_fail) if self.cfg['analyzer']['savefailedasserts']: return self.dbg.is_crash else: @@ -141,32 +137,29 @@ def get_signature(self): @raise CrasherHasNoSignatureError: if it's a valid crash, but we don't get a signature ''' if not self.signature: - self.signature = self.dbg.get_testcase_signature( - self.backtrace_lines) + self.signature = self.dbg.get_testcase_signature(self.backtrace_lines) if self.signature: logger.debug("Testcase signature is %s", self.signature) else: - raise CrashError('Testcase has no signature.') + raise TestCaseError('Testcase has no signature.') if self.dbg.total_stack_corruption: # total_stack_corruption. Use pin calltrace to get a backtrace analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self) try: analyzer_instance.go() except AnalyzerEmptyOutputError: - logger.warning( - 'Unexpected empty output from pin. Cannot determine call trace.') + logger.warning('Unexpected empty output from pin. Cannot determine call trace.') return self.signature calltrace = Calltracefile(analyzer_instance.outfile) - pinsignature = calltrace.get_testcase_signature( - self.backtrace_lines * 10) + pinsignature = calltrace.get_testcase_signature(self.backtrace_lines * 10) if pinsignature: self.signature = pinsignature return self.signature def _verify_crash_base_dir(self): if not self.crash_base_dir: - raise CrashError('crash_base_dir not set') + raise TestCaseError('crash_base_dir not set') if not os.path.exists(self.crash_base_dir): filetools.make_directories(self.crash_base_dir) @@ -177,3 +170,4 @@ def get_result_dir(self): self.result_dir = os.path.join(self.crash_base_dir, self.signature) return self.result_dir + From 1d830f6128ad6d30ac4c7a0b4b3674e80ed43f7f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 11:40:15 -0500 Subject: [PATCH 0955/1169] eliminate testcasebase in favor of testcasebase2 --- src/certfuzz/testcase/testcase_base2.py | 91 ++++++++++++++++--- src/certfuzz/testcase/testcase_linux.py | 1 - src/certfuzz/testcase/testcase_windows.py | 1 - .../testcase/test_testcase_base.py | 22 ----- 4 files changed, 79 insertions(+), 36 deletions(-) delete mode 100644 src/test_certfuzz/testcase/test_testcase_base.py diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py index 2dc6576..2b6757e 100644 --- a/src/certfuzz/testcase/testcase_base2.py +++ b/src/certfuzz/testcase/testcase_base2.py @@ -7,22 +7,41 @@ import os import tempfile -from certfuzz.testcase.testcase_base import TestCaseBase from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.fuzztools import filetools +from certfuzz.fuzztools import filetools, hamming +from certfuzz.fuzztools.filetools import check_zip_file, mkdir_p from certfuzz.testcase.errors import TestCaseError +from pprint import pformat logger = logging.getLogger(__name__) -class Testcase(TestCaseBase): - tmpdir_pfx = 'crash-' +class Testcase(object): + _tmp_sfx = '' + _tmp_pfx = 'BFF_testcase_' _debugger_cls = None + def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): logger.debug('Inititalize Testcase') - TestCaseBase.__init__(self, seedfile, fuzzedfile) + + self.seedfile = seedfile + self.fuzzedfile = fuzzedfile + self.workdir_base = None + + # Exploitability is UNKNOWN unless proven otherwise + self.exp = 'UNKNOWN' + + self.hd_bits = None + self.hd_bytes = None + self.signature = None + self.working_dir = None + self.is_zipfile = False + + # this will get overridden by calls to get_logger + self.logger = logger + self.debugger_timeout = dbg_timeout @@ -46,19 +65,17 @@ def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): self.total_stack_corruption = False self.pc_in_function = False - def _create_workdir_base(self): - # make sure the workdir_base exists - if not os.path.exists(self.workdir_base): - filetools.make_directories([self.workdir_base]) - def __enter__(self): - self._create_workdir_base() + mkdir_p(self.workdir_base) self.update_crash_details() return self def __exit__(self, etype, value, traceback): pass + def __repr__(self): + return pformat(self.__dict__) + def _get_output_dir(self, *args): raise NotImplementedError @@ -128,6 +145,56 @@ def set_debugger_template(self, option='bt_only'): raise NotImplementedError def update_crash_details(self): - self.tempdir = tempfile.mkdtemp(prefix=self.tmpdir_pfx, dir=self.workdir_base) + self.tempdir = tempfile.mkdtemp(prefix=self._tmp_pfx, suffix=self._tmp_sfx, dir=self.workdir_base) self.copy_files_to_temp() + # raise NotImplementedError + def calculate_hamming_distances(self): + # If the fuzzed file is a valid zip, then we're fuzzing zip contents, not the container + self.is_zipfile = check_zip_file(self.fuzzedfile.path) + try: + if self.is_zipfile: + self.hd_bits = hamming.bitwise_zip_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + self.hd_bytes = hamming.bytewise_zip_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + else: + self.hd_bits = hamming.bitwise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + self.hd_bytes = hamming.bytewise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + except KeyError: + # one of the files wasn't defined + logger.warning('Cannot find either sf_path or minimized file to calculate Hamming Distances') + + self.logger.info("bitwise_hd=%d", self.hd_bits) + self.logger.info("bytewise_hd=%d", self.hd_bytes) + + + def calculate_hamming_distances_a(self): + with open(self.fuzzedfile.path, 'rb') as fd: + fuzzed = fd.read() + + a_string = 'x' * len(fuzzed) + + self.hd_bits = hamming.bitwise_hd(a_string, fuzzed) + self.logger.info("bitwise_hd=%d", self.hd_bits) + + self.hd_bytes = hamming.bytewise_hd(a_string, fuzzed) + self.logger.info("bytewise_hd=%d", self.hd_bytes) + + def get_logger(self): + ''' + sets self.logger to a logger specific to this crash + ''' + self.logger = logging.getLogger(self.signature) + if len(self.logger.handlers) == 0: + if not os.path.exists(self.result_dir): + logger.error('Result path not found: %s', self.result_dir) + raise TestCaseError('Result path not found: {}'.format(self.result_dir)) + logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) + logfile = '%s.log' % self.signature + logger.debug('logfile=%s', logfile) + logpath = os.path.join(self.result_dir, logfile) + logger.debug('logpath=%s', logpath) + hdlr = logging.FileHandler(logpath) + self.logger.addHandler(hdlr) + + return self.logger + diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 203b36c..20bd337 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -32,7 +32,6 @@ class LinuxTestcase(Testcase): ''' classdocs ''' - tmpdir_pfx = 'bff-crash-' _debugger_cls = debugger_cls def __init__(self, cfg, seedfile, fuzzedfile, program, diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 32dba53..c3769b1 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -39,7 +39,6 @@ def logerror(func, path, excinfo): class WindowsTestcase(Testcase): - tmpdir_pfx = 'bff-crash-' _debugger_cls = MsecDebugger # TODO: do we still need fuzzer as an arg? diff --git a/src/test_certfuzz/testcase/test_testcase_base.py b/src/test_certfuzz/testcase/test_testcase_base.py deleted file mode 100644 index c5016fb..0000000 --- a/src/test_certfuzz/testcase/test_testcase_base.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Oct 7, 2013 - -@organization: cert.org -''' -import unittest -import certfuzz.testcase.testcase_base - -class Test(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def testName(self): - pass - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From 4251895ee7be38916cc5808959dcce6b87a6ed05 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 11:42:56 -0500 Subject: [PATCH 0956/1169] rename Testcase class to TestCaseBase --- src/certfuzz/testcase/testcase_base2.py | 4 ++-- src/certfuzz/testcase/testcase_linux.py | 12 ++++++------ src/certfuzz/testcase/testcase_windows.py | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py index 2b6757e..2515b9e 100644 --- a/src/certfuzz/testcase/testcase_base2.py +++ b/src/certfuzz/testcase/testcase_base2.py @@ -17,14 +17,14 @@ logger = logging.getLogger(__name__) -class Testcase(object): +class TestCaseBase(object): _tmp_sfx = '' _tmp_pfx = 'BFF_testcase_' _debugger_cls = None def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): - logger.debug('Inititalize Testcase') + logger.debug('Inititalize TestCaseBase') self.seedfile = seedfile self.fuzzedfile = fuzzedfile diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 20bd337..0d26d4b 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -6,7 +6,7 @@ import logging import os -from certfuzz.testcase.testcase_base2 import Testcase +from certfuzz.testcase.testcase_base2 import TestCaseBase from certfuzz.fuzztools import hostinfo, filetools from certfuzz.fuzztools.command_line_templating import get_command_args_list from certfuzz.testcase.errors import TestCaseError @@ -28,7 +28,7 @@ from certfuzz.debuggers.crashwrangler import CrashWrangler as debugger_cls -class LinuxTestcase(Testcase): +class LinuxTestcase(TestCaseBase): ''' classdocs ''' @@ -40,7 +40,7 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, ''' Constructor ''' - Testcase.__init__(self, seedfile, fuzzedfile, debugger_timeout) + TestCaseBase.__init__(self, seedfile, fuzzedfile, debugger_timeout) self.cfg = cfg self.program = program self.backtrace_lines = backtrace_lines @@ -71,7 +71,7 @@ def set_debugger_template(self, option='bt_only'): raise TestCaseError('Debugger template does not exist at %s' % self.debugger_template) def update_crash_details(self): - Testcase.update_crash_details(self) + TestCaseBase.update_crash_details(self) cmdlist = get_command_args_list(self.cfg['target']['cmdline_template'], infile=self.fuzzedfile.path, @@ -138,9 +138,9 @@ def get_signature(self): if not self.signature: self.signature = self.dbg.get_testcase_signature(self.backtrace_lines) if self.signature: - logger.debug("Testcase signature is %s", self.signature) + logger.debug("TestCaseBase signature is %s", self.signature) else: - raise TestCaseError('Testcase has no signature.') + raise TestCaseError('TestCaseBase has no signature.') if self.dbg.total_stack_corruption: # total_stack_corruption. Use pin calltrace to get a backtrace analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self) diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index c3769b1..faa8ee5 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -7,7 +7,7 @@ import logging import os -from certfuzz.testcase.testcase_base2 import Testcase +from certfuzz.testcase.testcase_base2 import TestCaseBase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import best_effort_move from certfuzz.helpers.misc import random_str @@ -38,7 +38,7 @@ def logerror(func, path, excinfo): } -class WindowsTestcase(Testcase): +class WindowsTestcase(TestCaseBase): _debugger_cls = MsecDebugger # TODO: do we still need fuzzer as an arg? @@ -48,7 +48,7 @@ def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, dbg_timeout = dbg_opts['runtimeout'] - Testcase.__init__(self, seedfile, fuzzedfile, dbg_timeout) + TestCaseBase.__init__(self, seedfile, fuzzedfile, dbg_timeout) self.dbg_opts = dbg_opts self.copy_fuzzedfile = copy_fuzzedfile @@ -96,7 +96,7 @@ def update_crash_details(self): as needed. Used in both object runtime context and for refresh after a crash object is copied. ''' - Testcase.update_crash_details(self) + TestCaseBase.update_crash_details(self) # Reset properties that need to be regenerated self.exception_depth = 0 self.parsed_outputs = [] From 63d22b5f936dd2fc1c71012e2e19a1b376a4d573 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 11:44:01 -0500 Subject: [PATCH 0957/1169] rename testcase_base2 to testcase_base --- src/certfuzz/testcase/testcase_base.py | 140 +++++++++--- src/certfuzz/testcase/testcase_base2.py | 200 ------------------ src/certfuzz/testcase/testcase_linux.py | 2 +- src/certfuzz/testcase/testcase_windows.py | 2 +- ...estcase_base2.py => test_testcase_base.py} | 2 +- 5 files changed, 118 insertions(+), 228 deletions(-) delete mode 100644 src/certfuzz/testcase/testcase_base2.py rename src/test_certfuzz/testcase/{test_testcase_base2.py => test_testcase_base.py} (88%) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 6d318de..2515b9e 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -1,17 +1,17 @@ ''' -Created on Apr 12, 2013 +Created on Oct 11, 2012 @organization: cert.org ''' import logging -import shutil +import os import tempfile -from certfuzz.fuzztools import hamming -from certfuzz.fuzztools.filetools import check_zip_file +from certfuzz.file_handlers.basicfile import BasicFile +from certfuzz.fuzztools import filetools, hamming +from certfuzz.fuzztools.filetools import check_zip_file, mkdir_p +from certfuzz.testcase.errors import TestCaseError from pprint import pformat -import os -from certfuzz.testcase.errors import CrashError logger = logging.getLogger(__name__) @@ -20,11 +20,15 @@ class TestCaseBase(object): _tmp_sfx = '' _tmp_pfx = 'BFF_testcase_' + _debugger_cls = None + + + def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): + logger.debug('Inititalize TestCaseBase') - def __init__(self, seedfile, fuzzedfile, workdir_base=None): self.seedfile = seedfile self.fuzzedfile = fuzzedfile - self.workdir_base = workdir_base + self.workdir_base = None # Exploitability is UNKNOWN unless proven otherwise self.exp = 'UNKNOWN' @@ -38,27 +42,113 @@ def __init__(self, seedfile, fuzzedfile, workdir_base=None): # this will get overridden by calls to get_logger self.logger = logger + + self.debugger_timeout = dbg_timeout + + self.debugger_template = None + # All crashes are heisenbugs until proven otherwise + self.is_heisenbug = True + + self.workdir_base = tempfile.gettempdir() + + # set some defaults + # Not a crash until we're sure + self.is_crash = False + self.dbg_file = None + self.is_unique = False + self.should_proceed_with_analysis = False + self.is_corrupt_stack = False + self.copy_fuzzedfile = True + self.pc = None + self.result_dir = None + self.debugger_missed_stack_corruption = False + self.total_stack_corruption = False + self.pc_in_function = False + def __enter__(self): - self._setup_workdir() - self.calculate_hamming_distances() + mkdir_p(self.workdir_base) + self.update_crash_details() return self def __exit__(self, etype, value, traceback): - self._teardown_workdir() - return - - def _setup_workdir(self): - self.working_dir = tempfile.mkdtemp(suffix=self._tmp_sfx, - prefix=self._tmp_pfx, - dir=self.workdir_base) + pass def __repr__(self): return pformat(self.__dict__) - def _teardown_workdir(self): - shutil.rmtree(self.working_dir) - self.working_dir = None + def _get_output_dir(self, *args): + raise NotImplementedError + + def _rename_dbg_file(self): + raise NotImplementedError + + def _rename_fuzzed_file(self): + raise NotImplementedError + + def _set_attr_from_dbg(self, attrname): + raise NotImplementedError + + def _verify_crash_base_dir(self): + raise NotImplementedError + + def clean_tmpdir(self): + logger.debug('Cleaning up %s', self.tempdir) + if os.path.exists(self.tempdir): + filetools.delete_files_or_dirs([self.tempdir]) + else: + logger.debug('No tempdir at %s', self.tempdir) + + if os.path.exists(self.tempdir): + logger.debug('Unable to remove tempdir %s', self.tempdir) + + def confirm_crash(self): + raise NotImplementedError + + def copy_files_to_temp(self): + if self.fuzzedfile and self.copy_fuzzedfile: + filetools.copy_file(self.fuzzedfile.path, self.tempdir) + + if self.seedfile: + filetools.copy_file(self.seedfile.path, self.tempdir) + + new_fuzzedfile = os.path.join(self.tempdir, self.fuzzedfile.basename) + self.fuzzedfile = BasicFile(new_fuzzedfile) + + def copy_files(self, outdir): + crash_files = os.listdir(self.tempdir) + for f in crash_files: + filepath = os.path.join(self.tempdir, f) + if os.path.isfile(filepath): + filetools.copy_file(filepath, outdir) + + def debug(self, tries_remaining=None): + raise NotImplementedError + + def debug_once(self): + raise NotImplementedError + + def delete_files(self): + if os.path.isdir(self.fuzzedfile.dirname): + logger.debug('Deleting files from %s', self.fuzzedfile.dirname) + filetools.delete_files_or_dirs([self.fuzzedfile.dirname]) + + def get_debug_output(self, f): + raise NotImplementedError + + def get_result_dir(self): + raise NotImplementedError + + def get_signature(self): + raise NotImplementedError + + def set_debugger_template(self, option='bt_only'): + raise NotImplementedError + + def update_crash_details(self): + self.tempdir = tempfile.mkdtemp(prefix=self._tmp_pfx, suffix=self._tmp_sfx, dir=self.workdir_base) + self.copy_files_to_temp() +# raise NotImplementedError def calculate_hamming_distances(self): # If the fuzzed file is a valid zip, then we're fuzzing zip contents, not the container self.is_zipfile = check_zip_file(self.fuzzedfile.path) @@ -95,13 +185,13 @@ def get_logger(self): ''' self.logger = logging.getLogger(self.signature) if len(self.logger.handlers) == 0: - if not os.path.exists(self.tempdir): - logger.error('Working path not found: %s', self.tempdir) - raise CrashError('Working path not found: {}'.format(self.tempdir)) - logger.debug('tempdir=%s sig=%s', self.tempdir, self.signature) + if not os.path.exists(self.result_dir): + logger.error('Result path not found: %s', self.result_dir) + raise TestCaseError('Result path not found: {}'.format(self.result_dir)) + logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) logfile = '%s.log' % self.signature logger.debug('logfile=%s', logfile) - logpath = os.path.join(self.tempdir, logfile) + logpath = os.path.join(self.result_dir, logfile) logger.debug('logpath=%s', logpath) hdlr = logging.FileHandler(logpath) self.logger.addHandler(hdlr) diff --git a/src/certfuzz/testcase/testcase_base2.py b/src/certfuzz/testcase/testcase_base2.py deleted file mode 100644 index 2515b9e..0000000 --- a/src/certfuzz/testcase/testcase_base2.py +++ /dev/null @@ -1,200 +0,0 @@ -''' -Created on Oct 11, 2012 - -@organization: cert.org -''' -import logging -import os -import tempfile - -from certfuzz.file_handlers.basicfile import BasicFile -from certfuzz.fuzztools import filetools, hamming -from certfuzz.fuzztools.filetools import check_zip_file, mkdir_p -from certfuzz.testcase.errors import TestCaseError -from pprint import pformat - - -logger = logging.getLogger(__name__) - - -class TestCaseBase(object): - _tmp_sfx = '' - _tmp_pfx = 'BFF_testcase_' - _debugger_cls = None - - - def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): - logger.debug('Inititalize TestCaseBase') - - self.seedfile = seedfile - self.fuzzedfile = fuzzedfile - self.workdir_base = None - - # Exploitability is UNKNOWN unless proven otherwise - self.exp = 'UNKNOWN' - - self.hd_bits = None - self.hd_bytes = None - self.signature = None - self.working_dir = None - self.is_zipfile = False - - # this will get overridden by calls to get_logger - self.logger = logger - - - self.debugger_timeout = dbg_timeout - - self.debugger_template = None - # All crashes are heisenbugs until proven otherwise - self.is_heisenbug = True - - self.workdir_base = tempfile.gettempdir() - - # set some defaults - # Not a crash until we're sure - self.is_crash = False - self.dbg_file = None - self.is_unique = False - self.should_proceed_with_analysis = False - self.is_corrupt_stack = False - self.copy_fuzzedfile = True - self.pc = None - self.result_dir = None - self.debugger_missed_stack_corruption = False - self.total_stack_corruption = False - self.pc_in_function = False - - def __enter__(self): - mkdir_p(self.workdir_base) - self.update_crash_details() - return self - - def __exit__(self, etype, value, traceback): - pass - - def __repr__(self): - return pformat(self.__dict__) - - def _get_output_dir(self, *args): - raise NotImplementedError - - def _rename_dbg_file(self): - raise NotImplementedError - - def _rename_fuzzed_file(self): - raise NotImplementedError - - def _set_attr_from_dbg(self, attrname): - raise NotImplementedError - - def _verify_crash_base_dir(self): - raise NotImplementedError - - def clean_tmpdir(self): - logger.debug('Cleaning up %s', self.tempdir) - if os.path.exists(self.tempdir): - filetools.delete_files_or_dirs([self.tempdir]) - else: - logger.debug('No tempdir at %s', self.tempdir) - - if os.path.exists(self.tempdir): - logger.debug('Unable to remove tempdir %s', self.tempdir) - - def confirm_crash(self): - raise NotImplementedError - - def copy_files_to_temp(self): - if self.fuzzedfile and self.copy_fuzzedfile: - filetools.copy_file(self.fuzzedfile.path, self.tempdir) - - if self.seedfile: - filetools.copy_file(self.seedfile.path, self.tempdir) - - new_fuzzedfile = os.path.join(self.tempdir, self.fuzzedfile.basename) - self.fuzzedfile = BasicFile(new_fuzzedfile) - - def copy_files(self, outdir): - crash_files = os.listdir(self.tempdir) - for f in crash_files: - filepath = os.path.join(self.tempdir, f) - if os.path.isfile(filepath): - filetools.copy_file(filepath, outdir) - - def debug(self, tries_remaining=None): - raise NotImplementedError - - def debug_once(self): - raise NotImplementedError - - def delete_files(self): - if os.path.isdir(self.fuzzedfile.dirname): - logger.debug('Deleting files from %s', self.fuzzedfile.dirname) - filetools.delete_files_or_dirs([self.fuzzedfile.dirname]) - - def get_debug_output(self, f): - raise NotImplementedError - - def get_result_dir(self): - raise NotImplementedError - - def get_signature(self): - raise NotImplementedError - - def set_debugger_template(self, option='bt_only'): - raise NotImplementedError - - def update_crash_details(self): - self.tempdir = tempfile.mkdtemp(prefix=self._tmp_pfx, suffix=self._tmp_sfx, dir=self.workdir_base) - self.copy_files_to_temp() - -# raise NotImplementedError - def calculate_hamming_distances(self): - # If the fuzzed file is a valid zip, then we're fuzzing zip contents, not the container - self.is_zipfile = check_zip_file(self.fuzzedfile.path) - try: - if self.is_zipfile: - self.hd_bits = hamming.bitwise_zip_hamming_distance(self.seedfile.path, self.fuzzedfile.path) - self.hd_bytes = hamming.bytewise_zip_hamming_distance(self.seedfile.path, self.fuzzedfile.path) - else: - self.hd_bits = hamming.bitwise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) - self.hd_bytes = hamming.bytewise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) - except KeyError: - # one of the files wasn't defined - logger.warning('Cannot find either sf_path or minimized file to calculate Hamming Distances') - - self.logger.info("bitwise_hd=%d", self.hd_bits) - self.logger.info("bytewise_hd=%d", self.hd_bytes) - - - def calculate_hamming_distances_a(self): - with open(self.fuzzedfile.path, 'rb') as fd: - fuzzed = fd.read() - - a_string = 'x' * len(fuzzed) - - self.hd_bits = hamming.bitwise_hd(a_string, fuzzed) - self.logger.info("bitwise_hd=%d", self.hd_bits) - - self.hd_bytes = hamming.bytewise_hd(a_string, fuzzed) - self.logger.info("bytewise_hd=%d", self.hd_bytes) - - def get_logger(self): - ''' - sets self.logger to a logger specific to this crash - ''' - self.logger = logging.getLogger(self.signature) - if len(self.logger.handlers) == 0: - if not os.path.exists(self.result_dir): - logger.error('Result path not found: %s', self.result_dir) - raise TestCaseError('Result path not found: {}'.format(self.result_dir)) - logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) - logfile = '%s.log' % self.signature - logger.debug('logfile=%s', logfile) - logpath = os.path.join(self.result_dir, logfile) - logger.debug('logpath=%s', logpath) - hdlr = logging.FileHandler(logpath) - self.logger.addHandler(hdlr) - - return self.logger - diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 0d26d4b..61ec65b 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -6,7 +6,7 @@ import logging import os -from certfuzz.testcase.testcase_base2 import TestCaseBase +from certfuzz.testcase.testcase_base import TestCaseBase from certfuzz.fuzztools import hostinfo, filetools from certfuzz.fuzztools.command_line_templating import get_command_args_list from certfuzz.testcase.errors import TestCaseError diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index faa8ee5..d984021 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -7,7 +7,7 @@ import logging import os -from certfuzz.testcase.testcase_base2 import TestCaseBase +from certfuzz.testcase.testcase_base import TestCaseBase from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.filetools import best_effort_move from certfuzz.helpers.misc import random_str diff --git a/src/test_certfuzz/testcase/test_testcase_base2.py b/src/test_certfuzz/testcase/test_testcase_base.py similarity index 88% rename from src/test_certfuzz/testcase/test_testcase_base2.py rename to src/test_certfuzz/testcase/test_testcase_base.py index 3859b99..9ba5c28 100644 --- a/src/test_certfuzz/testcase/test_testcase_base2.py +++ b/src/test_certfuzz/testcase/test_testcase_base.py @@ -4,7 +4,7 @@ @organization: cert.org ''' import unittest -import certfuzz.testcase.testcase_base2 +import certfuzz.testcase.testcase_base class Test(unittest.TestCase): From f6f50a1a33f071c3ebc1f81b1b441a5252ab0377 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 13:33:12 -0500 Subject: [PATCH 0958/1169] move null set_debugger_template to base class --- src/certfuzz/testcase/testcase_base.py | 4 ++-- src/certfuzz/testcase/testcase_windows.py | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 2515b9e..997e5a2 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -141,8 +141,8 @@ def get_result_dir(self): def get_signature(self): raise NotImplementedError - def set_debugger_template(self, option='bt_only'): - raise NotImplementedError + def set_debugger_template(self, *args): + pass def update_crash_details(self): self.tempdir = tempfile.mkdtemp(prefix=self._tmp_pfx, suffix=self._tmp_sfx, dir=self.workdir_base) diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index d984021..b631ee6 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -110,9 +110,6 @@ def update_crash_details(self): self._rename_fuzzed_file() self._rename_dbg_file() - def set_debugger_template(self, *args): - pass - def debug_once(self): outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) From f00b0876a1d707458b6c4eddd4434cd577df83fc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 13:33:40 -0500 Subject: [PATCH 0959/1169] use mkdir_p to create directories --- src/certfuzz/testcase/testcase_linux.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 61ec65b..8b7ff47 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -60,7 +60,6 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, def __exit__(self, etype, value, traceback): pass -# self.clean_tmpdir() def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): @@ -159,8 +158,8 @@ def get_signature(self): def _verify_crash_base_dir(self): if not self.crash_base_dir: raise TestCaseError('crash_base_dir not set') - if not os.path.exists(self.crash_base_dir): - filetools.make_directories(self.crash_base_dir) + + filetools.mkdir_p(self.crash_base_dir) def get_result_dir(self): assert self.crash_base_dir From 656c53934ec464271ceb712dd0917a7770404c40 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 25 Feb 2016 15:57:09 -0500 Subject: [PATCH 0960/1169] set cmdargs in similar fashion --- src/certfuzz/testcase/testcase_linux.py | 4 +- src/certfuzz/testcase/testcase_windows.py | 78 ++++++++++------------- 2 files changed, 36 insertions(+), 46 deletions(-) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 8b7ff47..fd57d04 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -73,8 +73,8 @@ def update_crash_details(self): TestCaseBase.update_crash_details(self) cmdlist = get_command_args_list(self.cfg['target']['cmdline_template'], - infile=self.fuzzedfile.path, - posix=True)[1] + infile=self.fuzzedfile.path, + posix=True)[1] self.cmdargs = cmdlist[1:] self.is_crash = self.confirm_crash() diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index b631ee6..4d33725 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -22,20 +22,20 @@ def logerror(func, path, excinfo): logger.warning('%s failed to remove %s: %s', func, path, excinfo) short_exp = { - 'UNKNOWN': 'UNK', - 'PROBABLY_NOT_EXPLOITABLE': 'PNE', - 'PROBABLY_EXPLOITABLE': 'PEX', - 'EXPLOITABLE': 'EXP', - 'HEISENBUG': 'HSB', -} + 'UNKNOWN': 'UNK', + 'PROBABLY_NOT_EXPLOITABLE': 'PNE', + 'PROBABLY_EXPLOITABLE': 'PEX', + 'EXPLOITABLE': 'EXP', + 'HEISENBUG': 'HSB', + } exp_rank = { - 'EXPLOITABLE': 1, - 'PROBABLY_EXPLOITABLE': 2, - 'UNKNOWN': 3, - 'PROBABLY_NOT_EXPLOITABLE': 4, - 'HEISENBUG': 5, -} + 'EXPLOITABLE': 1, + 'PROBABLY_EXPLOITABLE': 2, + 'UNKNOWN': 3, + 'PROBABLY_NOT_EXPLOITABLE': 4, + 'HEISENBUG': 5, + } class WindowsTestcase(TestCaseBase): @@ -44,7 +44,7 @@ class WindowsTestcase(TestCaseBase): # TODO: do we still need fuzzer as an arg? def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, dbg_opts, workingdir_base, keep_faddr, program, - heisenbug_retries=4, copy_fuzzedfile=True, is_nullrunner=False): + heisenbug_retries=4, copy_fuzzedfile=True): dbg_timeout = dbg_opts['runtimeout'] @@ -65,8 +65,7 @@ def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, self.dbg_file = '' self.cmd_template = cmd_template try: - self.max_handled_exceptions = self.dbg_opts[ - 'max_handled_exceptions'] + self.max_handled_exceptions = self.dbg_opts['max_handled_exceptions'] except KeyError: self.max_handled_exceptions = 6 try: @@ -78,7 +77,6 @@ def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, self.parsed_outputs = [] self.max_depth = heisenbug_retries - self.is_nullrunner = is_nullrunner def _get_file_basename(self): ''' @@ -104,8 +102,10 @@ def update_crash_details(self): fname = self._get_file_basename() outfile_base = os.path.join(self.tempdir, fname) # Regenerate target commandline with new crasher file - self.cmdargs = get_command_args_list( - self.cmd_template, outfile_base)[1][1:] + + cmdlist = get_command_args_list(self.cmd_template, + outfile_base)[1] + self.cmdargs = cmdlist[1:] self.debug() self._rename_fuzzed_file() self._rename_dbg_file() @@ -114,16 +114,15 @@ def debug_once(self): outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) with self._debugger_cls(program=self.program, - cmd_args=self.cmdargs, - outfile_base=outfile_base, - timeout=self.debugger_timeout, - exception_depth=self.exception_depth, - workingdir=self.tempdir, - watchcpu=self.watchcpu) as debugger: + cmd_args=self.cmdargs, + outfile_base=outfile_base, + timeout=self.debugger_timeout, + exception_depth=self.exception_depth, + workingdir=self.tempdir, + watchcpu=self.watchcpu) as debugger: self.parsed_outputs.append(debugger.go()) - self.reached_secondchance = self.parsed_outputs[ - self.exception_depth].secondchance + self.reached_secondchance = self.parsed_outputs[self.exception_depth].secondchance if self.reached_secondchance and self.exception_depth > 0: # No need to process second-chance exception @@ -139,10 +138,8 @@ def debug_once(self): elif exp_rank[current_exception_exp] < exp_rank[self.exp]: self.exp = current_exception_exp - current_exception_hash = self.parsed_outputs[ - self.exception_depth].crash_hash - current_exception_faddr = self.parsed_outputs[ - self.exception_depth].faddr + current_exception_hash = self.parsed_outputs[self.exception_depth].crash_hash + current_exception_faddr = self.parsed_outputs[self.exception_depth].faddr if current_exception_hash: # We have a hash for the current exception if self.exception_depth == 0: @@ -150,8 +147,7 @@ def debug_once(self): self.crash_hash = current_exception_hash elif self.crash_hash: # Append to the exception hash chain - self.crash_hash = self.crash_hash + \ - '_' + current_exception_hash + self.crash_hash = self.crash_hash + '_' + current_exception_hash if self.keep_uniq_faddr and current_exception_faddr: self.crash_hash += '.' + current_exception_faddr @@ -189,15 +185,13 @@ def debug(self, tries_remaining=None): break # get the signature now that we've got all of the exceptions self.get_signature() - elif not self.is_nullrunner: - # With the Windows XP hook, there's the concept of a heisenbug. + else: # if we are still a heisenbug after debugging # we might need to try again # or give up if we've tried enough already if tries_remaining: # keep diving - logger.debug( - "possible heisenbug (%d tries left)", tries_remaining) + logger.debug("possible heisenbug (%d tries left)", tries_remaining) self.debug(tries_remaining - 1) else: # we're at the bottom @@ -212,8 +206,7 @@ def _set_heisenbug_properties(self): # for whatever reason we couldn't get the real content, # and since we're just generating a string here # any string will do - logger.warning( - 'Unable to get md5 of %s, using random string for heisenbug signature: %s', self.fuzzedfile.path, e) + logger.warning('Unable to get md5 of %s, using random string for heisenbug signature: %s', self.fuzzedfile.path, e) fuzzed_content = random_str(64) self.is_heisenbug = True self.signature = hashlib.md5(fuzzed_content).hexdigest() @@ -234,11 +227,9 @@ def _rename_fuzzed_file(self): return logger.debug('Attempting to rename %s', self.fuzzedfile.path) - new_basename = '%s-%s%s' % (self.fuzzedfile.root, - self.faddr, self.fuzzedfile.ext) + new_basename = '%s-%s%s' % (self.fuzzedfile.root, self.faddr, self.fuzzedfile.ext) new_fuzzed_file = os.path.join(self.fuzzedfile.dirname, new_basename) - logger.debug( - 'renaming %s -> %s', self.fuzzedfile.path, new_fuzzed_file) + logger.debug('renaming %s -> %s', self.fuzzedfile.path, new_fuzzed_file) # best_effort move returns a tuple of booleans indicating (copied, deleted) # we only care about copied @@ -271,8 +262,7 @@ def _rename_dbg_file(self): parts.append(faddr_str) if exp_str: parts.append(exp_str) - new_basename = '-'.join(parts) + ext + \ - '.e%s' % exception_num + dbgext + new_basename = '-'.join(parts) + ext + '.e%s' % exception_num + dbgext new_dbg_file = os.path.join(path, new_basename) From 6aa4ed6ae58030f00ab6b9c0a9c88e0db6cbeff1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 26 Feb 2016 14:59:51 -0500 Subject: [PATCH 0961/1169] exit method in linux subclass is superfluous --- src/certfuzz/testcase/testcase_linux.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index fd57d04..2ab3d1a 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -58,9 +58,6 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, self.pc = None self.result_dir = None - def __exit__(self, etype, value, traceback): - pass - def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): dbg_template_name = '%s_%s_template.txt' % (self._debugger_cls._key, option) From f816e5722f94b1f56312d14c680499eeac762b3e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 21 Jun 2016 13:30:52 -0400 Subject: [PATCH 0962/1169] pep8 cleanup --- src/certfuzz/testcase/testcase_base.py | 31 +++++++----- src/certfuzz/testcase/testcase_linux.py | 55 ++++++++++++-------- src/certfuzz/testcase/testcase_windows.py | 62 ++++++++++++----------- 3 files changed, 86 insertions(+), 62 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 997e5a2..8b7bebe 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -22,7 +22,6 @@ class TestCaseBase(object): _tmp_pfx = 'BFF_testcase_' _debugger_cls = None - def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): logger.debug('Inititalize TestCaseBase') @@ -42,7 +41,6 @@ def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): # this will get overridden by calls to get_logger self.logger = logger - self.debugger_timeout = dbg_timeout self.debugger_template = None @@ -145,28 +143,34 @@ def set_debugger_template(self, *args): pass def update_crash_details(self): - self.tempdir = tempfile.mkdtemp(prefix=self._tmp_pfx, suffix=self._tmp_sfx, dir=self.workdir_base) + self.tempdir = tempfile.mkdtemp( + prefix=self._tmp_pfx, suffix=self._tmp_sfx, dir=self.workdir_base) self.copy_files_to_temp() # raise NotImplementedError def calculate_hamming_distances(self): - # If the fuzzed file is a valid zip, then we're fuzzing zip contents, not the container + # If the fuzzed file is a valid zip, then we're fuzzing zip contents, + # not the container self.is_zipfile = check_zip_file(self.fuzzedfile.path) try: if self.is_zipfile: - self.hd_bits = hamming.bitwise_zip_hamming_distance(self.seedfile.path, self.fuzzedfile.path) - self.hd_bytes = hamming.bytewise_zip_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + self.hd_bits = hamming.bitwise_zip_hamming_distance( + self.seedfile.path, self.fuzzedfile.path) + self.hd_bytes = hamming.bytewise_zip_hamming_distance( + self.seedfile.path, self.fuzzedfile.path) else: - self.hd_bits = hamming.bitwise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) - self.hd_bytes = hamming.bytewise_hamming_distance(self.seedfile.path, self.fuzzedfile.path) + self.hd_bits = hamming.bitwise_hamming_distance( + self.seedfile.path, self.fuzzedfile.path) + self.hd_bytes = hamming.bytewise_hamming_distance( + self.seedfile.path, self.fuzzedfile.path) except KeyError: # one of the files wasn't defined - logger.warning('Cannot find either sf_path or minimized file to calculate Hamming Distances') + logger.warning( + 'Cannot find either sf_path or minimized file to calculate Hamming Distances') self.logger.info("bitwise_hd=%d", self.hd_bits) self.logger.info("bytewise_hd=%d", self.hd_bytes) - def calculate_hamming_distances_a(self): with open(self.fuzzedfile.path, 'rb') as fd: fuzzed = fd.read() @@ -187,8 +191,10 @@ def get_logger(self): if len(self.logger.handlers) == 0: if not os.path.exists(self.result_dir): logger.error('Result path not found: %s', self.result_dir) - raise TestCaseError('Result path not found: {}'.format(self.result_dir)) - logger.debug('result_dir=%s sig=%s', self.result_dir, self.signature) + raise TestCaseError( + 'Result path not found: {}'.format(self.result_dir)) + logger.debug( + 'result_dir=%s sig=%s', self.result_dir, self.signature) logfile = '%s.log' % self.signature logger.debug('logfile=%s', logfile) logpath = os.path.join(self.result_dir, logfile) @@ -197,4 +203,3 @@ def get_logger(self): self.logger.addHandler(hdlr) return self.logger - diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 2ab3d1a..2b6296c 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -34,9 +34,18 @@ class LinuxTestcase(TestCaseBase): ''' _debugger_cls = debugger_cls - def __init__(self, cfg, seedfile, fuzzedfile, program, - debugger_timeout, backtrace_lines, - crashers_dir, workdir_base, seednum=None, range=None, keep_faddr=False): + def __init__(self, + cfg, + seedfile, + fuzzedfile, + program, + debugger_timeout, + backtrace_lines, + crashers_dir, + workdir_base, + seednum=None, + range=None, + keep_faddr=False): ''' Constructor ''' @@ -47,7 +56,8 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, self.crash_base_dir = crashers_dir self.seednum = seednum self.range = range - self.exclude_unmapped_frames = cfg['analyzer']['exclude_unmapped_frames'] + self.exclude_unmapped_frames = cfg[ + 'analyzer']['exclude_unmapped_frames'] self.set_debugger_template('bt_only') self.keep_uniq_faddr = keep_faddr @@ -60,11 +70,14 @@ def __init__(self, cfg, seedfile, fuzzedfile, program, def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): - dbg_template_name = '%s_%s_template.txt' % (self._debugger_cls._key, option) - self.debugger_template = os.path.join('certfuzz/debuggers/templates', dbg_template_name) + dbg_template_name = '%s_%s_template.txt' % ( + self._debugger_cls._key, option) + self.debugger_template = os.path.join( + 'certfuzz/debuggers/templates', dbg_template_name) logger.debug('Debugger template set to %s', self.debugger_template) if not os.path.exists(self.debugger_template): - raise TestCaseError('Debugger template does not exist at %s' % self.debugger_template) + raise TestCaseError( + 'Debugger template does not exist at %s' % self.debugger_template) def update_crash_details(self): TestCaseBase.update_crash_details(self) @@ -97,17 +110,16 @@ def get_debug_output(self, outfile_base): logger.debug('Debugger template: %s outfile_base: %s', self.debugger_template, outfile_base) debugger_obj = self._debugger_cls(self.program, - self.cmdargs, - outfile_base, - self.debugger_timeout, - template=self.debugger_template, - exclude_unmapped_frames=self.exclude_unmapped_frames, - keep_uniq_faddr=self.keep_uniq_faddr - ) + self.cmdargs, + outfile_base, + self.debugger_timeout, + template=self.debugger_template, + exclude_unmapped_frames=self.exclude_unmapped_frames, + keep_uniq_faddr=self.keep_uniq_faddr + ) self.dbg = debugger_obj.go() self.dbg_file = self.dbg.file - def confirm_crash(self): # get debugger output self.get_debug_output(self.fuzzedfile.path) @@ -115,7 +127,8 @@ def confirm_crash(self): if not self.dbg: raise TestCaseError('Debug object not found') - logger.debug('is_crash: %s is_assert_fail: %s', self.dbg.is_crash, self.dbg.is_assert_fail) + logger.debug('is_crash: %s is_assert_fail: %s', + self.dbg.is_crash, self.dbg.is_assert_fail) if self.cfg['analyzer']['savefailedasserts']: return self.dbg.is_crash else: @@ -132,7 +145,8 @@ def get_signature(self): @raise CrasherHasNoSignatureError: if it's a valid crash, but we don't get a signature ''' if not self.signature: - self.signature = self.dbg.get_testcase_signature(self.backtrace_lines) + self.signature = self.dbg.get_testcase_signature( + self.backtrace_lines) if self.signature: logger.debug("TestCaseBase signature is %s", self.signature) else: @@ -143,11 +157,13 @@ def get_signature(self): try: analyzer_instance.go() except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from pin. Cannot determine call trace.') + logger.warning( + 'Unexpected empty output from pin. Cannot determine call trace.') return self.signature calltrace = Calltracefile(analyzer_instance.outfile) - pinsignature = calltrace.get_testcase_signature(self.backtrace_lines * 10) + pinsignature = calltrace.get_testcase_signature( + self.backtrace_lines * 10) if pinsignature: self.signature = pinsignature return self.signature @@ -165,4 +181,3 @@ def get_result_dir(self): self.result_dir = os.path.join(self.crash_base_dir, self.signature) return self.result_dir - diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 4d33725..63fe0f8 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -22,20 +22,20 @@ def logerror(func, path, excinfo): logger.warning('%s failed to remove %s: %s', func, path, excinfo) short_exp = { - 'UNKNOWN': 'UNK', - 'PROBABLY_NOT_EXPLOITABLE': 'PNE', - 'PROBABLY_EXPLOITABLE': 'PEX', - 'EXPLOITABLE': 'EXP', - 'HEISENBUG': 'HSB', - } + 'UNKNOWN': 'UNK', + 'PROBABLY_NOT_EXPLOITABLE': 'PNE', + 'PROBABLY_EXPLOITABLE': 'PEX', + 'EXPLOITABLE': 'EXP', + 'HEISENBUG': 'HSB', +} exp_rank = { - 'EXPLOITABLE': 1, - 'PROBABLY_EXPLOITABLE': 2, - 'UNKNOWN': 3, - 'PROBABLY_NOT_EXPLOITABLE': 4, - 'HEISENBUG': 5, - } + 'EXPLOITABLE': 1, + 'PROBABLY_EXPLOITABLE': 2, + 'UNKNOWN': 3, + 'PROBABLY_NOT_EXPLOITABLE': 4, + 'HEISENBUG': 5, +} class WindowsTestcase(TestCaseBase): @@ -65,7 +65,8 @@ def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, self.dbg_file = '' self.cmd_template = cmd_template try: - self.max_handled_exceptions = self.dbg_opts['max_handled_exceptions'] + self.max_handled_exceptions = self.dbg_opts[ + 'max_handled_exceptions'] except KeyError: self.max_handled_exceptions = 6 try: @@ -113,16 +114,11 @@ def update_crash_details(self): def debug_once(self): outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) - with self._debugger_cls(program=self.program, - cmd_args=self.cmdargs, - outfile_base=outfile_base, - timeout=self.debugger_timeout, - exception_depth=self.exception_depth, - workingdir=self.tempdir, - watchcpu=self.watchcpu) as debugger: + with self._debugger_cls(program=self.program, cmd_args=self.cmdargs, outfile_base=outfile_base, timeout=self.debugger_timeout, exception_depth=self.exception_depth, workingdir=self.tempdir, watchcpu=self.watchcpu) as debugger: self.parsed_outputs.append(debugger.go()) - self.reached_secondchance = self.parsed_outputs[self.exception_depth].secondchance + self.reached_secondchance = self.parsed_outputs[ + self.exception_depth].secondchance if self.reached_secondchance and self.exception_depth > 0: # No need to process second-chance exception @@ -138,8 +134,10 @@ def debug_once(self): elif exp_rank[current_exception_exp] < exp_rank[self.exp]: self.exp = current_exception_exp - current_exception_hash = self.parsed_outputs[self.exception_depth].crash_hash - current_exception_faddr = self.parsed_outputs[self.exception_depth].faddr + current_exception_hash = self.parsed_outputs[ + self.exception_depth].crash_hash + current_exception_faddr = self.parsed_outputs[ + self.exception_depth].faddr if current_exception_hash: # We have a hash for the current exception if self.exception_depth == 0: @@ -147,7 +145,8 @@ def debug_once(self): self.crash_hash = current_exception_hash elif self.crash_hash: # Append to the exception hash chain - self.crash_hash = self.crash_hash + '_' + current_exception_hash + self.crash_hash = self.crash_hash + \ + '_' + current_exception_hash if self.keep_uniq_faddr and current_exception_faddr: self.crash_hash += '.' + current_exception_faddr @@ -191,7 +190,8 @@ def debug(self, tries_remaining=None): # or give up if we've tried enough already if tries_remaining: # keep diving - logger.debug("possible heisenbug (%d tries left)", tries_remaining) + logger.debug( + "possible heisenbug (%d tries left)", tries_remaining) self.debug(tries_remaining - 1) else: # we're at the bottom @@ -206,7 +206,8 @@ def _set_heisenbug_properties(self): # for whatever reason we couldn't get the real content, # and since we're just generating a string here # any string will do - logger.warning('Unable to get md5 of %s, using random string for heisenbug signature: %s', self.fuzzedfile.path, e) + logger.warning( + 'Unable to get md5 of %s, using random string for heisenbug signature: %s', self.fuzzedfile.path, e) fuzzed_content = random_str(64) self.is_heisenbug = True self.signature = hashlib.md5(fuzzed_content).hexdigest() @@ -227,9 +228,11 @@ def _rename_fuzzed_file(self): return logger.debug('Attempting to rename %s', self.fuzzedfile.path) - new_basename = '%s-%s%s' % (self.fuzzedfile.root, self.faddr, self.fuzzedfile.ext) + new_basename = '%s-%s%s' % (self.fuzzedfile.root, + self.faddr, self.fuzzedfile.ext) new_fuzzed_file = os.path.join(self.fuzzedfile.dirname, new_basename) - logger.debug('renaming %s -> %s', self.fuzzedfile.path, new_fuzzed_file) + logger.debug( + 'renaming %s -> %s', self.fuzzedfile.path, new_fuzzed_file) # best_effort move returns a tuple of booleans indicating (copied, deleted) # we only care about copied @@ -262,7 +265,8 @@ def _rename_dbg_file(self): parts.append(faddr_str) if exp_str: parts.append(exp_str) - new_basename = '-'.join(parts) + ext + '.e%s' % exception_num + dbgext + new_basename = '-'.join(parts) + ext + \ + '.e%s' % exception_num + dbgext new_dbg_file = os.path.join(path, new_basename) From 81af6cd49e1cb85ff831709008dd25c3d2e667d8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 21 Jun 2016 13:54:55 -0400 Subject: [PATCH 0963/1169] add unit tests --- .../testcase/test_testcase_base.py | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/test_certfuzz/testcase/test_testcase_base.py b/src/test_certfuzz/testcase/test_testcase_base.py index 9ba5c28..3730c4f 100644 --- a/src/test_certfuzz/testcase/test_testcase_base.py +++ b/src/test_certfuzz/testcase/test_testcase_base.py @@ -5,17 +5,54 @@ ''' import unittest import certfuzz.testcase.testcase_base +import tempfile +import shutil +from certfuzz.testcase.errors import TestCaseError +import logging + class Test(unittest.TestCase): def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='bff-test-') + self.sf = tempfile.mkstemp(prefix='seedfile-', + dir=self.tmpdir) + self.ff = tempfile.mkstemp(prefix='fuzzedfile-', + dir=self.tmpdir) + self.tc = certfuzz.testcase.testcase_base.TestCaseBase(self.sf, + self.ff) pass def tearDown(self): - pass + shutil.rmtree(self.tmpdir) + + def test_init(self): + self.assertEqual(self.sf, self.tc.seedfile) + self.assertEqual(self.ff, self.tc.fuzzedfile) + self.assertTrue(self.tc.is_heisenbug) + self.assertFalse(self.tc.is_zipfile) + self.assertFalse(self.tc.is_crash) + self.assertFalse(self.tc.is_unique) + self.assertFalse(self.tc.should_proceed_with_analysis) + self.assertFalse(self.tc.is_corrupt_stack) + self.assertTrue(self.tc.copy_fuzzedfile) + self.assertFalse(self.tc.debugger_missed_stack_corruption) + self.assertFalse(self.tc.total_stack_corruption) + self.assertFalse(self.tc.pc_in_function) + + def test_get_logger(self): + self.tc.signature = 'signature' + self.tc.result_dir = 'does_not_exist' + self.assertRaises(TestCaseError, self.tc.get_logger) + + self.tc.result_dir = tempfile.mkdtemp(suffix='-results', + prefix='bff-test-', + dir=self.tmpdir) + + self.tc.logger = None + x = self.tc.get_logger() + self.assertTrue(isinstance(x, logging.Logger)) - def testName(self): - pass if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From 7ef1c38bc3259cf098a64932db8230a638deec41 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 21 Jun 2016 13:55:17 -0400 Subject: [PATCH 0964/1169] default logger to None --- src/certfuzz/testcase/testcase_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 8b7bebe..e5df852 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -39,7 +39,7 @@ def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): self.is_zipfile = False # this will get overridden by calls to get_logger - self.logger = logger + self.logger = None self.debugger_timeout = dbg_timeout From a52f82b30442d3d3b2518e20f2102b3671f448c3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 21 Jun 2016 15:32:05 -0400 Subject: [PATCH 0965/1169] improve test cases --- src/test_certfuzz/mocks.py | 50 +++++++++++++++---- .../testcase/test_testcase_base.py | 31 ++++++++++-- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 21f3cd6..09fb99c 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -11,11 +11,15 @@ from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.config.simple_loader import fixup_config + class Mock(object): + def __init__(self, *args, **kwargs): pass + class MockCrasher(Mock): + def __init__(self): fd, f = tempfile.mkstemp(suffix='.ext', prefix='fileroot') os.close(fd) @@ -25,21 +29,29 @@ def __init__(self): def set_debugger_template(self, dummy): pass + class MockObj(object): + def __init__(self, **kwargs): for (kw, arg) in kwargs: self.__setattr__(kw, arg) + class MockCrash(MockObj): + def __init__(self): self.fuzzedfile = MockFile() + class MockFile(MockObj): + def __init__(self): self.dirname = 'dirname' self.path = 'path' + class MockRange(Mock): + def __init__(self): self.min = 0.01 self.max = 0.10 @@ -47,10 +59,13 @@ def __init__(self): def __str__(self): return '{}-{}'.format(self.min, self.max) + class MockRangefinder(Mock): + def next_item(self): return MockRange() + class MockSeedfile(Mock): basename = 'basename' root = 'root' @@ -66,6 +81,7 @@ def __init__(self, sz=1000): def read(self): return self.value + class MockFuzzedFile(Mock): path = u'foo' @@ -73,12 +89,21 @@ def __init__(self, path=None): if path is not None: self.path = path + class MockFuzzer(Mock): is_minimizable = False + +class MockLogger(object): + + def info(self, *args): + pass + + class MockRunner(Mock): is_nullrunner = False + class MockTestcase(Mock): signature = 'ABCDEFGHIJK' logger = logging.getLogger('mocktestcaselogger') @@ -90,6 +115,7 @@ class MockTestcase(Mock): debugger_extension = 'abcdefg' dbg_outfile = 'xyz' + class MockDbgOut(Mock): is_crash = False total_stack_corruption = False @@ -97,7 +123,9 @@ class MockDbgOut(Mock): def get_testcase_signature(self, *dummyargs): return 'AAAAA' + class MockDebugger(Mock): + def get(self): return MockDebugger @@ -106,29 +134,33 @@ def go(self): class MockCfg(dict): + def __init__(self, templated=True): self['debugger'] = {'backtracelevels': 5, - 'debugger': 'gdb', - } + 'debugger': 'gdb', + } self['target'] = {'cmdline_template': '$PROGRAM b c d $SEEDFILE', - 'killprocname': 'a', - 'program': 'a'} + 'killprocname': 'a', + 'program': 'a'} self['analyzer'] = {'exclude_unmapped_frames': False, - 'valgrind_timeout': 1} + 'valgrind_timeout': 1} self['directories'] = {'seedfile_dir': '', - 'results_dir': '', - 'working_dir': ''} + 'results_dir': '', + 'working_dir': ''} self['fuzzer'] = {'fuzzer': 'bytemut'} self['campaign'] = {'id': 'xyz'} self['runoptions'] = {'first_iteration': 0, - 'seed_interval': 10} + 'seed_interval': 10} self['runner'] = {'runner': 'zzufrun', 'runtimeout': 5} if templated: - self['target']['cmdline_template'] = string.Template(self['target']['cmdline_template']) + self['target']['cmdline_template'] = string.Template( + self['target']['cmdline_template']) + def MockFixupCfg(): return fixup_config(MockCfg(templated=False)) + class MockMinimizer(object): pass diff --git a/src/test_certfuzz/testcase/test_testcase_base.py b/src/test_certfuzz/testcase/test_testcase_base.py index 3730c4f..d7275e6 100644 --- a/src/test_certfuzz/testcase/test_testcase_base.py +++ b/src/test_certfuzz/testcase/test_testcase_base.py @@ -9,16 +9,17 @@ import shutil from certfuzz.testcase.errors import TestCaseError import logging +from test_certfuzz.mocks import MockSeedfile, MockFuzzedFile, MockLogger +import os class Test(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp(prefix='bff-test-') - self.sf = tempfile.mkstemp(prefix='seedfile-', - dir=self.tmpdir) - self.ff = tempfile.mkstemp(prefix='fuzzedfile-', - dir=self.tmpdir) + self.sf = MockSeedfile() + self.ff = MockFuzzedFile() + self.tc = certfuzz.testcase.testcase_base.TestCaseBase(self.sf, self.ff) pass @@ -53,6 +54,28 @@ def test_get_logger(self): x = self.tc.get_logger() self.assertTrue(isinstance(x, logging.Logger)) + def test_calculate_hamming_distances(self): + tc = self.tc + + tc.logger = MockLogger() + + fd, tc.seedfile.path = tempfile.mkstemp(suffix='.seed', + prefix='bff-test-', + dir=self.tmpdir) + os.write(fd, 'abcdefghijklmnopqrstuvwxyz\n') + os.close(fd) + + fd, tc.fuzzedfile.path = tempfile.mkstemp(suffix='.fuzzed', + prefix='bff-test-', + dir=self.tmpdir) + os.write(fd, 'ABCDefghijklmnopqrstuvwxyz\n') + os.close(fd) + + self.assertEqual(None, tc.hd_bits) + self.assertEqual(None, tc.hd_bytes) + tc.calculate_hamming_distances() + self.assertEqual(4, tc.hd_bits) + self.assertEqual(4, tc.hd_bytes) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] From b2dc1d40247ba953c2d6b9be6e3d428928ed8b31 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 09:21:44 -0400 Subject: [PATCH 0966/1169] pep8 cleanup --- src/certfuzz/testcase/testcase_linux.py | 5 ++++- src/certfuzz/testcase/testcase_windows.py | 20 ++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 2b6296c..352a42d 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -49,7 +49,10 @@ def __init__(self, ''' Constructor ''' - TestCaseBase.__init__(self, seedfile, fuzzedfile, debugger_timeout) + TestCaseBase.__init__(self, + seedfile, + fuzzedfile, + debugger_timeout) self.cfg = cfg self.program = program self.backtrace_lines = backtrace_lines diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 63fe0f8..a59a9a2 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -42,13 +42,25 @@ class WindowsTestcase(TestCaseBase): _debugger_cls = MsecDebugger # TODO: do we still need fuzzer as an arg? - def __init__(self, cmd_template, seedfile, fuzzedfile, cmdlist, fuzzer, - dbg_opts, workingdir_base, keep_faddr, program, - heisenbug_retries=4, copy_fuzzedfile=True): + def __init__(self, + cmd_template, + seedfile, + fuzzedfile, + cmdlist, + fuzzer, + dbg_opts, + workingdir_base, + keep_faddr, + program, + heisenbug_retries=4, + copy_fuzzedfile=True): dbg_timeout = dbg_opts['runtimeout'] - TestCaseBase.__init__(self, seedfile, fuzzedfile, dbg_timeout) + TestCaseBase.__init__(self, + seedfile, + fuzzedfile, + dbg_timeout) self.dbg_opts = dbg_opts self.copy_fuzzedfile = copy_fuzzedfile From 79124b0cfa99a95b99a40cb91cccd140ea3e73b5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 09:34:40 -0400 Subject: [PATCH 0967/1169] eliminate a few redundancies --- src/certfuzz/testcase/testcase_base.py | 14 ++++++++- src/certfuzz/testcase/testcase_linux.py | 21 +++++--------- src/certfuzz/testcase/testcase_windows.py | 35 ++++++++--------------- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index e5df852..949e6d9 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -22,7 +22,13 @@ class TestCaseBase(object): _tmp_pfx = 'BFF_testcase_' _debugger_cls = None - def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): + def __init__(self, + seedfile, + fuzzedfile, + program, + keep_faddr=False, + dbg_timeout=30): + logger.debug('Inititalize TestCaseBase') self.seedfile = seedfile @@ -38,6 +44,9 @@ def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): self.working_dir = None self.is_zipfile = False + self.result_dir = None + self.faddr = None + # this will get overridden by calls to get_logger self.logger = None @@ -63,6 +72,9 @@ def __init__(self, seedfile, fuzzedfile, dbg_timeout=30): self.total_stack_corruption = False self.pc_in_function = False + self.keep_uniq_faddr = keep_faddr + self.program = program + def __enter__(self): mkdir_p(self.workdir_base) self.update_crash_details() diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 352a42d..c05519a 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -29,9 +29,6 @@ class LinuxTestcase(TestCaseBase): - ''' - classdocs - ''' _debugger_cls = debugger_cls def __init__(self, @@ -52,24 +49,20 @@ def __init__(self, TestCaseBase.__init__(self, seedfile, fuzzedfile, + program, + keep_faddr, debugger_timeout) - self.cfg = cfg - self.program = program + self.backtrace_lines = backtrace_lines + self.cfg = cfg + self.cmdargs = None self.crash_base_dir = crashers_dir - self.seednum = seednum - self.range = range self.exclude_unmapped_frames = cfg[ 'analyzer']['exclude_unmapped_frames'] + self.range = range + self.seednum = seednum self.set_debugger_template('bt_only') - self.keep_uniq_faddr = keep_faddr - - self.cmdargs = None - self.is_crash = False self.signature = None - self.faddr = None - self.pc = None - self.result_dir = None def set_debugger_template(self, option='bt_only'): if host_info.is_linux(): diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index a59a9a2..7ac1639 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -60,36 +60,25 @@ def __init__(self, TestCaseBase.__init__(self, seedfile, fuzzedfile, + program, + keep_faddr, dbg_timeout) - self.dbg_opts = dbg_opts - self.copy_fuzzedfile = copy_fuzzedfile - + self.cmd_template = cmd_template self.cmdargs = cmdlist - self.workdir_base = workingdir_base - - self.keep_uniq_faddr = keep_faddr - self.program = program - self.dbg_result = {} + self.copy_fuzzedfile = copy_fuzzedfile self.crash_hash = None - self.result_dir = None - self.faddr = None self.dbg_file = '' - self.cmd_template = cmd_template - try: - self.max_handled_exceptions = self.dbg_opts[ - 'max_handled_exceptions'] - except KeyError: - self.max_handled_exceptions = 6 - try: - self.watchcpu = self.dbg_opts['watchcpu'] - except KeyError: - self.watchcpu = False + self.dbg_opts = dbg_opts + self.dbg_result = {} self.exception_depth = 0 - self.reached_secondchance = False - self.parsed_outputs = [] - self.max_depth = heisenbug_retries + self.max_handled_exceptions = self.dbg_opts.get( + 'max_handled_exceptions', 6) + self.parsed_outputs = [] + self.reached_secondchance = False + self.watchcpu = self.dbg_opts.get('watchcpu', False) + self.workdir_base = workingdir_base def _get_file_basename(self): ''' From 23026f096bbb9ff5ad9cbfe45fcf5ef1574afa6a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 09:47:58 -0400 Subject: [PATCH 0968/1169] alphabetize options --- src/certfuzz/testcase/testcase_base.py | 52 ++++++++++---------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 949e6d9..b337e39 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -31,49 +31,37 @@ def __init__(self, logger.debug('Inititalize TestCaseBase') - self.seedfile = seedfile - self.fuzzedfile = fuzzedfile - self.workdir_base = None - + self.copy_fuzzedfile = True + self.dbg_file = None + self.debugger_missed_stack_corruption = False + self.debugger_template = None + self.debugger_timeout = dbg_timeout # Exploitability is UNKNOWN unless proven otherwise self.exp = 'UNKNOWN' - self.hd_bits = None self.hd_bytes = None - self.signature = None - self.working_dir = None - self.is_zipfile = False - - self.result_dir = None self.faddr = None - - # this will get overridden by calls to get_logger - self.logger = None - - self.debugger_timeout = dbg_timeout - - self.debugger_template = None - # All crashes are heisenbugs until proven otherwise - self.is_heisenbug = True - - self.workdir_base = tempfile.gettempdir() - - # set some defaults + self.fuzzedfile = fuzzedfile + self.is_corrupt_stack = False # Not a crash until we're sure self.is_crash = False - self.dbg_file = None + # All crashes are heisenbugs until proven otherwise + self.is_heisenbug = True self.is_unique = False - self.should_proceed_with_analysis = False - self.is_corrupt_stack = False - self.copy_fuzzedfile = True + self.is_zipfile = False + self.keep_uniq_faddr = keep_faddr + # this will get overridden by calls to get_logger + self.logger = None self.pc = None - self.result_dir = None - self.debugger_missed_stack_corruption = False - self.total_stack_corruption = False self.pc_in_function = False - - self.keep_uniq_faddr = keep_faddr self.program = program + self.result_dir = None + self.seedfile = seedfile + self.should_proceed_with_analysis = False + self.signature = None + self.total_stack_corruption = False + self.workdir_base = tempfile.gettempdir() + self.working_dir = None def __enter__(self): mkdir_p(self.workdir_base) From bcdf86dfe842d6923de8e035f59b1c1cd08a3448 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 11:30:27 -0400 Subject: [PATCH 0969/1169] add config to args, reorder test case args --- src/certfuzz/iteration/iteration_windows.py | 6 ++-- src/certfuzz/testcase/testcase_base.py | 2 ++ src/certfuzz/testcase/testcase_linux.py | 2 +- src/certfuzz/testcase/testcase_windows.py | 9 ++--- src/certfuzz/tools/windows/minimize.py | 40 ++++++++++++--------- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 386c08d..d5ac77c 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -102,18 +102,18 @@ def _tidy(self): TmpReaper().clean_tmp() def _construct_testcase(self): - with WindowsTestcase(cmd_template=self.cmd_template, + with WindowsTestcase(cfg=self.cfg, seedfile=self.seedfile, fuzzedfile=BasicFile( self.fuzzer.output_file_path), + program=self.cfg['target']['program'], + cmd_template=self.cmd_template, cmdlist=get_command_args_list( self.cmd_template, self.fuzzer.output_file_path)[1], - fuzzer=self.fuzzer, dbg_opts=self.cfg['debugger'], workingdir_base=self.working_dir, keep_faddr=self.cfg['runoptions'][ 'keep_unique_faddr'], - program=self.cfg['target']['program'], heisenbug_retries=self.retries, copy_fuzzedfile=self.fuzzer.fuzzed_changes_input) as testcase: diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index b337e39..5f45780 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -23,6 +23,7 @@ class TestCaseBase(object): _debugger_cls = None def __init__(self, + cfg, seedfile, fuzzedfile, program, @@ -31,6 +32,7 @@ def __init__(self, logger.debug('Inititalize TestCaseBase') + self.cfg = cfg self.copy_fuzzedfile = True self.dbg_file = None self.debugger_missed_stack_corruption = False diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index c05519a..7d54ab3 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -47,6 +47,7 @@ def __init__(self, Constructor ''' TestCaseBase.__init__(self, + cfg, seedfile, fuzzedfile, program, @@ -54,7 +55,6 @@ def __init__(self, debugger_timeout) self.backtrace_lines = backtrace_lines - self.cfg = cfg self.cmdargs = None self.crash_base_dir = crashers_dir self.exclude_unmapped_frames = cfg[ diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 7ac1639..3db5926 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -43,21 +43,22 @@ class WindowsTestcase(TestCaseBase): # TODO: do we still need fuzzer as an arg? def __init__(self, - cmd_template, + cfg, seedfile, fuzzedfile, + program, + cmd_template, cmdlist, - fuzzer, dbg_opts, workingdir_base, - keep_faddr, - program, + keep_faddr=False, heisenbug_retries=4, copy_fuzzedfile=True): dbg_timeout = dbg_opts['runtimeout'] TestCaseBase.__init__(self, + cfg, seedfile, fuzzedfile, program, diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index b16927c..628653c 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -44,7 +44,8 @@ class DummyCfg(object): config.backtracelevels = 5 # doesn't matter what this is, we don't use it config.debugger_timeout = cfg['runner']['runtimeout'] template = string.Template(cfg['target']['cmdline_template']) - config.get_command_args_list = lambda x: get_command_args_list(template, x)[1] + config.get_command_args_list = lambda x: get_command_args_list( + template, x)[1] config.program = cfg['target']['program'] config.exclude_unmapped_frames = False config.watchdogfile = os.devnull @@ -104,7 +105,8 @@ def main(): logger.debug('WindowsConfig file: %s', cfg_file) if options.stringmode and options.target: - parser.error('Options --stringmode and --target are mutually exclusive.') + parser.error( + 'Options --stringmode and --target are mutually exclusive.') # Set some default options. Fast and loose if in string mode # More precise with minimize to seedfile @@ -155,20 +157,21 @@ def main(): retries = 0 debugger_class = msec.MsecDebugger - cmd_as_args = get_command_args_list(cfg['target']['cmdline_template'], fuzzed_file.path)[1] + cmd_as_args = get_command_args_list( + cfg['target']['cmdline_template'], fuzzed_file.path)[1] # Use runner timeout, since we now only specify the runner timeout cfg['debugger']['runtimeout'] = cfg['runner']['runtimeout'] - with WindowsTestcase(cmd_template=cfg['target']['cmdline_template'], - seedfile=seedfile, - fuzzedfile=fuzzed_file, - cmdlist=cmd_as_args, - fuzzer=None, - dbg_opts=cfg['debugger'], - workingdir_base=outdir, - keep_faddr=options.keep_uniq_faddr, - program=cfg['target']['program'], - heisenbug_retries=retries - ) as testcase: + with WindowsTestcase(cfg=cfg, + seedfile=seedfile, + fuzzedfile=fuzzed_file, + program=cfg['target']['program'], + cmd_template=cfg['target']['cmdline_template'], + cmdlist=cmd_as_args, + dbg_opts=cfg['debugger'], + workingdir_base=outdir, + keep_faddr=options.keep_uniq_faddr, + heisenbug_retries=retries + ) as testcase: filetools.make_directories(testcase.tempdir) logger.info('Copying %s to %s', fuzzed_file.path, testcase.tempdir) filetools.copy_file(fuzzed_file.path, testcase.tempdir) @@ -188,11 +191,13 @@ def main(): logger.debug('x character substitution') length = len(minimize.fuzzed_content) if options.prefer_x_target: - # We minimized to 'x', so we attempt to get metasploit as a freebie + # We minimized to 'x', so we attempt to get metasploit as a + # freebie targetstring = list(text.metasploit_pattern_orig(length)) filename_modifier = '-mtsp' else: - # We minimized to metasploit, so we attempt to get 'x' as a freebie + # We minimized to metasploit, so we attempt to get 'x' as a + # freebie targetstring = list('x' * length) filename_modifier = '-x' @@ -200,7 +205,8 @@ def main(): for idx in minimize.bytemap: logger.debug('Swapping index %d', idx) targetstring[idx] = fuzzed[idx] - filename = ''.join((testcase.fuzzedfile.root, filename_modifier, testcase.fuzzedfile.ext)) + filename = ''.join( + (testcase.fuzzedfile.root, filename_modifier, testcase.fuzzedfile.ext)) metasploit_file = os.path.join(testcase.tempdir, filename) f = open(metasploit_file, 'wb') From 4958150890b61bdfac4ad402e5f5df9e22698c72 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 14:15:36 -0400 Subject: [PATCH 0970/1169] keep merging --- src/certfuzz/iteration/iteration_linux.py | 7 ++++--- src/certfuzz/iteration/iteration_windows.py | 5 +++-- src/certfuzz/testcase/testcase_base.py | 7 +++++-- src/certfuzz/testcase/testcase_linux.py | 15 +++++++-------- src/certfuzz/testcase/testcase_windows.py | 8 +++----- src/test_certfuzz/testcase/test_testcase_base.py | 6 +++++- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 5eb0d84..1cb19b1 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -58,16 +58,17 @@ def __enter__(self): return IterationBase3.__enter__(self) def _construct_testcase(self): - with LinuxTestcase(cfg=self.cfg, - seedfile=self.seedfile, + with LinuxTestcase(seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), program=self.cfg['target']['program'], + cmd_template=self.cmd_template, debugger_timeout=self.cfg['runner']['runtimeout'], backtrace_lines=self.cfg[ 'debugger']['backtracelevels'], crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, seednum=self.seednum, - range=self.r) as testcase: + range=self.r, + exclude_unmapped_frames=self.cfg['analyzer']['exclude_unmapped_frames']) as testcase: # put it on the list for the analysis pipeline self.testcases.append(testcase) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index d5ac77c..dd6ea56 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -102,12 +102,13 @@ def _tidy(self): TmpReaper().clean_tmp() def _construct_testcase(self): - with WindowsTestcase(cfg=self.cfg, - seedfile=self.seedfile, + with WindowsTestcase(seedfile=self.seedfile, fuzzedfile=BasicFile( self.fuzzer.output_file_path), program=self.cfg['target']['program'], cmd_template=self.cmd_template, + debugger_timeout=self.cfg[ + 'runner']['runtimeout'], cmdlist=get_command_args_list( self.cmd_template, self.fuzzer.output_file_path)[1], dbg_opts=self.cfg['debugger'], diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 5f45780..1ca475e 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -18,21 +18,24 @@ class TestCaseBase(object): + ''' + A BFF test case represents everything we know about a fuzzer finding. + ''' _tmp_sfx = '' _tmp_pfx = 'BFF_testcase_' _debugger_cls = None def __init__(self, - cfg, seedfile, fuzzedfile, program, + cmd_template, keep_faddr=False, dbg_timeout=30): logger.debug('Inititalize TestCaseBase') - self.cfg = cfg + self.cmd_template = cmd_template self.copy_fuzzedfile = True self.dbg_file = None self.debugger_missed_stack_corruption = False diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 7d54ab3..0005882 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -36,29 +36,28 @@ def __init__(self, seedfile, fuzzedfile, program, + cmd_template, debugger_timeout, backtrace_lines, crashers_dir, workdir_base, seednum=None, range=None, - keep_faddr=False): - ''' - Constructor - ''' + keep_faddr=False, + exclude_unmapped_frames=False): + TestCaseBase.__init__(self, - cfg, seedfile, fuzzedfile, program, + cmd_template, keep_faddr, debugger_timeout) self.backtrace_lines = backtrace_lines self.cmdargs = None self.crash_base_dir = crashers_dir - self.exclude_unmapped_frames = cfg[ - 'analyzer']['exclude_unmapped_frames'] + self.exclude_unmapped_frames = exclude_unmapped_frames self.range = range self.seednum = seednum self.set_debugger_template('bt_only') @@ -78,7 +77,7 @@ def set_debugger_template(self, option='bt_only'): def update_crash_details(self): TestCaseBase.update_crash_details(self) - cmdlist = get_command_args_list(self.cfg['target']['cmdline_template'], + cmdlist = get_command_args_list(self.cmd_template, infile=self.fuzzedfile.path, posix=True)[1] self.cmdargs = cmdlist[1:] diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 3db5926..daee330 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -48,6 +48,7 @@ def __init__(self, fuzzedfile, program, cmd_template, + debugger_timeout, cmdlist, dbg_opts, workingdir_base, @@ -55,17 +56,14 @@ def __init__(self, heisenbug_retries=4, copy_fuzzedfile=True): - dbg_timeout = dbg_opts['runtimeout'] - TestCaseBase.__init__(self, - cfg, seedfile, fuzzedfile, program, + cmd_template, keep_faddr, - dbg_timeout) + debugger_timeout) - self.cmd_template = cmd_template self.cmdargs = cmdlist self.copy_fuzzedfile = copy_fuzzedfile self.crash_hash = None diff --git a/src/test_certfuzz/testcase/test_testcase_base.py b/src/test_certfuzz/testcase/test_testcase_base.py index d7275e6..480e8cc 100644 --- a/src/test_certfuzz/testcase/test_testcase_base.py +++ b/src/test_certfuzz/testcase/test_testcase_base.py @@ -19,9 +19,13 @@ def setUp(self): self.tmpdir = tempfile.mkdtemp(prefix='bff-test-') self.sf = MockSeedfile() self.ff = MockFuzzedFile() + program = 'foo' + cmd_template = 'foo a b c' self.tc = certfuzz.testcase.testcase_base.TestCaseBase(self.sf, - self.ff) + self.ff, + program, + cmd_template,) pass def tearDown(self): From a55bc277bb19c92b894c0fc45822960ccf7bdc70 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 14:26:47 -0400 Subject: [PATCH 0971/1169] make workdir_base consistent --- src/certfuzz/iteration/iteration_windows.py | 2 +- src/certfuzz/testcase/testcase_base.py | 3 ++- src/certfuzz/testcase/testcase_linux.py | 1 + src/certfuzz/testcase/testcase_windows.py | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index dd6ea56..d3f975d 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -112,7 +112,7 @@ def _construct_testcase(self): cmdlist=get_command_args_list( self.cmd_template, self.fuzzer.output_file_path)[1], dbg_opts=self.cfg['debugger'], - workingdir_base=self.working_dir, + workdir_base=self.working_dir, keep_faddr=self.cfg['runoptions'][ 'keep_unique_faddr'], heisenbug_retries=self.retries, diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 1ca475e..cee9cfe 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -30,6 +30,7 @@ def __init__(self, fuzzedfile, program, cmd_template, + workdir_base, keep_faddr=False, dbg_timeout=30): @@ -65,7 +66,7 @@ def __init__(self, self.should_proceed_with_analysis = False self.signature = None self.total_stack_corruption = False - self.workdir_base = tempfile.gettempdir() + self.workdir_base = workdir_base self.working_dir = None def __enter__(self): diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 0005882..3d05e26 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -51,6 +51,7 @@ def __init__(self, fuzzedfile, program, cmd_template, + workdir_base, keep_faddr, debugger_timeout) diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index daee330..284011b 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -51,7 +51,7 @@ def __init__(self, debugger_timeout, cmdlist, dbg_opts, - workingdir_base, + workdir_base, keep_faddr=False, heisenbug_retries=4, copy_fuzzedfile=True): @@ -61,6 +61,7 @@ def __init__(self, fuzzedfile, program, cmd_template, + workdir_base, keep_faddr, debugger_timeout) @@ -77,7 +78,6 @@ def __init__(self, self.parsed_outputs = [] self.reached_secondchance = False self.watchcpu = self.dbg_opts.get('watchcpu', False) - self.workdir_base = workingdir_base def _get_file_basename(self): ''' From 9d6a177f02079cc205b816ca29f5895545370a16 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 14:31:58 -0400 Subject: [PATCH 0972/1169] don't need seednum or range in test case anymore --- src/certfuzz/iteration/iteration_linux.py | 2 -- src/certfuzz/testcase/testcase_linux.py | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 1cb19b1..b7b5da8 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -67,8 +67,6 @@ def _construct_testcase(self): 'debugger']['backtracelevels'], crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, - seednum=self.seednum, - range=self.r, exclude_unmapped_frames=self.cfg['analyzer']['exclude_unmapped_frames']) as testcase: # put it on the list for the analysis pipeline self.testcases.append(testcase) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 3d05e26..5f6d364 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -41,8 +41,6 @@ def __init__(self, backtrace_lines, crashers_dir, workdir_base, - seednum=None, - range=None, keep_faddr=False, exclude_unmapped_frames=False): @@ -59,8 +57,6 @@ def __init__(self, self.cmdargs = None self.crash_base_dir = crashers_dir self.exclude_unmapped_frames = exclude_unmapped_frames - self.range = range - self.seednum = seednum self.set_debugger_template('bt_only') self.signature = None From 59b725ca276726dc8b52e6162597225ad51e5a92 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 14:55:56 -0400 Subject: [PATCH 0973/1169] add cfg back into test case --- src/certfuzz/iteration/iteration_linux.py | 7 ++++++- src/certfuzz/iteration/iteration_windows.py | 4 ++-- src/certfuzz/testcase/testcase_base.py | 2 ++ src/certfuzz/testcase/testcase_linux.py | 5 ++++- src/certfuzz/testcase/testcase_windows.py | 1 + 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index b7b5da8..805172b 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -58,7 +58,8 @@ def __enter__(self): return IterationBase3.__enter__(self) def _construct_testcase(self): - with LinuxTestcase(seedfile=self.seedfile, + with LinuxTestcase(cfg=self.cfg, + seedfile=self.seedfile, fuzzedfile=BasicFile(self.fuzzer.output_file_path), program=self.cfg['target']['program'], cmd_template=self.cmd_template, @@ -67,6 +68,10 @@ def _construct_testcase(self): 'debugger']['backtracelevels'], crashers_dir=self.testcase_base_dir, workdir_base=self.working_dir, + keep_faddr=self.cfg['runoptions'].get( + 'keep_unique_faddr', False), + save_failed_asserts=self.cfg['analyzer'].get( + 'savefailedasserts', False), exclude_unmapped_frames=self.cfg['analyzer']['exclude_unmapped_frames']) as testcase: # put it on the list for the analysis pipeline self.testcases.append(testcase) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index d3f975d..9e7ba48 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -113,8 +113,8 @@ def _construct_testcase(self): self.cmd_template, self.fuzzer.output_file_path)[1], dbg_opts=self.cfg['debugger'], workdir_base=self.working_dir, - keep_faddr=self.cfg['runoptions'][ - 'keep_unique_faddr'], + keep_faddr=self.cfg['runoptions'].get( + 'keep_unique_faddr', False), heisenbug_retries=self.retries, copy_fuzzedfile=self.fuzzer.fuzzed_changes_input) as testcase: diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index cee9cfe..27daf1d 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -26,6 +26,7 @@ class TestCaseBase(object): _debugger_cls = None def __init__(self, + cfg, seedfile, fuzzedfile, program, @@ -36,6 +37,7 @@ def __init__(self, logger.debug('Inititalize TestCaseBase') + self.cfg = cfg self.cmd_template = cmd_template self.copy_fuzzedfile = True self.dbg_file = None diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 5f6d364..3a21f77 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -42,9 +42,11 @@ def __init__(self, crashers_dir, workdir_base, keep_faddr=False, + save_failed_asserts=False, exclude_unmapped_frames=False): TestCaseBase.__init__(self, + cfg, seedfile, fuzzedfile, program, @@ -57,6 +59,7 @@ def __init__(self, self.cmdargs = None self.crash_base_dir = crashers_dir self.exclude_unmapped_frames = exclude_unmapped_frames + self.save_failed_asserts = save_failed_asserts self.set_debugger_template('bt_only') self.signature = None @@ -121,7 +124,7 @@ def confirm_crash(self): logger.debug('is_crash: %s is_assert_fail: %s', self.dbg.is_crash, self.dbg.is_assert_fail) - if self.cfg['analyzer']['savefailedasserts']: + if self.savefailedasserts: return self.dbg.is_crash else: # only keep real crashes (not failed assertions) diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 284011b..95038b0 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -57,6 +57,7 @@ def __init__(self, copy_fuzzedfile=True): TestCaseBase.__init__(self, + cfg, seedfile, fuzzedfile, program, From 7fbd7a973648385fb6e3abb6c44550b003fe9613 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 15:16:06 -0400 Subject: [PATCH 0974/1169] rename iteration base 3 to iteration base --- .../{iteration_base3.py => iteration_base.py} | 2 +- src/certfuzz/iteration/iteration_linux.py | 28 +++++++++---------- src/certfuzz/iteration/iteration_windows.py | 28 +++++++++---------- src/certfuzz/testcase/testcase_linux.py | 2 +- 4 files changed, 30 insertions(+), 30 deletions(-) rename src/certfuzz/iteration/{iteration_base3.py => iteration_base.py} (99%) diff --git a/src/certfuzz/iteration/iteration_base3.py b/src/certfuzz/iteration/iteration_base.py similarity index 99% rename from src/certfuzz/iteration/iteration_base3.py rename to src/certfuzz/iteration/iteration_base.py index 0219135..de8e64a 100644 --- a/src/certfuzz/iteration/iteration_base3.py +++ b/src/certfuzz/iteration/iteration_base.py @@ -19,7 +19,7 @@ MAX_IOERRORS = 5 -class IterationBase3(object): +class IterationBase(object): __metaclass__ = abc.ABCMeta _tmpdir_pfx = 'iteration_' _iteration_counter = 0 diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 805172b..e1f9391 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -8,7 +8,7 @@ from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools.ppid_observer import check_ppid -from certfuzz.iteration.iteration_base3 import IterationBase3 +from certfuzz.iteration.iteration_base import IterationBase from certfuzz.tc_pipeline.tc_pipeline_linux import LinuxTestCasePipeline from certfuzz.helpers.misc import fixup_path from certfuzz.testcase.testcase_linux import LinuxTestcase @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) -class LinuxIteration(IterationBase3): +class LinuxIteration(IterationBase): tcpipeline_cls = LinuxTestCasePipeline def __init__(self, @@ -32,17 +32,17 @@ def __init__(self, runner_cls=None, ): - IterationBase3.__init__(self, - seedfile=seedfile, - seednum=seednum, - workdirbase=workdirbase, - outdir=outdir, - sf_set=sf_set, - uniq_func=uniq_func, - config=config, - fuzzer_cls=fuzzer_cls, - runner_cls=runner_cls, - ) + IterationBase.__init__(self, + seedfile=seedfile, + seednum=seednum, + workdirbase=workdirbase, + outdir=outdir, + sf_set=sf_set, + uniq_func=uniq_func, + config=config, + fuzzer_cls=fuzzer_cls, + runner_cls=runner_cls, + ) self.testcase_base_dir = os.path.join(self.outdir, 'crashers') @@ -55,7 +55,7 @@ def __init__(self, def __enter__(self): check_ppid() - return IterationBase3.__enter__(self) + return IterationBase.__enter__(self) def _construct_testcase(self): with LinuxTestcase(cfg=self.cfg, diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 9e7ba48..883f41e 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -11,7 +11,7 @@ from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.file_handlers.tmp_reaper import TmpReaper from certfuzz.fuzztools.filetools import delete_files_or_dirs -from certfuzz.iteration.iteration_base3 import IterationBase3 +from certfuzz.iteration.iteration_base import IterationBase from certfuzz.tc_pipeline.tc_pipeline_windows import WindowsTestCasePipeline from certfuzz.fuzztools.command_line_templating import get_command_args_list @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) -class WindowsIteration(IterationBase3): +class WindowsIteration(IterationBase): tcpipeline_cls = WindowsTestCasePipeline def __init__(self, @@ -35,17 +35,17 @@ def __init__(self, runner_cls=None, debug=False, ): - IterationBase3.__init__(self, - seedfile=seedfile, - seednum=seednum, - workdirbase=workdirbase, - outdir=outdir, - sf_set=sf_set, - uniq_func=uniq_func, - config=config, - fuzzer_cls=fuzzer_cls, - runner_cls=runner_cls, - ) + IterationBase.__init__(self, + seedfile=seedfile, + seednum=seednum, + workdirbase=workdirbase, + outdir=outdir, + sf_set=sf_set, + uniq_func=uniq_func, + config=config, + fuzzer_cls=fuzzer_cls, + runner_cls=runner_cls, + ) self.debug = debug # TODO: do we use keep_uniq_faddr at all? @@ -71,7 +71,7 @@ def __init__(self, def __exit__(self, etype, value, traceback): try: - handled = IterationBase3.__exit__(self, etype, value, traceback) + handled = IterationBase.__exit__(self, etype, value, traceback) except WindowsError as e: logger.warning('Caught WindowsError in iteration exit: %s', e) handled = True diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 3a21f77..94be546 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -124,7 +124,7 @@ def confirm_crash(self): logger.debug('is_crash: %s is_assert_fail: %s', self.dbg.is_crash, self.dbg.is_assert_fail) - if self.savefailedasserts: + if self.save_failed_asserts: return self.dbg.is_crash else: # only keep real crashes (not failed assertions) From 056301d63184bd2308407faf22e989b7c1da13c7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 15:19:06 -0400 Subject: [PATCH 0975/1169] pep8 cleanup --- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index 9011d01..2468165 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -72,15 +72,18 @@ def _verify(self, testcase): keep_all = self.cfg['runoptions'].get('keep_duplicates', False) - tc.should_proceed_with_analysis = keep_all or (is_new_to_campaign and not crash_dir_found) + tc.should_proceed_with_analysis = keep_all or ( + is_new_to_campaign and not crash_dir_found) if tc.should_proceed_with_analysis: logger.info('%s first seen at %d', tc.signature, tc.seednum) self.dbg_out_file_orig = tc.dbg.file - logger.debug('Original debugger file: %s', self.dbg_out_file_orig) + logger.debug( + 'Original debugger file: %s', self.dbg_out_file_orig) self.success = True else: - logger.info('Testcase signature %s was already seen, skipping further analysis', tc.signature) + logger.info( + 'Testcase signature %s was already seen, skipping further analysis', tc.signature) else: logger.debug('not a crash, continuing') @@ -93,9 +96,11 @@ def _pre_analyze(self, testcase): # change the debugger template testcase.set_debugger_template('complete') else: - # use a debugger template that specifies fixed offsets from $pc for disassembly + # use a debugger template that specifies fixed offsets from $pc for + # disassembly testcase.set_debugger_template('complete_nofunction') - logger.info('Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) + logger.info( + 'Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) testcase.get_debug_output(testcase.fuzzedfile.path) if self.dbg_out_file_orig != testcase.dbg.file: @@ -103,9 +108,11 @@ def _pre_analyze(self, testcase): # remove the old one filetools.delete_files(self.dbg_out_file_orig) if os.path.exists(self.dbg_out_file_orig): - logger.warning('Failed to remove old debugger file %s', self.dbg_out_file_orig) + logger.warning( + 'Failed to remove old debugger file %s', self.dbg_out_file_orig) else: - logger.debug('Removed old debug file %s', self.dbg_out_file_orig) + logger.debug( + 'Removed old debug file %s', self.dbg_out_file_orig) def _post_analyze(self, testcase): if self.options.get('use_valgrind'): @@ -114,7 +121,8 @@ def _post_analyze(self, testcase): annotate_callgrind(testcase) annotate_callgrind_tree(testcase) except CallgrindAnnotateEmptyOutputFileError: - logger.warning('Unexpected empty output from annotate_callgrind. Continuing') + logger.warning( + 'Unexpected empty output from annotate_callgrind. Continuing') except CallgrindAnnotateMissingInputFileError: logger.warning('Missing callgrind output. Continuing') @@ -124,17 +132,22 @@ def _pre_report(self, testcase): # We know HD info, since we minimized if testcase.range is not None: # Fuzzer specifies a range - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) + uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, + testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) else: - uniqlogger.info('%s crash_id=%s seed=%d bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.hd_bits, testcase.hd_bytes) + uniqlogger.info('%s crash_id=%s seed=%d bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, + testcase.signature, testcase.seednum, testcase.hd_bits, testcase.hd_bytes) else: # We don't know the HD info if testcase.range is not None: # We have a fuzzer that uses a range - uniqlogger.info('%s crash_id=%s seed=%d range=%s', testcase.seedfile.basename, testcase.signature, testcase.seednum, testcase.range) + uniqlogger.info('%s crash_id=%s seed=%d range=%s', testcase.seedfile.basename, + testcase.signature, testcase.seednum, testcase.range) else: - uniqlogger.info('%s crash_id=%s seed=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum) - logger.info('%s first seen at %d', testcase.signature, testcase.seednum) + uniqlogger.info( + '%s crash_id=%s seed=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum) + logger.info( + '%s first seen at %d', testcase.signature, testcase.seednum) def _report(self, testcase): with CopyFilesReporter(testcase, self.tc_dir) as reporter: @@ -148,4 +161,3 @@ def _post_report(self, testcase): testcase.clean_tmpdir() # clean up testcase.delete_files() - From 699608838215b227d2767a192bcfdbbc35768ef2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 15:19:31 -0400 Subject: [PATCH 0976/1169] eliminate need for seed num in test case --- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index 2468165..a004bae 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -76,7 +76,7 @@ def _verify(self, testcase): is_new_to_campaign and not crash_dir_found) if tc.should_proceed_with_analysis: - logger.info('%s first seen at %d', tc.signature, tc.seednum) + logger.info('%s is new', tc.signature) self.dbg_out_file_orig = tc.dbg.file logger.debug( 'Original debugger file: %s', self.dbg_out_file_orig) From 6e5939b17dcbba1c2c3d37dccd90238605581037 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 15:34:53 -0400 Subject: [PATCH 0977/1169] fix a missed rename --- src/certfuzz/iteration/iteration_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_base.py b/src/certfuzz/iteration/iteration_base.py index de8e64a..4c0ce5d 100644 --- a/src/certfuzz/iteration/iteration_base.py +++ b/src/certfuzz/iteration/iteration_base.py @@ -87,7 +87,7 @@ def __exit__(self, etype, value, traceback): handled = False # increment the iteration counter - IterationBase3._iteration_counter += 1 + IterationBase._iteration_counter += 1 # increment the seedfile try counter self.seedfile.tries += 1 From b3c6e0c77d6d253e20f08edb4e9d64a77b05dc29 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 22 Jun 2016 15:35:36 -0400 Subject: [PATCH 0978/1169] get rid of crash-specific logging --- src/certfuzz/testcase/testcase_base.py | 32 ++++---------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 27daf1d..a76c986 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -58,8 +58,6 @@ def __init__(self, self.is_unique = False self.is_zipfile = False self.keep_uniq_faddr = keep_faddr - # this will get overridden by calls to get_logger - self.logger = None self.pc = None self.pc_in_function = False self.program = program @@ -176,8 +174,8 @@ def calculate_hamming_distances(self): logger.warning( 'Cannot find either sf_path or minimized file to calculate Hamming Distances') - self.logger.info("bitwise_hd=%d", self.hd_bits) - self.logger.info("bytewise_hd=%d", self.hd_bytes) + logger.info("crasher=%s bitwise_hd=%d", self.signature, self.hd_bits) + logger.info("crasher=%s bytewise_hd=%d", self.signature, self.hd_bytes) def calculate_hamming_distances_a(self): with open(self.fuzzedfile.path, 'rb') as fd: @@ -186,28 +184,8 @@ def calculate_hamming_distances_a(self): a_string = 'x' * len(fuzzed) self.hd_bits = hamming.bitwise_hd(a_string, fuzzed) - self.logger.info("bitwise_hd=%d", self.hd_bits) + logger.info("crasher=%s bitwise_hd=%d", self.signature, self.hd_bits) self.hd_bytes = hamming.bytewise_hd(a_string, fuzzed) - self.logger.info("bytewise_hd=%d", self.hd_bytes) - - def get_logger(self): - ''' - sets self.logger to a logger specific to this crash - ''' - self.logger = logging.getLogger(self.signature) - if len(self.logger.handlers) == 0: - if not os.path.exists(self.result_dir): - logger.error('Result path not found: %s', self.result_dir) - raise TestCaseError( - 'Result path not found: {}'.format(self.result_dir)) - logger.debug( - 'result_dir=%s sig=%s', self.result_dir, self.signature) - logfile = '%s.log' % self.signature - logger.debug('logfile=%s', logfile) - logpath = os.path.join(self.result_dir, logfile) - logger.debug('logpath=%s', logpath) - hdlr = logging.FileHandler(logpath) - self.logger.addHandler(hdlr) - - return self.logger + logger.info( + "crasher=%s bytewise_hd=%d", self.signature, self.hd_bytes) From e1a531c371672458c82946926535519e9a29ca68 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 23 Jun 2016 10:03:49 -0400 Subject: [PATCH 0979/1169] pep8 cleanup --- src/certfuzz/drillresults/result_driller_base.py | 12 +++++++----- src/certfuzz/drillresults/result_driller_linux.py | 13 +++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/drillresults/result_driller_base.py b/src/certfuzz/drillresults/result_driller_base.py index 29f5621..8b376fb 100644 --- a/src/certfuzz/drillresults/result_driller_base.py +++ b/src/certfuzz/drillresults/result_driller_base.py @@ -77,7 +77,8 @@ def _check_dirs(self): logger.debug('found dir: %s', self.tld) return # if you got here, none of them exist - raise DrillResultsError('None of {} appears to be a dir'.format(check_dirs)) + raise DrillResultsError( + 'None of {} appears to be a dir'.format(check_dirs)) def load_cached(self): if self.force: @@ -96,7 +97,7 @@ def crash_scores(self): return dict([(tcb.crash_hash, tcb.score) for tcb in self.testcase_bundles]) def print_crash_report(self, crash_key, score, details): -# details = self.results[crash_key] + # details = self.results[crash_key] print '\n%s - Exploitability rank: %s' % (crash_key, score) print 'Fuzzed file: %s' % details['fuzzedfile'] for exception in details['exceptions']: @@ -120,7 +121,8 @@ def sorted_crashes(self): return sorted(self.crash_scores.iteritems(), key=lambda(k, v): (v, k)) def print_reports(self): - results = dict([(tcb.crash_hash, tcb.details) for tcb in self.testcase_bundles]) + results = dict([(tcb.crash_hash, tcb.details) + for tcb in self.testcase_bundles]) print "--- Interesting crashes ---\n" for crash_key, score in self.sorted_crashes: if self.max_score is not None: @@ -132,7 +134,8 @@ def print_reports(self): try: self.print_crash_report(crash_key, score, details) except KeyError as e: - logger.warning('Tescase %s is missing information: %s', crash_key, e) + logger.warning( + 'Tescase %s is missing information: %s', crash_key, e) def cache_results(self): pkldir = os.path.dirname(self.pickle_file) @@ -148,4 +151,3 @@ def drill_results(self): self.process_testcases() self.print_reports() self.cache_results() - diff --git a/src/certfuzz/drillresults/result_driller_linux.py b/src/certfuzz/drillresults/result_driller_linux.py index e4238d3..2c025e6 100755 --- a/src/certfuzz/drillresults/result_driller_linux.py +++ b/src/certfuzz/drillresults/result_driller_linux.py @@ -12,22 +12,23 @@ class LinuxResultDriller(ResultDriller): + def _platform_find_testcases(self, crash_hash, files, root): - # Only use directories that are hashes + # Only use directories that are hashes # if "0x" in crash_hash: - # Create dictionary for hashes in results dictionary + # Create dictionary for hashes in results dictionary crasherfile = '' # Check each of the files in the hash directory for current_file in files: # Go through all of the .gdb files and parse them if current_file.endswith('.gdb'): -# if regex['gdb_report'].match(current_file): - #print 'checking %s' % current_file + # if regex['gdb_report'].match(current_file): + # print 'checking %s' % current_file dbg_file = os.path.join(root, current_file) logger.debug('found gdb file: %s', dbg_file) crasherfile = dbg_file.replace('.gdb', '') - #crasherfile = os.path.join(root, crasherfile) + # crasherfile = os.path.join(root, crasherfile) with TestCaseBundle(dbg_file, crasherfile, crash_hash, - self.ignore_jit) as tcb: + self.ignore_jit) as tcb: tcb.go() self.testcase_bundles.append(tcb) From f03f37f72397281758191fae6d886710c4c7dbc1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jun 2016 11:39:52 -0400 Subject: [PATCH 0980/1169] continue removing dependencies on seednum and range in test case --- src/certfuzz/reporters/testcase_logger.py | 20 ++++++++++--------- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 20 ++++--------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/certfuzz/reporters/testcase_logger.py b/src/certfuzz/reporters/testcase_logger.py index b595ce9..77a654b 100644 --- a/src/certfuzz/reporters/testcase_logger.py +++ b/src/certfuzz/reporters/testcase_logger.py @@ -4,6 +4,11 @@ @author: adh ''' from certfuzz.reporters.reporter_base import ReporterBase +import logging + + +logger = logging.getLogger(__name__) + class TestcaseLoggerReporter(ReporterBase): ''' @@ -12,12 +17,9 @@ class TestcaseLoggerReporter(ReporterBase): def go(self): tc = self.testcase - # whether it was unique or not, record some details for posterity - # record the details of this crash so we can regenerate it later if needed - tc.logger.info('seen in seedfile=%s at seed=%d range=%s outfile=%s', - tc.seedfile.basename, - tc.seednum, - tc.range, - tc.fuzzedfile.path - ) - tc.logger.info('PC=%s', tc.pc) + logger.info('crash=%s seen in seedfile=%s outfile=%s at pc=%s', + tc.signature, + tc.seedfile.basename, + tc.fuzzedfile.path, + tc.pc + ) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index a004bae..854af38 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -130,24 +130,12 @@ def _pre_report(self, testcase): uniqlogger = get_uniq_logger(self.options.get('uniq_log')) if testcase.hd_bits is not None: # We know HD info, since we minimized - if testcase.range is not None: - # Fuzzer specifies a range - uniqlogger.info('%s crash_id=%s seed=%d range=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, - testcase.signature, testcase.seednum, testcase.range, testcase.hd_bits, testcase.hd_bytes) - else: - uniqlogger.info('%s crash_id=%s seed=%d bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, - testcase.signature, testcase.seednum, testcase.hd_bits, testcase.hd_bytes) + uniqlogger.info('%s crash_id=%s bitwise_hd=%d bytewise_hd=%d', testcase.seedfile.basename, + testcase.signature, testcase.hd_bits, testcase.hd_bytes) else: # We don't know the HD info - if testcase.range is not None: - # We have a fuzzer that uses a range - uniqlogger.info('%s crash_id=%s seed=%d range=%s', testcase.seedfile.basename, - testcase.signature, testcase.seednum, testcase.range) - else: - uniqlogger.info( - '%s crash_id=%s seed=%d', testcase.seedfile.basename, testcase.signature, testcase.seednum) - logger.info( - '%s first seen at %d', testcase.signature, testcase.seednum) + uniqlogger.info( + '%s crash_id=%s', testcase.seedfile.basename, testcase.signature) def _report(self, testcase): with CopyFilesReporter(testcase, self.tc_dir) as reporter: From 38dc721f084b92c6e50d97b46051f765f949b01c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jun 2016 14:19:21 -0400 Subject: [PATCH 0981/1169] move cmdlist to base class --- src/certfuzz/iteration/iteration_linux.py | 4 ++++ src/certfuzz/testcase/testcase_base.py | 2 ++ src/certfuzz/testcase/testcase_linux.py | 8 +++----- src/certfuzz/testcase/testcase_windows.py | 3 ++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index e1f9391..9cb69ae 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -12,6 +12,7 @@ from certfuzz.tc_pipeline.tc_pipeline_linux import LinuxTestCasePipeline from certfuzz.helpers.misc import fixup_path from certfuzz.testcase.testcase_linux import LinuxTestcase +from certfuzz.fuzztools.command_line_templating import get_command_args_list logger = logging.getLogger(__name__) @@ -64,6 +65,9 @@ def _construct_testcase(self): program=self.cfg['target']['program'], cmd_template=self.cmd_template, debugger_timeout=self.cfg['runner']['runtimeout'], + cmdlist=get_command_args_list(self.cmd_template, + infile=self.fuzzedfile.path, + posix=True)[1], backtrace_lines=self.cfg[ 'debugger']['backtracelevels'], crashers_dir=self.testcase_base_dir, diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index a76c986..8dc06ad 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -32,6 +32,7 @@ def __init__(self, program, cmd_template, workdir_base, + cmdlist, keep_faddr=False, dbg_timeout=30): @@ -39,6 +40,7 @@ def __init__(self, self.cfg = cfg self.cmd_template = cmd_template + self.cmdlist = cmdlist self.copy_fuzzedfile = True self.dbg_file = None self.debugger_missed_stack_corruption = False diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 94be546..9a2f081 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -38,6 +38,7 @@ def __init__(self, program, cmd_template, debugger_timeout, + cmdlist, backtrace_lines, crashers_dir, workdir_base, @@ -52,11 +53,12 @@ def __init__(self, program, cmd_template, workdir_base, + cmdlist, keep_faddr, debugger_timeout) self.backtrace_lines = backtrace_lines - self.cmdargs = None + self.cmdargs = self.cmdlist[1:] self.crash_base_dir = crashers_dir self.exclude_unmapped_frames = exclude_unmapped_frames self.save_failed_asserts = save_failed_asserts @@ -77,10 +79,6 @@ def set_debugger_template(self, option='bt_only'): def update_crash_details(self): TestCaseBase.update_crash_details(self) - cmdlist = get_command_args_list(self.cmd_template, - infile=self.fuzzedfile.path, - posix=True)[1] - self.cmdargs = cmdlist[1:] self.is_crash = self.confirm_crash() if self.is_crash: diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index 95038b0..b176c91 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -63,10 +63,11 @@ def __init__(self, program, cmd_template, workdir_base, + cmdlist, keep_faddr, debugger_timeout) - self.cmdargs = cmdlist + self.cmdargs = self.cmdlist self.copy_fuzzedfile = copy_fuzzedfile self.crash_hash = None self.dbg_file = '' From 6c817504abc14e91b2a9eb8f08239110d2610e9e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jun 2016 14:38:48 -0400 Subject: [PATCH 0982/1169] harmonize names across linux/windows --- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 2 +- src/certfuzz/testcase/testcase_base.py | 4 ++-- src/certfuzz/testcase/testcase_linux.py | 10 +++++----- src/test_certfuzz/testcase/test_testcase_base.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index 854af38..b00ae8b 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -68,7 +68,7 @@ def _verify(self, testcase): # fall back to checking if the crash directory exists # - crash_dir_found = filetools.find_or_create_dir(tc.result_dir) + crash_dir_found = filetools.find_or_create_dir(tc.target_dir) keep_all = self.cfg['runoptions'].get('keep_duplicates', False) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 8dc06ad..99abeb1 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -63,7 +63,7 @@ def __init__(self, self.pc = None self.pc_in_function = False self.program = program - self.result_dir = None + self.target_dir = None self.seedfile = seedfile self.should_proceed_with_analysis = False self.signature = None @@ -141,7 +141,7 @@ def delete_files(self): def get_debug_output(self, f): raise NotImplementedError - def get_result_dir(self): + def _get_output_dir(self): raise NotImplementedError def get_signature(self): diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 9a2f081..308cced 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -84,14 +84,14 @@ def update_crash_details(self): if self.is_crash: self.signature = self.get_signature() self.pc = self.dbg.registers_hex.get(self.dbg.pc_name) - self.result_dir = self.get_result_dir() + self.target_dir = self._get_output_dir() self.debugger_missed_stack_corruption = self.dbg.debugger_missed_stack_corruption self.total_stack_corruption = self.dbg.total_stack_corruption self.pc_in_function = self.dbg.pc_in_function self.faddr = self.dbg.faddr logger.debug('sig: %s', self.signature) logger.debug('pc: %s', self.pc) - logger.debug('result_dir: %s', self.result_dir) + logger.debug('target_dir: %s', self.target_dir) else: # clean up on non-crashes self.delete_files() @@ -167,10 +167,10 @@ def _verify_crash_base_dir(self): filetools.mkdir_p(self.crash_base_dir) - def get_result_dir(self): + def _get_output_dir(self): assert self.crash_base_dir assert self.signature self._verify_crash_base_dir() - self.result_dir = os.path.join(self.crash_base_dir, self.signature) + self.target_dir = os.path.join(self.crash_base_dir, self.signature) - return self.result_dir + return self.target_dir diff --git a/src/test_certfuzz/testcase/test_testcase_base.py b/src/test_certfuzz/testcase/test_testcase_base.py index 480e8cc..74fec0e 100644 --- a/src/test_certfuzz/testcase/test_testcase_base.py +++ b/src/test_certfuzz/testcase/test_testcase_base.py @@ -47,10 +47,10 @@ def test_init(self): def test_get_logger(self): self.tc.signature = 'signature' - self.tc.result_dir = 'does_not_exist' + self.tc.target_dir = 'does_not_exist' self.assertRaises(TestCaseError, self.tc.get_logger) - self.tc.result_dir = tempfile.mkdtemp(suffix='-results', + self.tc.target_dir = tempfile.mkdtemp(suffix='-results', prefix='bff-test-', dir=self.tmpdir) From 04f06a69bdaf44ecc8a45ed377b99b60882af7e1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 23 Jun 2016 14:39:15 -0400 Subject: [PATCH 0983/1169] refactor for readability --- src/certfuzz/testcase/testcase_linux.py | 53 +++++++++++++++---------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 308cced..3882be6 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -137,28 +137,39 @@ def get_signature(self): Runs the debugger on the crash and gets its signature. @raise CrasherHasNoSignatureError: if it's a valid crash, but we don't get a signature ''' + # short circuit if we already know the sig + if self.signature: + return self.signature + + # sig wasn't set, so let's find out what it is + self.signature = self.dbg.get_testcase_signature( + self.backtrace_lines) + if not self.signature: - self.signature = self.dbg.get_testcase_signature( - self.backtrace_lines) - if self.signature: - logger.debug("TestCaseBase signature is %s", self.signature) - else: - raise TestCaseError('TestCaseBase has no signature.') - if self.dbg.total_stack_corruption: - # total_stack_corruption. Use pin calltrace to get a backtrace - analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self) - try: - analyzer_instance.go() - except AnalyzerEmptyOutputError: - logger.warning( - 'Unexpected empty output from pin. Cannot determine call trace.') - return self.signature - - calltrace = Calltracefile(analyzer_instance.outfile) - pinsignature = calltrace.get_testcase_signature( - self.backtrace_lines * 10) - if pinsignature: - self.signature = pinsignature + raise TestCaseError('Testcase has no signature.') + + logger.debug("Testcase signature is %s", self.signature) + + if not self.dbg.total_stack_corruption: + return self.signature + + # total_stack_corruption. + # Use pin calltrace to get a backtrace + analyzer_instance = pin_calltrace.Pin_calltrace(self.cfg, self) + try: + analyzer_instance.go() + except AnalyzerEmptyOutputError: + logger.warning( + 'Unexpected empty output from pin. Cannot determine call trace.') + return self.signature + + calltrace = Calltracefile(analyzer_instance.outfile) + pinsignature = calltrace.get_testcase_signature( + self.backtrace_lines * 10) + + if pinsignature: + self.signature = pinsignature + return self.signature def _verify_crash_base_dir(self): From 4b802a3e5226e2217ea8acd984c12927725148d6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 24 Jun 2016 12:53:49 -0400 Subject: [PATCH 0984/1169] update doctoring --- src/certfuzz/testcase/testcase_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 3882be6..a58458e 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -135,7 +135,7 @@ def __repr__(self): def get_signature(self): ''' Runs the debugger on the crash and gets its signature. - @raise CrasherHasNoSignatureError: if it's a valid crash, but we don't get a signature + @raise TestCaseError: if it's a valid crash, but we don't get a signature ''' # short circuit if we already know the sig if self.signature: From a797da4ccaa071e78831b8b62919449e56151a6d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 24 Jun 2016 12:54:16 -0400 Subject: [PATCH 0985/1169] remove duplicate method signature --- src/certfuzz/testcase/testcase_base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 99abeb1..02290e6 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -141,9 +141,6 @@ def delete_files(self): def get_debug_output(self, f): raise NotImplementedError - def _get_output_dir(self): - raise NotImplementedError - def get_signature(self): raise NotImplementedError From 3e501d39dab9cb47fd7690febbe40e8af40af1c4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 24 Jun 2016 13:00:28 -0400 Subject: [PATCH 0986/1169] fix unit tests --- .../reporters/test_testcase_logger.py | 6 ++--- .../testcase/test_testcase_base.py | 23 +++++++------------ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/test_certfuzz/reporters/test_testcase_logger.py b/src/test_certfuzz/reporters/test_testcase_logger.py index ccbeaff..131933b 100644 --- a/src/test_certfuzz/reporters/test_testcase_logger.py +++ b/src/test_certfuzz/reporters/test_testcase_logger.py @@ -7,17 +7,15 @@ import certfuzz.reporters.testcase_logger from test_certfuzz.mocks import MockTestcase -class Test(unittest.TestCase): +class Test(unittest.TestCase): def setUp(self): pass - def tearDown(self): pass - def test_go(self): import logging import io @@ -40,7 +38,7 @@ def test_go(self): log_capture_string.close() - for x in ['seen in', 'at seed', 'range', 'outfile', 'PC']: + for x in ['seen in', 'outfile', 'PC']: self.assertTrue(x in log_contents, '"{}" not in log'.format(x)) diff --git a/src/test_certfuzz/testcase/test_testcase_base.py b/src/test_certfuzz/testcase/test_testcase_base.py index 74fec0e..f5970c8 100644 --- a/src/test_certfuzz/testcase/test_testcase_base.py +++ b/src/test_certfuzz/testcase/test_testcase_base.py @@ -19,13 +19,19 @@ def setUp(self): self.tmpdir = tempfile.mkdtemp(prefix='bff-test-') self.sf = MockSeedfile() self.ff = MockFuzzedFile() + cfg = {} program = 'foo' cmd_template = 'foo a b c' + workdir_base = os.path.join(self.tmpdir, 'workdir_base') + cmdlist = cmd_template.split() - self.tc = certfuzz.testcase.testcase_base.TestCaseBase(self.sf, + self.tc = certfuzz.testcase.testcase_base.TestCaseBase(cfg, + self.sf, self.ff, program, - cmd_template,) + cmd_template, + workdir_base, + cmdlist,) pass def tearDown(self): @@ -45,19 +51,6 @@ def test_init(self): self.assertFalse(self.tc.total_stack_corruption) self.assertFalse(self.tc.pc_in_function) - def test_get_logger(self): - self.tc.signature = 'signature' - self.tc.target_dir = 'does_not_exist' - self.assertRaises(TestCaseError, self.tc.get_logger) - - self.tc.target_dir = tempfile.mkdtemp(suffix='-results', - prefix='bff-test-', - dir=self.tmpdir) - - self.tc.logger = None - x = self.tc.get_logger() - self.assertTrue(isinstance(x, logging.Logger)) - def test_calculate_hamming_distances(self): tc = self.tc From 21e495fd335c39fd00e82b61988baf6c2f5b63bf Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 24 Jun 2016 14:42:21 -0400 Subject: [PATCH 0987/1169] bugfix --- src/certfuzz/iteration/iteration_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 9cb69ae..cc80032 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -66,7 +66,7 @@ def _construct_testcase(self): cmd_template=self.cmd_template, debugger_timeout=self.cfg['runner']['runtimeout'], cmdlist=get_command_args_list(self.cmd_template, - infile=self.fuzzedfile.path, + infile=self.fuzzer.output_file_path, posix=True)[1], backtrace_lines=self.cfg[ 'debugger']['backtracelevels'], From c80fa9ec935c78e6df58230a84764214da6607a5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 27 Jun 2016 15:13:33 -0400 Subject: [PATCH 0988/1169] pep8 cleanup --- .../drillresults/result_driller_windows.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index 566a58e..eda0232 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -12,12 +12,13 @@ logger = logging.getLogger(__name__) regex = { - 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]+.+e0.+'), - 'msec_report': re.compile('.+.msec$'), - } + 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]+.+e0.+'), + 'msec_report': re.compile('.+.msec$'), +} class WindowsResultDriller(ResultDriller): + def _platform_find_testcases(self, crash_hash, files, root): if "0x" in crash_hash: # Create dictionary for hashes in results dictionary @@ -42,13 +43,15 @@ def _platform_find_testcases(self, crash_hash, files, root): if crasherfile and root not in crasherfile: crasherfile = os.path.join(root, crasherfile) with TestCaseBundle(dbg_file, crasherfile, crash_hash, - self.ignore_jit) as tcb: + self.ignore_jit) as tcb: tcb.go() _updated_existing = False for index, tcbundle in enumerate(self.testcase_bundles): if tcbundle.crash_hash == crash_hash: - # This is a new exception for the same crash hash - self.testcase_bundles[index].details['exceptions'].update(tcb.details['exceptions']) + # This is a new exception for the same crash + # hash + self.testcase_bundles[index].details[ + 'exceptions'].update(tcb.details['exceptions']) _updated_existing = True if not _updated_existing: # This is a new crash hash From 11a6e770c1671f89a2c7ac1a4bfc9cac02b35557 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 27 Jun 2016 15:17:36 -0400 Subject: [PATCH 0989/1169] Ratchet score on each exception parsed for a given crash_hash. BFF-955 --- src/certfuzz/drillresults/result_driller_windows.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index eda0232..866bf51 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -52,6 +52,10 @@ def _platform_find_testcases(self, crash_hash, files, root): # hash self.testcase_bundles[index].details[ 'exceptions'].update(tcb.details['exceptions']) + # If the current exception score is lower than + # the existing crash_hash score, update it + self.testcase_bundles[index].score = min( + self.testcase_bundles[index].score, tcb.score) _updated_existing = True if not _updated_existing: # This is a new crash hash From f74c0beeedb39cd406c101864c86baaf174b284f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 28 Jun 2016 14:16:11 -0400 Subject: [PATCH 0990/1169] Windows testcase object needs a cfg argument passed. BFF-957 --- src/certfuzz/iteration/iteration_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 883f41e..54185f2 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -102,7 +102,7 @@ def _tidy(self): TmpReaper().clean_tmp() def _construct_testcase(self): - with WindowsTestcase(seedfile=self.seedfile, + with WindowsTestcase(cfg=self.cfg, seedfile=self.seedfile, fuzzedfile=BasicFile( self.fuzzer.output_file_path), program=self.cfg['target']['program'], From 736cc55727e238ffccdda719c3338cbe3895af50 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 28 Jun 2016 16:29:46 -0400 Subject: [PATCH 0991/1169] Fix standalone minimizer WindowsTestcase arguments. --- src/certfuzz/tools/windows/minimize.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 628653c..dbc4f90 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -166,9 +166,10 @@ def main(): fuzzedfile=fuzzed_file, program=cfg['target']['program'], cmd_template=cfg['target']['cmdline_template'], + debugger_timeout=cfg['debugger']['runtimeout'], cmdlist=cmd_as_args, dbg_opts=cfg['debugger'], - workingdir_base=outdir, + workdir_base=outdir, keep_faddr=options.keep_uniq_faddr, heisenbug_retries=retries ) as testcase: From 6434a48bf1554f05f3af59ff849b842615429f5c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 1 Jul 2016 10:45:10 -0400 Subject: [PATCH 0992/1169] No need for quiet_flag anymore. We run an iteration at the beginning of the campaign with output turned on. BFF-952 --- src/certfuzz/iteration/iteration_base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base.py b/src/certfuzz/iteration/iteration_base.py index 4c0ce5d..3991f9b 100644 --- a/src/certfuzz/iteration/iteration_base.py +++ b/src/certfuzz/iteration/iteration_base.py @@ -149,10 +149,6 @@ def __exit__(self, etype, value, traceback): return handled - @property - def quiet_flag(self): - return self._iteration_counter < 2 - def _pre_fuzz(self): self.fuzzer = self.fuzzer_cls( self.seedfile, self.working_dir, self.seednum, self._fuzz_opts) @@ -171,7 +167,6 @@ def _post_fuzz(self): def _pre_run(self): fuzzed_file = self.fuzzer.output_file_path workingdir_base = self.working_dir - self._runner_options['hideoutput'] = self.quiet_flag self.cmd_template = self.cfg['target']['cmdline_template'] self.runner = self.runner_cls( self._runner_options, self.cmd_template, fuzzed_file, workingdir_base) From c8eb387a97511c0122185392e840af47d23ce867 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 1 Jul 2016 16:03:19 -0400 Subject: [PATCH 0993/1169] pep8 cleanup --- .../drillresults/testcasebundle_base.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_base.py b/src/certfuzz/analyzers/drillresults/testcasebundle_base.py index 918b04d..8332bfe 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_base.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_base.py @@ -37,7 +37,8 @@ def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit=False): self.regdict = {} if not os.path.exists(self.dbg_outfile): - raise TestCaseBundleError('Debugger file not found: {}'.format(self.dbg_outfile)) + raise TestCaseBundleError( + 'Debugger file not found: {}'.format(self.dbg_outfile)) self.reporttext = read_text_file(self.dbg_outfile) self._find_testcase_file() @@ -100,7 +101,8 @@ def __exit__(self, etype, value, traceback): def _find_testcase_file(self): if not os.path.isfile(self.testcase_file): # Can't find the crasher file - raise TestCaseBundleError('Cannot find testcase file %s', self.testcase_file) + raise TestCaseBundleError( + 'Cannot find testcase file %s', self.testcase_file) @abc.abstractmethod def _get_classification(self): @@ -156,11 +158,13 @@ def _parse_testcase(self): faultaddr, instraddr = self._64bit_addr_fixup(faultaddr, instraddr) if instraddr: - self.details['exceptions'][exceptionnum]['pcmodule'] = self.pc_in_mapped_address(instraddr) + self.details['exceptions'][exceptionnum][ + 'pcmodule'] = self.pc_in_mapped_address(instraddr) # Get the cdb line that contains the crashing instruction instructionline = self.get_instr(instraddr) - self.details['exceptions'][exceptionnum]['instructionline'] = instructionline + self.details['exceptions'][exceptionnum][ + 'instructionline'] = instructionline if instructionline: self.instructionpieces = instructionline.split() faultaddr = self._prefix_0x(faultaddr) @@ -168,7 +172,6 @@ def _parse_testcase(self): if self.shortdesc == 'ReturnAv': faultaddr = self.fix_return_efa(faultaddr) - # Fix faulting pattern endian faultaddr = faultaddr.replace('0x', '') @@ -280,7 +283,8 @@ def _match_rgx(self, rgx, return_value_func): def _record_exception_info(self, exceptionnum): if self.classification: # Create a new exception dictionary to add to the crash - self.details['exceptions'][exceptionnum] = {'classification': self.classification} + self.details['exceptions'][exceptionnum] = { + 'classification': self.classification} if not self.shortdesc: logger.debug('no short description') @@ -305,7 +309,7 @@ def _score_interesting(self): if exception['shortdesc'] in self.re_set: if eif: - # The faulting address pattern is in the fuzzed file + # The faulting address pattern is in the fuzzed file if '0x000000' in efa: # Faulting address is near null scores.append(30) @@ -324,7 +328,6 @@ def _score_interesting(self): return scores - def _get_efa_mod_eif(self, exception): try: efa = '0x' + exception['efa'] @@ -360,7 +363,7 @@ def _score_less_interesting(self): # non-continued potential stack buffer overflow scores.append(40) elif eif: - # The faulting address pattern is in the fuzzed file + # The faulting address pattern is in the fuzzed file if '0x000000' in efa: # Faulting address is near null scores.append(70) @@ -402,7 +405,8 @@ def fix_return_efa(self, faultaddr): ''' if int(faultaddr, base=16) == 0: derived_faultaddr = self.get_return_addr() - logger.debug('New faulting address derived from backtrace: %s' % derived_faultaddr) + logger.debug( + 'New faulting address derived from backtrace: %s' % derived_faultaddr) if derived_faultaddr is not None: faultaddr = derived_faultaddr return faultaddr From fd3ee8a6a5b26778f89acbf4373cdfebbcc06e4b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 1 Jul 2016 16:03:52 -0400 Subject: [PATCH 0994/1169] work-in-progress - windows multi-exception testcase bundles / drillresults --- .../analyzers/drillresults/drillresults.py | 21 ++++++++++++++++++- src/certfuzz/testcase/testcase_base.py | 2 +- src/certfuzz/testcase/testcase_windows.py | 18 +++++++++------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/drillresults.py b/src/certfuzz/analyzers/drillresults/drillresults.py index 7c4f34a..e53ab80 100644 --- a/src/certfuzz/analyzers/drillresults/drillresults.py +++ b/src/certfuzz/analyzers/drillresults/drillresults.py @@ -73,7 +73,7 @@ def _write_outfile(self): def go(self): # turn testcase into tescase_bundle - with self._tcb_cls(dbg_outfile=self.testcase.dbg_file, + with self._tcb_cls(dbg_outfile=self.testcase.dbg_files[0], testcase_file=self.testcase.fuzzedfile.path, crash_hash=self.testcase.signature, ignore_jit=False) as tcb: @@ -84,6 +84,25 @@ def go(self): 'Skipping drillresults on testcase %s: %s', self.testcase.signature, e) return + for index, exception in enumerate(self.testcase.dbg_files): + dbg_file = self.testcase.dbg_files[exception] + if exception > 0: + with self._tcb_cls(dbg_outfile=self.testcase.dbg_files[exception], + testcase_file=self.testcase.fuzzedfile.path, + crash_hash=self.testcase.signature, + ignore_jit=False) as temp_tcb: + try: + temp_tcb.go() + except TestCaseBundleError as e: + logger.warning( + 'Skipping drillresults on testcase %s: %s', self.testcase.signature, e) + continue + + tcb.details['exceptions'].update( + temp_tcb.details['exceptions']) + + tcb.score = min(tcb.score, temp_tcb.score) + self._process_tcb(tcb) self._write_outfile() # if score < max_score do something (more interesting) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 02290e6..247ea31 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -85,7 +85,7 @@ def __repr__(self): def _get_output_dir(self, *args): raise NotImplementedError - def _rename_dbg_file(self): + def _rename_dbg_files(self): raise NotImplementedError def _rename_fuzzed_file(self): diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index b176c91..d898bfe 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -70,7 +70,7 @@ def __init__(self, self.cmdargs = self.cmdlist self.copy_fuzzedfile = copy_fuzzedfile self.crash_hash = None - self.dbg_file = '' + self.dbg_files = {} self.dbg_opts = dbg_opts self.dbg_result = {} self.exception_depth = 0 @@ -111,7 +111,7 @@ def update_crash_details(self): self.cmdargs = cmdlist[1:] self.debug() self._rename_fuzzed_file() - self._rename_dbg_file() + self._rename_dbg_files() def debug_once(self): outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) @@ -154,8 +154,8 @@ def debug_once(self): self.crash_hash += '.' + current_exception_faddr # The first exception is the one that is representative for the crasher + self.dbg_files[self.exception_depth] = debugger.outfile if self.exception_depth == 0: - self.dbg_file = debugger.outfile # add debugger results to our own attributes self.is_crash = self.parsed_outputs[0].is_crash self.dbg_type = self.parsed_outputs[0]._key @@ -244,17 +244,18 @@ def _rename_fuzzed_file(self): # replace fuzzed file self.fuzzedfile = BasicFile(new_fuzzed_file) - def _rename_dbg_file(self): + def _rename_dbg_files(self): if not self.faddr: return - (path, basename) = os.path.split(self.dbg_file) + (path, basename) = os.path.split(self.dbg_files[0]) (basename, dbgext) = os.path.splitext(basename) (root, ext) = os.path.splitext(basename) for exception_num in range(0, self.exception_depth + 1): if exception_num > 0: new_basename = root + ext + '.e%s' % exception_num + dbgext - self.dbg_file = os.path.join(path, new_basename) + self.dbg_files[exception_num] = os.path.join( + path, new_basename) if not self.parsed_outputs[exception_num].is_crash: return @@ -274,6 +275,7 @@ def _rename_dbg_file(self): # best_effort move returns a tuple of booleans indicating (copied, deleted) # we only care about copied - copied = best_effort_move(self.dbg_file, new_dbg_file)[0] + copied = best_effort_move( + self.dbg_files[exception_num], new_dbg_file)[0] if copied: - self.dbg_file = new_dbg_file + self.dbg_files[exception_num] = new_dbg_file From f236b8973f4d6534633a26dd1decf21a97b1de67 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 1 Jul 2016 16:19:36 -0400 Subject: [PATCH 0995/1169] Get linux working as well. BFF-954 --- src/certfuzz/analyzers/drillresults/drillresults.py | 5 ++++- src/certfuzz/testcase/testcase_base.py | 1 + src/certfuzz/testcase/testcase_linux.py | 2 +- src/certfuzz/testcase/testcase_windows.py | 1 - 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/drillresults.py b/src/certfuzz/analyzers/drillresults/drillresults.py index e53ab80..f28b3d5 100644 --- a/src/certfuzz/analyzers/drillresults/drillresults.py +++ b/src/certfuzz/analyzers/drillresults/drillresults.py @@ -73,6 +73,7 @@ def _write_outfile(self): def go(self): # turn testcase into tescase_bundle + # Get crash details for first exception with self._tcb_cls(dbg_outfile=self.testcase.dbg_files[0], testcase_file=self.testcase.fuzzedfile.path, crash_hash=self.testcase.signature, @@ -84,8 +85,10 @@ def go(self): 'Skipping drillresults on testcase %s: %s', self.testcase.signature, e) return + # Get temporary testase bundle for exceptions beyond the first + # Update tcb with those exceptions, updating cumulative score + # On any platform other that Windows, this is a no-op for index, exception in enumerate(self.testcase.dbg_files): - dbg_file = self.testcase.dbg_files[exception] if exception > 0: with self._tcb_cls(dbg_outfile=self.testcase.dbg_files[exception], testcase_file=self.testcase.fuzzedfile.path, diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 247ea31..6cc6aeb 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -43,6 +43,7 @@ def __init__(self, self.cmdlist = cmdlist self.copy_fuzzedfile = True self.dbg_file = None + self.dbg_files = {} self.debugger_missed_stack_corruption = False self.debugger_template = None self.debugger_timeout = dbg_timeout diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index a58458e..c3759a3 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -111,7 +111,7 @@ def get_debug_output(self, outfile_base): keep_uniq_faddr=self.keep_uniq_faddr ) self.dbg = debugger_obj.go() - self.dbg_file = self.dbg.file + self.dbg_files[0] = self.dbg.file def confirm_crash(self): # get debugger output diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index d898bfe..af16944 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -70,7 +70,6 @@ def __init__(self, self.cmdargs = self.cmdlist self.copy_fuzzedfile = copy_fuzzedfile self.crash_hash = None - self.dbg_files = {} self.dbg_opts = dbg_opts self.dbg_result = {} self.exception_depth = 0 From fe78c9da33de611fed74e94efdece442ebb9bb10 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 1 Jul 2016 16:28:08 -0400 Subject: [PATCH 0996/1169] Fix unit test --- .../drillresults/test_drillresults.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/test_certfuzz/analyzers/drillresults/test_drillresults.py b/src/test_certfuzz/analyzers/drillresults/test_drillresults.py index e9acfbe..cea0f72 100644 --- a/src/test_certfuzz/analyzers/drillresults/test_drillresults.py +++ b/src/test_certfuzz/analyzers/drillresults/test_drillresults.py @@ -13,6 +13,7 @@ go_count = 0 process_count = 0 + class MockTcb(object): details = {'fuzzedfile': 'foo', 'exceptions': {}, } @@ -32,13 +33,14 @@ def go(self): global go_count go_count += 1 + def _inc_proc_count(*args): global process_count process_count += 1 -class Test(unittest.TestCase): +class Test(unittest.TestCase): def setUp(self): global go_count @@ -53,12 +55,10 @@ def setUp(self): self.tmpdir = tempfile.mkdtemp() - def tearDown(self): shutil.rmtree(self.tmpdir) pass - def testInit(self): self.assertTrue('debugger' in self.dra.cfg) @@ -66,11 +66,12 @@ def test_go(self): fd, ff = tempfile.mkstemp(prefix='fuzzed-', dir=self.tmpdir) os.close(fd) - dbgf = os.path.join(self.tmpdir, '{}.{}'.format(ff, self.dra.testcase.debugger_extension)) + dbgf = os.path.join( + self.tmpdir, '{}.{}'.format(ff, self.dra.testcase.debugger_extension)) # touch the file open(dbgf, 'w').close() - self.dra.testcase.dbg_file = dbgf + self.dra.testcase.dbg_files = {0: 'dbgf'} self.dra.testcase.fuzzedfile = MockFuzzedFile(ff) self.dra._tcb_cls = MockTcb self.dra._process_tcb = _inc_proc_count @@ -95,10 +96,12 @@ def test_process_tcb(self): self.assertTrue(MockTcb.crash_hash in self.dra.output_lines[0]) self.assertTrue(str(MockTcb.score) in self.dra.output_lines[0]) - self.assertTrue(MockTcb.details['fuzzedfile'] in self.dra.output_lines[1]) + self.assertTrue( + MockTcb.details['fuzzedfile'] in self.dra.output_lines[1]) def test_write_outfile(self): - fd, f = tempfile.mkstemp(suffix='-fuzzed', prefix='test-', dir=self.tmpdir) + fd, f = tempfile.mkstemp( + suffix='-fuzzed', prefix='test-', dir=self.tmpdir) os.close(fd) self.dra.output_lines = ['a', 'b', 'c'] @@ -118,7 +121,8 @@ def test_write_outfile(self): def test_getfile(self): x = 'asdfghjklqwertyuiop' self.assertTrue(drillresults.get_file(x).startswith(x)) - self.assertTrue(drillresults.get_file(x).endswith(drillresults.OUTFILE_EXT)) + self.assertTrue( + drillresults.get_file(x).endswith(drillresults.OUTFILE_EXT)) if __name__ == "__main__": From b049493c76af5bee6b537707eedb7d3ed9e8b3a6 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 4 Jul 2016 11:10:16 -0400 Subject: [PATCH 0997/1169] Make sure quickstats.sh is only looking in the crashers directory --- src/linux/quickstats.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/linux/quickstats.sh b/src/linux/quickstats.sh index 7be8006..ce53c78 100755 --- a/src/linux/quickstats.sh +++ b/src/linux/quickstats.sh @@ -3,13 +3,13 @@ platform=`uname -a` echo "Exploitability Summary for campaign thus far:" if [[ "$platform" =~ "Darwin" ]]; then - exploitable=`find -L ~/results -name '*.cw' | xargs grep is_exploitable=y | awk -Fcrashers/ '{print $2}' | awk -F/ '{print $1}' | sort | uniq | wc -l` - total=`find -L ~/results -name '*.cw' | grep -v gmalloc | wc -l` + exploitable=`find -L ~/results -name '*.cw' | grep crashers | xargs grep is_exploitable=y | awk -Fcrashers/ '{print $2}' | awk -F/ '{print $1}' | sort | uniq | wc -l` + total=`find -L ~/results -name '*.cw' | grep crashers | grep -v gmalloc | wc -l` not_exploitable=$(expr $total - $exploitable) echo $exploitable Exploitable echo $not_exploitable Unknown echo $total Total else - find -L ~/results -name '*.gdb' | xargs grep -h "Exploitability Classification" | cut -d" " -f3 | sort | uniq -c - echo `find -L ~/results -name '*.gdb' | wc -l` TOTAL + find -L ~/results -name '*.gdb' | grep crashers | xargs grep -h "Exploitability Classification" | cut -d" " -f3 | sort | uniq -c + echo `find -L ~/results -name '*.gdb' | grep crashers | wc -l` TOTAL fi \ No newline at end of file From dca1936861ece3b308cce641aa774901f7824557 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 5 Jul 2016 12:28:52 -0400 Subject: [PATCH 0998/1169] pep8 cleanup --- .../output_parsers/debugger_file_base.py | 142 +++++++++++------- 1 file changed, 84 insertions(+), 58 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py index f4e7d62..7dc3981 100644 --- a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py +++ b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py @@ -18,30 +18,30 @@ registers = ('eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', 'edi', 'eip', 'cs', 'ss', 'ds', 'es', 'fs', 'gs') registers64 = ('rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi', 'rbp', - 'rsp', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', 'r14', - 'r15', 'rip', 'cs', 'ss', 'ds', 'es', 'fs', 'gs') + 'rsp', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', 'r14', + 'r15', 'rip', 'cs', 'ss', 'ds', 'es', 'fs', 'gs') regex = { - 'bt_line_basic': re.compile(r'^#\d'), - 'bt_line': re.compile(r'^#\d+\s+(.*)$'), - 'bt_function': re.compile(r'.+in\s+(\S+)\s+\('), - 'bt_at': re.compile(r'\s+at\s+(\S+:\d+)'), - 'bt_addr': re.compile(r'(0x[0-9a-fA-F]+)\s+.+$'), - 'signal': re.compile(r'Program\sreceived\ssignal\s+([^,]+)'), - 'exit_code': re.compile(r'Program exited with code (\d+)'), - 'faddr': re.compile(r'^si_addr.+(0x[0-9a-zA-Z]+)$'), - 'bt_line_from': re.compile(r'\bfrom\b'), - 'bt_line_at': re.compile(r'\bat\b'), - 'register': re.compile(r'(0x[0-9a-zA-Z]+)\s+(.+)$'), - 'libc_location': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+.+/libc[-.]'), - 'libgcc_location': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+.+/libgcc(_s)?[-.]'), - 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)'), - 'gdb_bt_threads': re.compile(r'^\[New Thread.+'), - 'konqi_bt_threads': re.compile(r'^\[Current thread is \d+\s\(Thread\s([0-9a-zA-Z]+).+\]$'), - 'detect_konqi': re.compile(r'-- Backtrace:'), - 'detect_abrt': re.compile(r'Core was generated by'), - 'detect_gdb': re.compile(r'#\d+\s+') - } + 'bt_line_basic': re.compile(r'^#\d'), + 'bt_line': re.compile(r'^#\d+\s+(.*)$'), + 'bt_function': re.compile(r'.+in\s+(\S+)\s+\('), + 'bt_at': re.compile(r'\s+at\s+(\S+:\d+)'), + 'bt_addr': re.compile(r'(0x[0-9a-fA-F]+)\s+.+$'), + 'signal': re.compile(r'Program\sreceived\ssignal\s+([^,]+)'), + 'exit_code': re.compile(r'Program exited with code (\d+)'), + 'faddr': re.compile(r'^si_addr.+(0x[0-9a-zA-Z]+)$'), + 'bt_line_from': re.compile(r'\bfrom\b'), + 'bt_line_at': re.compile(r'\bat\b'), + 'register': re.compile(r'(0x[0-9a-zA-Z]+)\s+(.+)$'), + 'libc_location': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+.+/libc[-.]'), + 'libgcc_location': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+.+/libgcc(_s)?[-.]'), + 'mapped_frame': re.compile(r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(/.+)'), + 'gdb_bt_threads': re.compile(r'^\[New Thread.+'), + 'konqi_bt_threads': re.compile(r'^\[Current thread is \d+\s\(Thread\s([0-9a-zA-Z]+).+\]$'), + 'detect_konqi': re.compile(r'-- Backtrace:'), + 'detect_abrt': re.compile(r'Core was generated by'), + 'detect_gdb': re.compile(r'#\d+\s+') +} # There are a number of functions that are typically found in crash backtraces, # yet are side effects of a crash and are not directly relevant to identifying @@ -59,6 +59,7 @@ '__posix_memalign', 'malloc_consolidate', '__libc_malloc', '__libc_realloc' ) + def check_thread_type(line): if regex['detect_konqi'].match(line): return 'konqi' @@ -69,6 +70,7 @@ def check_thread_type(line): else: return False + def detect_format(debugger_output_file): logger.debug('Checking format of %s', debugger_output_file) with open(debugger_output_file, 'r') as f: @@ -77,13 +79,17 @@ def detect_format(debugger_output_file): if thread_format: return thread_format - # if you got here it's because you couldn't figure out what kind of file you're dealing with - raise UnknownDebuggerError('Unrecognized debugger for %s' % debugger_output_file) + # if you got here it's because you couldn't figure out what kind of file + # you're dealing with + raise UnknownDebuggerError( + 'Unrecognized debugger for %s' % debugger_output_file) + class DebuggerFile(object): ''' classdocs ''' + def __init__(self, path, exclude_unmapped_frames=True, keep_uniq_faddr=False): ''' Create a GDB file object from the gdb output file @@ -133,18 +139,18 @@ def __init__(self, path, exclude_unmapped_frames=True, keep_uniq_faddr=False): # will not be added if not hasattr(self, 'line_callbacks'): self.line_callbacks = [ - self._look_for_64bit, - self._look_for_exit_code, - self._look_for_debug_build, - self._look_for_corrupt_stack, - self._look_for_libc_location, - self._look_for_libgcc_location, - self._look_for_signal, - self._look_for_crash, - self._look_for_registers, - self._look_for_faddr, - self._build_module_map, - ] + self._look_for_64bit, + self._look_for_exit_code, + self._look_for_debug_build, + self._look_for_corrupt_stack, + self._look_for_libc_location, + self._look_for_libgcc_location, + self._look_for_signal, + self._look_for_crash, + self._look_for_registers, + self._look_for_faddr, + self._build_module_map, + ] self._read_file() self._process_file() @@ -172,7 +178,8 @@ def _hashable_backtrace(self): elif self.registers_hex.get(self.pc_name) and not self.used_pc: # Backtrace entry #0 doesn't have an address listed, so use EIP instead - # But set a flag not to use EIP again, as inline frames behave the same way + # But set a flag not to use EIP again, as inline frames + # behave the same way self.used_pc = True frame_address = int(self.registers_hex[self.pc_name], 16) @@ -189,7 +196,8 @@ def _hashable_backtrace(self): if x and x.group(1) in blacklist: continue - # If debug symbols are available, the backtrace will include the line number + # If debug symbols are available, the backtrace will include + # the line number m = re.search(regex['bt_at'], bt) if m: bt_frame = m.group(1) @@ -232,19 +240,23 @@ def _hashable_backtrace(self): return self.hashable_backtrace def _hashable_backtrace_string(self, level): - self.hashable_backtrace_string = ' '.join(self.hashable_backtrace[:level]).strip() + self.hashable_backtrace_string = ' '.join( + self.hashable_backtrace[:level]).strip() if self.keep_uniq_faddr: try: - self.hashable_backtrace_string = self.hashable_backtrace_string + ' ' + self.faddr + self.hashable_backtrace_string = self.hashable_backtrace_string + \ + ' ' + self.faddr except: logger.debug('Cannot use PC in hash') - logger.warning('_hashable_backtrace_string: %s', self.hashable_backtrace_string) + logger.warning( + '_hashable_backtrace_string: %s', self.hashable_backtrace_string) return self.hashable_backtrace_string def _backtrace_without_questionmarks(self): logger.debug('_backtrace_without_questionmarks') if not self.backtrace_without_questionmarks: - self.backtrace_without_questionmarks = [bt for bt in self.backtrace if not '??' in bt] + self.backtrace_without_questionmarks = [ + bt for bt in self.backtrace if not '??' in bt] return self.backtrace_without_questionmarks def backtrace_line(self, idx, l): @@ -252,7 +264,8 @@ def backtrace_line(self, idx, l): if m: item = m.group(1) # sometimes gdb splits across lines - # so get the next one if it looks like ' at ' or ' from ' + # so get the next one if it looks like ' at ' or + # ' from ' next_idx = idx + 1 while next_idx < len(self.lines): nextline = self.lines[next_idx] @@ -274,7 +287,8 @@ def _read_file(self): try: with open(self.file, 'r') as f: self.debugger_output = f.read() - self.lines = [l.strip() for l in self.debugger_output.splitlines()] + self.lines = [l.strip() + for l in self.debugger_output.splitlines()] except IOError, e: raise DebuggerFileError(e) except MemoryError, e: @@ -291,7 +305,8 @@ def _process_backtrace(self): # we can no longer trust the last backtrace line # so remove it removed_bt_line = self.backtrace.pop() - logger.debug("GDB detected corrupt stack. Removing backtrace line: %s", removed_bt_line) + logger.debug( + "GDB detected corrupt stack. Removing backtrace line: %s", removed_bt_line) else: # if the last line of the backtrace is unmapped, we're in # corrupt stack land @@ -330,7 +345,8 @@ def _is_mapped_frame(self, frame_address): for module in self.module_map: logger.debug('Module: %s %s', module['start'], module['end']) if module['start'] < frame_address < module['end']: - logger.debug('Found address %x in module: %s' % (frame_address, module['objfile'])) + logger.debug('Found address %x in module: %s' % + (frame_address, module['objfile'])) return True # if you got here, it's not mapped return False @@ -361,7 +377,8 @@ def _look_for_debugger_missed_stack_corruption(self): self.debugger_missed_stack_corruption = True # we can't use this line in a backtrace, so pop it removed_bt_line = self.backtrace.pop() - logger.debug("GDB missed corrupt stack detection. Removing backtrace line: %s", removed_bt_line) + logger.debug( + "GDB missed corrupt stack detection. Removing backtrace line: %s", removed_bt_line) else: # as soon as we hit a line that is a mapped # frame, we're done trimming the backtrace @@ -379,7 +396,7 @@ def _look_for_debugger_missed_stack_corruption(self): logger.debug('Total stack corruption. No backtrace lines left.') def _look_for_exit_code(self, line): -# if self.exit_code: return + # if self.exit_code: return m = re.match(regex['exit_code'], line) if m: @@ -434,7 +451,8 @@ def _remove_unmapped_frames(self): if frame_address is not None: mapped_frame = self._is_mapped_frame(frame_address) if not mapped_frame: - logger.debug('Removing unmapped backtrace frame address: %s' % bt) + logger.debug( + 'Removing unmapped backtrace frame address: %s' % bt) del self.backtrace[i] if not len(self.backtrace): # No frame in the backtrace is in a mapped module @@ -553,16 +571,17 @@ def get_testcase_signature(self, backtrace_level): else: return None + def _detect_and_generate(debugger_file): import konqifile import abrtfile import gdbfile BacktraceClass = { - 'gdb': gdbfile.GDBfile, - 'abrt': abrtfile.ABRTfile, - 'konqi': konqifile.Konqifile, - } + 'gdb': gdbfile.GDBfile, + 'abrt': abrtfile.ABRTfile, + 'konqi': konqifile.Konqifile, + } try: bt_type = detect_format(debugger_file) except UnknownDebuggerError: @@ -572,11 +591,13 @@ def _detect_and_generate(debugger_file): try: bt = BacktraceClass[bt_type](debugger_file) except KeyError: - logger.warning("No class defined for type %s (%s)", bt_type, debugger_file) + logger.warning( + "No class defined for type %s (%s)", bt_type, debugger_file) return None return bt + def _print_line(sig, filepath, bthash, include_bt=False): format_string = '%-32s\t%s' print format_string % (sig, filepath) @@ -584,6 +605,7 @@ def _print_line(sig, filepath, bthash, include_bt=False): print format_string % ('', bthash) print + def _analyze_file(filepath, include_bt=False): ''' Gets the crash signature for the file located at @@ -618,10 +640,14 @@ def _analyze_file(filepath, include_bt=False): usage = 'Usage: %prog [options] ' parser = OptionParser(usage) - parser.add_option('', '--debug', dest='debug', action='store_true', help='Enable debug messages (overrides --verbose)') - parser.add_option('', '--verbose', dest='verbose', action='store_true', help='Enable verbose messages') - parser.add_option('', '--print-hashable-backtrace', dest='print_backtrace', action='store_true', default=False, help='Prints the hashable backtrace string on a second line') - parser.add_option('', '--pattern', dest='pattern', help='File glob pattern (optional)') + parser.add_option('', '--debug', dest='debug', action='store_true', + help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--verbose', dest='verbose', + action='store_true', help='Enable verbose messages') + parser.add_option('', '--print-hashable-backtrace', dest='print_backtrace', action='store_true', + default=False, help='Prints the hashable backtrace string on a second line') + parser.add_option( + '', '--pattern', dest='pattern', help='File glob pattern (optional)') (options, args) = parser.parse_args() logger.setLevel(logging.WARNING) From af4f8040cb6f406a8e58523f97a0f52c8c1e950b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 5 Jul 2016 13:46:35 -0400 Subject: [PATCH 0999/1169] A couple more backtrace frames to ignore --- src/certfuzz/debuggers/output_parsers/debugger_file_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py index 7dc3981..49cbb7c 100644 --- a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py +++ b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py @@ -56,7 +56,8 @@ '_dl_addr_inside_object', '_int_free', '*__GI___libc_free', '__malloc_assert', 'sYSMALLOc', '_int_realloc', '*__GI___libc_malloc', '*__GI___libc_realloc', '_int_memalign', '*__GI___libc_memalign', - '__posix_memalign', 'malloc_consolidate', '__libc_malloc', '__libc_realloc' + '__posix_memalign', 'malloc_consolidate', '__libc_malloc', '__libc_realloc', + 'g_assertion_message', 'g_assertion_message_expr', ) From 5ab8854594db9c9a7f15e8e19f274b93904a25c3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 5 Jul 2016 15:53:58 -0400 Subject: [PATCH 1000/1169] Fix standalone minimization on Linux --- src/certfuzz/tools/linux/minimize.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 5ff88c6..149b7f0 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -15,6 +15,7 @@ from certfuzz.fuzztools import filetools, text from certfuzz.minimizer.unix_minimizer import UnixMinimizer as Minimizer from certfuzz.config.simple_loader import load_and_fix_config +from certfuzz.fuzztools.command_line_templating import get_command_args_list mydir = os.path.dirname(os.path.abspath(__file__)) @@ -146,7 +147,10 @@ def main(): seedfile=seedfile, fuzzedfile=fuzzed_file, program=cfg['target']['program'], + cmd_template=cfg['target']['cmdline_template'], debugger_timeout=cfg['runner']['runtimeout'], + cmdlist=get_command_args_list( + cfg['target']['cmdline_template'], fuzzed_file.path)[1], backtrace_lines=cfg['debugger']['backtracelevels'], crashers_dir=crashers_dir, workdir_base=outdir, From f7fb0942a056272c5aae1baa2f3f8e0af45f5427 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 5 Jul 2016 15:54:43 -0400 Subject: [PATCH 1001/1169] Give minimizer more breathing room in timings. --- src/certfuzz/minimizer/minimizer_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 8fce7a8..e257272 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -400,10 +400,10 @@ def _set_crash_hashes(self): # get stdev avg_time = numpy.average(times) stdev_time = numpy.std(times) - # set debugger timeout to 0.95 confidence + # set debugger timeout to 0.99 confidence # TODO: What if the VM becomes slower. # We may give up on crashes before they happen. - zscore = 1.645 + zscore = 2.58 self.measured_dbg_time = avg_time + (zscore * stdev_time) return self.crash_hashes From 9fe69490d42d9a437593711f2f7ed6d449be29b3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 7 Jul 2016 16:47:53 -0400 Subject: [PATCH 1002/1169] First pass at using existing .drillresults files --- .../drillresults/result_driller_base.py | 45 +++++++++++++++++++ .../drillresults/result_driller_linux.py | 20 ++++++++- .../drillresults/result_driller_windows.py | 22 +++++++-- 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/drillresults/result_driller_base.py b/src/certfuzz/drillresults/result_driller_base.py index 8b376fb..7b946c6 100644 --- a/src/certfuzz/drillresults/result_driller_base.py +++ b/src/certfuzz/drillresults/result_driller_base.py @@ -6,6 +6,7 @@ import abc import logging import os +import re import cPickle as pickle from certfuzz.drillresults.errors import DrillResultsError @@ -14,6 +15,10 @@ logger = logging.getLogger(__name__) +regex = { + 'dr_score': re.compile('.+ - Exploitability rank: (\d+)') +} + class ResultDriller(object): __metaclass__ = abc.ABCMeta @@ -37,6 +42,8 @@ def __init__(self, self.pickle_file = os.path.join('fuzzdir', 'drillresults.pkl') self.cached_testcases = None self.testcase_bundles = [] + self.dr_outputs = {} + self.dr_scores = {} def __enter__(self): return self @@ -54,6 +61,22 @@ def __exit__(self, etype, value, traceback): def _platform_find_testcases(self, crash_hash): pass + def _load_dr_output(self, crash_hash, drillresults_file): + logger.debug( + 'Loading precalculated drillresults output from %s' % drillresults_file) + dr_output = '' + with open(drillresults_file, 'r') as f: + dr_output = f.read() + self.dr_outputs[crash_hash] = dr_output + self.dr_scores[crash_hash] = self._get_dr_score(dr_output) + return + + def store_dr_output(self, crash_hash, dr_output, score): + logger.debug( + 'Storing recalculated drillresults output for %s' % crash_hash) + self.dr_scores[crash_hash] = score + return + def process_testcases(self): ''' Crawls self.tld looking for crash directories to process. Puts a list @@ -96,6 +119,12 @@ def load_cached(self): def crash_scores(self): return dict([(tcb.crash_hash, tcb.score) for tcb in self.testcase_bundles]) + def _get_dr_score(self, dr_output): + firstline = dr_output.splitlines()[0] + m = regex['dr_score'].match(firstline) + score = int(m.group(1)) + return score + def print_crash_report(self, crash_key, score, details): # details = self.results[crash_key] print '\n%s - Exploitability rank: %s' % (crash_key, score) @@ -120,10 +149,26 @@ def print_crash_report(self, crash_key, score, details): def sorted_crashes(self): return sorted(self.crash_scores.iteritems(), key=lambda(k, v): (v, k)) + @property + def sorted_drillresults_output(self): + return sorted(self.dr_scores.iteritems(), key=lambda(k, v): (v, k)) + def print_reports(self): results = dict([(tcb.crash_hash, tcb.details) for tcb in self.testcase_bundles]) print "--- Interesting crashes ---\n" + + if len(self.dr_scores) > 0: + # We're using existing .drillresults files + for crash_key, score in self.sorted_drillresults_output: + # print('crash_key: %s, score: %s' % (crash_key, score)) + if score > self.max_score: + # skip test cases with scores above our max + continue + print self.dr_outputs[crash_key] + print os.linesep + return + for crash_key, score in self.sorted_crashes: if self.max_score is not None: if score > self.max_score: diff --git a/src/certfuzz/drillresults/result_driller_linux.py b/src/certfuzz/drillresults/result_driller_linux.py index 2c025e6..37bf652 100755 --- a/src/certfuzz/drillresults/result_driller_linux.py +++ b/src/certfuzz/drillresults/result_driller_linux.py @@ -19,11 +19,28 @@ def _platform_find_testcases(self, crash_hash, files, root): # Create dictionary for hashes in results dictionary crasherfile = '' # Check each of the files in the hash directory + + for current_file in files: + # Look for a .drillresults file first. If there is one, we get the + # drillresults info from there and move on. + if current_file.endswith('.drillresults'): + # Use the .drillresults output for this crash hash + self._load_dr_output(crash_hash, + os.path.join(root, current_file)) + # Move on to next file + continue + for current_file in files: + + if crash_hash in self.dr_scores: + # We are currently working with a crash hash + if self.dr_scores[crash_hash] is not None: + # We've already got a score for this crash_hash + continue + # Go through all of the .gdb files and parse them if current_file.endswith('.gdb'): # if regex['gdb_report'].match(current_file): - # print 'checking %s' % current_file dbg_file = os.path.join(root, current_file) logger.debug('found gdb file: %s', dbg_file) crasherfile = dbg_file.replace('.gdb', '') @@ -31,4 +48,5 @@ def _platform_find_testcases(self, crash_hash, files, root): with TestCaseBundle(dbg_file, crasherfile, crash_hash, self.ignore_jit) as tcb: tcb.go() + raw_input('check tcb') self.testcase_bundles.append(tcb) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index 866bf51..9042175 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -13,7 +13,6 @@ regex = { 'first_msec': re.compile('^sf_.+-\w+-0x.+.-[A-Z]+.+e0.+'), - 'msec_report': re.compile('.+.msec$'), } @@ -25,20 +24,35 @@ def _platform_find_testcases(self, crash_hash, files, root): hash_dict = {} hash_dict['hash'] = crash_hash crasherfile = '' + # Check each of the files in the hash directory for current_file in files: - # If it's exception #0, strip out the exploitability part of - # the file name. This gives us the crasher file name if regex['first_msec'].match(current_file): + # If it's exception #0, strip out the exploitability part of + # the file name. This gives us the crasher file name crasherfile, _junk = os.path.splitext(current_file) crasherfile = crasherfile.replace('-EXP', '') crasherfile = crasherfile.replace('-PEX', '') crasherfile = crasherfile.replace('-PNE', '') crasherfile = crasherfile.replace('-UNK', '') crasherfile = crasherfile.replace('.e0', '') + elif current_file.endswith('.drillresults'): + # If we have a drillresults file for this crash hash, we use + # that output instead of recalculating it + # Use the .drillresults output for this crash hash + self._load_dr_output(crash_hash, + os.path.join(root, current_file)) + for current_file in files: + if crash_hash in self.dr_scores: + # We are currently working with a crash hash + if self.dr_scores[crash_hash] is not None: + # We've already got a score for this crash_hash + logger.debug('Skipping %s' % current_file) + continue + # Go through all of the .msec files and parse them - if regex['msec_report'].match(current_file): + if current_file.endswith('.msec'): dbg_file = os.path.join(root, current_file) if crasherfile and root not in crasherfile: crasherfile = os.path.join(root, crasherfile) From 7dd81d4d967eb59d85bfc7c82f6ab7b8355e3c20 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 7 Jul 2016 17:03:12 -0400 Subject: [PATCH 1003/1169] remove raw_input used for debugging --- src/certfuzz/drillresults/result_driller_linux.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certfuzz/drillresults/result_driller_linux.py b/src/certfuzz/drillresults/result_driller_linux.py index 37bf652..ad637cc 100755 --- a/src/certfuzz/drillresults/result_driller_linux.py +++ b/src/certfuzz/drillresults/result_driller_linux.py @@ -48,5 +48,4 @@ def _platform_find_testcases(self, crash_hash, files, root): with TestCaseBundle(dbg_file, crasherfile, crash_hash, self.ignore_jit) as tcb: tcb.go() - raw_input('check tcb') self.testcase_bundles.append(tcb) From 3dd54108416070114f38b3c37ff28f344a32510c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 7 Jul 2016 17:18:11 -0400 Subject: [PATCH 1004/1169] New exc_handler_sierra CrashWrangler build for upcoming MacOS version --- .../BFF_installer.pmdoc/31exc-contents.xml | 1 + .../installer/BFF_installer.pmdoc/31exc.xml | 18 ++++++++++++++++++ .../installer/BFF_installer.pmdoc/index.xml | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 build/distmods/osx/installer/BFF_installer.pmdoc/31exc-contents.xml create mode 100644 build/distmods/osx/installer/BFF_installer.pmdoc/31exc.xml diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/31exc-contents.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/31exc-contents.xml new file mode 100644 index 0000000..69b34ea --- /dev/null +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/31exc-contents.xml @@ -0,0 +1 @@ + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/31exc.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/31exc.xml new file mode 100644 index 0000000..86625ed --- /dev/null +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/31exc.xml @@ -0,0 +1,18 @@ + + +com.cert.cc.certBff.exc_handler_sierra.pkg +1.0 + + + +crashwrangler/binaries/exc_handler_sierra +/usr/local/bin + + + + +parent +installFrom.isRelativeType +installTo + + diff --git a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml index ae35923..1f02aea 100644 --- a/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml +++ b/build/distmods/osx/installer/BFF_installer.pmdoc/index.xml @@ -66,6 +66,7 @@ + @@ -128,6 +129,7 @@ 28exc.xml 29exc.xml 30exc.xml +31exc.xml properties.customizeOption properties.title From ad6add04ed522e2c16bd70be1dcd23fc25e99128 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 8 Jul 2016 15:17:20 -0400 Subject: [PATCH 1005/1169] Fall back to recalculating drillresults output when --force is used. BFF-959 --- src/certfuzz/drillresults/result_driller_base.py | 3 ++- src/certfuzz/drillresults/result_driller_linux.py | 4 ++-- src/certfuzz/drillresults/result_driller_windows.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/drillresults/result_driller_base.py b/src/certfuzz/drillresults/result_driller_base.py index 7b946c6..2f18d83 100644 --- a/src/certfuzz/drillresults/result_driller_base.py +++ b/src/certfuzz/drillresults/result_driller_base.py @@ -87,7 +87,8 @@ def process_testcases(self): logger.debug('Looking for testcases in %s', root) dir_basename = os.path.basename(root) try: - self._platform_find_testcases(dir_basename, files, root) + self._platform_find_testcases( + dir_basename, files, root, force=self.force) except TestCaseBundleError as e: logger.warning('Skipping %s: %s', dir_basename, e) continue diff --git a/src/certfuzz/drillresults/result_driller_linux.py b/src/certfuzz/drillresults/result_driller_linux.py index ad637cc..f12cdb5 100755 --- a/src/certfuzz/drillresults/result_driller_linux.py +++ b/src/certfuzz/drillresults/result_driller_linux.py @@ -13,7 +13,7 @@ class LinuxResultDriller(ResultDriller): - def _platform_find_testcases(self, crash_hash, files, root): + def _platform_find_testcases(self, crash_hash, files, root, force=False): # Only use directories that are hashes # if "0x" in crash_hash: # Create dictionary for hashes in results dictionary @@ -23,7 +23,7 @@ def _platform_find_testcases(self, crash_hash, files, root): for current_file in files: # Look for a .drillresults file first. If there is one, we get the # drillresults info from there and move on. - if current_file.endswith('.drillresults'): + if current_file.endswith('.drillresults') and not force: # Use the .drillresults output for this crash hash self._load_dr_output(crash_hash, os.path.join(root, current_file)) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index 9042175..4681dc7 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -18,7 +18,7 @@ class WindowsResultDriller(ResultDriller): - def _platform_find_testcases(self, crash_hash, files, root): + def _platform_find_testcases(self, crash_hash, files, root, force=False): if "0x" in crash_hash: # Create dictionary for hashes in results dictionary hash_dict = {} @@ -36,7 +36,7 @@ def _platform_find_testcases(self, crash_hash, files, root): crasherfile = crasherfile.replace('-PNE', '') crasherfile = crasherfile.replace('-UNK', '') crasherfile = crasherfile.replace('.e0', '') - elif current_file.endswith('.drillresults'): + elif current_file.endswith('.drillresults') and not force: # If we have a drillresults file for this crash hash, we use # that output instead of recalculating it # Use the .drillresults output for this crash hash From f8a284e9ef089d3e73bb921bf4c29ad606efa822 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 8 Jul 2016 16:27:44 -0400 Subject: [PATCH 1006/1169] Readme -> 2.8 --- src/linux/README | 2 +- src/windows/README.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linux/README b/src/linux/README index d3aff3a..85ed6fb 100644 --- a/src/linux/README +++ b/src/linux/README @@ -1,4 +1,4 @@ -CERT Basic Fuzzing Framework (BFF) 2.7 +CERT Basic Fuzzing Framework (BFF) 2.8 ===== Change Log ===== diff --git a/src/windows/README.txt b/src/windows/README.txt index a166144..950bd51 100644 --- a/src/windows/README.txt +++ b/src/windows/README.txt @@ -19,7 +19,7 @@ Because fuzzing can fill temporary directories, put the target application in an unusable state, or trigger other operating-system-level bugs, we recommend that BFF be used in a virtual machine. -Run BFF-2.1-setup.exe in a virtual machine to install BFF 2.1. +Run BFF-2.8-setup.exe in a virtual machine to install BFF 2.8. The installer should detect and attempt to download prerequisites and configure your environment appropriately. From 699ff92c6ac7935169c3c2142e3aebcb6a8b9316 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 11 Jul 2016 11:30:54 -0400 Subject: [PATCH 1007/1169] Basic drillresults support on OSX. BFF-900 --- .../drillresults/testcasebundle_base.py | 5 + .../drillresults/testcasebundle_darwin.py | 106 ++++++++++++++++++ .../drillresults/result_driller_darwin.py | 50 +++++++++ src/linux/tools/drillresults.py | 10 +- 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py create mode 100644 src/certfuzz/drillresults/result_driller_darwin.py diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_base.py b/src/certfuzz/analyzers/drillresults/testcasebundle_base.py index 8332bfe..658e9c2 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_base.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_base.py @@ -418,6 +418,11 @@ def get_return_addr(self): ''' def fix_efa_offset(self, instructionline, faultaddr): + ''' + Adjust faulting address for instructions that use offsets + Currently only works for instructions like CALL [reg + offset] + ''' + try: index = self.instructionpieces.index('call') except ValueError: diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py b/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py new file mode 100644 index 0000000..763e30e --- /dev/null +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py @@ -0,0 +1,106 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import logging +import re + +from certfuzz.drillresults.common import carve +from certfuzz.analyzers.drillresults.testcasebundle_base import TestCaseBundle + + +logger = logging.getLogger(__name__) + +# compile our regular expresssions once +RE_BT_ADDR = re.compile(r'.+(0x[0-9a-fA-F]+)\s+.+$') +RE_CURRENT_INSTR = re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)') +RE_FRAME_0 = re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+') +RE_MAPPED_FRAME = re.compile( + r'\s+(0x[0-9a-fA-F]+)\s-\s+(0x[0-9a-fA-F]+)\s+.+\s(/.+)') +RE_VDSO = re.compile( + r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(\[vdso\])') +RE_RETURN_ADDR = re.compile(r'^#1\s.(0x[0-9a-fA-F]+)\s') + + +class DarwinTestCaseBundle(TestCaseBundle): + really_exploitable = [ + 'SegFaultOnPc', + 'SegFaultOnPcNearNull', + 'BranchAv', + 'BranchAvNearNull', + 'StackCodeExection', + 'BadInstruction', + 'ReturnAv', + 'BadInstruction', + ] + + def _get_classification(self): + self.classification = carve(self.reporttext, "is_exploitable=", ":") + logger.debug('Classification: %s', self.classification) + + def _get_shortdesc(self): + self.shortdesc = carve(self.reporttext, "exception=", ":") + logger.debug('Short Description: %s', self.shortdesc) + + def _check_64bit(self): + for line in self.reporttext.splitlines(): + m = re.match(RE_BT_ADDR, line) + if m: + start_addr = m.group(1) + if len(start_addr) > 10: + self._64bit_debugger = True + logger.debug('Using a 64-bit debugger') + + def _64bit_addr_fixup(self, faultaddr, instraddr): + return faultaddr, instraddr + + @property + def _64bit_target_app(self): + return TestCaseBundle._64bit_target_app + + def _look_for_loaded_module(self, instraddr, line): + # convert to an int as hex + instraddr = int(instraddr, 16) + + for pattern in [RE_MAPPED_FRAME, RE_VDSO]: + n = re.search(pattern, line) + if n: + begin_address = int(n.group(1), 16) + end_address = int(n.group(2), 16) + module_name = n.group(3) + logger.debug( + '%x %x %s %x', begin_address, end_address, module_name, instraddr) + if begin_address < instraddr < end_address: + logger.debug('Matched: %x in %x %x %s', instraddr, + begin_address, end_address, module_name) + # as soon as we find this, we're done + return module_name + + def get_instr(self, instraddr): + currentinstr = carve(self.reporttext, "instruction_disassembly=", ":") + logger.debug('currentinstr: %s' % currentinstr) + return currentinstr + + def get_return_addr(self): + rvfunc = lambda x, l: x.group(1) + rgx = RE_RETURN_ADDR + + return self._match_rgx(rgx, rvfunc) + + def get_instr_addr(self): + ''' + Find the address for the current (crashing) instruction + ''' + instraddr = None + instraddr = carve(self.reporttext, 'instruction_address=', ':') + logger.debug('carved instruction address: %s' % instraddr) + return self.format_addr(instraddr) + + def get_fault_addr(self): + ''' + Find the EFA + ''' + faultaddr = carve(self.reporttext, 'access_address=', ':') + logger.debug('carved fault address: %s' % faultaddr) + return self.format_addr(faultaddr) diff --git a/src/certfuzz/drillresults/result_driller_darwin.py b/src/certfuzz/drillresults/result_driller_darwin.py new file mode 100644 index 0000000..6ab096b --- /dev/null +++ b/src/certfuzz/drillresults/result_driller_darwin.py @@ -0,0 +1,50 @@ +''' +This script looks for interesting crashes and rate them by potential exploitability +''' + +import logging +import os + +from certfuzz.analyzers.drillresults.testcasebundle_darwin import DarwinTestCaseBundle as TestCaseBundle +from certfuzz.drillresults.result_driller_base import ResultDriller + +logger = logging.getLogger(__name__) + + +class DarwinResultDriller(ResultDriller): + + def _platform_find_testcases(self, crash_hash, files, root, force=False): + # Only use directories that are hashes + # if "0x" in crash_hash: + # Create dictionary for hashes in results dictionary + crasherfile = '' + # Check each of the files in the hash directory + + for current_file in files: + # Look for a .drillresults file first. If there is one, we get the + # drillresults info from there and move on. + if current_file.endswith('.drillresults') and not force: + # Use the .drillresults output for this crash hash + self._load_dr_output(crash_hash, + os.path.join(root, current_file)) + # Move on to next file + continue + + for current_file in files: + + if crash_hash in self.dr_scores: + # We are currently working with a crash hash + if self.dr_scores[crash_hash] is not None: + # We've already got a score for this crash_hash + continue + + # Go through all of the .cw files and parse them + if current_file.endswith('.cw'): + dbg_file = os.path.join(root, current_file) + logger.debug('found CrashWrangler file: %s', dbg_file) + crasherfile = dbg_file.replace('.gmalloc', '') + crasherfile = crasherfile('.cw', '') + with TestCaseBundle(dbg_file, crasherfile, crash_hash, + self.ignore_jit) as tcb: + tcb.go() + self.testcase_bundles.append(tcb) diff --git a/src/linux/tools/drillresults.py b/src/linux/tools/drillresults.py index 218b406..73c44f7 100755 --- a/src/linux/tools/drillresults.py +++ b/src/linux/tools/drillresults.py @@ -4,9 +4,12 @@ ''' import os import sys +import platform + try: from certfuzz.drillresults.common import main from certfuzz.drillresults.result_driller_linux import LinuxResultDriller + from certfuzz.drillresults.result_driller_darwin import DarwinResultDriller except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH mydir = os.path.dirname(os.path.abspath(__file__)) @@ -14,6 +17,11 @@ sys.path.append(parentdir) from certfuzz.drillresults.common import main from certfuzz.drillresults.result_driller_linux import LinuxResultDriller + from certfuzz.drillresults.result_driller_darwin import DarwinResultDriller if __name__ == '__main__': - main(driller_class=LinuxResultDriller) + plat = platform.system() + if plat == 'Darwin': + main(driller_class=DarwinResultDriller) + else: + main(driller_class=LinuxResultDriller) From 717f140d060756b1eea7811a2049cc3550ea07d3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 11 Jul 2016 14:00:59 -0400 Subject: [PATCH 1008/1169] pep8 cleanup --- .../debuggers/output_parsers/cwfile.py | 63 +++++++++++-------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index 3a86d70..c657e6c 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -17,26 +17,26 @@ registers = ['eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', 'edi', 'eip', 'cs', 'ss', 'ds', 'es', 'fs', 'gs'] registers64 = ('rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi', 'rbp', - 'rsp', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', 'r14', - 'r15', 'rip', 'rfl', 'cr2') + 'rsp', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', 'r14', + 'r15', 'rip', 'rfl', 'cr2') regex = { - 'code_type': re.compile('Code Type:\s+(.+)'), - 'exception_line': re.compile('^exception=.+instruction_address=(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), - 'bt_thread': re.compile('^Thread.+'), - 'bt_line_basic': re.compile('^\d'), - 'bt_line': re.compile('^\d+\s+(.*)$'), - 'bt_function': re.compile('.+\s+(\S+)\s+(\S+)\s'), - 'bt_at': re.compile('.+\s+at\s+(\S+)'), - 'bt_tab': re.compile('.+\t'), - 'bt_space': re.compile('.+\s'), - 'bt_addr': re.compile('(0x[0-9a-fA-F]+)\s'), - 'signal': re.compile('Program\sreceived\ssignal\s+([^,]+)'), - 'exit_code': re.compile('Program exited with code (\d+)'), - 'bt_line_from': re.compile(r'\bfrom\b'), - 'bt_line_at': re.compile(r'\bat\b'), - 'register': re.compile('\s\s\s?[0-9a-zA-Z]+:\s(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), - } + 'code_type': re.compile('Code Type:\s+(.+)'), + 'exception_line': re.compile('^exception=.+instruction_address=(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), + 'bt_thread': re.compile('^Thread.+'), + 'bt_line_basic': re.compile('^\d'), + 'bt_line': re.compile('^\d+\s+(.*)$'), + 'bt_function': re.compile('.+\s+(\S+)\s+(\S+)\s'), + 'bt_at': re.compile('.+\s+at\s+(\S+)'), + 'bt_tab': re.compile('.+\t'), + 'bt_space': re.compile('.+\s'), + 'bt_addr': re.compile('(0x[0-9a-fA-F]+)\s'), + 'signal': re.compile('Program\sreceived\ssignal\s+([^,]+)'), + 'exit_code': re.compile('Program exited with code (\d+)'), + 'bt_line_from': re.compile(r'\bfrom\b'), + 'bt_line_at': re.compile(r'\bat\b'), + 'register': re.compile('\s\s\s?[0-9a-zA-Z]+:\s(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), +} # There are a number of functions that are typically found in crash backtraces, # yet are side effects of a crash and are not directly relevant to identifying @@ -47,7 +47,9 @@ '__kill', '_sigtramp' ) + class CWfile: + def __init__(self, f): ''' Create a GDB file object from the gdb output file @@ -111,7 +113,8 @@ def _hashable_backtrace(self): if m: logger.debug("found tab: %s" % t) val = t - # remember the value for the first line in case we need it later + # remember the value for the first line in case we need it + # later if not line_0: line_0 = val @@ -147,22 +150,26 @@ def _hashable_backtrace(self): return self.hashable_backtrace def _hashable_backtrace_string(self, level): - self.hashable_backtrace_string = ' '.join(self.hashable_backtrace[:level]).strip() - logger.warning('_hashable_backtrace_string: %s', self.hashable_backtrace_string) + self.hashable_backtrace_string = ' '.join( + self.hashable_backtrace[:level]).strip() + logger.warning( + '_hashable_backtrace_string: %s', self.hashable_backtrace_string) return self.hashable_backtrace_string def _backtrace_without_questionmarks(self): logger.debug('_backtrace_without_questionmarks') if not self.backtrace_without_questionmarks: - self.backtrace_without_questionmarks = [bt for bt in self.backtrace if not '??' in bt] + self.backtrace_without_questionmarks = [ + bt for bt in self.backtrace if not '??' in bt] return self.backtrace_without_questionmarks def backtrace_line(self, idx, l): self._look_for_crashing_thread(l) m = re.match(regex['bt_line'], l) - if m and self.crashing_thread: + if m and self.crashing_thread: item = m.group(1) # sometimes gdb splits across lines - # so get the next one if it looks like ' at ' or ' from ' + # so get the next one if it looks like ' at ' or + # ' from ' next_idx = idx + 1 while next_idx < len(self.lines): nextline = self.lines[next_idx] @@ -218,7 +225,8 @@ def _process_lines(self): # so remove it if self.is_corrupt_stack and len(self.backtrace): removed_bt_line = self.backtrace.pop() - logger.debug("Corrupt stack found. Removing backtrace line: %s", removed_bt_line) + logger.debug( + "Corrupt stack found. Removing backtrace line: %s", removed_bt_line) def _look_for_crashing_thread(self, line): m = re.match(regex['bt_thread'], line) @@ -243,7 +251,7 @@ def _look_for_64bit(self, line): self.pc_name = 'rip' self.registers_sought = list(registers64) - #TODO: CrashWrangler equivalents of the below + # TODO: CrashWrangler equivalents of the below def _look_for_corrupt_stack(self, line): if 'corrupt stack' in line: self.is_corrupt_stack = True @@ -321,7 +329,8 @@ def get_testcase_signature(self, backtrace_level): logger.addHandler(hdlr) parser = OptionParser() - parser.add_option('', '--debug', dest='debug', action='store_true', help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--debug', dest='debug', action='store_true', + help='Enable debug messages (overrides --verbose)') (options, args) = parser.parse_args() if options.debug: From d773d745332104d223d7798b43e88c3bd947f213 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 11 Jul 2016 15:49:50 -0400 Subject: [PATCH 1009/1169] analyzer mode of drillresults working on Darwin. BFF-900 --- .../analyzers/drillresults/drillresults.py | 5 ++ .../drillresults/testcasebundle_darwin.py | 16 +++---- .../debuggers/output_parsers/cwfile.py | 46 ++++++------------- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 8 +++- 4 files changed, 32 insertions(+), 43 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/drillresults.py b/src/certfuzz/analyzers/drillresults/drillresults.py index f28b3d5..92f6307 100644 --- a/src/certfuzz/analyzers/drillresults/drillresults.py +++ b/src/certfuzz/analyzers/drillresults/drillresults.py @@ -8,6 +8,7 @@ from certfuzz.analyzers.drillresults.testcasebundle_base import TestCaseBundle from certfuzz.drillresults.errors import TestCaseBundleError from certfuzz.analyzers.drillresults.testcasebundle_linux import LinuxTestCaseBundle +from certfuzz.analyzers.drillresults.testcasebundle_darwin import DarwinTestCaseBundle from certfuzz.analyzers.drillresults.testcasebundle_windows import WindowsTestCaseBundle logger = logging.getLogger(__name__) @@ -116,5 +117,9 @@ class LinuxDrillResults(DrillResults): _tcb_cls = LinuxTestCaseBundle +class DarwinDrillResults(DrillResults): + _tcb_cls = DarwinTestCaseBundle + + class WindowsDrillResults(DrillResults): _tcb_cls = WindowsTestCaseBundle diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py b/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py index 763e30e..a1ee3d2 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py @@ -17,9 +17,7 @@ RE_CURRENT_INSTR = re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)') RE_FRAME_0 = re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+') RE_MAPPED_FRAME = re.compile( - r'\s+(0x[0-9a-fA-F]+)\s-\s+(0x[0-9a-fA-F]+)\s+.+\s(/.+)') -RE_VDSO = re.compile( - r'(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+0x[0-9a-fA-F]+\s+0(x0)?\s+(\[vdso\])') + r'\s?(0x[0-9a-fA-F]+)\s-\s+(0x[0-9a-fA-F]+)\s+.+\s(/.+)') RE_RETURN_ADDR = re.compile(r'^#1\s.(0x[0-9a-fA-F]+)\s') @@ -63,7 +61,7 @@ def _look_for_loaded_module(self, instraddr, line): # convert to an int as hex instraddr = int(instraddr, 16) - for pattern in [RE_MAPPED_FRAME, RE_VDSO]: + for pattern in [RE_MAPPED_FRAME]: n = re.search(pattern, line) if n: begin_address = int(n.group(1), 16) @@ -82,11 +80,11 @@ def get_instr(self, instraddr): logger.debug('currentinstr: %s' % currentinstr) return currentinstr - def get_return_addr(self): - rvfunc = lambda x, l: x.group(1) - rgx = RE_RETURN_ADDR - - return self._match_rgx(rgx, rvfunc) + def fix_return_efa(self, faultaddr): + ''' + No need for this on Darwin + ''' + return faultaddr def get_instr_addr(self): ''' diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index c657e6c..f75ef33 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -25,7 +25,7 @@ 'exception_line': re.compile('^exception=.+instruction_address=(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), 'bt_thread': re.compile('^Thread.+'), 'bt_line_basic': re.compile('^\d'), - 'bt_line': re.compile('^\d+\s+(.*)$'), + 'bt_line': re.compile('^\d+\s+(\S+)\s+0x[0-9a-zA-Z][0-9a-zA-Z]+\s+(.*)$'), 'bt_function': re.compile('.+\s+(\S+)\s+(\S+)\s'), 'bt_at': re.compile('.+\s+at\s+(\S+)'), 'bt_tab': re.compile('.+\t'), @@ -47,6 +47,10 @@ '__kill', '_sigtramp' ) +# These libraries should be used in the uniqueness determination of a crash +blacklist_libs = ('libSystem.B.dylib', 'libsystem_malloc.dylib' + ) + class CWfile: @@ -99,37 +103,11 @@ def _hashable_backtrace(self): logger.debug("checking backtrace line") # skip blacklisted functions - x = re.match(regex['bt_function'], bt) - if x and x.group(1) in blacklist: - continue - - if '???' in bt: - logger.debug('Unmapped frame, skipping') + if bt in blacklist: continue + else: + hashable.append(bt) - m = re.match(regex['bt_tab'], bt) - s = re.sub(regex['bt_tab'], "", bt) - t = re.sub(regex['bt_addr'], "", s) - if m: - logger.debug("found tab: %s" % t) - val = t - # remember the value for the first line in case we need it - # later - if not line_0: - line_0 = val - - # skip anything in /sysdeps/ since they're - # typically part of the post-crash - if '/sysdeps/' in val: - logger.debug('Found sysdeps, skipping') - continue - - hashable.append(val) -# elif n: -# val = n.group(1) -# # remember the value for the first line in case we need it later -# if not line_0: line_0 = val -# hashable.append(val) if not hashable: if self.exit_code: hashable.append(self.exit_code) @@ -167,7 +145,8 @@ def backtrace_line(self, idx, l): self._look_for_crashing_thread(l) m = re.match(regex['bt_line'], l) if m and self.crashing_thread: - item = m.group(1) # sometimes gdb splits across lines + library = m.group(1) + item = m.group(2) # sometimes gdb splits across lines # so get the next one if it looks like ' at ' or # ' from ' next_idx = idx + 1 @@ -179,8 +158,9 @@ def backtrace_line(self, idx, l): item = ' '.join((item, nextline)) next_idx += 1 - self.backtrace.append(item) - logger.debug('Appending to backtrace: %s', item) + if library not in blacklist_libs: + self.backtrace.append(item) + logger.debug('Appending to backtrace: %s', item) def _read_file(self): ''' diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index b00ae8b..a01d71f 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -5,6 +5,7 @@ ''' import logging import os +import platform from certfuzz.analyzers.callgrind.annotate import annotate_callgrind from certfuzz.analyzers.callgrind.annotate import annotate_callgrind_tree @@ -17,6 +18,7 @@ from certfuzz.reporters.copy_files import CopyFilesReporter from certfuzz.reporters.testcase_logger import TestcaseLoggerReporter from certfuzz.analyzers.drillresults import LinuxDrillResults +from certfuzz.analyzers.drillresults import DarwinDrillResults from certfuzz.analyzers.pin_calltrace import Pin_calltrace from certfuzz.analyzers.callgrind.callgrind import Callgrind from certfuzz.analyzers.valgrind import Valgrind @@ -48,7 +50,11 @@ def _setup_analyzers(self): if self.options.get('use_pin_calltrace'): self.analyzer_classes.append(Pin_calltrace) - self.analyzer_classes.append(LinuxDrillResults) + plat = platform.system() + if plat == 'Darwin': + self.analyzer_classes.append(DarwinDrillResults) + else: + self.analyzer_classes.append(LinuxDrillResults) def _verify(self, testcase): ''' From 164bb4cda534c1ed83a8571af64a607f85458686 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 11 Jul 2016 16:02:29 -0400 Subject: [PATCH 1010/1169] cleanup --- .../drillresults/testcasebundle_darwin.py | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py b/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py index a1ee3d2..10e098f 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py @@ -13,24 +13,14 @@ logger = logging.getLogger(__name__) # compile our regular expresssions once -RE_BT_ADDR = re.compile(r'.+(0x[0-9a-fA-F]+)\s+.+$') -RE_CURRENT_INSTR = re.compile(r'^=>\s(0x[0-9a-fA-F]+)(.+)?:\s+(\S.+)') -RE_FRAME_0 = re.compile(r'^#0\s+(0x[0-9a-fA-F]+)\s.+') +RE_CODE_TYPE = re.compile(r'^Code Type:\s+(\S+)') RE_MAPPED_FRAME = re.compile( r'\s?(0x[0-9a-fA-F]+)\s-\s+(0x[0-9a-fA-F]+)\s+.+\s(/.+)') -RE_RETURN_ADDR = re.compile(r'^#1\s.(0x[0-9a-fA-F]+)\s') class DarwinTestCaseBundle(TestCaseBundle): really_exploitable = [ - 'SegFaultOnPc', - 'SegFaultOnPcNearNull', - 'BranchAv', - 'BranchAvNearNull', - 'StackCodeExection', - 'BadInstruction', - 'ReturnAv', - 'BadInstruction', + 'EXC_BAD_INSTRUCTION', ] def _get_classification(self): @@ -43,12 +33,12 @@ def _get_shortdesc(self): def _check_64bit(self): for line in self.reporttext.splitlines(): - m = re.match(RE_BT_ADDR, line) + m = re.match(RE_CODE_TYPE, line) if m: - start_addr = m.group(1) - if len(start_addr) > 10: + code_type = m.group(1) + if code_type == 'X86-64': self._64bit_debugger = True - logger.debug('Using a 64-bit debugger') + logger.debug('Using a 64-bit target') def _64bit_addr_fixup(self, faultaddr, instraddr): return faultaddr, instraddr From 13b4d21764efe092f56f76d14cabb594732acb60 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 11 Jul 2016 16:15:26 -0400 Subject: [PATCH 1011/1169] Add stub unit tests for new modules. --- .../test_testcasebundle_darwin.py | 23 +++++++++++++++++++ .../test_result_driller_darwin.py | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/test_certfuzz/analyzers/drillresults/test_testcasebundle_darwin.py create mode 100644 src/test_certfuzz/drillresults/test_result_driller_darwin.py diff --git a/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_darwin.py b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_darwin.py new file mode 100644 index 0000000..be6f5b2 --- /dev/null +++ b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_darwin.py @@ -0,0 +1,23 @@ +''' +Created on Jul 2, 2014 + +@organization: cert.org +''' +import unittest +from certfuzz.analyzers.drillresults import testcasebundle_darwin + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/src/test_certfuzz/drillresults/test_result_driller_darwin.py b/src/test_certfuzz/drillresults/test_result_driller_darwin.py new file mode 100644 index 0000000..00dc79e --- /dev/null +++ b/src/test_certfuzz/drillresults/test_result_driller_darwin.py @@ -0,0 +1,23 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest +from certfuzz.drillresults import result_driller_darwin + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From b4e3d73f270dfe5abf44f2a3116a72c518513d74 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 11 Jul 2016 16:52:23 -0400 Subject: [PATCH 1012/1169] Fix typo --- src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py b/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py index 10e098f..01d4772 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_darwin.py @@ -70,6 +70,10 @@ def get_instr(self, instraddr): logger.debug('currentinstr: %s' % currentinstr) return currentinstr + def get_return_addr(self): + # This isn't needed on OSX + pass + def fix_return_efa(self, faultaddr): ''' No need for this on Darwin From 1cef7379023d2ea4de6fff5f156cb40d6ef8088d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 11 Jul 2016 16:52:38 -0400 Subject: [PATCH 1013/1169] ABC method in darwin class required --- src/certfuzz/drillresults/result_driller_darwin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/drillresults/result_driller_darwin.py b/src/certfuzz/drillresults/result_driller_darwin.py index 6ab096b..6c7c838 100644 --- a/src/certfuzz/drillresults/result_driller_darwin.py +++ b/src/certfuzz/drillresults/result_driller_darwin.py @@ -43,7 +43,7 @@ def _platform_find_testcases(self, crash_hash, files, root, force=False): dbg_file = os.path.join(root, current_file) logger.debug('found CrashWrangler file: %s', dbg_file) crasherfile = dbg_file.replace('.gmalloc', '') - crasherfile = crasherfile('.cw', '') + crasherfile = crasherfile.replace('.cw', '') with TestCaseBundle(dbg_file, crasherfile, crash_hash, self.ignore_jit) as tcb: tcb.go() From f83f929452dcc9f0cfa2a6073314db5790cbea81 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 11 Jul 2016 17:40:52 -0400 Subject: [PATCH 1014/1169] Don't disable the TWDF after enabling it. BFF-961 --- src/certfuzz/file_handlers/watchdog_file.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/file_handlers/watchdog_file.py b/src/certfuzz/file_handlers/watchdog_file.py index 70e899d..330a6c6 100644 --- a/src/certfuzz/file_handlers/watchdog_file.py +++ b/src/certfuzz/file_handlers/watchdog_file.py @@ -51,9 +51,9 @@ def _enable_iff_compat(self): logger.debug('%s is watchdog compatible' % hostname) self.use_watchdog = True self.enable() - - logger.debug('%s is not watchdog compatible' % hostname) - self.disable() + else: + logger.debug('%s is not watchdog compatible' % hostname) + self.disable() def _check_hostname(self): hostname = 'System' From 1fd2740cd1f48e0a66e7d53c311e30c7bfd1e8dc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 13 Jul 2016 14:26:46 -0400 Subject: [PATCH 1015/1169] Fix up fuzzed file path when printing precalculated drillresults output. BFF-965 --- .../drillresults/result_driller_base.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/drillresults/result_driller_base.py b/src/certfuzz/drillresults/result_driller_base.py index 2f18d83..dfacd1c 100644 --- a/src/certfuzz/drillresults/result_driller_base.py +++ b/src/certfuzz/drillresults/result_driller_base.py @@ -44,6 +44,7 @@ def __init__(self, self.testcase_bundles = [] self.dr_outputs = {} self.dr_scores = {} + self.dr_paths = {} def __enter__(self): return self @@ -69,6 +70,7 @@ def _load_dr_output(self, crash_hash, drillresults_file): dr_output = f.read() self.dr_outputs[crash_hash] = dr_output self.dr_scores[crash_hash] = self._get_dr_score(dr_output) + self.dr_paths[crash_hash] = os.path.dirname(drillresults_file) return def store_dr_output(self, crash_hash, dr_output, score): @@ -154,6 +156,19 @@ def sorted_crashes(self): def sorted_drillresults_output(self): return sorted(self.dr_scores.iteritems(), key=lambda(k, v): (v, k)) + def print_drillresults_file(self, crash_key): + ff_line_indicator = 'Fuzzed file: ' + for line in self.dr_outputs[crash_key].splitlines(): + if line.startswith(ff_line_indicator): + pathname = line.replace(ff_line_indicator, '') + fuzzedfile = os.path.basename(pathname) + realdir = self.dr_paths[crash_key] + fixed_ff_path = os.path.join(realdir, fuzzedfile) + print ('%s%s' % (ff_line_indicator, fixed_ff_path)) + else: + print line + print os.linesep + def print_reports(self): results = dict([(tcb.crash_hash, tcb.details) for tcb in self.testcase_bundles]) @@ -166,8 +181,7 @@ def print_reports(self): if score > self.max_score: # skip test cases with scores above our max continue - print self.dr_outputs[crash_key] - print os.linesep + self.print_drillresults_file(crash_key) return for crash_key, score in self.sorted_crashes: From 688931ae864bbcef34eb94127b1dcef042e706af Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 15 Jul 2016 10:02:27 -0400 Subject: [PATCH 1016/1169] If on a hook-compatible Windows platform, give the debugger more time to run than configured. BFF-966 --- src/certfuzz/campaign/campaign_windows.py | 12 +++++++++++ src/certfuzz/iteration/iteration_windows.py | 6 +----- src/certfuzz/minimizer/minimizer_base.py | 3 ++- src/certfuzz/tools/linux/minimize.py | 2 +- src/certfuzz/tools/windows/minimize.py | 24 +++++++++++++++++---- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 5fc8910..96418a7 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -85,12 +85,24 @@ def _pre_enter(self): hook_incompatible = winver > 5 or machine == 'AMD64' if not hook_incompatible: + # Since we've simplified configuration to include only one run timeout, we + # need to account for the fact that a debugger-run instance could take + # longer than a hooked instance.q + debugger_timeout = self.config['runner']['runtimeout'] * 2 + if debugger_timeout < 10: + debugger_timeout = 10 + self.config['debugger']['runtimeout'] = debugger_timeout return logger.debug( 'winrun is not compatible with Windows %s %s. Overriding.', winver, machine) self.runner_module_name = 'certfuzz.runners.nullrun' + # Assume that since we're not using the hook, the user has configured the timeout + # to be reasonble for debugger-invoked instances. + self.config['debugger']['runtimeout'] = self.config[ + 'runner']['runtimeout'] + def _post_enter(self): self._start_buttonclicker() self._cache_app() diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index 54185f2..d36f1e1 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -65,10 +65,6 @@ def __init__(self, 'null_runner': self.runner_cls.is_nullrunner, }) - # Windows testcase object needs a timeout, and we only pass debugger - # options - self.cfg['debugger']['runtimeout'] = self.cfg['runner']['runtimeout'] - def __exit__(self, etype, value, traceback): try: handled = IterationBase.__exit__(self, etype, value, traceback) @@ -108,7 +104,7 @@ def _construct_testcase(self): program=self.cfg['target']['program'], cmd_template=self.cmd_template, debugger_timeout=self.cfg[ - 'runner']['runtimeout'], + 'debugger']['runtimeout'], cmdlist=get_command_args_list( self.cmd_template, self.fuzzer.output_file_path)[1], dbg_opts=self.cfg['debugger'], diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index e257272..ed3d666 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -184,7 +184,8 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, filetools.copy_file(self.testcase.fuzzedfile.path, self.tempfile) # figure out what testcase signatures belong to this fuzzedfile - self.debugger_timeout = self.cfg['runner']['runtimeout'] + raw_input('timeout?') + self.debugger_timeout = self.cfg['debugger']['runtimeout'] self.crash_hashes = [] self.measured_dbg_time = None self._set_crash_hashes() diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 149b7f0..0b87d0b 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -77,7 +77,7 @@ def main(): cfg_file = os.path.expanduser(options.config) else: if os.path.isfile("../configs/bff.yaml"): - cfg_file = "../configs/bff.cfg" + cfg_file = "../configs/bff.yaml" elif os.path.isfile("configs/bff.yaml"): cfg_file = "configs/bff.yaml" else: diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index dbc4f90..023cbdd 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -6,7 +6,9 @@ import logging import os +import sys import string +import platform try: @@ -42,7 +44,9 @@ class DummyCfg(object): pass config = DummyCfg() config.backtracelevels = 5 # doesn't matter what this is, we don't use it - config.debugger_timeout = cfg['runner']['runtimeout'] + config.debugger_timeout = cfg['runner']['runtimeout'] * 2 + if config.debugger_timeout < 10: + config.debugger_timeout = 10 template = string.Template(cfg['target']['cmdline_template']) config.get_command_args_list = lambda x: get_command_args_list( template, x)[1] @@ -101,7 +105,7 @@ def main(): if options.config: cfg_file = options.config else: - cfg_file = "../configs/bff.cfg" + cfg_file = "../configs/bff.yaml" logger.debug('WindowsConfig file: %s', cfg_file) if options.stringmode and options.target: @@ -159,8 +163,20 @@ def main(): cmd_as_args = get_command_args_list( cfg['target']['cmdline_template'], fuzzed_file.path)[1] - # Use runner timeout, since we now only specify the runner timeout - cfg['debugger']['runtimeout'] = cfg['runner']['runtimeout'] + + # Figure out an appropriate timeout to use based on the config + winver = sys.getwindowsversion().major + machine = platform.machine() + hook_incompatible = winver > 5 or machine == 'AMD64' + debugger_timeout = cfg['runner']['runtimeout'] + if not hook_incompatible: + # Assume the user has tuned timeout to the hook. + # Allow extra time for the debugger to run + debugger_timeout *= 2 + if debugger_timeout < 10: + debugger_timeout = 10 + cfg['debugger']['runtimeout'] = debugger_timeout + with WindowsTestcase(cfg=cfg, seedfile=seedfile, fuzzedfile=fuzzed_file, From 385c8b9de463eda6a389f92a50e4262cfcb25443 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 15 Jul 2016 11:37:40 -0400 Subject: [PATCH 1017/1169] Give zzuf platforms more time to run the debugger than configured. BFF-966 --- src/certfuzz/campaign/campaign_base.py | 52 ++++++++++++++++------- src/certfuzz/iteration/iteration_linux.py | 2 +- src/certfuzz/minimizer/minimizer_base.py | 1 - src/certfuzz/tools/linux/minimize.py | 5 ++- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index cca37cd..58cf6c6 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -89,6 +89,15 @@ def __init__(self, config_file, result_dir=None, debug=False): self._read_config_file() + # Create a debugger timeout that allows for slack space to account + # for the difference between a zzuf-invoked iteration and a + # debugger-invoked iteration + + debugger_timeout = self.config['runner']['runtimeout'] * 2 + if debugger_timeout < 10: + debugger_timeout = 10 + self.config['debugger']['runtimeout'] = debugger_timeout + self.campaign_id = self.config['campaign']['id'] self.current_seed = self.config['runoptions']['first_iteration'] @@ -112,18 +121,21 @@ def __init__(self, config_file, result_dir=None, debug=False): self.sf_set_out = os.path.join(self.outdir, 'seedfiles') if not self.cached_state_file: cachefile = 'campaign_%s.pkl' % _campaign_id_with_underscores - self.cached_state_file = os.path.join(self.work_dir_base, cachefile) + self.cached_state_file = os.path.join( + self.work_dir_base, cachefile) if not self.seed_interval: self.seed_interval = 1 if not self.current_seed: self.current_seed = 0 - self.fuzzer_module_name = 'certfuzz.fuzzers.{}'.format(self.config['fuzzer']['fuzzer']) + self.fuzzer_module_name = 'certfuzz.fuzzers.{}'.format( + self.config['fuzzer']['fuzzer']) def _read_config_file(self): logger.info('Reading config from %s', self.config_file) self.config = load_and_fix_config(self.config_file) - logger.info('Using target program: %s', self.config['target']['program']) + logger.info( + 'Using target program: %s', self.config['target']['program']) @abc.abstractmethod def _pre_enter(self): @@ -180,7 +192,8 @@ def _handle_common_errors(self, etype, value, mytraceback): handled = True elif etype is RunnerArchitectureError: logger.error('Unsupported architecture: %s', value) - logger.error('Set "verify_architecture=false" in the runner section of your config to override this check') + logger.error( + 'Set "verify_architecture=false" in the runner section of your config to override this check') handled = True elif etype is RunnerPlatformVersionError: logger.error('Unsupported platform: %s', value) @@ -245,7 +258,8 @@ def __exit__(self, etype, value, mytraceback): def _check_prog(self): if not os.path.exists(self.program): - msg = 'Cannot find program "%s" (resolves to "%s")' % (self.program, os.path.abspath(self.program)) + msg = 'Cannot find program "%s" (resolves to "%s")' % ( + self.program, os.path.abspath(self.program)) raise CampaignError(msg) def _set_fuzzer(self): @@ -263,7 +277,8 @@ def _check_runner(self): self.runner_module.check_runner() except AttributeError: # not a big deal if it's not there, just note it and keep going. - logger.warn('Runner module %s has no check_runner method. Skipping runner check.') + logger.warn( + 'Runner module %s has no check_runner method. Skipping runner check.') @property def _version_file(self): @@ -285,7 +300,8 @@ def _setup_workdir(self): # it even if work_dir_base already exists filetools.make_directories(self.work_dir_base) # now we're sure work_dir_base exists, so it's safe to create temp dirs - self.working_dir = tempfile.mkdtemp(prefix='campaign_', dir=self.work_dir_base) + self.working_dir = tempfile.mkdtemp( + prefix='campaign_', dir=self.work_dir_base) self.seed_dir_local = os.path.join(self.working_dir, 'seedfiles') def _cleanup_workdir(self): @@ -295,7 +311,8 @@ def _cleanup_workdir(self): pass if os.path.exists(self.working_dir): - logger.warning("Unable to remove campaign working dir: %s", self.working_dir) + logger.warning( + "Unable to remove campaign working dir: %s", self.working_dir) else: logger.debug('Removed campaign working dir: %s', self.working_dir) @@ -330,20 +347,24 @@ def _read_state(self, cache_file=None): with open(cache_file, 'rb') as fp: campaign = pickle.load(fp) except Exception, e: - logger.warning('Unable to read %s, will use new campaign instead: %s', cache_file, e) + logger.warning( + 'Unable to read %s, will use new campaign instead: %s', cache_file, e) return if campaign: try: if self.config['config_timestamp'] != campaign.__dict__['config_timestamp']: - logger.warning('Config file modified. Discarding cached campaign') + logger.warning( + 'Config file modified. Discarding cached campaign') else: self.__dict__.update(campaign.__dict__) logger.info('Reloaded campaign from %s', cache_file) except KeyError: - logger.warning('No config date detected. Discarding cached campaign') + logger.warning( + 'No config date detected. Discarding cached campaign') else: - logger.warning('Unable to reload campaign from %s, will use new campaign instead', cache_file) + logger.warning( + 'Unable to reload campaign from %s, will use new campaign instead', cache_file) def _save_state(self, cachefile=None): if not cachefile: @@ -361,7 +382,8 @@ def _testcase_is_unique(self, testcase_id, exploitability='UNKNOWN'): ''' if not testcase_id in self.testcases_seen: self.testcases_seen.add(testcase_id) - logger.debug("%s did not exist in cache, testcase is unique", testcase_id) + logger.debug( + "%s did not exist in cache, testcase is unique", testcase_id) return True logger.debug('%s was found, not unique', testcase_id) return False @@ -396,7 +418,8 @@ def _do_interval(self): # start an iteration interval # note that range does not include interval_limit - logger.debug('Starting interval %d-%d', self.current_seed, interval_limit) + logger.debug( + 'Starting interval %d-%d', self.current_seed, interval_limit) for seednum in xrange(self.current_seed, interval_limit): self._do_iteration(sf, r, seednum) @@ -424,4 +447,3 @@ def go(self): signal.signal(signal.SIGINT, self.signal_handler) while self._keep_going(): self._do_interval() - diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index cc80032..54a8e21 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -64,7 +64,7 @@ def _construct_testcase(self): fuzzedfile=BasicFile(self.fuzzer.output_file_path), program=self.cfg['target']['program'], cmd_template=self.cmd_template, - debugger_timeout=self.cfg['runner']['runtimeout'], + debugger_timeout=self.cfg['debugger']['runtimeout'], cmdlist=get_command_args_list(self.cmd_template, infile=self.fuzzer.output_file_path, posix=True)[1], diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index ed3d666..dc17f34 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -184,7 +184,6 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, filetools.copy_file(self.testcase.fuzzedfile.path, self.tempfile) # figure out what testcase signatures belong to this fuzzedfile - raw_input('timeout?') self.debugger_timeout = self.cfg['debugger']['runtimeout'] self.crash_hashes = [] self.measured_dbg_time = None diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 0b87d0b..18a7d5b 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -142,13 +142,16 @@ def main(): crashers_dir = '.' cfg = load_and_fix_config(cfg_file) + debugger_timeout = cfg['runner']['runtimeout'] * 2 + if debugger_timeout < 10: + debugger_timeout = 10 with LinuxTestcase(cfg=cfg, seedfile=seedfile, fuzzedfile=fuzzed_file, program=cfg['target']['program'], cmd_template=cfg['target']['cmdline_template'], - debugger_timeout=cfg['runner']['runtimeout'], + debugger_timeout=debugger_timeout, cmdlist=get_command_args_list( cfg['target']['cmdline_template'], fuzzed_file.path)[1], backtrace_lines=cfg['debugger']['backtracelevels'], From c059a92dd2a3af1eb3d9428090b02132130d3218 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 15 Jul 2016 12:14:26 -0400 Subject: [PATCH 1018/1169] fix unit test --- src/test_certfuzz/mocks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 09fb99c..18aa4a2 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -138,6 +138,7 @@ class MockCfg(dict): def __init__(self, templated=True): self['debugger'] = {'backtracelevels': 5, 'debugger': 'gdb', + 'runtimeout': 10, } self['target'] = {'cmdline_template': '$PROGRAM b c d $SEEDFILE', 'killprocname': 'a', From 5820f06672d1c4dae9d5a2967e9b462cf1e2ca4d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 15 Jul 2016 16:06:28 -0400 Subject: [PATCH 1019/1169] Allow drillresults analyzer to work even if Cert Triage Tools output wasn't included in the gdb file. BFF-967 --- src/certfuzz/analyzers/drillresults/testcasebundle_linux.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py b/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py index 6645213..c03a871 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_linux.py @@ -37,10 +37,14 @@ class LinuxTestCaseBundle(TestCaseBundle): def _get_classification(self): self.classification = carve(self.reporttext, "Classification: ", "\n") + if not self.classification: + self.classification = 'UNKNOWN' logger.debug('Classification: %s', self.classification) def _get_shortdesc(self): self.shortdesc = carve(self.reporttext, "Short description: ", " (") + if not self.shortdesc: + self.shortdesc = 'UNKNOWN' logger.debug('Short Description: %s', self.shortdesc) def _check_64bit(self): From 89db9ddca68bea7d2afe7080b074478904688fbd Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 15 Jul 2016 16:12:34 -0400 Subject: [PATCH 1020/1169] Don't use CTT exploitable on platforms that don't support it. BFF-967 --- src/certfuzz/campaign/campaign_linux.py | 15 +++++++++++ ...gdb_noctt_complete_nofunction_template.txt | 25 +++++++++++++++++++ .../templates/gdb_noctt_complete_template.txt | 24 ++++++++++++++++++ src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 24 +++++++++++++----- 4 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/certfuzz/debuggers/templates/gdb_noctt_complete_nofunction_template.txt create mode 100644 src/certfuzz/debuggers/templates/gdb_noctt_complete_template.txt diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index fb0fc3e..a4691a0 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -64,6 +64,8 @@ def __init__(self, config_file, result_dir=None, debug=False): CampaignBase.__init__(self, config_file, result_dir, debug) self.runner_module_name = 'certfuzz.runners.zzufrun' self.debugger_module_name = 'certfuzz.debuggers.gdb' + # Assume gdb compatible with CERT Triage Tools + self.config['debugger']['ctt_compat'] = True def _full_path_original(self, seedfile): # yes, two seedfile mentions are intended - adh @@ -83,6 +85,7 @@ def _pre_enter(self): self._check_for_redirect() self._set_unbuffered_stdout() self._setup_environment() + self._check_ctt_compat() def _post_enter(self): if self.config['runoptions']['watchdogtimeout']: @@ -141,6 +144,18 @@ def _setup_watchdog(self): def _setup_environment(self): os.environ['KDE_DEBUG'] = '1' + def _check_ctt_compat(self): + logger.debug('checking CERT Triage Tool compatibility') + current_dir = os.path.dirname(__file__) + ctt_path = os.path.join( + current_dir, '..', '..', 'CERT_triage_tools', 'exploitable', 'exploitable.py') + gdb_output = subprocess.check_output([ + 'gdb', '-ex', 'source %s' % ctt_path, '-ex', 'q']) + if 'Error: ' in gdb_output: + logger.warning( + 'gdb is not compatible with CERT Triage Tools (older than 7.2?). Disabling.') + self.config['debugger']['ctt_compat'] = False + def _check_for_script(self): logger.debug('check for script') if check_program_file_type('text', self.program): diff --git a/src/certfuzz/debuggers/templates/gdb_noctt_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_noctt_complete_nofunction_template.txt new file mode 100644 index 0000000..e710432 --- /dev/null +++ b/src/certfuzz/debuggers/templates/gdb_noctt_complete_nofunction_template.txt @@ -0,0 +1,25 @@ +set disassembly-flavor intel +set logging file $OUTFILE +set logging on +file $PROGRAM +run $CMD_ARGS +echo \n +echo Found_with_CERT_BFF_2.8\n +echo Running: $PROGRAM $CMD_ARGS +echo \n +info proc map +echo \n +echo siginfo: +print $_siginfo +echo si_addr: +print $_siginfo._sifields._sigfault.si_addr +echo \n +bt full 512 +echo \n +info registers +echo \n +x/i $pc +echo \n +disass $pc-32,$pc +disass $pc,$pc+32 +quit \ No newline at end of file diff --git a/src/certfuzz/debuggers/templates/gdb_noctt_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_noctt_complete_template.txt new file mode 100644 index 0000000..ac620ed --- /dev/null +++ b/src/certfuzz/debuggers/templates/gdb_noctt_complete_template.txt @@ -0,0 +1,24 @@ +set disassembly-flavor intel +set logging file $OUTFILE +set logging on +file $PROGRAM +run $CMD_ARGS +echo \n +echo Found_with_CERT_BFF_2.8\n +echo Running: $PROGRAM $CMD_ARGS +echo \n +info proc map +echo \n +echo siginfo: +print $_siginfo +echo si_addr: +print $_siginfo._sifields._sigfault.si_addr +echo \n +bt full 512 +echo \n +info registers +echo \n +x/i $pc +echo \n +disass +quit \ No newline at end of file diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index a01d71f..046a016 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -98,13 +98,25 @@ def _pre_minimize(self, testcase): def _pre_analyze(self, testcase): # get one last debugger output for the newly minimized file - if testcase.pc_in_function: - # change the debugger template - testcase.set_debugger_template('complete') + if self.cfg['debugger']['ctt_compat']: + # CERT Triage Tools compatible gdb vrsion + if testcase.pc_in_function: + # change the debugger template + testcase.set_debugger_template('complete') + else: + # use a debugger template that specifies fixed offsets from $pc for + # disassembly + testcase.set_debugger_template('complete_nofunction') else: - # use a debugger template that specifies fixed offsets from $pc for - # disassembly - testcase.set_debugger_template('complete_nofunction') + # gdb version not compatible with CERT Triage Tools + if testcase.pc_in_function: + # change the debugger template + testcase.set_debugger_template('noctt_complete') + else: + # use a debugger template that specifies fixed offsets from $pc for + # disassembly + testcase.set_debugger_template('noctt_complete_nofunction') + logger.info( 'Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) testcase.get_debug_output(testcase.fuzzedfile.path) From e2ca37eacc6889b0ed94e93a4a1f6bb20c3d7f9c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 15 Jul 2016 16:46:48 -0400 Subject: [PATCH 1021/1169] Don't check for CTT compatibility on OSX. BFF-967 --- src/certfuzz/campaign/campaign_linux.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index a4691a0..70c6757 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -16,6 +16,7 @@ from certfuzz.debuggers import gdb # @UnusedImport from certfuzz.file_handlers.watchdog_file import TWDF, touch_watchdog_file from certfuzz.fuzztools import subprocess_helper as subp +from certfuzz.fuzztools import hostinfo from certfuzz.fuzztools.ppid_observer import check_ppid from certfuzz.fuzztools.watchdog import WatchDog from certfuzz.iteration.iteration_linux import LinuxIteration @@ -24,7 +25,7 @@ logger = logging.getLogger(__name__) - +host_info = hostinfo SEEDFILE_REPLACE_STRING = '\$SEEDFILE' @@ -85,7 +86,9 @@ def _pre_enter(self): self._check_for_redirect() self._set_unbuffered_stdout() self._setup_environment() - self._check_ctt_compat() + if not host_info.is_osx: + # OSX doesn't use gdb, + self._check_ctt_compat() def _post_enter(self): if self.config['runoptions']['watchdogtimeout']: From 306de28ecd52df6008b02b765f3a46e474ae0af7 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 15 Jul 2016 17:07:53 -0400 Subject: [PATCH 1022/1169] Fix typo --- src/certfuzz/campaign/campaign_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 70c6757..17e2729 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -86,7 +86,7 @@ def _pre_enter(self): self._check_for_redirect() self._set_unbuffered_stdout() self._setup_environment() - if not host_info.is_osx: + if not host_info.is_osx(): # OSX doesn't use gdb, self._check_ctt_compat() From 36a514087d67ab67f888e0b1fb73b627fc3953bf Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 15 Jul 2016 17:10:17 -0400 Subject: [PATCH 1023/1169] One more typo fix. --- src/certfuzz/campaign/campaign_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 17e2729..342b1d7 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) -host_info = hostinfo +host_info = hostinfo.HostInfo() SEEDFILE_REPLACE_STRING = '\$SEEDFILE' From c74a78bf105fb51cb0be6410d3cedb54e9c3f1c1 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 19 Jul 2016 14:21:06 -0400 Subject: [PATCH 1024/1169] Raise an error if drillresults can't proceed --- src/certfuzz/analyzers/drillresults/testcasebundle_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_base.py b/src/certfuzz/analyzers/drillresults/testcasebundle_base.py index 658e9c2..ccede43 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_base.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_base.py @@ -148,11 +148,11 @@ def _parse_testcase(self): # No faulting address means no crash. if not faultaddr: - # raise TestCaseBundleError('No faulting address means no crash') + raise TestCaseBundleError('No faulting address means no crash') return if not instraddr: - # raise TestCaseBundleError('No instraddr address means no crash') + raise TestCaseBundleError('No instraddr address means no crash') return faultaddr, instraddr = self._64bit_addr_fixup(faultaddr, instraddr) From b19048c4c64ec9522c3337c31ae4e6816c24d9cb Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 19 Jul 2016 14:22:38 -0400 Subject: [PATCH 1025/1169] Check if the current platform has /proc for gdb templating. --- src/certfuzz/campaign/campaign_linux.py | 14 +++++- src/certfuzz/debuggers/gdb.py | 21 ++++++--- .../templates/gdb_noproc_bt_only_template.txt | 10 ++++ ...db_noproc_complete_nofunction_template.txt | 18 +++++++ .../gdb_noproc_complete_template.txt | 17 +++++++ src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 21 +-------- src/certfuzz/testcase/testcase_linux.py | 47 ++++++++++++++----- 7 files changed, 108 insertions(+), 40 deletions(-) create mode 100644 src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt create mode 100644 src/certfuzz/debuggers/templates/gdb_noproc_complete_nofunction_template.txt create mode 100644 src/certfuzz/debuggers/templates/gdb_noproc_complete_template.txt diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 342b1d7..c0e1f07 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -9,6 +9,7 @@ import subprocess import sys import time +import platform from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.campaign.errors import CampaignScriptError, CmdlineTemplateError @@ -67,6 +68,8 @@ def __init__(self, config_file, result_dir=None, debug=False): self.debugger_module_name = 'certfuzz.debuggers.gdb' # Assume gdb compatible with CERT Triage Tools self.config['debugger']['ctt_compat'] = True + # Assume we're on Linux, which has /proc + self.config['debugger']['proc_compat'] = True def _full_path_original(self, seedfile): # yes, two seedfile mentions are intended - adh @@ -88,7 +91,7 @@ def _pre_enter(self): self._setup_environment() if not host_info.is_osx(): # OSX doesn't use gdb, - self._check_ctt_compat() + self._check_gdb_compat() def _post_enter(self): if self.config['runoptions']['watchdogtimeout']: @@ -147,7 +150,7 @@ def _setup_watchdog(self): def _setup_environment(self): os.environ['KDE_DEBUG'] = '1' - def _check_ctt_compat(self): + def _check_gdb_compat(self): logger.debug('checking CERT Triage Tool compatibility') current_dir = os.path.dirname(__file__) ctt_path = os.path.join( @@ -159,6 +162,13 @@ def _check_ctt_compat(self): 'gdb is not compatible with CERT Triage Tools (older than 7.2?). Disabling.') self.config['debugger']['ctt_compat'] = False + logger.debug('checking /proc compatibility') + current_platform = platform.system() + if current_platform is not 'Linux': + logger.debug( + '%s does not support /proc. Adjusting debugger templates.' % current_platform) + self.config['debugger']['proc_compat'] = False + def _check_for_script(self): logger.debug('check for script') if check_program_file_type('text', self.program): diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index d6969dc..7fae787 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -25,7 +25,8 @@ class GDB(Debugger): _ext = 'gdb' def __init__(self, program, cmd_args, outfile_base, timeout, template=None, exclude_unmapped_frames=True, keep_uniq_faddr=False, **options): - Debugger.__init__(self, program, cmd_args, outfile_base, timeout, **options) + Debugger.__init__( + self, program, cmd_args, outfile_base, timeout, **options) self.template = template self.exclude_unmapped_frames = exclude_unmapped_frames self.keep_uniq_faddr = keep_uniq_faddr @@ -33,16 +34,19 @@ def __init__(self, program, cmd_args, outfile_base, timeout, template=None, excl def _get_cmdline(self): self._create_input_file() if not os.path.exists(self.input_file): - raise DebuggerError('Input file does not exist: %s', self.input_file) + raise DebuggerError( + 'Input file does not exist: %s', self.input_file) - args = [self.debugger_app(), '-n', '-batch', '-command', self.input_file] + args = [ + self.debugger_app(), '-n', '-batch', '-command', self.input_file] logger.log(5, "GDB command: [%s]", ' '.join(args)) return args def _create_input_file(self): # short-circuit if input_file already exists if os.path.exists(self.input_file): - logger.log(5, "GDB input file already exists at %s", self.input_file) + logger.log( + 5, "GDB input file already exists at %s", self.input_file) return if not self.template: @@ -72,13 +76,15 @@ def _create_input_file(self): if os.path.exists(self.input_file): logger.log(5, "GDB input file is %s", self.input_file) else: - logger.warning("Failed to create GDB input file %s", self.input_file) + logger.warning( + "Failed to create GDB input file %s", self.input_file) def _remove_temp_file(self): try: os.remove(self.input_file) except OSError as e: - logger.warning("Caught OSError attempting to remove %s: %s", self.input_file, e) + logger.warning( + "Caught OSError attempting to remove %s: %s", self.input_file, e) if os.path.exists(self.input_file): logger.warning("Failed to delete %s", self.input_file) @@ -108,7 +114,8 @@ def go(self): # build the command line in a separate function so we can unit test # it without actually running the command cmdline = self._get_cmdline() - subp.run_with_timer(cmdline, self.timeout, self.program, stdout=os.devnull) + subp.run_with_timer( + cmdline, self.timeout, self.program, stdout=os.devnull) self._remove_temp_file() if not os.path.exists(self.outfile): diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt new file mode 100644 index 0000000..8ca8d24 --- /dev/null +++ b/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt @@ -0,0 +1,10 @@ +set disassembly-flavor intel +set logging file $OUTFILE +set logging on +file $PROGRAM +run $CMD_ARGS +bt full 512 +info registers +echo si_addr: +print $_siginfo._sifields._sigfault.si_addr +quit \ No newline at end of file diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_complete_nofunction_template.txt new file mode 100644 index 0000000..da139e2 --- /dev/null +++ b/src/certfuzz/debuggers/templates/gdb_noproc_complete_nofunction_template.txt @@ -0,0 +1,18 @@ +set disassembly-flavor intel +set logging file $OUTFILE +set logging on +file $PROGRAM +run $CMD_ARGS +echo \n +echo Found_with_CERT_BFF_2.8\n +echo Running: $PROGRAM $CMD_ARGS +echo \n +bt full 512 +echo \n +info registers +echo \n +x/i $pc +echo \n +disass $pc-32,$pc +disass $pc,$pc+32 +quit \ No newline at end of file diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_complete_template.txt new file mode 100644 index 0000000..587fc5b --- /dev/null +++ b/src/certfuzz/debuggers/templates/gdb_noproc_complete_template.txt @@ -0,0 +1,17 @@ +set disassembly-flavor intel +set logging file $OUTFILE +set logging on +file $PROGRAM +run $CMD_ARGS +echo \n +echo Found_with_CERT_BFF_2.8\n +echo Running: $PROGRAM $CMD_ARGS +echo \n +bt full 512 +echo \n +info registers +echo \n +x/i $pc +echo \n +disass +quit \ No newline at end of file diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index 046a016..f56114c 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -97,25 +97,8 @@ def _pre_minimize(self, testcase): touch_watchdog_file() def _pre_analyze(self, testcase): - # get one last debugger output for the newly minimized file - if self.cfg['debugger']['ctt_compat']: - # CERT Triage Tools compatible gdb vrsion - if testcase.pc_in_function: - # change the debugger template - testcase.set_debugger_template('complete') - else: - # use a debugger template that specifies fixed offsets from $pc for - # disassembly - testcase.set_debugger_template('complete_nofunction') - else: - # gdb version not compatible with CERT Triage Tools - if testcase.pc_in_function: - # change the debugger template - testcase.set_debugger_template('noctt_complete') - else: - # use a debugger template that specifies fixed offsets from $pc for - # disassembly - testcase.set_debugger_template('noctt_complete_nofunction') + + testcase.set_debugger_template('complete') logger.info( 'Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index c3759a3..d0ca870 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -22,10 +22,10 @@ host_info = hostinfo.HostInfo() -if host_info.is_linux(): - from certfuzz.debuggers.gdb import GDB as debugger_cls -elif host_info.is_osx(): +if host_info.is_osx(): from certfuzz.debuggers.crashwrangler import CrashWrangler as debugger_cls +else: + from certfuzz.debuggers.gdb import GDB as debugger_cls class LinuxTestcase(TestCaseBase): @@ -66,15 +66,38 @@ def __init__(self, self.signature = None def set_debugger_template(self, option='bt_only'): - if host_info.is_linux(): - dbg_template_name = '%s_%s_template.txt' % ( - self._debugger_cls._key, option) - self.debugger_template = os.path.join( - 'certfuzz/debuggers/templates', dbg_template_name) - logger.debug('Debugger template set to %s', self.debugger_template) - if not os.path.exists(self.debugger_template): - raise TestCaseError( - 'Debugger template does not exist at %s' % self.debugger_template) + if host_info.is_osx(): + # OSX doesn't use gdb templates + return + + # Available templates, based on current platform capabilities: + # bt_only + # noproc_bt_only + # complete + # complete_nofunction + # noctt_complete + # noctt_complete_nofunction + # noproc_complete + # noproc_complete_nofunction + + if not self.cfg['debugger']['proc_compat']: + option = 'noproc_' + option + + if 'complete' in option: + if not self.cfg['debugger']['ctt_compat']: + option = 'noctt_' + option + + if not self.pc_in_function: + option = option + '_nofunction' + + dbg_template_name = '%s_%s_template.txt' % ( + self._debugger_cls._key, option) + self.debugger_template = os.path.join( + 'certfuzz/debuggers/templates', dbg_template_name) + logger.debug('Debugger template set to %s', self.debugger_template) + if not os.path.exists(self.debugger_template): + raise TestCaseError( + 'Debugger template does not exist at %s' % self.debugger_template) def update_crash_details(self): TestCaseBase.update_crash_details(self) From 993d809be3ff3420366b07ec9c0bae8d652e9dda Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 19 Jul 2016 14:22:56 -0400 Subject: [PATCH 1026/1169] use /bin/sh instead of bash, for POSIX compat. --- src/linux/batch.sh | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index ca3e7e8..bd162fa 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh ############################################################################## # Use of the CERT Basic Fuzzing Framework and related source code is subject @@ -46,11 +46,28 @@ # contract clause at 252.227.7013. ############################################################################## + +# contains(string, substring) +# +# Returns 0 if the specified string contains the specified substring, +# otherwise returns 1. +contains() { + string="$1" + substring="$2" + if test "${string#*$substring}" != "$string" + then + return 0 # $substring is in $string + else + return 1 # $substring is not in $string + fi +} + + scriptlocation=`echo "$(cd "$(dirname "$0")"; pwd)/"` echo Script location: $scriptlocation/bff.py platform=`uname -a` PINURL=https://software.intel.com/sites/landingpage/pintool/downloads/pin-3.0-76991-gcc-linux.tar.gz -if [[ "$platform" =~ "Darwin Kernel Version 11" ]]; then +if ( contains "$platform" "Darwin Kernel Version 11" ); then mypython="/Library/Frameworks/Python.framework/Versions/2.7/bin/python" else mypython="python" @@ -60,14 +77,14 @@ fi # Prevent creation of huge files ulimit -f 1048576 -if [[ "$platform" =~ "Linux" ]]; then - if [[ ! -f ~/pin/pin ]]; then +if ( contains "$platform" "Linux" ); then + if [ ! -f ~/pin/pin ]; then mkdir -p ~/fuzzing echo PIN not detected. Downloading... tarball=~/fuzzing/`basename $PINURL` pindir=`basename $tarball .tar.gz` wget --tries=1 $PINURL -O $tarball - if [[ -f $tarball ]]; then + if [ -f $tarball ]; then tar xzvf $tarball -C ~ mv ~/$pindir ~/pin else @@ -77,13 +94,13 @@ if [[ "$platform" =~ "Linux" ]]; then cp -au $scriptlocation/pintool ~ - if [[ ! -f ~/pintool/calltrace.so ]]; then + if [ ! -f ~/pintool/calltrace.so ]; then echo Building calltrace pintool... cd ~/pintool $mypython make.py fi - if [[ ~/pintool/calltrace.cpp -ot $scriptlocation/pintool/calltrace.cpp ]]; then + if [ ~/pintool/calltrace.cpp -ot $scriptlocation/pintool/calltrace.cpp ]; then echo Updating calltrace pintool... cd ~/pintool $mypython make.py @@ -93,7 +110,7 @@ fi cd $scriptlocation echo "Using python interpreter: $mypython" -if [[ -f "$scriptlocation/bff.py" ]]; then +if [ -f "$scriptlocation/bff.py" ]; then $mypython $scriptlocation/bff.py "$@" else read -p "Cannot find $scriptlocation/bff.py Please verify script locations." From a51b2bbf436669c42d11d353a6958796617375ee Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 19 Jul 2016 14:58:15 -0400 Subject: [PATCH 1027/1169] Fix standalone minimization for BFF-967 fix --- src/certfuzz/tools/linux/minimize.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 18a7d5b..0183c0f 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -6,6 +6,7 @@ import logging import os import sys +import platform from certfuzz import debuggers from certfuzz.testcase.testcase_linux import LinuxTestcase @@ -145,6 +146,9 @@ def main(): debugger_timeout = cfg['runner']['runtimeout'] * 2 if debugger_timeout < 10: debugger_timeout = 10 + proc_compat = platform.system() == 'Linux' + cfg['debugger']['proc_compat'] = proc_compat + cfg['debugger']['runtimeout'] = debugger_timeout with LinuxTestcase(cfg=cfg, seedfile=seedfile, From 9d36920e613a273b266ece9102eecb503bace9e2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 26 Jul 2016 09:54:31 -0400 Subject: [PATCH 1028/1169] Fix platform-checking logic, using already-imported module. --- src/certfuzz/campaign/campaign_linux.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index c0e1f07..779694a 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -9,7 +9,6 @@ import subprocess import sys import time -import platform from certfuzz.campaign.campaign_base import CampaignBase from certfuzz.campaign.errors import CampaignScriptError, CmdlineTemplateError @@ -163,8 +162,7 @@ def _check_gdb_compat(self): self.config['debugger']['ctt_compat'] = False logger.debug('checking /proc compatibility') - current_platform = platform.system() - if current_platform is not 'Linux': + if not host_info.is_linux: logger.debug( '%s does not support /proc. Adjusting debugger templates.' % current_platform) self.config['debugger']['proc_compat'] = False From 36ad5a7265240a55ef15c65aab878e0ee91c2b61 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 26 Jul 2016 09:55:36 -0400 Subject: [PATCH 1029/1169] Templates for a platform that uses gdb, but isn't linux, and isn't CTT compatible. --- ...roc_noctt_complete_nofunction_template.txt | 23 +++++++++++++++++++ .../gdb_noproc_noctt_complete_template.txt | 22 ++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt create mode 100644 src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt new file mode 100644 index 0000000..5f487eb --- /dev/null +++ b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt @@ -0,0 +1,23 @@ +set disassembly-flavor intel +set logging file $OUTFILE +set logging on +file $PROGRAM +run $CMD_ARGS +echo \n +echo Found_with_CERT_BFF_2.8\n +echo Running: $PROGRAM $CMD_ARGS +echo \n +echo siginfo: +print $_siginfo +echo si_addr: +print $_siginfo._sifields._sigfault.si_addr +echo \n +bt full 512 +echo \n +info registers +echo \n +x/i $pc +echo \n +disass $pc-32,$pc +disass $pc,$pc+32 +quit \ No newline at end of file diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt new file mode 100644 index 0000000..2066ba4 --- /dev/null +++ b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt @@ -0,0 +1,22 @@ +set disassembly-flavor intel +set logging file $OUTFILE +set logging on +file $PROGRAM +run $CMD_ARGS +echo \n +echo Found_with_CERT_BFF_2.8\n +echo Running: $PROGRAM $CMD_ARGS +echo \n +echo siginfo: +print $_siginfo +echo si_addr: +print $_siginfo._sifields._sigfault.si_addr +echo \n +bt full 512 +echo \n +info registers +echo \n +x/i $pc +echo \n +disass +quit \ No newline at end of file From 47abcd7352198dc95e4091fa3ef1dffb20d39835 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 27 Jul 2016 14:16:49 -0400 Subject: [PATCH 1030/1169] pep8 cleanup --- .../drillresults/testcasebundle_windows.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py index bf0ab60..33433ed 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py @@ -15,30 +15,33 @@ # compile our regular expresssions once RE_64BIT_DEBUGGER = re.compile('^Microsoft.*AMD64$') -RE_MAPPED_ADDRESS = re.compile('^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)') -RE_MAPPED_ADDRESS64 = re.compile('^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), +RE_MAPPED_ADDRESS = re.compile( + '^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)') +RE_MAPPED_ADDRESS64 = re.compile( + '^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), RE_SYSWOW64 = re.compile('ModLoad:.*syswow64.*', re.IGNORECASE) class WindowsTestCaseBundle(TestCaseBundle): # These !exploitable short descriptions indicate a very interesting crash really_exploitable = [ - 'ReadAVonIP', - 'TaintedDataControlsCodeFlow', - 'ReadAVonControlFlow', - 'DEPViolation', - 'IllegalInstruction', - 'PrivilegedInstruction', - ] + 'ReadAVonIP', + 'TaintedDataControlsCodeFlow', + 'ReadAVonControlFlow', + 'DEPViolation', + 'IllegalInstruction', + 'PrivilegedInstruction', + ] def __init__(self, dbg_outfile, testcase_file, crash_hash, ignore_jit): super(self.__class__, self).__init__(dbg_outfile, testcase_file, crash_hash, - ignore_jit) + ignore_jit) self.wow64_app = False def _get_classification(self): - self.classification = carve(self.reporttext, "Exploitability Classification: ", "\n") + self.classification = carve( + self.reporttext, "Exploitability Classification: ", "\n") logger.debug('Classification: %s', self.classification) def _get_shortdesc(self): @@ -77,9 +80,11 @@ def _find_testcase_file(self): fileparts = self.testcase_file.split('-') m = re.search('\..+', fileparts[1]) # Recreate the original file name, minus the iteration - self.testcase_file = os.path.join(current_dir, fileparts[0] + m.group(0)) + self.testcase_file = os.path.join( + current_dir, fileparts[0] + m.group(0)) else: - self.testcase_file = os.path.join(current_dir, self.testcase_file) + self.testcase_file = os.path.join( + current_dir, self.testcase_file) TestCaseBundle._find_testcase_file(self) @@ -115,7 +120,8 @@ def _look_for_loaded_module(self, instraddr, line): begin_address = int(n.group(1).replace('`', ''), 16) end_address = int(n.group(2).replace('`', ''), 16) module_name = n.group(3) - logger.debug('%x %x %s %x', begin_address, end_address, module_name, instraddr) + logger.debug( + '%x %x %s %x', begin_address, end_address, module_name, instraddr) if begin_address < instraddr < end_address: logger.debug('Matched: %x in %x %x %s', instraddr, begin_address, end_address, module_name) From 7f4d8540824f77f92afa546d60941337615c739d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 27 Jul 2016 14:21:29 -0400 Subject: [PATCH 1031/1169] Support running drillresults on a minimizer_out directory on Windows. BFF-970 --- .../drillresults/testcasebundle_windows.py | 9 ++++--- .../drillresults/result_driller_windows.py | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py index 33433ed..2693b74 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py @@ -10,7 +10,6 @@ from certfuzz.drillresults.common import carve from certfuzz.analyzers.drillresults.testcasebundle_base import TestCaseBundle - logger = logging.getLogger(__name__) # compile our regular expresssions once @@ -73,7 +72,10 @@ def _find_testcase_file(self): for arg in args: if "sf_" in arg: self.testcase_file = os.path.basename(arg) - if "-" in self.testcase_file: + if os.path.isfile(os.path.join(current_dir, self.testcase_file)): + self.testcase_file = os.path.join( + current_dir, self.testcase_file) + elif "-" in self.testcase_file: # FOE 2.0 verify mode puts a '-' part on the # filename when invoking cdb, however the resulting file # is really just 'sf_.' @@ -82,9 +84,6 @@ def _find_testcase_file(self): # Recreate the original file name, minus the iteration self.testcase_file = os.path.join( current_dir, fileparts[0] + m.group(0)) - else: - self.testcase_file = os.path.join( - current_dir, self.testcase_file) TestCaseBundle._find_testcase_file(self) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index 4681dc7..ea7c815 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -8,7 +8,6 @@ from certfuzz.analyzers.drillresults.testcasebundle_windows import WindowsTestCaseBundle as TestCaseBundle from certfuzz.drillresults.result_driller_base import ResultDriller - logger = logging.getLogger(__name__) regex = { @@ -18,16 +17,17 @@ class WindowsResultDriller(ResultDriller): - def _platform_find_testcases(self, crash_hash, files, root, force=False): - if "0x" in crash_hash: + def _platform_find_testcases(self, crash_dir, files, root, force=False): + if "0x" in crash_dir or 'BFF_testcase' in crash_dir: # Create dictionary for hashes in results dictionary hash_dict = {} - hash_dict['hash'] = crash_hash + hash_dict['hash'] = crash_dir crasherfile = '' # Check each of the files in the hash directory for current_file in files: - if regex['first_msec'].match(current_file): + # if regex['first_msec'].match(current_file): + if current_file.endswith('.msec') and '.e0.' in current_file: # If it's exception #0, strip out the exploitability part of # the file name. This gives us the crasher file name crasherfile, _junk = os.path.splitext(current_file) @@ -40,14 +40,14 @@ def _platform_find_testcases(self, crash_hash, files, root, force=False): # If we have a drillresults file for this crash hash, we use # that output instead of recalculating it # Use the .drillresults output for this crash hash - self._load_dr_output(crash_hash, + self._load_dr_output(crash_dir, os.path.join(root, current_file)) for current_file in files: - if crash_hash in self.dr_scores: + if crash_dir in self.dr_scores: # We are currently working with a crash hash if self.dr_scores[crash_hash] is not None: - # We've already got a score for this crash_hash + # We've already got a score for this crash_dir logger.debug('Skipping %s' % current_file) continue @@ -56,18 +56,20 @@ def _platform_find_testcases(self, crash_hash, files, root, force=False): dbg_file = os.path.join(root, current_file) if crasherfile and root not in crasherfile: crasherfile = os.path.join(root, crasherfile) - with TestCaseBundle(dbg_file, crasherfile, crash_hash, + with TestCaseBundle(dbg_file, crasherfile, crash_dir, self.ignore_jit) as tcb: tcb.go() _updated_existing = False + # if not self.testcase_bundles: + # continue for index, tcbundle in enumerate(self.testcase_bundles): - if tcbundle.crash_hash == crash_hash: + if tcbundle.crash_hash == crash_dir: # This is a new exception for the same crash # hash self.testcase_bundles[index].details[ 'exceptions'].update(tcb.details['exceptions']) # If the current exception score is lower than - # the existing crash_hash score, update it + # the existing crash_dir score, update it self.testcase_bundles[index].score = min( self.testcase_bundles[index].score, tcb.score) _updated_existing = True From 2d9f9dc12bda1c11114e3a678f4e94d5f02401a9 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 27 Jul 2016 14:47:54 -0400 Subject: [PATCH 1032/1169] pep8 cleanup --- src/certfuzz/drillresults/common.py | 31 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/certfuzz/drillresults/common.py b/src/certfuzz/drillresults/common.py index 8c67fc2..c98c62a 100644 --- a/src/certfuzz/drillresults/common.py +++ b/src/certfuzz/drillresults/common.py @@ -33,22 +33,22 @@ def _build_arg_parser(): group = parser.add_mutually_exclusive_group() group.add_argument('--debug', dest='debug', action='store_true', - help='Set logging to DEBUG and enable additional debuggers if available') + help='Set logging to DEBUG and enable additional debuggers if available') group.add_argument('-v', '--verbose', dest='verbose', action='store_true', - help='Set logging to INFO level') + help='Set logging to INFO level') parser.add_argument('-d', '--dir', - help='directory to look for results in. Default is "results"', - dest='resultsdir', - default='../results', - type=str) + help='directory to look for results in. Default is "results"', + dest='resultsdir', + default='../results', + type=str) parser.add_argument('-j', '--ignore-jit', dest='ignore_jit', - action='store_true', - help='Ignore PC in unmapped module (JIT)', - default=False) + action='store_true', + help='Ignore PC in unmapped module (JIT)', + default=False) parser.add_argument('-f', '--force', dest='force', - action='store_true', - help='Force recalculation of results') + action='store_true', + help='Force recalculation of results') parser.add_argument('-a', '--all', dest='report_all', help='Report all scores (default is to only print if <=70)', default=False) @@ -176,10 +176,11 @@ def main(driller_class=None): root_logger_to_console(args) if driller_class is None: - raise DrillResultsError('A platform-specific driller_class must be specified.') + raise DrillResultsError( + 'A platform-specific driller_class must be specified.') with driller_class(ignore_jit=args.ignore_jit, - base_dir=args.resultsdir, - force_reload=args.force, - report_all=args.report_all) as rd: + base_dir=args.resultsdir, + force_reload=args.force, + report_all=args.report_all) as rd: rd.drill_results() From f2fff20dd3b828cc0680472215efcc0896f5e109 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 27 Jul 2016 14:49:40 -0400 Subject: [PATCH 1033/1169] bt_only gdb output has a different syntax for the exception faulting address. BFF-970 --- src/certfuzz/drillresults/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/drillresults/common.py b/src/certfuzz/drillresults/common.py index c98c62a..93c4543 100644 --- a/src/certfuzz/drillresults/common.py +++ b/src/certfuzz/drillresults/common.py @@ -96,7 +96,8 @@ def carve(string, token1, token2): # Todo: fix this up. Was added to bring gdb support def carve2(string): delims = [("Exception Faulting Address: ", "\n"), - ("si_addr:$2 = (void *)", "\n")] + ("si_addr:$2 = (void *)", "\n"), + ("si_addr:$1 = (void *)", "\n")] for token1, token2 in delims: substring = carve(string, token1, token2) if len(substring): From 00cdb2701cfc8238ba737d2ad2a8b9c56f64780e Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 27 Jul 2016 17:14:37 -0400 Subject: [PATCH 1034/1169] python errors emitted by gdb are in stderr. We need to see that in checking for CTT compatibility. --- src/certfuzz/campaign/campaign_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 779694a..c24c6de 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -155,7 +155,7 @@ def _check_gdb_compat(self): ctt_path = os.path.join( current_dir, '..', '..', 'CERT_triage_tools', 'exploitable', 'exploitable.py') gdb_output = subprocess.check_output([ - 'gdb', '-ex', 'source %s' % ctt_path, '-ex', 'q']) + 'gdb', '-ex', 'source %s' % ctt_path, '-ex', 'q'], stderr=subprocess.STDOUT) if 'Error: ' in gdb_output: logger.warning( 'gdb is not compatible with CERT Triage Tools (older than 7.2?). Disabling.') From be1fcea7166faa0049215f0813745d46cd2de03b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jul 2016 10:17:18 -0400 Subject: [PATCH 1035/1169] Don't just check if the output directory exists. Check that it has files in it as well. BFF-971 --- src/certfuzz/tc_pipeline/tc_pipeline_windows.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 0e47bc1..1fea874 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -69,7 +69,10 @@ def keep_testcase(self, testcase): # Check if crasher directory exists already target_dir = testcase._get_output_dir(self.outdir) if os.path.exists(target_dir): - return (False, 'skip duplicate %s' % testcase.signature) + if len(os.listdir(target_dir)) > 0: + return (False, 'skip duplicate %s' % testcase.signature) + else: + return(True, 'Empty output directory') else: return (True, 'unique') else: From 2348081affbfe88dbbb532efa92b23a729741e23 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jul 2016 10:25:28 -0400 Subject: [PATCH 1036/1169] fix renamed variable --- src/certfuzz/drillresults/result_driller_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index ea7c815..9b84b20 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -46,7 +46,7 @@ def _platform_find_testcases(self, crash_dir, files, root, force=False): for current_file in files: if crash_dir in self.dr_scores: # We are currently working with a crash hash - if self.dr_scores[crash_hash] is not None: + if self.dr_scores[crash_dir] is not None: # We've already got a score for this crash_dir logger.debug('Skipping %s' % current_file) continue From 632104cee21bfb6008af3ba7b1ed291dd283b758 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jul 2016 14:03:19 -0400 Subject: [PATCH 1037/1169] Use exploitability in the output directory. BFF-972 --- src/certfuzz/campaign/campaign_base.py | 2 +- .../debuggers/output_parsers/cwfile.py | 20 ++++++++++++++++++- .../output_parsers/debugger_file_base.py | 14 ++++++++++++- src/certfuzz/reporters/copy_files.py | 11 +++++++--- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 15 +++++++++----- src/certfuzz/tc_pipeline/tc_pipeline_linux.py | 9 +++++++-- .../tc_pipeline/tc_pipeline_windows.py | 12 +++++------ src/certfuzz/testcase/testcase_linux.py | 4 +++- src/certfuzz/testcase/testcase_windows.py | 5 +++-- 9 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index 58cf6c6..ab01c07 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -380,7 +380,7 @@ def _testcase_is_unique(self, testcase_id, exploitability='UNKNOWN'): @param testcase_id: the testcase_id to look up @param exploitability: not used at this time ''' - if not testcase_id in self.testcases_seen: + if testcase_id not in self.testcases_seen: self.testcases_seen.add(testcase_id) logger.debug( "%s did not exist in cache, testcase is unique", testcase_id) diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index f75ef33..982c45a 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -36,6 +36,7 @@ 'bt_line_from': re.compile(r'\bfrom\b'), 'bt_line_at': re.compile(r'\bat\b'), 'register': re.compile('\s\s\s?[0-9a-zA-Z]+:\s(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), + 'exploitability': re.compile('exception=.+:is_exploitable=( no|yes):'), } # There are a number of functions that are typically found in crash backtraces, @@ -88,6 +89,7 @@ def __init__(self, f): self.pc_name = 'eip' self.keep_uniq_faddr = False self.faddr = None + self.exp = None self._process_lines() @@ -198,6 +200,9 @@ def _process_lines(self): if not self.is_corrupt_stack: self._look_for_corrupt_stack(line) + if not self.exp: + self._look_for_exploitability(line) + self._look_for_registers(line) # if we found that the stack was corrupt, @@ -221,7 +226,7 @@ def _look_for_64bit(self, line): ''' if self.is_64bit: return - #raw_input('checking exception regex') + m = re.match(regex['code_type'], line) if m: code_type = m.group(0) @@ -246,6 +251,19 @@ def _look_for_signal(self, line): if m: self.signal = m.group(1) + def _look_for_exploitability(self, line): + if self.faddr: + return + + m = re.match(regex['exploitability'], line) + if m: + exploitable = m.group(1) + if exploitable == 'yes': + self.exp = 'EXPLOITABLE' + else: + self.exp = 'UNKNOWN' + logger.debug('Exploitable: %s', self.exp) + def _look_for_crash(self, line): if 'SIGKILL' in line: self.is_crash = False diff --git a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py index 49cbb7c..320895d 100644 --- a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py +++ b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py @@ -40,7 +40,8 @@ 'konqi_bt_threads': re.compile(r'^\[Current thread is \d+\s\(Thread\s([0-9a-zA-Z]+).+\]$'), 'detect_konqi': re.compile(r'-- Backtrace:'), 'detect_abrt': re.compile(r'Core was generated by'), - 'detect_gdb': re.compile(r'#\d+\s+') + 'detect_gdb': re.compile(r'#\d+\s+'), + 'exploitability': re.compile(r'^Exploitability Classification: (.+)$') } # There are a number of functions that are typically found in crash backtraces, @@ -134,6 +135,7 @@ def __init__(self, path, exclude_unmapped_frames=True, keep_uniq_faddr=False): self.pc_name = 'eip' self.keep_uniq_faddr = keep_uniq_faddr self.faddr = None + self.exp = 'UNKNOWN' # a list of functions to be called on each line # if line_callbacks is set in child classes these ones @@ -151,6 +153,7 @@ def __init__(self, path, exclude_unmapped_frames=True, keep_uniq_faddr=False): self._look_for_registers, self._look_for_faddr, self._build_module_map, + self._look_for_exploitability, ] self._read_file() self._process_file() @@ -423,6 +426,15 @@ def _look_for_faddr(self, line): self.faddr = m.group(1) logger.debug('Faulting address: %s', self.faddr) + def _look_for_exploitability(self, line): + if self.faddr: + return + + m = re.match(regex['exploitability'], line) + if m: + self.exp = m.group(1) + logger.debug('Exploitability: %s', self.exp) + def _look_for_crash(self, line): if not self.is_crash: return diff --git a/src/certfuzz/reporters/copy_files.py b/src/certfuzz/reporters/copy_files.py index ba5a133..270185f 100644 --- a/src/certfuzz/reporters/copy_files.py +++ b/src/certfuzz/reporters/copy_files.py @@ -20,21 +20,26 @@ class CopyFilesReporter(ReporterBase): Copies files to a location ''' - def __init__(self, testcase, target_dir): + def __init__(self, testcase, keep_duplicates): ''' Constructor ''' ReporterBase.__init__(self, testcase) - self.target_dir = target_dir + self.target_dir = testcase.target_dir + self.keep_duplicates = keep_duplicates def go(self): - dst_dir = os.path.join(self.target_dir, self.testcase.signature) + dst_dir = self.target_dir if len(dst_dir) > 130: # Don't make a path too deep. Windows won't support it dst_dir = dst_dir[:130] + '__' # ensure target dir exists already (it might because of crash logging) filetools.mkdir_p(dst_dir) + if (len(os.listdir(dst_dir)) > 0 and not self.keep_duplicates): + logger.debug( + 'Output path %s already contains output. Skipping.' % dst_dir) + return src_dir = self.testcase.tempdir if not os.path.exists(src_dir): diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index 4418324..4e8b1d9 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -216,7 +216,8 @@ def _minimize(self, testcase): for new_tc in m.other_crashes.values(): self.tc_candidate_q.put(new_tc) except MinimizerError as e: - logger.warning('Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) + logger.warning( + 'Unable to minimize %s, proceeding with original fuzzed crash file: %s', testcase.signature, e) # calculate the hamming distances for this crash # between the original seedfile and the minimized fuzzed file @@ -225,9 +226,12 @@ def _minimize(self, testcase): def _post_minimize(self, testcase): if self.cfg['runoptions']['recycle_crashers']: logger.debug('Recycling crash as seedfile') - iterstring = testcase.fuzzedfile.basename.split('-')[1].split('.')[0] - crasherseedname = 'sf_' + testcase.seedfile.md5 + '-' + iterstring + testcase.seedfile.ext - crasherseed_path = os.path.join(self.cfg['directories']['seedfile_dir'], crasherseedname) + iterstring = testcase.fuzzedfile.basename.split( + '-')[1].split('.')[0] + crasherseedname = 'sf_' + testcase.seedfile.md5 + \ + '-' + iterstring + testcase.seedfile.ext + crasherseed_path = os.path.join( + self.cfg['directories']['seedfile_dir'], crasherseedname) filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) self.sf_set.add_file(crasherseed_path) @@ -247,7 +251,8 @@ def _analyze(self, testcase): try: analyzer_instance.go() except AnalyzerEmptyOutputError: - logger.warning('Unexpected empty output from analyzer_class. Continuing') + logger.warning( + 'Unexpected empty output from analyzer_class. Continuing') def _post_analyze(self, testcase): pass diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py index f56114c..ec80e49 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_linux.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_linux.py @@ -74,7 +74,9 @@ def _verify(self, testcase): # fall back to checking if the crash directory exists # - crash_dir_found = filetools.find_or_create_dir(tc.target_dir) + # TODO: Before getting the full debugger ouput, the output dir will always + # be in UNKNOWN. Fix this logic + crash_dir_found = os.path.exists(tc.target_dir) keep_all = self.cfg['runoptions'].get('keep_duplicates', False) @@ -103,6 +105,9 @@ def _pre_analyze(self, testcase): logger.info( 'Getting complete debugger output for crash: %s', testcase.fuzzedfile.path) testcase.get_debug_output(testcase.fuzzedfile.path) + # We now have full debugger output, including exploitability. + # Update the crash object with this info. + testcase.update_crash_details() if self.dbg_out_file_orig != testcase.dbg.file: # we have a new debugger output @@ -139,7 +144,7 @@ def _pre_report(self, testcase): '%s crash_id=%s', testcase.seedfile.basename, testcase.signature) def _report(self, testcase): - with CopyFilesReporter(testcase, self.tc_dir) as reporter: + with CopyFilesReporter(testcase, keep_duplicates=self.cfg['runoptions'].get('keep_duplicates', False)) as reporter: reporter.go() with TestcaseLoggerReporter(testcase) as reporter: diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 1fea874..35235ad 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -13,7 +13,6 @@ from certfuzz.analyzers.drillresults import WindowsDrillResults - logger = logging.getLogger(__name__) @@ -21,7 +20,7 @@ class WindowsTestCasePipeline(TestCasePipelineBase): _minimizer_cls = WindowsMinimizer def _setup_analyzers(self): - #self.analyzer_classes.append(StdErr) + # self.analyzer_classes.append(StdErr) self.analyzer_classes.append(WindowsDrillResults) def _pre_verify(self, testcase): @@ -46,13 +45,14 @@ def _verify(self, testcase): logger.debug('Keeping testcase (reason=%s)', reason) testcase.should_proceed_with_analysis = True - logger.info("Crash confirmed: %s Exploitability: %s Faulting Address: %s", testcase.crash_hash, testcase.exp, testcase.faddr) - if self.options['minimizable']: - testcase.should_proceed_with_analysis = True + logger.info("Crash confirmed: %s Exploitability: %s Faulting Address: %s", + testcase.crash_hash, testcase.exp, testcase.faddr) + # if self.options['minimizable']: + # testcase.should_proceed_with_analysis = True self.success = True def _report(self, testcase): - with CopyFilesReporter(testcase, self.tc_dir) as reporter: + with CopyFilesReporter(testcase, keep_duplicates=self.options['keep_duplicates']) as reporter: reporter.go() def keep_testcase(self, testcase): diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index d0ca870..8d9d18c 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -106,6 +106,7 @@ def update_crash_details(self): if self.is_crash: self.signature = self.get_signature() + self.exp = self.dbg.exp self.pc = self.dbg.registers_hex.get(self.dbg.pc_name) self.target_dir = self._get_output_dir() self.debugger_missed_stack_corruption = self.dbg.debugger_missed_stack_corruption @@ -205,6 +206,7 @@ def _get_output_dir(self): assert self.crash_base_dir assert self.signature self._verify_crash_base_dir() - self.target_dir = os.path.join(self.crash_base_dir, self.signature) + self.target_dir = os.path.join( + self.crash_base_dir, self.exp, self.signature) return self.target_dir diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index af16944..d957528 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -100,7 +100,7 @@ def update_crash_details(self): # Reset properties that need to be regenerated self.exception_depth = 0 self.parsed_outputs = [] - self.exp = None + self.exp = 'UNKNOWN' fname = self._get_file_basename() outfile_base = os.path.join(self.tempdir, fname) # Regenerate target commandline with new crasher file @@ -217,7 +217,8 @@ def _get_output_dir(self, target_base): logger.debug('target_base: %s', target_base) logger.debug('signature: %s', self.signature) - self.target_dir = os.path.join(target_base, 'crashers', self.signature) + self.target_dir = os.path.join( + target_base, 'crashers', self.exp, self.signature) if len(self.target_dir) > 130: # Don't make a path too deep. Windows won't support it self.target_dir = self.target_dir[:130] + '__' From 1d38b1a43fbb89c251372238a5ebc82f908d333d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jul 2016 14:05:18 -0400 Subject: [PATCH 1038/1169] Removed commented-out lines. --- src/certfuzz/tc_pipeline/tc_pipeline_windows.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 35235ad..3f198b3 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -47,8 +47,6 @@ def _verify(self, testcase): testcase.should_proceed_with_analysis = True logger.info("Crash confirmed: %s Exploitability: %s Faulting Address: %s", testcase.crash_hash, testcase.exp, testcase.faddr) - # if self.options['minimizable']: - # testcase.should_proceed_with_analysis = True self.success = True def _report(self, testcase): From 01485c75fb4e53fa40c3c2915b5e8c6aa922e402 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jul 2016 14:33:46 -0400 Subject: [PATCH 1039/1169] Fix unit test by adding target_dir to MockTestcase object. --- src/test_certfuzz/mocks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 18aa4a2..c4e5c8b 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -114,6 +114,7 @@ class MockTestcase(Mock): pc = u'dummyPCstring' debugger_extension = 'abcdefg' dbg_outfile = 'xyz' + target_dir = 'asdf' class MockDbgOut(Mock): From b5f1425090dd8b26b6911368cb7611105516f8e8 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jul 2016 14:39:37 -0400 Subject: [PATCH 1040/1169] Use real temporary target_dir --- src/test_certfuzz/mocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index c4e5c8b..259cdf7 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -114,7 +114,7 @@ class MockTestcase(Mock): pc = u'dummyPCstring' debugger_extension = 'abcdefg' dbg_outfile = 'xyz' - target_dir = 'asdf' + target_dir = tempfile.mkdtemp() class MockDbgOut(Mock): From fc3539a1a618afa5984b0d12b71c9abb11a26c14 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 29 Jul 2016 15:30:15 -0400 Subject: [PATCH 1041/1169] CopyFilesReporter doesn't take a target directory as a parameter anymore. It uses the target_dir value from the testcase object. --- src/certfuzz/reporters/copy_files.py | 2 +- src/test_certfuzz/reporters/test_copy_files.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/reporters/copy_files.py b/src/certfuzz/reporters/copy_files.py index 270185f..fe64a73 100644 --- a/src/certfuzz/reporters/copy_files.py +++ b/src/certfuzz/reporters/copy_files.py @@ -20,7 +20,7 @@ class CopyFilesReporter(ReporterBase): Copies files to a location ''' - def __init__(self, testcase, keep_duplicates): + def __init__(self, testcase, keep_duplicates=False): ''' Constructor ''' diff --git a/src/test_certfuzz/reporters/test_copy_files.py b/src/test_certfuzz/reporters/test_copy_files.py index d1f9106..0d0fbfa 100644 --- a/src/test_certfuzz/reporters/test_copy_files.py +++ b/src/test_certfuzz/reporters/test_copy_files.py @@ -13,14 +13,12 @@ class Test(unittest.TestCase): - def setUp(self): self.tmpdir = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.tmpdir) - def testCopyFiles(self): tc = MockTestcase() tc.tempdir = tempfile.mkdtemp(dir=self.tmpdir) @@ -37,17 +35,17 @@ def testCopyFiles(self): # source dir has only one file self.assertEqual([fname], os.listdir(tc.tempdir)) - r = copy_files.CopyFilesReporter(tc, target_dir) + r = copy_files.CopyFilesReporter(tc, keep_duplicates=False) with r: r.go() - # target dir should contain an outdir with the file we copied - outdir = os.path.join(target_dir, tc.signature) + # target dir, which is taken from the testcase object, + # should contain the file we copied + outdir = tc.target_dir self.assertTrue(os.path.exists(outdir)) self.assertEqual([fname], os.listdir(outdir)) - if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 9fbd617e053b835a9d1777021a656ab454b3edeb Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 1 Aug 2016 10:30:44 -0400 Subject: [PATCH 1042/1169] pep8 cleanup --- .../debuggers/output_parsers/calltracefile.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/calltracefile.py b/src/certfuzz/debuggers/output_parsers/calltracefile.py index 1c30885..6a12352 100644 --- a/src/certfuzz/debuggers/output_parsers/calltracefile.py +++ b/src/certfuzz/debuggers/output_parsers/calltracefile.py @@ -15,13 +15,14 @@ logger.setLevel(logging.WARNING) regex = { - 'ct_lib': re.compile(r'^/.+/(.+:.+)'), - 'ct_lib_function': re.compile(r'^(/.+):\s(.+)'), - 'ct_system_lib': re.compile(r'^/(usr/)?lib.+'), - } + 'ct_lib': re.compile(r'^/.+/(.+:.+)'), + 'ct_lib_function': re.compile(r'^(/.+):\s(.+)'), + 'ct_system_lib': re.compile(r'^/(usr/)?lib.+'), +} class Calltracefile: + def __init__(self, f): ''' Create a GDB file object from the gdb output file @@ -59,8 +60,10 @@ def _hashable_backtrace(self): return self.hashable_backtrace def _hashable_backtrace_string(self, level): - self.hashable_backtrace_string = ' '.join(self.hashable_backtrace[-level:]).strip() - logger.warning('_hashable_backtrace_string: %s', self.hashable_backtrace_string) + self.hashable_backtrace_string = ' '.join( + self.hashable_backtrace[-level:]).strip() + logger.warning( + '_hashable_backtrace_string: %s', self.hashable_backtrace_string) return self.hashable_backtrace_string def calltrace_line(self, l): @@ -100,7 +103,8 @@ def get_testcase_signature(self, backtrace_level): logger.addHandler(hdlr) parser = OptionParser() - parser.add_option('', '--debug', dest='debug', action='store_true', help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--debug', dest='debug', action='store_true', + help='Enable debug messages (overrides --verbose)') (options, args) = parser.parse_args() if options.debug: From ca5aa2da79e8a24e20c99222f9b386bbd7402afd Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 1 Aug 2016 10:33:16 -0400 Subject: [PATCH 1043/1169] Handle malformed PIN calltrace output lines. BFF-973 --- src/certfuzz/debuggers/output_parsers/calltracefile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/calltracefile.py b/src/certfuzz/debuggers/output_parsers/calltracefile.py index 6a12352..eff8906 100644 --- a/src/certfuzz/debuggers/output_parsers/calltracefile.py +++ b/src/certfuzz/debuggers/output_parsers/calltracefile.py @@ -73,10 +73,10 @@ def calltrace_line(self, l): n = re.match(regex['ct_lib_function'], l) if n: function = n.group(2) - if not system_lib and function != '.plt' and function != '.text' and function != 'invalid_rtn': - item = m.group(1) - self.backtrace.append(item) - logger.debug('Appending to backtrace: %s', item) + if not system_lib and function != '.plt' and function != '.text' and function != 'invalid_rtn': + item = m.group(1) + self.backtrace.append(item) + logger.debug('Appending to backtrace: %s', item) def _process_lines(self): logger.debug('_process_lines') From 772f4ade3a153a809a0dc462f584e63b02e00f0d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 1 Aug 2016 15:47:12 -0400 Subject: [PATCH 1044/1169] POSIX compatibility for quickstats.sh --- src/linux/quickstats.sh | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/linux/quickstats.sh b/src/linux/quickstats.sh index ce53c78..005148b 100755 --- a/src/linux/quickstats.sh +++ b/src/linux/quickstats.sh @@ -1,8 +1,23 @@ -#!/bin/bash +#!/bin/sh + +# contains(string, substring) +# +# Returns 0 if the specified string contains the specified substring, +# otherwise returns 1. +contains() { + string="$1" + substring="$2" + if test "${string#*$substring}" != "$string" + then + return 0 # $substring is in $string + else + return 1 # $substring is not in $string + fi +} platform=`uname -a` echo "Exploitability Summary for campaign thus far:" -if [[ "$platform" =~ "Darwin" ]]; then +if ( contains "$platform" "Darwin" ); then exploitable=`find -L ~/results -name '*.cw' | grep crashers | xargs grep is_exploitable=y | awk -Fcrashers/ '{print $2}' | awk -F/ '{print $1}' | sort | uniq | wc -l` total=`find -L ~/results -name '*.cw' | grep crashers | grep -v gmalloc | wc -l` not_exploitable=$(expr $total - $exploitable) From a1bf03c957a8a57c10b43c09a24e26b31bcb28f8 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 1 Aug 2016 16:44:32 -0400 Subject: [PATCH 1045/1169] put uniquelog.txt in the campaign_id subdirectory --- src/certfuzz/iteration/iteration_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_linux.py b/src/certfuzz/iteration/iteration_linux.py index 54a8e21..ac060a4 100644 --- a/src/certfuzz/iteration/iteration_linux.py +++ b/src/certfuzz/iteration/iteration_linux.py @@ -49,7 +49,7 @@ def __init__(self, self.pipeline_options.update({'use_valgrind': self.cfg['analyzer']['use_valgrind'], 'use_pin_calltrace': self.cfg['analyzer']['use_pin_calltrace'], - 'uniq_log': os.path.join(self.cfg['directories']['results_dir'], 'uniquelog.txt'), + 'uniq_log': os.path.join(self.outdir, 'uniquelog.txt'), 'local_dir': fixup_path(self.cfg['directories']['working_dir']), 'minimizertimeout': self.cfg['runoptions']['minimizer_timeout'], }) From abd068d03d720fd2bc076a12b907793d4ab2866f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 1 Aug 2016 18:20:13 -0400 Subject: [PATCH 1046/1169] Make sure that minimizer_log.txt persists through an update_crash_details invocation. BFF-975 --- src/certfuzz/testcase/testcase_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 6cc6aeb..f63a48e 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -118,6 +118,12 @@ def copy_files_to_temp(self): if self.seedfile: filetools.copy_file(self.seedfile.path, self.tempdir) + # TODO: This seems hacky. Should be better way to have + # minimizer_log.txt survive update_crash_details + minlog = os.path.join(self.fuzzedfile.dirname, 'minimizer_log.txt') + if os.path.exists(minlog): + filetools.copy_file(minlog, self.tempdir) + new_fuzzedfile = os.path.join(self.tempdir, self.fuzzedfile.basename) self.fuzzedfile = BasicFile(new_fuzzedfile) From f510845fc8724b4753239943cda7d663316875a1 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 1 Aug 2016 23:26:54 -0400 Subject: [PATCH 1047/1169] Don't use os.linesep. Cygwin less shows it as ^M --- src/certfuzz/drillresults/result_driller_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/drillresults/result_driller_base.py b/src/certfuzz/drillresults/result_driller_base.py index dfacd1c..5b52dec 100644 --- a/src/certfuzz/drillresults/result_driller_base.py +++ b/src/certfuzz/drillresults/result_driller_base.py @@ -167,7 +167,7 @@ def print_drillresults_file(self, crash_key): print ('%s%s' % (ff_line_indicator, fixed_ff_path)) else: print line - print os.linesep + print '' def print_reports(self): results = dict([(tcb.crash_hash, tcb.details) From a33b66dfef5957f6239fe095114b0eb9dacef8bf Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 1 Aug 2016 23:46:03 -0400 Subject: [PATCH 1048/1169] Basic prep for 2.8 release --- src/linux/NEWS | 14 +++ src/linux/README | 133 ++++++++++---------------- src/linux/configs/bff.yaml | 50 +++++----- src/windows/NEWS.txt | 7 ++ src/windows/README.txt | 65 ++++++------- src/windows/configs/examples/bff.yaml | 70 +++++++------- 6 files changed, 161 insertions(+), 178 deletions(-) diff --git a/src/linux/NEWS b/src/linux/NEWS index 8f27568..543df46 100644 --- a/src/linux/NEWS +++ b/src/linux/NEWS @@ -1,6 +1,20 @@ CERT Basic Fuzzing Framework (BFF) Significant changes +BFF 2.8 (FIXME) + * Virtual Machine changes: + - Update from Ubuntu 12.04 to 16.04 + - Updated version of zzuf + + * Code changes: + - Use of configurable mutators rather than use zzuf's bitwise mutation + - Drillresults is automatically executed at fuzz-time + - Simplified configuration + - Support for OSX Maverics, Yosemite, El Capitan, and MacOS Sierra + - Removed X dependency + - "null" runner mode for zzuf, which only detects crashes + - Bug fixes + BFF 2.7 (September 23, 2013) * Virtual Machine changes: - Switch to Ubuntu from Debian diff --git a/src/linux/README b/src/linux/README index 85ed6fb..9682ef5 100644 --- a/src/linux/README +++ b/src/linux/README @@ -7,7 +7,7 @@ See the NEWS file for changes ===== Requirements ===== -UbuFuzz requires VMWare Workstation 7 or later, or compatible +The UbuFuzz VM requires VMWare Workstation 7 or later, or compatible virtualization software. The OS X installer requires Snow Leopard or later. Xcode Tools is recommended, which provides libgmalloc (Guard Malloc). @@ -16,14 +16,14 @@ recommended, which provides libgmalloc (Guard Malloc). ===== Demo Quick Start ===== UbuFuzz: -1) Unzip BFF-2.7.zip to c:\fuzz or another folder to be shared as "fuzz" -2) Unzip UbuFuzz-2.7.zip +1) Unzip BFF-2.8.zip to c:\fuzz or another folder to be shared as "fuzz" +2) Unzip UbuFuzz-2.8.zip 3) Open UbuFuzz.vmx 4) Create a snapshot in VMWare 5) Power on the VM If you do not wish to use a shared folder, simply remove ~/bff -and unzip BFF-2.7.zip to the ~/bff directory. +and unzip BFF-2.8.zip to the ~/bff directory. OS X: 1) Install BFF @@ -77,61 +77,48 @@ unique crashes. Each unique crash will be placed in the configured output directory. results/ -|-- bff.cfg -|-- bff.log -|-- crashers -| |-- -| | |-- .log -| | |-- .stderr -| | |-- minimizer_log.txt -| | |-- sf_--minimized. -| | |-- sf_--minimized..callgrind -| | |-- sf_--minimized..callgrind.annotated -| | |-- sf_--minimized..callgrind.calltree -| | |-- sf_--minimized..gdb -| | |-- sf_--minimized..stderr -| | |-- sf_--minimized..valgrind -| | |-- sf_-. -| | |-- sf_-..gdb -| | `-- sf_. -|-- seeds -| |-- -| | `-- zzuf_log.txt -| |-- -| | `-- zzuf_log.txt -| |-- seedfile_set.log +|-- +| |-- crashers +| | |-- +| | | |-- +| | | | |-- .stderr +| | | | |-- minimizer_log.txt +| | | | |-- sf_--minimized. +| | | | |-- sf_--minimized..callgrind +| | | | |-- sf_--minimized..callgrind.annotated +| | | | |-- sf_--minimized..callgrind.calltree +| | | | |-- sf_--minimized..gdb +| | | | |-- sf_--minimized..stderr +| | | | |-- sf_--minimized..valgrind +| | | | |-- sf_-. +| | | | `-- sf_. +| |-- seedfiles | |-- sf_. | |-- sf_. `-- uniquelog.txt -The UbuFuzz startup script, batch.sh, looks for the BFF code -in /home/fuzz/bff, which by default is a soft link to the VMWare -shared folder /mnt/hgfs/fuzz. The default results location is -/home/fuzz/results, which in the UbuFuzz vm is a soft link to -/home/fuzz/bff/results (thus /mnt/hgfs/fuzz/results). Either -of these soft links can be changed if desired, or you can edit -configs/bff.cfg to point BFF at a different destination. +The UbuFuzz startup script, batch.sh, looks for the bff.py script +in the same directory, which by default is a soft link to the VMWare +shared folder /mnt/hgfs/fuzz. -BFF will copy its configuration to results/bff.cfg, and will -log messages of level INFO or higher into results/bff.log. The -config file copied here is just for recording purposes, another -copy is made in /home/fuzz and it is this copy of the file that -actually gets used by BFF. +BFF will copy its configuration to results//bff.yaml, and will +log messages of level INFO or higher into log/bff.log. The +config file copied here is just for recording purposes. Additionally the following subdirectories are created in the -results dir: +results dir, in the subdirectory: * crashers: Contains a subdir for each uniquely-crashing test case and its analyzed results * seeds: Contains the original seedfiles as well as logs specific to that seedfile Other files of note: -results/uniquelog.txt +results//uniquelog.txt a log file that tracks the unique crashers found during the run -The "results/crashers" directory will contain the uniquely-crashing +The "results//crashers" directory will contain the uniquely-crashing test cases. The variants that have crashed the target application -will be stored here with the zzuf seed number appended to the +will be stored here with the BFF iteration number appended to the seed file name. For each uniquely-crashing case, there will also be a .stderr, .gdb, .callgrind and .valgrind file that contains the stderr, gdb, callgrind and valgrind output for that case, @@ -229,21 +216,19 @@ same command-line parameters as configure for the fuzzing campaign. This can be used to test crashing testcases interactively. +tools/zipdiff.py can be used to compare zip-based files. + +tools/debuggerfile.py will parse GDB output to create a hash that uniquely +identifies the crash. + + ===== Fuzzing on your own ===== When the UbuFuzz VM is powered on, it will automatically execute the batch.sh script in the VMWare shared folder. In order to power on the virtual machine without it beginning a fuzzing campaign, you should rename batch.sh. This will allow you to -power on the virtual machine to install the target software. Once -BFF has started a fuzzing run, it will copy bff.cfg to the -/home/fuzz directory in the virtual machine. This configuration -file will be used for subsequent fuzzing runs, rather than the -copy in the shared folder. This is why it is important to make -a snapshot of the VM in its clean state. If you wish to reset -a fuzzing machine to a clean state, e.g. to start a new fuzzing -campaign or if you've change fuzzing parameters or seed files, -you should run the ~/bff/reset_bff.sh script. +power on the virtual machine to install the target software. The first step to beginning a fuzzing run is to obtain and install the target software. Usually this process will involve @@ -257,13 +242,13 @@ build if you like. Create a new snapshot of the VM after the target software has been installed. -The bff.cfg file contains all of the parameters for the fuzzing +The bff.yaml file contains all of the parameters for the fuzzing run. This file must be edited to suit the software that you will -be fuzzing. The bff.cfg file is annotated and should be +be fuzzing. The bff.yaml file is annotated and should be relatively self-explanatory. -The default bff.cfg file will start a fuzzing run that invokes: -convert $SEEDFILE /dev/null +The default bff.yaml file will start a fuzzing run that invokes: +~/convert $SEEDFILE /dev/null This will use ImageMagick's "convert" program to process the seed file, outputting to /dev/null. Because BFF will mangle the seed @@ -287,19 +272,8 @@ hash for the crash. We have found this value to be effective for most applications, however you can adjust the value to fit your needs. -Another important bff.cfg option is the "copymode" setting. By -default, zzuf will use LD_PRELOAD to hook into an application to -perform input mangling and intercept signals. In some cases, the -target application will not behave properly in this mode. The -file may fail to be mangled, or the target application may fail -to run properly at all. If this is the case, set "copymode" to 1 -and zzuf will create a temporary file and then open that file -with the target application. Most OS X applications require copy -mode, so the default bff.cfg provided on OS X will default to this -mode. - -There are several options within bff.cfg related to application -timeouts. "progtimeout" is the maximum time that BFF will allow +There are several options within bff.yaml related to application +timeouts. "runtimeout" is the maximum time that BFF will allow a single invocation of the target application to run before terminating it. In the case of the "convert" component of ImageMagick, it is reasonable to expect the program to finish @@ -308,24 +282,17 @@ fuzzing, this value will need to be adjusted. Especially with GUI applications, the goal is to allow the application to run long enough to process the input file, but not so long that the application is sitting idle for an amount of time for each -invocation. The "killprocname" and "killproctimeout" options are -used for the external process killer. When running some analyzers, -the application that you are fuzzing may be left running after -the analyzer is terminated. The external process killer -(killproc.sh) is a script that polls running processes and makes -sure that no single instance of the specified application is -allowed to run for longer than the specified time. - -Once you have configured the options in bff.cfg, power on the +invocation. + +Once you have configured the options in bff.yaml, power on the UbuFuzz VM. It should begin fuzzing automatically. If the target is a GUI application, you should see the application launch repeatedly as it is being fuzzed. If it is a command-line -application, you should notice increased CPU usage in the top -window. The first seed_interval that BFF executes will also +application, you should notice increased CPU usage in the htop +window. The first iteration that BFF executes will also display stderr to the terminal window to let you see if something is obviously wrong. If you need to tweak your settings, such as by using a smaller progtimeout value to improve throughput, you -can edit the bff.cfg file and restore the VM to its previous +can edit the bff.yaml file and restore the VM to its previous snapshot. Alternatively, you can run ~/bff/reset_bff.sh and -restart X to cause the VM to re-read the shared bff.cfg file and -start fuzzing again. +restart ~/bff/batch.sh to start fuzzing again. diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index 89236a4..753d902 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -36,6 +36,31 @@ target: cmdline_template: $PROGRAM $SEEDFILE /dev/null +############################################################################## +# RUNNER OPTIONS +# +# runtimeout: +# maximum program execution time (seconds) that BFF will the target to execute +# +############################################################################## +runner: + runtimeout: 5 + + +############################################################################## +# DEBUGGER OPTIONS +# +# backtracelevels: +# Number of backtrace frames to hash for uniqueness +# Increase this number for more crash uniqueness granularity. +# Decrease this number if you think that you are getting too many duplicate +# crashes. +# +############################################################################## +debugger: + backtracelevels: 5 + + ############################################################################## # LOCATIONS FOR FUZZ RUN FILES: # @@ -158,31 +183,6 @@ fuzzer: fuzz_zip_container: False -############################################################################## -# RUNNER OPTIONS -# -# runtimeout: -# maximum program execution time (seconds) that BFF will the target to execute -# -############################################################################## -runner: - runtimeout: 5 - - -############################################################################## -# DEBUGGER OPTIONS -# -# backtracelevels: -# Number of backtrace frames to hash for uniqueness -# Increase this number for more crash uniqueness granularity. -# Decrease this number if you think that you are getting too many duplicate -# crashes. -# -############################################################################## -debugger: - backtracelevels: 5 - - ############################################################################## # VERIFIER PARAMETERS # diff --git a/src/windows/NEWS.txt b/src/windows/NEWS.txt index 8310be6..cfe70ec 100644 --- a/src/windows/NEWS.txt +++ b/src/windows/NEWS.txt @@ -4,6 +4,13 @@ BFF for Windows was formerly known as CERT Failure Observation Engine (FOE) Significant changes +BFF 2.8 (FIXME) + + * Code changes: + - Convergence of BFF and FOE code into just BFF + - Simplified configuration + - Bug fixes + FOE 2.1 (September 23, 2013) * Environment changes: diff --git a/src/windows/README.txt b/src/windows/README.txt index 950bd51..50e559a 100644 --- a/src/windows/README.txt +++ b/src/windows/README.txt @@ -70,21 +70,22 @@ campaign. ===== Analyzing results ===== ============================= -.\results\\ - +- .yaml - +- version.txt - +- / - +- / - +- minimizer_log.txt - +- sf_. - +- sf_--. - +- sf_---..msec - +- sf_---minimized. - +- sf_-..e.msec - +- / - +- ... - +- / - +results\\ + |-- .yaml + |-- version.txt + |-- seedfiles\ + |-- crashers\ + |-- \ + |-- \ + | |-- minimizer_log.txt + | |-- sf_. + | |-- sf_--. + | |-- sf_---..e.msec + | |-- sf_---minimized. + |-- \ + |-- ... + |-- \ + .yaml This is a copy of the config file used for this run. It is stored for @@ -119,20 +120,19 @@ sf_--. This is the fuzzed file that caused the crash. is the exception faulting address, as reported by !exploitable. -sf_---..msec +sf_---..e.msec This is the cdb text output from the crash, which includes output from the -!exploitable tool. +!exploitable tool. e represents the number of times that an exception +is continued. One file is provided for each continued exception until an +uncontinuable exception is encountered, or the handled exception limit has +been reached, or the target application proceeds without encountering another +exception. sf_---minimized. This is the minimized version of the crashing test case. It is the "least different" version of the original fuzzed file that caused a specific crash (hash). -sf_-..e.msec -This is the cdb output for an exception that is continued number of times. -One file is provided for each continued exception until an uncontinuable -exception is encountered, or the handled exception limit has been reached, or -the target application proceeds without encountering another exception. ===== Fuzzing on your own ===== @@ -161,18 +161,13 @@ target: program: target: cmdline_template: This specifies the commandline syntax for invoking the target application. + Be sure to remove the default "NUL" option if your target application + doesn't use that as a commandline parameter. runner: runtimeout: This value specifies how long BFF should wait before terminating the application and moving on to the next iteration. - Note that this setting only applies to the "winrun" runner (32-bit Windows - XP and Server 2003 systems). - -debugger: runtimeout: - This value specifies how long BFF should allow the target application to - run when it is invoked from the debugger. On platforms that use the "null" - runner (64-bit Windows or Windows Vista or newer), this is the only - timeout value that is used. + BFF periodically saves state of a fuzzing campaign, so it will by default continue a cached campaign if bff.yaml has not been modified. @@ -292,19 +287,19 @@ To install BFF manually, you will need the following prerequisites: Other Windows versions will use debugger mode (nullrun) - Python 2.7 - http://www.python.org/download/releases/2.7.5/ + https://www.python.org/downloads/release/python-2712/ - SciPy - http://sourceforge.net/projects/scipy/files/scipy/0.10.1/scipy-0.10.1-win32-superpack-python2.7.exe/download + http://sourceforge.net/projects/scipy/files/scipy/0.16.1/scipy-0.16.1-win32-superpack-python2.7.exe/download - NumPy - http://sourceforge.net/projects/numpy/files/NumPy/1.6.1/numpy-1.6.1-win32-superpack-python2.7.exe/download + http://sourceforge.net/projects/numpy/files/NumPy/1.10.2/numpy-1.10.2-win32-superpack-python2.7.exe/download - PyYAML - http://pyyaml.org/download/pyyaml/PyYAML-3.10.win32-py2.7.exe + http://pyyaml.org/download/pyyaml/PyYAML-3.11.win32-py2.7.exe - pywin32 - http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win32-py2.7.exe/download + https://sourceforge.net/projects/pywin32/files/pywin32/Build%20220/pywin32-220.win32-py2.7.exe/download - Python WMI https://pypi.python.org/packages/any/W/WMI/WMI-1.4.9.win32.exe diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 93073b6..4312165 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -55,6 +55,41 @@ target: # forward slashes or double quotes. For example: # cmdline_template: $PROGRAM -in $SEEDFILE -out c:/some/path/to/file # cmdline_template: $PROGRAM -in $SEEDFILE -out "c:\some path\to file" + + +############################################################################## +# Runner options +# +# hideoutput: +# Hide stdout of target application +# +# runtimeout: +# Number of seconds to allow target application to execute +# +# watchcpu: +# Kill target process when its CPU usage drops towards zero +# (Auto, True, False) +# +############################################################################## +runner: + hideoutput: False + runtimeout: 5 + watchcpu: Auto + + +############################################################################## +# Debugger options +# +# debugheap: +# Use the debug heap for the target application +# +# max_handled_exceptions: +# Maximum number of times to continue exceptions +# +############################################################################## +debugger: + debugheap: False + max_handled_exceptions: 6 ############################################################################## @@ -173,38 +208,3 @@ fuzzer: # - [0x1000, 0x100F] fuzz_zip_container: False - -############################################################################## -# Runner options -# -# hideoutput: -# Hide stdout of target application -# -# runtimeout: -# Number of seconds to allow target application to execute -# -# watchcpu: -# Kill target process when its CPU usage drops towards zero -# (Auto, True, False) -# -############################################################################## -runner: - hideoutput: False - runtimeout: 5 - watchcpu: Auto - - -############################################################################## -# Debugger options -# -# debugheap: -# Use the debug heap for the target application -# -# max_handled_exceptions: -# Maximum number of times to continue exceptions -# -############################################################################## -debugger: - debugheap: False - max_handled_exceptions: 6 - From d8b2252950986b3a8b60ebfb91bbb0ef36ae5ecd Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 09:55:02 -0400 Subject: [PATCH 1049/1169] Update Windows BFF dependencies in installer script. --- build/distmods/windows/nsis/nsis_mid.txt | 60 ++++++++++++------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/build/distmods/windows/nsis/nsis_mid.txt b/build/distmods/windows/nsis/nsis_mid.txt index c50b1a1..321af8b 100644 --- a/build/distmods/windows/nsis/nsis_mid.txt +++ b/build/distmods/windows/nsis/nsis_mid.txt @@ -88,11 +88,11 @@ ${If} $NEED_VC2010 == 1 ${EndIf} ${If} $DEP_MISSING == 1 - IfFileExists "$INSTALLERDIR\deps\python-2.7.5.msi" 0 depMissing - IfFileExists "$INSTALLERDIR\deps\scipy-0.10.1-win32-superpack-python2.7.exe" 0 depMissing - IfFileExists "$INSTALLERDIR\deps\numpy-1.6.1-win32-superpack-python2.7.exe" 0 depMissing - IfFileExists "$INSTALLERDIR\deps\PyYAML-3.10.win32-py2.7.exe" 0 depMissing - IfFileExists "$INSTALLERDIR\deps\pywin32-218.win32-py2.7.exe" 0 depMissing + IfFileExists "$INSTALLERDIR\deps\python-2.7.12.msi" 0 depMissing + IfFileExists "$INSTALLERDIR\deps\scipy-0.16.1-win32-superpack-python2.7.exe" 0 depMissing + IfFileExists "$INSTALLERDIR\deps\numpy-1.10.2-win32-superpack-python2.7.exe" 0 depMissing + IfFileExists "$INSTALLERDIR\deps\PyYAML-3.11.win32-py2.7.exe" 0 depMissing + IfFileExists "$INSTALLERDIR\deps\pywin32-220.win32-py2.7.exe" 0 depMissing IfFileExists "$INSTALLERDIR\deps\WMI-1.4.9.win32.exe" 0 depMissing IfFileExists "$INSTALLERDIR\deps\$VCR.exe" 0 depMissing IfFileExists "$INSTALLERDIR\deps\$DEBUGGER" 0 depMissing @@ -110,14 +110,14 @@ skipDeps: ${If} $NEED_PYTHON == 1 - IfFileExists "$INSTALLERDIR\deps\python-2.7.5.msi" localPythonInstall + IfFileExists "$INSTALLERDIR\deps\python-2.7.12.msi" localPythonInstall ${If} $DOWNLOAD_OK == 1 - inetc::get "https://www.cert.org/downloads/foe/python-2.7.5.msi" "$TEMP\python-2.7.5.msi" + inetc::get "https://www.cert.org/downloads/foe/python-2.7.12.msi" "$TEMP\python-2.7.12.msi" Pop $0 ${If} $0 == "OK" - ExecWait '"msiexec.exe" /i "$TEMP\python-2.7.5.msi" /qb ALLUSERS=1' - Delete "$TEMP\python-2.7.5.msi" + ExecWait '"msiexec.exe" /i "$TEMP\python-2.7.12.msi" /qb ALLUSERS=1' + Delete "$TEMP\python-2.7.12.msi" ${Else} MessageBox MB_YESNO|MB_ICONEXCLAMATION "Python download failed. Retry?" /SD IDNO IDYES skipDeps ${EndIf} @@ -126,7 +126,7 @@ ${If} $NEED_PYTHON == 1 localPythonInstall: ${If} $DEPS_OK == 1 - ExecWait '"msiexec.exe" /i "$INSTALLERDIR\deps\python-2.7.5.msi" /qb ALLUSERS=1' + ExecWait '"msiexec.exe" /i "$INSTALLERDIR\deps\python-2.7.12.msi" /qb ALLUSERS=1' ${EndIf} ${EndIf} @@ -143,13 +143,13 @@ ${EndIf} skipPython: ${If} $NEED_SCIPY == 1 - IfFileExists "$INSTALLERDIR\deps\scipy-0.10.1-win32-superpack-python2.7.exe" localScipyInstall + IfFileExists "$INSTALLERDIR\deps\scipy-0.16.1-win32-superpack-python2.7.exe" localScipyInstall ${If} $DOWNLOAD_OK == 1 - inetc::get "https://www.cert.org/downloads/foe/scipy-0.10.1-win32-superpack-python2.7.bin" "$TEMP\scipy-0.10.1-win32-superpack-python2.7.exe" + inetc::get "https://www.cert.org/downloads/foe/scipy-0.16.1-win32-superpack-python2.7.bin" "$TEMP\scipy-0.16.1-win32-superpack-python2.7.exe" Pop $0 ${If} $0 == "OK" - ExecWait "$TEMP\scipy-0.10.1-win32-superpack-python2.7.exe" - Delete "$TEMP\scipy-0.10.1-win32-superpack-python2.7.exe" + ExecWait "$TEMP\scipy-0.16.1-win32-superpack-python2.7.exe" + Delete "$TEMP\scipy-0.16.1-win32-superpack-python2.7.exe" ${Else} MessageBox MB_YESNO|MB_ICONEXCLAMATION "Scipy download failed. Retry?" /SD IDNO IDYES skipPython ${EndIf} @@ -158,19 +158,19 @@ ${If} $NEED_SCIPY == 1 localScipyInstall: ${If} $DEPS_OK == 1 - ExecWait "$INSTALLERDIR\deps\scipy-0.10.1-win32-superpack-python2.7.exe" + ExecWait "$INSTALLERDIR\deps\scipy-0.16.1-win32-superpack-python2.7.exe" ${EndIf} ${EndIf} ScipyInstalled: ${If} $NEED_NUMPY == 1 - IfFileExists "$INSTALLERDIR\deps\numpy-1.6.1-win32-superpack-python2.7.exe" localNumpyInstall + IfFileExists "$INSTALLERDIR\deps\numpy-1.10.2-win32-superpack-python2.7.exe" localNumpyInstall ${If} $DOWNLOAD_OK == 1 - inetc::get "https://www.cert.org/downloads/foe/numpy-1.6.1-win32-superpack-python2.7.bin" "$TEMP\numpy-1.6.1-win32-superpack-python2.7.exe" + inetc::get "https://www.cert.org/downloads/foe/numpy-1.10.2-win32-superpack-python2.7.bin" "$TEMP\numpy-1.10.2-win32-superpack-python2.7.exe" Pop $0 ${If} $0 == "OK" - ExecWait "$TEMP\numpy-1.6.1-win32-superpack-python2.7.exe" - Delete "$TEMP\numpy-1.6.1-win32-superpack-python2.7.exe" + ExecWait "$TEMP\numpy-1.10.2-win32-superpack-python2.7.exe" + Delete "$TEMP\numpy-1.10.2-win32-superpack-python2.7.exe" ${Else} MessageBox MB_YESNO|MB_ICONEXCLAMATION "Numpy download failed. Retry?" /SD IDNO IDYES ScipyInstalled ${EndIf} @@ -179,19 +179,19 @@ ${If} $NEED_NUMPY == 1 localNumpyInstall: ${If} $DEPS_OK == 1 - ExecWait "$INSTALLERDIR\deps\numpy-1.6.1-win32-superpack-python2.7.exe" + ExecWait "$INSTALLERDIR\deps\numpy-1.10.2-win32-superpack-python2.7.exe" ${EndIf} ${EndIf} NumpyInstalled: ${If} $NEED_PYYAML == 1 - IfFileExists "$INSTALLERDIR\deps\PyYAML-3.10.win32-py2.7.exe" localPyYAMLInstall + IfFileExists "$INSTALLERDIR\deps\PyYAML-3.11.win32-py2.7.exe" localPyYAMLInstall ${If} $DOWNLOAD_OK == 1 - inetc::get "https://www.cert.org/downloads/foe/PyYAML-3.10.win32-py2.7.bin" "$TEMP\PyYAML-3.10.win32-py2.7.exe" + inetc::get "https://www.cert.org/downloads/foe/PyYAML-3.11.win32-py2.7.bin" "$TEMP\PyYAML-3.11.win32-py2.7.exe" Pop $0 ${If} $0 == "OK" - ExecWait "$TEMP\PyYAML-3.10.win32-py2.7.exe" - Delete "$TEMP\PyYAML-3.10.win32-py2.7.exe" + ExecWait "$TEMP\PyYAML-3.11.win32-py2.7.exe" + Delete "$TEMP\PyYAML-3.11.win32-py2.7.exe" ${Else} MessageBox MB_YESNO|MB_ICONEXCLAMATION "PyYAML download failed. Retry?" /SD IDNO IDYES NumpyInstalled ${EndIf} @@ -200,19 +200,19 @@ ${If} $NEED_PYYAML == 1 localPyYAMLInstall: ${If} $DEPS_OK == 1 - ExecWait "$INSTALLERDIR\deps\PyYAML-3.10.win32-py2.7.exe" + ExecWait "$INSTALLERDIR\deps\PyYAML-3.11.win32-py2.7.exe" ${EndIf} ${EndIf} PyYAMLInstalled: ${If} $NEED_PYWIN32 == 1 - IfFileExists "$INSTALLERDIR\deps\pywin32-218.win32-py2.7.exe" localpywin32Install + IfFileExists "$INSTALLERDIR\deps\pywin32-220.win32-py2.7.exe" localpywin32Install ${If} $DOWNLOAD_OK == 1 - inetc::get "https://www.cert.org/downloads/foe/pywin32-218.win32-py2.7.bin" "$TEMP\pywin32-218.win32-py2.7.exe" + inetc::get "https://www.cert.org/downloads/foe/pywin32-220.win32-py2.7.bin" "$TEMP\pywin32-220.win32-py2.7.exe" Pop $0 ${If} $0 == "OK" - ExecWait "$TEMP\pywin32-218.win32-py2.7.exe" - Delete "$TEMP\pywin32-218.win32-py2.7.exe" + ExecWait "$TEMP\pywin32-220.win32-py2.7.exe" + Delete "$TEMP\pywin32-220.win32-py2.7.exe" ${Else} MessageBox MB_YESNO|MB_ICONEXCLAMATION "pywin32 download failed. Retry?" /SD IDNO IDYES PyYAMLInstalled ${EndIf} @@ -221,7 +221,7 @@ ${If} $NEED_PYWIN32 == 1 localpywin32Install: ${If} $DEPS_OK == 1 - ExecWait "$INSTALLERDIR\deps\pywin32-218.win32-py2.7.exe" + ExecWait "$INSTALLERDIR\deps\pywin32-220.win32-py2.7.exe" ${EndIf} ${EndIf} From 535b709f378e2ba4b050c70e780545b9c6d59ec3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 11:48:04 -0400 Subject: [PATCH 1050/1169] gdb 6.x is neither CTT compatible, nor can it even handle the -ex option, so don't proceed with CTT checking. Check for /proc compat first as well. --- src/certfuzz/campaign/campaign_linux.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index c24c6de..7338ec0 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -150,7 +150,17 @@ def _setup_environment(self): os.environ['KDE_DEBUG'] = '1' def _check_gdb_compat(self): + logger.debug('checking /proc compatibility') + if not host_info.is_linux: + logger.debug( + 'Current platform does not support /proc. Adjusting debugger templates.') + self.config['debugger']['proc_compat'] = False + logger.debug('checking CERT Triage Tool compatibility') + gdb_version = subprocess.check_output(['gdb', '--version']) + if 'gdb 6' in gdb_version: + self.config['debugger']['ctt_compat'] = False + return current_dir = os.path.dirname(__file__) ctt_path = os.path.join( current_dir, '..', '..', 'CERT_triage_tools', 'exploitable', 'exploitable.py') @@ -161,12 +171,6 @@ def _check_gdb_compat(self): 'gdb is not compatible with CERT Triage Tools (older than 7.2?). Disabling.') self.config['debugger']['ctt_compat'] = False - logger.debug('checking /proc compatibility') - if not host_info.is_linux: - logger.debug( - '%s does not support /proc. Adjusting debugger templates.' % current_platform) - self.config['debugger']['proc_compat'] = False - def _check_for_script(self): logger.debug('check for script') if check_program_file_type('text', self.program): From 0721af8fa12d2783733f1db9601da8c58fa49e87 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 11:48:24 -0400 Subject: [PATCH 1051/1169] Use ~/bff/results instead of ~/results --- src/linux/quickstats.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/linux/quickstats.sh b/src/linux/quickstats.sh index 005148b..8ead22d 100755 --- a/src/linux/quickstats.sh +++ b/src/linux/quickstats.sh @@ -18,13 +18,13 @@ contains() { platform=`uname -a` echo "Exploitability Summary for campaign thus far:" if ( contains "$platform" "Darwin" ); then - exploitable=`find -L ~/results -name '*.cw' | grep crashers | xargs grep is_exploitable=y | awk -Fcrashers/ '{print $2}' | awk -F/ '{print $1}' | sort | uniq | wc -l` - total=`find -L ~/results -name '*.cw' | grep crashers | grep -v gmalloc | wc -l` + exploitable=`find -L ~/bff/results -name '*.cw' | grep crashers | xargs grep is_exploitable=y | awk -Fcrashers/ '{print $2}' | awk -F/ '{print $1}' | sort | uniq | wc -l` + total=`find -L ~/bff/results -name '*.cw' | grep crashers | grep -v gmalloc | wc -l` not_exploitable=$(expr $total - $exploitable) echo $exploitable Exploitable echo $not_exploitable Unknown echo $total Total else - find -L ~/results -name '*.gdb' | grep crashers | xargs grep -h "Exploitability Classification" | cut -d" " -f3 | sort | uniq -c - echo `find -L ~/results -name '*.gdb' | grep crashers | wc -l` TOTAL + find -L ~/bff/results -name '*.gdb' | grep crashers | xargs grep -h "Exploitability Classification" | cut -d" " -f3 | sort | uniq -c + echo `find -L ~/bff/results -name '*.gdb' | grep crashers | wc -l` TOTAL fi \ No newline at end of file From 91ea2f0c7f04ad3752a8a08d0abfee3589b3e96d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 11:48:58 -0400 Subject: [PATCH 1052/1169] Tweak install guide --- src/linux/INSTALL | 92 ++++++++++++++++------------------------------- 1 file changed, 31 insertions(+), 61 deletions(-) diff --git a/src/linux/INSTALL b/src/linux/INSTALL index b10dbcb..c036e3c 100644 --- a/src/linux/INSTALL +++ b/src/linux/INSTALL @@ -1,4 +1,4 @@ -The easiest way to get BFF up and running is to use the DebianFuzz +The easiest way to get BFF up and running is to use the UbuFuzz virtual machine. However, if this is not an option for you, it should possible to run BFF on any UNIX-like operating system, as long as the dependencies are met. @@ -10,7 +10,7 @@ Python 2.7 Python Numpy Python Scipy Python Yaml -gdb 7.1 or later +gdb 7.2 or later on Linux is required for exploitability information zzuf (patched by CERT) In order to build zzuf and the other BFF dependencies, the following @@ -36,7 +36,7 @@ For the location of the scripts (including bff.py): ~/bff For the results: -~/results +~/bff/results The default fuzzing target of ImageMagick: ~/convert @@ -51,7 +51,7 @@ Simply run ~/bff/batch.sh to start fuzzing. ===== Tuning the operating system ===== -DebianFuzz has several optimizations that improve fuzzing performance. +UbuFuzz has several optimizations that improve fuzzing performance. If using your own operating system, you may wish to make the following changes: @@ -85,15 +85,7 @@ can be performed: 1) Install dependencies present in the package system: yum install numpy scipy python-yaml valgrind svn automake libtool gcc-c++ ncurses-devel -2) Install libcaca, which is a dependency for building zzuf: -svn co https://github.com/cacalabs/libcaca/trunk libcaca -cd libcaca -./bootstrap -./configure -make -sudo make install - -3) Install the zzuf version patched by CERT: +2) Install the zzuf version patched by CERT: export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig unzip zzuf-patched.zip cd zzuf-patched @@ -102,26 +94,24 @@ cd zzuf-patched make sudo make install -4) Install old ImageMagick version as default fuzz target: +3) Install old ImageMagick version as default fuzz target: sudo yum groupinstall "X Software Development" sudo ln -sf /usr/include/asm/byteorder.h /usr/include/sys/byteorder.h -wget http://downloads.sourceforge.net/project/imagemagick/old-sources/5.x/5.2/ImageMagick-5.2.0.tar.gz -tar xzvf ImageMagick-5.2.0.tar.gz -cd ImageMagick-5.2.0 +wget http://downloads.sourceforge.net/project/imagemagick/old-sources/5.x/5.3/ImageMagick-5.3.0.tar.gz +tar xzvf ImageMagick-5.3.0.tar.gz +cd ImageMagick-5.3.0 ./configure make sudo make install -5) Unzip BFF scripts: +4) Unzip BFF scripts: mkdir ~/bff -unzip scripts.zip -d ~/bff +unzip BFF-2.8.zip -d ~/bff -6) Configure symlinks +5) Configure symlink ln -s /usr/local/bin/convert ~/convert -ln -s ~/bff/scripts ~/bff -ln -s ~/bff/results ~/results -7) Start fuzzing +6) Start fuzzing ~/bff/batch.sh -- System Performance Configurations for Fedora -- @@ -154,15 +144,7 @@ can be performed: 1) Install dependencies present in the package system: sudo apt-get install python-numpy python-scipy python-yaml valgrind subversion automake libtool build-essential libncurses5-dev -2) Install libcaca, which is a dependency for building zzuf: -svn co svn://svn.zoy.org/caca/libcaca/trunk libcaca -cd libcaca -./bootstrap -./configure -make -sudo make install - -3) Install the zzuf version patched by CERT: +2) Install the zzuf version patched by CERT: unzip zzuf-patched.zip cd zzuf-patched ./bootstrap @@ -170,26 +152,24 @@ cd zzuf-patched make sudo make install -4) Install old ImageMagick version as default fuzz target: +3) Install old ImageMagick version as default fuzz target: sudo apt-get install libx11-dev libxt-dev sudo ln -sf /usr/include/i386-linux-gnu/asm/byteorder.h /usr/include/sys/byteorder.h -wget http://downloads.sourceforge.net/project/imagemagick/old-sources/5.x/5.2/ImageMagick-5.2.0.tar.gz -tar zxf ImageMagick-5.2.0.tar.gz -cd ImageMagick-5.2.0 +wget http://downloads.sourceforge.net/project/imagemagick/old-sources/5.x/5.3/ImageMagick-5.3.0.tar.gz +tar zxf ImageMagick-5.3.0.tar.gz +cd ImageMagick-5.3.0 ./configure make sudo make install -5) Unzip BFF scripts: +4) Unzip BFF scripts: mkdir ~/bff -unzip scripts.zip -d ~/bff +unzip BFF-2.8.zip -d ~/bff -6) Configure symlinks +5) Configure symlink ln -s /usr/local/bin/convert ~/convert -ln -s ~/bff/scripts ~/bff -ln -s ~/bff/results ~/results -7) Start fuzzing +6) Start fuzzing ~/bff/batch.sh -- System Performance Configurations for Ubuntu -- @@ -222,15 +202,7 @@ can be performed: sudo zypper ar -f 'http://download.opensuse.org/repositories/devel:/languages:/python/openSUSE_12.1/' python sudo zypper install python-numpy python-scipy valgrind subversion automake libtool gcc-c++ ncurses-devel make -2) Install libcaca, which is a dependency for building zzuf: -svn co svn://svn.zoy.org/caca/libcaca/trunk libcaca -cd libcaca -./bootstrap -./configure -make -sudo make install - -3) Install the zzuf version patched by CERT: +2) Install the zzuf version patched by CERT: unzip zzuf-patched.zip cd zzuf-patched ./bootstrap @@ -238,26 +210,24 @@ cd zzuf-patched make sudo make install -4) Install old ImageMagick version as default fuzz target: +3) Install old ImageMagick version as default fuzz target: sudo zypper install xorg-x11-devel sudo ln -sf /usr/include/asm/byteorder.h /usr/include/sys/byteorder.h -wget http://downloads.sourceforge.net/project/imagemagick/old-sources/5.x/5.2/ImageMagick-5.2.0.tar.gz -tar xzvf ImageMagick-5.2.0.tar.gz -cd ImageMagick-5.2.0 +wget http://downloads.sourceforge.net/project/imagemagick/old-sources/5.x/5.3/ImageMagick-5.3.0.tar.gz +tar xzvf ImageMagick-5.3.0.tar.gz +cd ImageMagick-5.3.0 ./configure make sudo make install -5) Unzip BFF scripts: +4) Unzip BFF scripts: mkdir ~/bff -unzip scripts.zip -d ~/bff +unzip BFF-2.8.zip -d ~/bff -6) Configure symlinks +5) Configure symlink ln -s /usr/local/bin/convert ~/convert -ln -s ~/bff/scripts ~/bff -ln -s ~/bff/results ~/results -7) Start fuzzing +6) Start fuzzing ~/bff/batch.sh -- System Performance Configurations for Fedora -- From 7adfb3412f21d04949644a1e9a04954f3c02f8e5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 11:50:32 -0400 Subject: [PATCH 1053/1169] clarify gdb requirement --- src/linux/INSTALL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/INSTALL b/src/linux/INSTALL index c036e3c..6a38fc4 100644 --- a/src/linux/INSTALL +++ b/src/linux/INSTALL @@ -10,7 +10,7 @@ Python 2.7 Python Numpy Python Scipy Python Yaml -gdb 7.2 or later on Linux is required for exploitability information +gdb (7.2 or later on Linux is required for exploitability information) zzuf (patched by CERT) In order to build zzuf and the other BFF dependencies, the following From 2a3e4f660d2dd2071778e2744b9ce3eea4d97b48 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 14:52:43 -0400 Subject: [PATCH 1054/1169] Fix typo is_linux -> is_linux() --- src/certfuzz/campaign/campaign_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 7338ec0..17658d1 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -151,7 +151,7 @@ def _setup_environment(self): def _check_gdb_compat(self): logger.debug('checking /proc compatibility') - if not host_info.is_linux: + if not host_info.is_linux(): logger.debug( 'Current platform does not support /proc. Adjusting debugger templates.') self.config['debugger']['proc_compat'] = False From 834d369f2e9fc85d321333384fb7411b5a4139be Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 15:13:15 -0400 Subject: [PATCH 1055/1169] Fix ordering of gdb support flags for non-Linux systems. --- .../gdb_noproc_noctt_complete_nofunction_template.txt | 5 ----- .../templates/gdb_noproc_noctt_complete_template.txt | 5 ----- src/certfuzz/testcase/testcase_linux.py | 8 +++++--- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt index 5f487eb..da139e2 100644 --- a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt @@ -7,11 +7,6 @@ echo \n echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n -echo siginfo: -print $_siginfo -echo si_addr: -print $_siginfo._sifields._sigfault.si_addr -echo \n bt full 512 echo \n info registers diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt index 2066ba4..587fc5b 100644 --- a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt @@ -7,11 +7,6 @@ echo \n echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n -echo siginfo: -print $_siginfo -echo si_addr: -print $_siginfo._sifields._sigfault.si_addr -echo \n bt full 512 echo \n info registers diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 8d9d18c..7d09adf 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -79,9 +79,8 @@ def set_debugger_template(self, option='bt_only'): # noctt_complete_nofunction # noproc_complete # noproc_complete_nofunction - - if not self.cfg['debugger']['proc_compat']: - option = 'noproc_' + option + # noproc_noctt_complete + # noproc_noctt_complete_nofunction if 'complete' in option: if not self.cfg['debugger']['ctt_compat']: @@ -90,6 +89,9 @@ def set_debugger_template(self, option='bt_only'): if not self.pc_in_function: option = option + '_nofunction' + if not self.cfg['debugger']['proc_compat']: + option = 'noproc_' + option + dbg_template_name = '%s_%s_template.txt' % ( self._debugger_cls._key, option) self.debugger_template = os.path.join( From acabd55e1f9629a54b8dad82d8ff08f5594d4b1d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 16:06:59 -0400 Subject: [PATCH 1056/1169] Save core dumps if present. BFF-968 --- src/certfuzz/testcase/testcase_base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index f63a48e..27d968e 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -119,11 +119,15 @@ def copy_files_to_temp(self): filetools.copy_file(self.seedfile.path, self.tempdir) # TODO: This seems hacky. Should be better way to have - # minimizer_log.txt survive update_crash_details + # minimizer_log.txt and core files survive update_crash_details minlog = os.path.join(self.fuzzedfile.dirname, 'minimizer_log.txt') if os.path.exists(minlog): filetools.copy_file(minlog, self.tempdir) + corefile = os.path.join(self.workdir_base, 'core') + if os.path.exists(corefile): + filetools.copy_file(corefile, self.tempdir) + new_fuzzedfile = os.path.join(self.tempdir, self.fuzzedfile.basename) self.fuzzedfile = BasicFile(new_fuzzedfile) From 0ef33c4505777a799b925f14c37845fbaaf8b7e3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 16:07:44 -0400 Subject: [PATCH 1057/1169] Interval size of 5 is reasonable compromise for both GUI and CLI apps. Fine for Linux as we no longer have the interval benefit of zzuf. --- src/linux/configs/bff.yaml | 2 +- src/windows/configs/examples/bff.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index 753d902..70d0789 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -113,7 +113,7 @@ directories: ############################################################################## runoptions: first_iteration: 0 - seed_interval: 20 + seed_interval: 5 minimize: True minimizer_timeout: 3600 # keep_unique_faddr: False diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 4312165..5669fc6 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -118,7 +118,7 @@ directories: # # seed_interval: # The number of iterations to perform before selecting a new seed file and -# mutation range. Default is zero if not present. +# mutation range. Default is 1 if not present. # # minimize: # Create a file that is minimally-different than the seed file, yet crashes @@ -140,7 +140,7 @@ directories: ############################################################################## runoptions: first_iteration: 0 - seed_interval: 1 + seed_interval: 5 minimize: True minimizer_timeout: 3600 keep_unique_faddr: False From eb99ded368cfac9647b3b16946154949624b99d6 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 2 Aug 2016 16:17:08 -0400 Subject: [PATCH 1058/1169] Enable <= 4MB core files when launched via batch.sh. BFF-969 --- src/linux/batch.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index bd162fa..7dce152 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -77,6 +77,9 @@ fi # Prevent creation of huge files ulimit -f 1048576 +# Enable reasonably-sized core dumps +ulimit -c 4096 + if ( contains "$platform" "Linux" ); then if [ ! -f ~/pin/pin ]; then mkdir -p ~/fuzzing From 2e2d71b532732c3b1f5b2c93fd1784a4d32f93d2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 4 Aug 2016 11:23:01 -0400 Subject: [PATCH 1059/1169] Don't attempt to minimize a crash that doesn't settle in to a small number of target crash hashes. e.g. ASLR enabled w/ no symbols. BFF-978 --- src/certfuzz/minimizer/minimizer_base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index dc17f34..fe86f89 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -59,6 +59,7 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, self.keep_uniq_faddr = keep_uniq_faddr self.watchcpu = watchcpu self.exhaustivesearch_threshold = 10 + self.max_target_hashes = 10 self.minchar = 'x' self.save_others = True @@ -362,6 +363,10 @@ def _set_crash_hashes(self): times = [] # loop until we've found ALL the testcase signatures while miss_count < max_misses: + target_hash_count = len(sigs_set) + if target_hash_count > self.max_target_hashes: + self._raise( + 'Too many crash hashes seen to minimize. Is memory randomization disabled?') # (sometimes testcase sigs change for the same input file) (fd, f) = tempfile.mkstemp( prefix='minimizer_set_crash_hashes_', text=True, dir=self.tempdir) From 645b5baeb73932432f80543e3431dd3bf6b49fc5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 4 Aug 2016 13:41:31 -0400 Subject: [PATCH 1060/1169] Remove bff_stats.py. BFF-953 --- src/linux/tools/bff_stats.py | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100755 src/linux/tools/bff_stats.py diff --git a/src/linux/tools/bff_stats.py b/src/linux/tools/bff_stats.py deleted file mode 100755 index 87e9e1e..0000000 --- a/src/linux/tools/bff_stats.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -''' -Created on Jan 12, 2011 - -@organization: cert.org - -''' -import os -import sys -try: - from certfuzz.tools.linux.bff_stats import main -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.tools.linux.bff_stats import main - - -if __name__ == '__main__': - main() From 25bef94447eff2f3df2551786192ac0c7d6bba51 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 4 Aug 2016 14:26:41 -0400 Subject: [PATCH 1061/1169] Get rid of extra keep_duplicates line. --- src/linux/configs/bff.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index 70d0789..40f76a1 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -214,6 +214,5 @@ analyzer: savefailedasserts: False use_valgrind: True use_pin_calltrace: False - # keep_duplicates: False valgrind_timeout: 120 From e4e483b058cb6b15342cd53e5ffe6449d5810f1a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 4 Aug 2016 14:51:20 -0400 Subject: [PATCH 1062/1169] Get rid of range_list config option --- src/linux/configs/bff.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index 40f76a1..5df5e53 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -177,9 +177,6 @@ fuzzer: # fuzzer: crlfmut # fuzzer: nullmut # fuzzer: verify - # range_list: - # - [0x0000, 0x0400] - # - [0x1000, 0x100F] fuzz_zip_container: False From 015920bbb4f99275894864df18cd15bbc0b28480 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 4 Aug 2016 14:54:31 -0400 Subject: [PATCH 1063/1169] Get rid of rest of range_list option info --- src/linux/configs/bff.yaml | 3 --- src/windows/configs/examples/bff.yaml | 6 ------ 2 files changed, 9 deletions(-) diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index 5df5e53..a0b28f4 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -159,9 +159,6 @@ runoptions: # # OPTIONS APPLIED TO THE ABOVE MUTATORS: # -# range_list: -# byte ranges to be fuzzed. One range per line, hex or decimal -# # fuzz_zip_container: # rather than fuzzing zip file contents, fuzz the zip container itself # diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 5669fc6..2d194f3 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -185,9 +185,6 @@ runoptions: # # OPTIONS APPLIED TO THE ABOVE MUTATORS: # -# range_list: -# byte ranges to be fuzzed. One range per line, hex or decimal -# # fuzz_zip_container: # rather than fuzzing zip file contents, fuzz the zip container itself # @@ -203,8 +200,5 @@ fuzzer: # fuzzer: crlfmut # fuzzer: nullmut # fuzzer: verify - # range_list: - # - [0x0000, 0x0400] - # - [0x1000, 0x100F] fuzz_zip_container: False From 31defb90dd476a60b809603bb137c0a8271f89d2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 4 Aug 2016 15:17:36 -0400 Subject: [PATCH 1064/1169] move minimizer plot to experimental out of tools also remove dead code, add a few future-enhancement comments --- src/certfuzz/campaign/campaign_windows.py | 7 ------- src/certfuzz/debuggers/gdb.py | 4 ++++ .../stats_and_other_tools}/minimizer_plot.py | 0 3 files changed, 4 insertions(+), 7 deletions(-) rename src/{linux/tools => experimental/stats_and_other_tools}/minimizer_plot.py (100%) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 96418a7..2c8d388 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -216,10 +216,3 @@ def _do_iteration(self, seedfile, range_obj, seednum): if not seednum % self.status_interval: logger.info('Iteration: %d crashes found: %d', self.current_seed, len(self.testcases_seen)) - # FIXME - # self.seedfile_set.update_csv() - # logger.info('Seedfile Set Status:') - # logger.info('FIXME') - # for k, score, successes, tries, p in self.seedfile_set.status(): - # logger.info('%s %0.6f %d %d %0.6f', k, score, successes, - # tries, p) diff --git a/src/certfuzz/debuggers/gdb.py b/src/certfuzz/debuggers/gdb.py index 7fae787..d10a78e 100644 --- a/src/certfuzz/debuggers/gdb.py +++ b/src/certfuzz/debuggers/gdb.py @@ -37,6 +37,8 @@ def _get_cmdline(self): raise DebuggerError( 'Input file does not exist: %s', self.input_file) + # BFF-977 add -ex options instead of reading self.input_file + args = [ self.debugger_app(), '-n', '-batch', '-command', self.input_file] logger.log(5, "GDB command: [%s]", ' '.join(args)) @@ -66,6 +68,8 @@ def _create_input_file(self): new_script = s.safe_substitute(PROGRAM=self.program, CMD_ARGS=cmdargs, OUTFILE=self.outfile, BFFDIR=bffdir) + # BFF-977 split new_script by newlines into a list... + (fd, f) = tempfile.mkstemp(text=True) try: os.write(fd, new_script) diff --git a/src/linux/tools/minimizer_plot.py b/src/experimental/stats_and_other_tools/minimizer_plot.py similarity index 100% rename from src/linux/tools/minimizer_plot.py rename to src/experimental/stats_and_other_tools/minimizer_plot.py From 1f04267962efab4e7e2cb14b856b0a47640c9e08 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 4 Aug 2016 15:30:53 -0400 Subject: [PATCH 1065/1169] update docstring --- src/certfuzz/reporters/testcase_logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/reporters/testcase_logger.py b/src/certfuzz/reporters/testcase_logger.py index 77a654b..07a01b7 100644 --- a/src/certfuzz/reporters/testcase_logger.py +++ b/src/certfuzz/reporters/testcase_logger.py @@ -12,7 +12,7 @@ class TestcaseLoggerReporter(ReporterBase): ''' - Invokes the testcase's logger to report out testcase data + Logs testcase data ''' def go(self): From 0f1885f599c6dc124aa319e12a444892be3fb4ae Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 4 Aug 2016 15:31:12 -0400 Subject: [PATCH 1066/1169] remove unused import --- src/certfuzz/testcase/testcase_linux.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 7d09adf..5030be9 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -8,7 +8,6 @@ from certfuzz.testcase.testcase_base import TestCaseBase from certfuzz.fuzztools import hostinfo, filetools -from certfuzz.fuzztools.command_line_templating import get_command_args_list from certfuzz.testcase.errors import TestCaseError try: From d48e9b4ac495a5695616fccb183b99397db324d9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 4 Aug 2016 15:32:45 -0400 Subject: [PATCH 1067/1169] pep8 cleanup --- src/certfuzz/testcase/testcase_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 27d968e..dfd017e 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -10,7 +10,6 @@ from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools import filetools, hamming from certfuzz.fuzztools.filetools import check_zip_file, mkdir_p -from certfuzz.testcase.errors import TestCaseError from pprint import pformat @@ -198,4 +197,4 @@ def calculate_hamming_distances_a(self): self.hd_bytes = hamming.bytewise_hd(a_string, fuzzed) logger.info( - "crasher=%s bytewise_hd=%d", self.signature, self.hd_bytes) + "crasher=%s bytewise_hd=%d", self.signature, self.hd_bytes) From ff78df957753ac55a89c35736f0f16840bdef073 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Aug 2016 16:17:11 -0400 Subject: [PATCH 1068/1169] pep8 cleanup --- src/certfuzz/analyzers/analyzer_base.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/analyzers/analyzer_base.py b/src/certfuzz/analyzers/analyzer_base.py index 9d3d584..98e9a68 100644 --- a/src/certfuzz/analyzers/analyzer_base.py +++ b/src/certfuzz/analyzers/analyzer_base.py @@ -18,12 +18,14 @@ class Analyzer(object): ''' classdocs ''' + def __init__(self, cfg, testcase, outfile=None, timeout=None, **options): logger.debug('Initializing %s', self.__class__.__name__) self.cfg = cfg self.testcase = testcase - self.cmdargs = get_command_args_list(self.cfg['target']['cmdline_template'], testcase.fuzzedfile.path)[1] + self.cmdargs = get_command_args_list( + self.cfg['target']['cmdline_template'], testcase.fuzzedfile.path)[1] self.outfile = outfile self.timeout = float(timeout) self.progname = self.cmdargs[1] @@ -84,9 +86,11 @@ def go(self): logger.debug('%s cmd: [%s]', self.__class__.__name__, ' '.join(args)) # short-circuit if analyzer is missing - analyzer = str(args[0]) # make a copy of the string so we don't mess up args[0] + # make a copy of the string so we don't mess up args[0] + analyzer = str(args[0]) if (not self._analyzer_exists(analyzer)): - logger.warning('Skipping analyzer %s: Not found in path.', analyzer) + logger.warning( + 'Skipping analyzer %s: Not found in path.', analyzer) return subp.run_with_timer(args, self.timeout, self.progname, **self.options) @@ -96,7 +100,8 @@ def go(self): # try again? self.retry_count += 1 if self.retry_count < self.max_retries: - logger.warning('Empty output file on attempt %d of %d', self.retry_count, self.max_retries) + logger.warning( + 'Empty output file on attempt %d of %d', self.retry_count, self.max_retries) # get a new name for the stderr output if we can if not self.preserve_stderr: self._set_stderrpath() @@ -104,7 +109,8 @@ def go(self): self.timeout *= 2 self.go() else: - logger.warning('Unable to produce output after %d tries', self.retry_count) + logger.warning( + 'Unable to produce output after %d tries', self.retry_count) raise AnalyzerEmptyOutputError(self.outfile) else: # delete the stderr file since we didn't need it From ab0e207ad50d2389d697bd077abee4764c2f8f4c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 8 Aug 2016 16:23:30 -0400 Subject: [PATCH 1069/1169] Set CWD when running an analyzer. BFF-984 --- src/certfuzz/analyzers/analyzer_base.py | 3 ++- src/certfuzz/fuzztools/subprocess_helper.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/analyzers/analyzer_base.py b/src/certfuzz/analyzers/analyzer_base.py index 98e9a68..4c799dc 100644 --- a/src/certfuzz/analyzers/analyzer_base.py +++ b/src/certfuzz/analyzers/analyzer_base.py @@ -93,7 +93,8 @@ def go(self): 'Skipping analyzer %s: Not found in path.', analyzer) return - subp.run_with_timer(args, self.timeout, self.progname, **self.options) + subp.run_with_timer( + args, self.timeout, self.progname, cwd=self.tmpdir, **self.options) if not self.missing_output_ok and not os.path.exists(self.outfile): raise AnalyzerOutputMissingError(self.outfile) if not self.empty_output_ok and not os.path.getsize(self.outfile): diff --git a/src/certfuzz/fuzztools/subprocess_helper.py b/src/certfuzz/fuzztools/subprocess_helper.py index deea8f2..e3d734f 100644 --- a/src/certfuzz/fuzztools/subprocess_helper.py +++ b/src/certfuzz/fuzztools/subprocess_helper.py @@ -34,7 +34,7 @@ def on_linux(): signal.signal(signal.SIGTTOU, signal.SIG_IGN) -def run_with_timer(args, timeout, progname, use_shell=False, **options): +def run_with_timer(args, timeout, progname, cwd=None, use_shell=False, **options): ''' Runs . If it takes longer than we'll kill as well as hunt down any processes named @@ -74,10 +74,10 @@ def run_with_timer(args, timeout, progname, use_shell=False, **options): if _seeoutput: # os.setsid sets process group p = subprocess.Popen( - args, env=env, shell=use_shell, preexec_fn=os.setsid) + args, cwd=cwd, env=env, shell=use_shell, preexec_fn=os.setsid) else: p = subprocess.Popen( - args, stdout=output, stderr=errors, env=env, shell=use_shell, preexec_fn=os.setsid) + args, cwd=cwd, stdout=output, stderr=errors, env=env, shell=use_shell, preexec_fn=os.setsid) except: print "Failed to run [%s]" % ' '.join(args) sys.exit(-1) From ca92fe8b5771f40cc684da81c81914ca5f32d216 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Aug 2016 12:53:38 -0400 Subject: [PATCH 1070/1169] Obey debugheap config option. BFF-985 --- src/certfuzz/debuggers/msec.py | 6 ++++-- src/certfuzz/testcase/testcase_windows.py | 6 +++++- src/certfuzz/tools/windows/repro.py | 5 +++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/debuggers/msec.py b/src/certfuzz/debuggers/msec.py index 46af9e6..6a13885 100644 --- a/src/certfuzz/debuggers/msec.py +++ b/src/certfuzz/debuggers/msec.py @@ -29,7 +29,7 @@ class MsecDebugger(DebuggerBase): _key = 'msec' _ext = 'msec' - def __init__(self, program, cmd_args, outfile_base, timeout, watchcpu, exception_depth=0, **options): + def __init__(self, program, cmd_args, outfile_base, timeout, watchcpu, exception_depth=0, cdb_command='!exploitable -v', debug_heap=False, ** options): DebuggerBase.__init__( self, program, cmd_args, outfile_base, timeout, **options) self.exception_depth = exception_depth @@ -38,6 +38,8 @@ def __init__(self, program, cmd_args, outfile_base, timeout, watchcpu, exception self.wmiInterface = wmi.WMI() self.t = None self.savedpid = None + self.cdb_command = cdb_command + self.debugheap = debug_heap def kill(self, pid, returncode): """kill function for Win32""" @@ -64,7 +66,7 @@ def debugger_test(self): return [self.debugger_app(), '-version'] def _get_cmdline(self, outfile): - cdb_command = '$$Found_with_CERT_BFF_2.8;r;!exploitable -v;q' + cdb_command = '$$Found_with_CERT_BFF_2.8;r;%s;q' % self.cdb_command args = [] args.append(self.debugger_app()) args.append('-amsec.dll') diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index d957528..d5abe6c 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -115,7 +115,11 @@ def update_crash_details(self): def debug_once(self): outfile_base = os.path.join(self.tempdir, self.fuzzedfile.basename) - with self._debugger_cls(program=self.program, cmd_args=self.cmdargs, outfile_base=outfile_base, timeout=self.debugger_timeout, exception_depth=self.exception_depth, workingdir=self.tempdir, watchcpu=self.watchcpu) as debugger: + with self._debugger_cls(program=self.program, cmd_args=self.cmdargs, + outfile_base=outfile_base, timeout=self.debugger_timeout, + exception_depth=self.exception_depth, + debug_heap=self.cfg['debugger']['debugheap'], + workingdir=self.tempdir, watchcpu=self.watchcpu) as debugger: self.parsed_outputs.append(debugger.go()) self.reached_secondchance = self.parsed_outputs[ diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index a28c8f2..d674d95 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -101,7 +101,8 @@ def main(): os.path.join(iterationdir, iterationfile)) fuzzed_file.path = iterationpath - cmd_as_args = get_command_args_list(config['target']['cmdline_template'], fuzzed_file.path)[1] + cmd_as_args = get_command_args_list( + config['target']['cmdline_template'], fuzzed_file.path)[1] targetdir = os.path.dirname(cmd_as_args[0]) args = [] @@ -121,7 +122,7 @@ def main(): if not options.debugger: # Using cdb or windbg args.append('-amsec.dll') - if options.debugheap: + if options.debugheap or config['debugger']['debugheap']: # do not use hd, xd options if debugheap is set pass else: From 6004b4de2afea129575a994f681abe6198d091a9 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Aug 2016 14:36:03 -0400 Subject: [PATCH 1071/1169] pep8 cleanup --- src/certfuzz/debuggers/debugger_base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/debuggers/debugger_base.py b/src/certfuzz/debuggers/debugger_base.py index d175655..bcd4ee6 100644 --- a/src/certfuzz/debuggers/debugger_base.py +++ b/src/certfuzz/debuggers/debugger_base.py @@ -53,7 +53,8 @@ def _reset_result(self): def _validate_exploitability(self): if not self.result['exp'] in allowed_exploitability_values: - raise DebuggerError('Unknown exploitability value: %s' % self.result['exp']) + raise DebuggerError( + 'Unknown exploitability value: %s' % self.result['exp']) def outfile_basename(self, basename): return '.'.join((basename, self.type)) @@ -98,4 +99,4 @@ def __exit__(self, etype, value, traceback): @property def extension(self): - return self._ext \ No newline at end of file + return self._ext From be318c3a928e5d58189bd4a10d25eab158d09b4e Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Aug 2016 14:39:37 -0400 Subject: [PATCH 1072/1169] Run !analyze for each unique crash on Windows. BFF-272 --- src/certfuzz/analyzers/cdbanalyze.py | 46 +++++++++++++++++++ .../tc_pipeline/tc_pipeline_windows.py | 2 + .../analyzers/test_cdbanalyze.py | 21 +++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/certfuzz/analyzers/cdbanalyze.py create mode 100644 src/test_certfuzz/analyzers/test_cdbanalyze.py diff --git a/src/certfuzz/analyzers/cdbanalyze.py b/src/certfuzz/analyzers/cdbanalyze.py new file mode 100644 index 0000000..1cd58aa --- /dev/null +++ b/src/certfuzz/analyzers/cdbanalyze.py @@ -0,0 +1,46 @@ +''' +Created on Aug 5, 2011 + +@organization: cert.org +''' +import platform +import os.path +from certfuzz.analyzers.analyzer_base import Analyzer + +_platforms = ['Windows'] +_platform_is_supported = platform.system() in _platforms + + +OUTFILE_EXT = "analyze" +get_file = lambda x: '%s.%s' % (x, OUTFILE_EXT) + + +class CdbAnalyze(Analyzer): + ''' + classdocs + ''' + + def __init__(self, cfg, testcase): + ''' + Constructor + ''' + if not _platform_is_supported: + return None + + self.outfile = get_file(testcase.fuzzedfile.path) + # !analyze takes longer to complete than !exploitable. Give it 2x the time + self.timeout = cfg['runner']['runtimeout'] * 2 + self.watchcpu = cfg['debugger']['watchcpu'] + + Analyzer.__init__(self, cfg, testcase, self.outfile, self.timeout) + + def go(self): + if not _platform_is_supported: + return None + + prg = self.cmdargs[0] + args = self.cmdargs[1:] + + from ..debuggers.msec import MsecDebugger + MsecDebugger( + program=prg, cmd_args=args, outfile_base=self.outfile, timeout=self.timeout, watchcpu=self.watchcpu, exception_depth=0, debug_heap=self.cfg['debugger']['debugheap'], cdb_command='!analyze -v').go() diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index 3f198b3..e2a79a9 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -10,6 +10,7 @@ from certfuzz.tc_pipeline.tc_pipeline_base import TestCasePipelineBase from certfuzz.reporters.copy_files import CopyFilesReporter from certfuzz.analyzers.stderr import StdErr +from certfuzz.analyzers.cdbanalyze import CdbAnalyze from certfuzz.analyzers.drillresults import WindowsDrillResults @@ -21,6 +22,7 @@ class WindowsTestCasePipeline(TestCasePipelineBase): def _setup_analyzers(self): # self.analyzer_classes.append(StdErr) + self.analyzer_classes.append(CdbAnalyze) self.analyzer_classes.append(WindowsDrillResults) def _pre_verify(self, testcase): diff --git a/src/test_certfuzz/analyzers/test_cdbanalyze.py b/src/test_certfuzz/analyzers/test_cdbanalyze.py new file mode 100644 index 0000000..6e33993 --- /dev/null +++ b/src/test_certfuzz/analyzers/test_cdbanalyze.py @@ -0,0 +1,21 @@ +''' +Created on Apr 10, 2012 + +@organization: cert.org +''' +import unittest + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From ce5bc147242c03e228a99f76452b14fc3ef1cd30 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Aug 2016 16:41:10 -0400 Subject: [PATCH 1073/1169] Retain multi-part file extensions. BFF-902 --- src/certfuzz/file_handlers/basicfile.py | 6 ++++-- src/certfuzz/fuzztools/filetools.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/file_handlers/basicfile.py b/src/certfuzz/file_handlers/basicfile.py index de3a4b5..207f76c 100644 --- a/src/certfuzz/file_handlers/basicfile.py +++ b/src/certfuzz/file_handlers/basicfile.py @@ -13,10 +13,13 @@ class BasicFile(object): ''' Object to contain basic info about file: path, basename, dirname, len, md5 ''' + def __init__(self, path): self.path = path (self.dirname, self.basename) = os.path.split(self.path) - (self.root, self.ext) = os.path.splitext(self.basename) + # Split on first '.' to retain multiple dotted extensions + self.root = self.basename.split('.', 1)[0] + self.ext = '.' + self.basename.split('.', 1)[1] self.len = None self.md5 = None @@ -56,4 +59,3 @@ def to_FileDoc(self, doc=None): doc.sha1 = self.sha1 doc.size_in_bytes = self.len return doc - diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 99f028b..b606d6c 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -253,7 +253,9 @@ def get_newpath(oldpath, str_to_insert): :param oldpath: :param str_to_insert: ''' - root, ext = os.path.splitext(oldpath) + # Split on first '.' to retain multiple dotted extensions + root = oldpath.split('.', 1)[0] + ext = '.' + oldpath.split('.', 1)[1] newpath = ''.join([root, str_to_insert, ext]) return newpath From ace67d59d8892763cdb110b2b690035027959679 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 9 Aug 2016 16:41:24 -0400 Subject: [PATCH 1074/1169] pep8 cleanup --- src/certfuzz/fuzztools/filetools.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index b606d6c..d034fd9 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -35,7 +35,8 @@ def wrapper(*args, **kwargs): try: return F(*args, **kwargs) except Exception as detail: - logmsg = '... [try %d of %d]: %s' % (current_depth + 1, MAXDEPTH, detail) + logmsg = '... [try %d of %d]: %s' % ( + current_depth + 1, MAXDEPTH, detail) logger.debug(logmsg) # increment naptimefor the next time around naptime = SLEEPTIMER * pow(BACKOFF_FACTOR, current_depth) @@ -311,14 +312,17 @@ def delete_contents_of(dirs, print_via_log=True): except Exception, e: skipped_items.append((directory, e)) - to_delete = [os.path.join(directory, item) for item in dirlist if not item == '.svn'] - skipped_items.extend(delete_files_or_dirs(to_delete, print_via_log)) + to_delete = [os.path.join(directory, item) + for item in dirlist if not item == '.svn'] + skipped_items.extend( + delete_files_or_dirs(to_delete, print_via_log)) return skipped_items def check_zip_fh(file_like_content): - # Make sure that it's not an embedded zip (e.g. a DOC file from Office 2007) + # Make sure that it's not an embedded zip (e.g. a DOC file from Office + # 2007) file_like_content.seek(0) zipmagic = file_like_content.read(2) file_like_content.seek(0) @@ -338,6 +342,7 @@ def check_zip_file(filepath): with open(filepath, 'rb') as filehandle: return check_zip_fh(filehandle) + def get_zipcontents(filepath): # If the file is zip-based, fuzz the contents rather than the container logger.debug('Reading zip file: %s', filepath) @@ -355,7 +360,6 @@ def get_zipcontents(filepath): return unzippedbytes - def make_writable(filename): mode = os.stat(filename).st_mode os.chmod(filename, mode | stat.S_IWRITE) @@ -385,7 +389,6 @@ def read_bin_file(binfile): return _read_file(binfile, 'rb') - # Adapted from Python Cookbook 2nd Ed. p.88 def all_files(root, patterns='*', single_level=False, yield_folders=False): # Expand patterns from semicolon-separated string to list From fa80a35657512050da3bb4913a4f3fd7fd2641be Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 10 Aug 2016 09:36:46 -0400 Subject: [PATCH 1075/1169] Fix minimizer fail when missing calltrace. BFF-986 --- src/certfuzz/minimizer/minimizer_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index fe86f89..441c71c 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -486,6 +486,7 @@ def _crash_builder(self): def _get_pin_signature(self, backtracelevels): # total_stack_corruption. Use pin calltrace to get a backtrace + signature = '' analyzer_instance = pin_calltrace.Pin_calltrace( self.cfg, self.testcase) try: From 1779c7a8a80c5c0ed3ed8db0118f347354f5e54b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 10 Aug 2016 09:39:07 -0400 Subject: [PATCH 1076/1169] Handle case where there's no '.' in the seedfile name. BFF-902 --- src/certfuzz/file_handlers/basicfile.py | 10 +++++++--- src/certfuzz/fuzztools/filetools.py | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/file_handlers/basicfile.py b/src/certfuzz/file_handlers/basicfile.py index 207f76c..2a41f2e 100644 --- a/src/certfuzz/file_handlers/basicfile.py +++ b/src/certfuzz/file_handlers/basicfile.py @@ -17,9 +17,13 @@ class BasicFile(object): def __init__(self, path): self.path = path (self.dirname, self.basename) = os.path.split(self.path) - # Split on first '.' to retain multiple dotted extensions - self.root = self.basename.split('.', 1)[0] - self.ext = '.' + self.basename.split('.', 1)[1] + if '.' in self.basename: + # Split on first '.' to retain multiple dotted extensions + self.root = self.basename.split('.', 1)[0] + self.ext = '.' + self.basename.split('.', 1)[1] + else: + self.root = self.basename + self.ext = '' self.len = None self.md5 = None diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index d034fd9..45044a1 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -254,9 +254,13 @@ def get_newpath(oldpath, str_to_insert): :param oldpath: :param str_to_insert: ''' - # Split on first '.' to retain multiple dotted extensions - root = oldpath.split('.', 1)[0] - ext = '.' + oldpath.split('.', 1)[1] + if '.' in oldpath: + # Split on first '.' to retain multiple dotted extensions + root = oldpath.split('.', 1)[0] + ext = '.' + oldpath.split('.', 1)[1] + else: + root = oldpath + ext = '' newpath = ''.join([root, str_to_insert, ext]) return newpath From b748f748d896b4daf4f31cc13c7084da075f6fc9 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 10 Aug 2016 12:59:24 -0400 Subject: [PATCH 1077/1169] watchcpu is in "runner" section of config. BFF-987 --- src/certfuzz/analyzers/cdbanalyze.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/analyzers/cdbanalyze.py b/src/certfuzz/analyzers/cdbanalyze.py index 1cd58aa..6b10304 100644 --- a/src/certfuzz/analyzers/cdbanalyze.py +++ b/src/certfuzz/analyzers/cdbanalyze.py @@ -30,7 +30,7 @@ def __init__(self, cfg, testcase): self.outfile = get_file(testcase.fuzzedfile.path) # !analyze takes longer to complete than !exploitable. Give it 2x the time self.timeout = cfg['runner']['runtimeout'] * 2 - self.watchcpu = cfg['debugger']['watchcpu'] + self.watchcpu = cfg['runner']['watchcpu'] Analyzer.__init__(self, cfg, testcase, self.outfile, self.timeout) From c0f34565d5a361387aa2e310e2729b75d37bdda0 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 10 Aug 2016 16:16:59 -0400 Subject: [PATCH 1078/1169] Don't recalculate bytemap if exhaustive minimization completed. BFF-988 --- src/certfuzz/minimizer/minimizer_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 441c71c..be7b066 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -642,7 +642,7 @@ def _write_file(self): write_file(''.join(self.newfuzzed), self.tempfile) def _set_bytemap(self): - if self.fuzzed_content: + if self.fuzzed_content and not self.bytemap: self.bytemap = hamming.bytemap(self.seed, self.fuzzed_content) def go(self): From 67c32a6c71b40e1301204f21143868a88f4cde49 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 10 Aug 2016 20:49:38 -0400 Subject: [PATCH 1079/1169] Shell redirection is allowed, actually. --- src/certfuzz/campaign/campaign_linux.py | 7 ------- src/linux/configs/bff.yaml | 1 - 2 files changed, 8 deletions(-) diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index 17658d1..fa2b20b 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -85,7 +85,6 @@ def _full_path_original(self, seedfile): def _pre_enter(self): # give up if prog is a script self._check_for_script() - self._check_for_redirect() self._set_unbuffered_stdout() self._setup_environment() if not host_info.is_osx(): @@ -177,12 +176,6 @@ def _check_for_script(self): logger.warning("Target application is a shell script.") raise CampaignScriptError() - def _check_for_redirect(self): - logger.debug('check for redirect') - if '>' in self.config['target']['cmdline_template'].template: - logger.warning("Redirect (>) present in cmdline_template.") - raise CmdlineTemplateError() - def _set_debugger(self): ''' Overrides parent class diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index a0b28f4..a457500 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -28,7 +28,6 @@ campaign: # $SEEDFILE will be replaced at runtime with the appropriate # seed file name. # Use quotes if the target application has spaces in the path -# Shell redirection (>) is not allowed # ############################################################################## target: From 88fedc3e4045d040020577017dd57cc52e5d7a3d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 10 Aug 2016 22:44:35 -0400 Subject: [PATCH 1080/1169] enable keep_unique_faddr option --- src/certfuzz/debuggers/output_parsers/cwfile.py | 10 ++++++++-- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 1 + src/linux/configs/bff.yaml | 3 +-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index 982c45a..b031262 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -55,7 +55,7 @@ class CWfile: - def __init__(self, f): + def __init__(self, f, keep_uniq_faddr=False): ''' Create a GDB file object from the gdb output file @param lines: The lines of the gdb file @@ -87,7 +87,7 @@ def __init__(self, f): self.crashing_thread = False self.pc_in_function = True self.pc_name = 'eip' - self.keep_uniq_faddr = False + self.keep_uniq_faddr = keep_uniq_faddr self.faddr = None self.exp = None @@ -132,6 +132,12 @@ def _hashable_backtrace(self): def _hashable_backtrace_string(self, level): self.hashable_backtrace_string = ' '.join( self.hashable_backtrace[:level]).strip() + if self.keep_uniq_faddr: + try: + self.hashable_backtrace_string = self.hashable_backtrace_string + \ + ' ' + self.faddr + except: + logger.debug('Cannot use PC in hash') logger.warning( '_hashable_backtrace_string: %s', self.hashable_backtrace_string) return self.hashable_backtrace_string diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index 4e8b1d9..7df7dea 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -208,6 +208,7 @@ def _minimize(self, testcase): 'confidence': 0.999, 'tempdir': self.working_dir, 'maxtime': self.cfg['runoptions']['minimizer_timeout'], + 'keep_uniq_faddr': self.cfg['runoptions']['keep_unique_faddr'], } try: diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index a457500..7bed02c 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -63,7 +63,6 @@ debugger: ############################################################################## # LOCATIONS FOR FUZZ RUN FILES: # -# Output files are placed in [outputdir]/[seedfile] # seedfile_dir: # Location of seed files (relative to bff.py) # @@ -115,7 +114,7 @@ runoptions: seed_interval: 5 minimize: True minimizer_timeout: 3600 -# keep_unique_faddr: False + keep_unique_faddr: False keep_duplicates: False recycle_crashers: False watchdogtimeout: 3600 From 60ef139b872408545f58c97877e31e382a998d2a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Aug 2016 11:18:27 -0400 Subject: [PATCH 1081/1169] Get unique_faddr working on OSX and also BFF testcase level --- src/certfuzz/debuggers/crashwrangler.py | 5 +++-- src/certfuzz/debuggers/output_parsers/cwfile.py | 13 ++++++++++++- src/certfuzz/testcase/testcase_linux.py | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/certfuzz/debuggers/crashwrangler.py b/src/certfuzz/debuggers/crashwrangler.py index 660b639..224a81d 100755 --- a/src/certfuzz/debuggers/crashwrangler.py +++ b/src/certfuzz/debuggers/crashwrangler.py @@ -43,8 +43,9 @@ class CrashWrangler(Debugger): _key = 'cw' _ext = 'cw' - def __init__(self, program, cmd_args, outfile, timeout, template=None, exclude_unmapped_frames=True, **options): + def __init__(self, program, cmd_args, outfile, timeout, template=None, exclude_unmapped_frames=True, keep_uniq_faddr=False, **options): Debugger.__init__(self, program, cmd_args, outfile, timeout) + self.keep_uniq_faddr = keep_uniq_faddr def _get_crashwrangler_cmdline(self): if (self.program == cwapp): @@ -91,4 +92,4 @@ def go(self): if not os.path.exists(self.outfile): open(self.outfile, 'w').close() - return CWfile(self.outfile) + return CWfile(self.outfile, keep_uniq_faddr=self.keep_uniq_faddr) diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index b031262..9c19fc7 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -37,6 +37,7 @@ 'bt_line_at': re.compile(r'\bat\b'), 'register': re.compile('\s\s\s?[0-9a-zA-Z]+:\s(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), 'exploitability': re.compile('exception=.+:is_exploitable=( no|yes):'), + 'faddr': re.compile('exception=.+:access_address=((0x[0-9a-zA-Z][0-9a-zA-Z]+)):'), } # There are a number of functions that are typically found in crash backtraces, @@ -209,6 +210,9 @@ def _process_lines(self): if not self.exp: self._look_for_exploitability(line) + if not self.faddr: + self._look_for_faddr(line) + self._look_for_registers(line) # if we found that the stack was corrupt, @@ -252,13 +256,20 @@ def _look_for_exit_code(self, line): if m: self.exit_code = m.group(1) + def _look_for_faddr(self, line): + if self.faddr: + return + m = re.match(regex['faddr'], line) + if m: + self.exit_code = m.group(1) + def _look_for_signal(self, line): m = re.match(regex['signal'], line) if m: self.signal = m.group(1) def _look_for_exploitability(self, line): - if self.faddr: + if self.exp: return m = re.match(regex['exploitability'], line) diff --git a/src/certfuzz/testcase/testcase_linux.py b/src/certfuzz/testcase/testcase_linux.py index 5030be9..5579452 100644 --- a/src/certfuzz/testcase/testcase_linux.py +++ b/src/certfuzz/testcase/testcase_linux.py @@ -63,6 +63,7 @@ def __init__(self, self.save_failed_asserts = save_failed_asserts self.set_debugger_template('bt_only') self.signature = None + self.keep_uniq_faddr = keep_faddr def set_debugger_template(self, option='bt_only'): if host_info.is_osx(): From 8c95e9882496b000872680f62b9b15146ab16f06 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 11 Aug 2016 13:18:10 -0400 Subject: [PATCH 1082/1169] Fix typos in OSX faddr support. BFF-989 --- src/certfuzz/debuggers/output_parsers/cwfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/debuggers/output_parsers/cwfile.py b/src/certfuzz/debuggers/output_parsers/cwfile.py index 9c19fc7..38d0fde 100644 --- a/src/certfuzz/debuggers/output_parsers/cwfile.py +++ b/src/certfuzz/debuggers/output_parsers/cwfile.py @@ -37,7 +37,7 @@ 'bt_line_at': re.compile(r'\bat\b'), 'register': re.compile('\s\s\s?[0-9a-zA-Z]+:\s(0x[0-9a-zA-Z][0-9a-zA-Z]+)'), 'exploitability': re.compile('exception=.+:is_exploitable=( no|yes):'), - 'faddr': re.compile('exception=.+:access_address=((0x[0-9a-zA-Z][0-9a-zA-Z]+)):'), + 'faddr': re.compile('exception=.+:access_address=(0x[0-9a-zA-Z][0-9a-zA-Z]+):'), } # There are a number of functions that are typically found in crash backtraces, @@ -261,7 +261,7 @@ def _look_for_faddr(self, line): return m = re.match(regex['faddr'], line) if m: - self.exit_code = m.group(1) + self.faddr = m.group(1) def _look_for_signal(self, line): m = re.match(regex['signal'], line) From 355d9353e3051dbedf5725033e11153108bb0506 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 19 Aug 2016 10:11:01 -0400 Subject: [PATCH 1083/1169] Fix heuristics for extracting fuzzed file from the gdb cmdline. BFF-990 --- src/certfuzz/tools/linux/repro.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index 0cfb881..70423a3 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -34,7 +34,9 @@ def parseiterpath(commandline): # Return the path to the iteration's fuzzed file for part in commandline.split(): - if 'bff-crash' in part: + if 'campaign_' in part and 'iteration_' in part: + # This is the part of the commandline that looks like it's + # the fuzzed file return part @@ -95,17 +97,19 @@ def main(): print '** using gdb: %s' % gdbfile iterationpath = getiterpath(gdbfile) break - iterationdir = os.path.dirname(iterationpath) - iterationfile = os.path.basename(iterationpath) - if iterationdir: - mkdir_p(iterationdir) - copy_file(fuzzed_file.path, - os.path.join(iterationdir, iterationfile)) - fullpath_fuzzed_file = iterationpath + if iterationpath: + iterationdir = os.path.dirname(iterationpath) + iterationfile = os.path.basename(iterationpath) + if iterationdir: + mkdir_p(iterationdir) + copy_file(fuzzed_file.path, + os.path.join(iterationdir, iterationfile)) + fullpath_fuzzed_file = iterationpath config = load_and_fix_config(cfg_file) - cmd_as_args = get_command_args_list(config['target']['cmdline_template'], fullpath_fuzzed_file)[1] + cmd_as_args = get_command_args_list( + config['target']['cmdline_template'], fullpath_fuzzed_file)[1] args = [] if options.use_edb and options.debugger: From aaaffda12716ff84ffa8d4710b035ec4092fec37 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 19 Aug 2016 10:21:12 -0400 Subject: [PATCH 1084/1169] Get rid of spaces in extension. BFF-902 --- src/certfuzz/file_handlers/basicfile.py | 4 +++- src/certfuzz/fuzztools/filetools.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/file_handlers/basicfile.py b/src/certfuzz/file_handlers/basicfile.py index 2a41f2e..cd71a6f 100644 --- a/src/certfuzz/file_handlers/basicfile.py +++ b/src/certfuzz/file_handlers/basicfile.py @@ -20,7 +20,9 @@ def __init__(self, path): if '.' in self.basename: # Split on first '.' to retain multiple dotted extensions self.root = self.basename.split('.', 1)[0] - self.ext = '.' + self.basename.split('.', 1)[1] + ext = '.' + self.basename.split('.', 1)[1] + # Get rid of any spaces in extension + self.ext = ext.replace(' ', '') else: self.root = self.basename self.ext = '' diff --git a/src/certfuzz/fuzztools/filetools.py b/src/certfuzz/fuzztools/filetools.py index 45044a1..5d34924 100644 --- a/src/certfuzz/fuzztools/filetools.py +++ b/src/certfuzz/fuzztools/filetools.py @@ -258,6 +258,7 @@ def get_newpath(oldpath, str_to_insert): # Split on first '.' to retain multiple dotted extensions root = oldpath.split('.', 1)[0] ext = '.' + oldpath.split('.', 1)[1] + ext = ext.replace(' ', '') else: root = oldpath ext = '' From c095f23555e2842512f2c1d34a271c08764b680d Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 19 Aug 2016 11:09:00 -0400 Subject: [PATCH 1085/1169] Specify crash directory rather than MD5. BFF-991 --- src/certfuzz/tools/linux/minimizer_plot.py | 58 +++++++++++++--------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/certfuzz/tools/linux/minimizer_plot.py b/src/certfuzz/tools/linux/minimizer_plot.py index d12dc1e..3ccf245 100755 --- a/src/certfuzz/tools/linux/minimizer_plot.py +++ b/src/certfuzz/tools/linux/minimizer_plot.py @@ -26,24 +26,32 @@ def parse_options(): - usage = "usage: %prog [options] " + usage = "usage: %prog [options] " parser = OptionParser(usage) - parser.add_option("-d", "--debug", dest="debug", help="Turn on debugging output (overrides --verbose)", action='store_true', default=False) - parser.add_option("-v", "--verbose", dest="verbose", help="Turn on verbose output", action='store_true', default=False) - parser.add_option('', '--dir', dest='dir', help='Specify crasher parent dir') - parser.add_option("-F", "--config", dest="cfgfile", help="read config data from CFGFILE", metavar='CFGFILE') - parser.add_option('', '--ylin', dest='linear_y', help="use linear scale on Y axis (default is logarithmic)", action='store_true', default=False) - parser.add_option('', '--xlog', dest='log_x', help="use log scale on X axis (default is linear)", action='store_true', default=False) - parser.add_option('', '--no-crash-id', dest='include_crash_id', help='suppress inclusion of crash_id in chart title', action='store_false', default=True) - parser.add_option('', '--infile', dest="infile", help="read minimizer log from FILE", metavar='FILE') + parser.add_option("-d", "--debug", dest="debug", + help="Turn on debugging output (overrides --verbose)", action='store_true', default=False) + parser.add_option("-v", "--verbose", dest="verbose", + help="Turn on verbose output", action='store_true', default=False) + parser.add_option( + '', '--dir', dest='dir', help='Specify crasher parent dir') + parser.add_option("-F", "--config", dest="cfgfile", + help="read config data from CFGFILE", metavar='CFGFILE') + parser.add_option('', '--ylin', dest='linear_y', + help="use linear scale on Y axis (default is logarithmic)", action='store_true', default=False) + parser.add_option('', '--xlog', dest='log_x', + help="use log scale on X axis (default is linear)", action='store_true', default=False) + parser.add_option('', '--no-crash-id', dest='include_crash_dir', + help='suppress inclusion of crash_dir in chart title', action='store_false', default=True) + parser.add_option('', '--infile', dest="infile", + help="read minimizer log from FILE", metavar='FILE') options, args = parser.parse_args() if not len(args): parser.print_help() - parser.error("Please specify a crash md5 to plot") + parser.error("Please specify a crash directory to plot") return options, args -def plot(options, crash_id, log): +def plot(options, crash_dir, log): logfile = LogFile(log) # results = logfile.results starts = [] @@ -67,9 +75,9 @@ def plot(options, crash_id, log): plt.xlabel('Iteration') plt.ylabel('Hamming Distance') title = 'Crash Minimization' -# prepend the crash_id unless the user told us not to - if options.include_crash_id: - title = '\n'.join((crash_id, title)) +# prepend the crash_dir unless the user told us not to + if options.include_crash_dir: + title = '\n'.join((crash_dir, title)) plt.title(title) plt.plot(starts, label='start_hd') plt.plot(mins, label='min_found') @@ -82,17 +90,20 @@ def plot(options, crash_id, log): class Line(): + def __init__(self, line): self.line = line.strip() self.value = False self._process() def _process(self): - m = re.match('^start=(\d+)\s+min=(\d+)\s+target_guess=(\d+)\s+curr=(\d+)', self.line) + m = re.match( + '^start=(\d+)\s+min=(\d+)\s+target_guess=(\d+)\s+curr=(\d+)', self.line) if not m: return - (start, minimum, target, current) = (int(x) for x in (m.group(1), m.group(2), m.group(3), m.group(4))) + (start, minimum, target, current) = (int(x) + for x in (m.group(1), m.group(2), m.group(3), m.group(4))) logger.debug('start: %d', start) logger.debug('min: %d', minimum) logger.debug('target: %d', target) @@ -101,6 +112,7 @@ def _process(self): class LogFile(): + def __init__(self, logfile): self.file = logfile self.results = [] @@ -151,15 +163,15 @@ def main(): cfg = load_and_fix_config(cfg_file) _campaign_id = cfg['campaign']['id'] _campaign_id_no_space = re.sub('\s', '_', _campaign_id) - result_dir = os.path.join(cfg['directories']['results_dir'], _campaign_id_no_space, 'crashers') + result_dir = os.path.join( + cfg['directories']['results_dir'], _campaign_id_no_space, 'crashers') logger.info('Reading results from %s', result_dir) log = None - crash_id = None + crash_dir = None if len(args): - crash_id = args.pop(0) - logger.debug('Crash_id=%s', crash_id) - crashdir = os.path.join(result_dir, crash_id) + crash_dir = args.pop(0) + logger.debug('crash_dir=%s', crash_dir) if not os.path.isdir(crashdir): logger.debug('%s is not a dir', crashdir) raise @@ -167,14 +179,14 @@ def main(): logger.debug('Looking for minimizer log in %s', crashdir) log = os.path.join(crashdir, 'minimizer_log.txt') elif options.infile: - crash_id = os.path.basename(options.infile) + crash_dir = os.path.basename(options.infile) log = options.infile if not os.path.exists(log): logger.warning('No minimizer log found at %s', log) raise logger.info('Found log at %s', log) - plot(options, crash_id, log) + plot(options, crash_dir, log) logger.info('All done. Bye.') From 20a968cac5f12a47a69f9d71e2dd69c10c2b933f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 19 Aug 2016 11:29:09 -0400 Subject: [PATCH 1086/1169] Fix callsim.py to factor in exploitability in crasher directories. BFF-992 --- src/certfuzz/fuzztools/similarity_matrix.py | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/certfuzz/fuzztools/similarity_matrix.py b/src/certfuzz/fuzztools/similarity_matrix.py index 419d9e2..9826802 100644 --- a/src/certfuzz/fuzztools/similarity_matrix.py +++ b/src/certfuzz/fuzztools/similarity_matrix.py @@ -20,6 +20,7 @@ class SimilarityMatrix(object): + def __init__(self, dirs): self.dirs = dirs self.pattern = '*.annotated' @@ -35,12 +36,15 @@ def __init__(self, dirs): self.find_files() # get the list of files to analyze if len(self.files) < 2: - raise SimilarityMatrixError('Must have at least 2 files to compare') + raise SimilarityMatrixError( + 'Must have at least 2 files to compare') self.read_coverage() # build a dict of coverage keyed by file - self.measure_doc_count_by_term() # calculate document frequency for each coverage string + # calculate document frequency for each coverage string + self.measure_doc_count_by_term() self.calculate_idf() # calculate the IDF for each term self.calculate_tf_idf() # calculate the TF-IDF vectors keyed by file - self.build_matrix() # Pairwise compare files based on their TF-IDF vectors + # Pairwise compare files based on their TF-IDF vectors + self.build_matrix() def find_files(self): for d in self.dirs: @@ -69,7 +73,8 @@ def calculate_idf(self): self.idf[term] = math.log(numerator / denominator) def calculate_tf_idf(self): - logger.info('Calculating Term Frequency - Inverse Document Frequency scores') + logger.info( + 'Calculating Term Frequency - Inverse Document Frequency scores') for f in self.files: tf_idf = {} cov = self.coverage[f] @@ -93,9 +98,11 @@ def build_matrix(self): def _crash_id_from_path(self, path): parts = path.split('/') - # we assume a directory structure of /crashers// - idx = parts.index('crashers') + 1 - return parts[idx] + # we assume a directory structure of + # /crashers// + exp = parts.index('crashers') + 1 + crashhash = parts.index('crashers') + 2 + return parts[exp] + '/' + parts[crashhash] def print_to(self, target=None): fmt = '%0.' + self.precision + 'f\t%s\t%s' @@ -105,7 +112,8 @@ def print_to(self, target=None): else: output = sys.stdout - sorted_similarity = sorted(self.sim.iteritems(), key=operator.itemgetter(1)) + sorted_similarity = sorted( + self.sim.iteritems(), key=operator.itemgetter(1)) for (k1, k2), v in sorted_similarity: crash_id1 = self._crash_id_from_path(k1) crash_id2 = self._crash_id_from_path(k2) From 5cfb586577365072a9a343f4c6222a56278cb09c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Aug 2016 14:14:05 -0400 Subject: [PATCH 1087/1169] Fix all calls to do_call_indirect and do_call_args_indirect so that the first argument is the offset. BFF-995 --- src/linux/pintool/calltrace.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linux/pintool/calltrace.cpp b/src/linux/pintool/calltrace.cpp index a2d4890..1d1c469 100644 --- a/src/linux/pintool/calltrace.cpp +++ b/src/linux/pintool/calltrace.cpp @@ -261,12 +261,12 @@ VOID Trace(TRACE trace, VOID *v) if( print_args ) { INS_InsertCall(tail, IPOINT_BEFORE, AFUNPTR(do_call_args_indirect), - IARG_BRANCH_TARGET_ADDR, IARG_BRANCH_TAKEN, IARG_G_ARG0_CALLER, IARG_END); + IARG_ADDRINT, offset, IARG_BRANCH_TARGET_ADDR, IARG_BRANCH_TAKEN, IARG_G_ARG0_CALLER, IARG_END); } else { INS_InsertCall(tail, IPOINT_BEFORE, AFUNPTR(do_call_indirect), - IARG_BRANCH_TARGET_ADDR, IARG_BRANCH_TAKEN, IARG_END); + IARG_ADDRINT, offset, IARG_BRANCH_TARGET_ADDR, IARG_BRANCH_TAKEN, IARG_END); } } From 410ba2f01767de0b92bc86613fa88a65e6e31503 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Aug 2016 16:02:35 -0400 Subject: [PATCH 1088/1169] Fix logic of whether pintool needs to be recompiled. --- src/linux/batch.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linux/batch.sh b/src/linux/batch.sh index 7dce152..a7fb5ef 100755 --- a/src/linux/batch.sh +++ b/src/linux/batch.sh @@ -95,16 +95,16 @@ if ( contains "$platform" "Linux" ); then fi fi - cp -au $scriptlocation/pintool ~ - if [ ! -f ~/pintool/calltrace.so ]; then echo Building calltrace pintool... + cp -au $scriptlocation/pintool ~ cd ~/pintool $mypython make.py fi if [ ~/pintool/calltrace.cpp -ot $scriptlocation/pintool/calltrace.cpp ]; then echo Updating calltrace pintool... + cp -au $scriptlocation/pintool ~ cd ~/pintool $mypython make.py fi From b78ab6fbad134f2927b5f08fada49f1d70bae973 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 22 Aug 2016 17:02:41 -0400 Subject: [PATCH 1089/1169] More informative welcome message. --- src/linux/welcome.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/linux/welcome.sh b/src/linux/welcome.sh index f87ab1c..0c64e76 100755 --- a/src/linux/welcome.sh +++ b/src/linux/welcome.sh @@ -25,4 +25,8 @@ program=`egrep -m1 '^ program:' $currentcfg | sed 's/^ program://' | sed ' echo "Target commandline: " `egrep -m1 '^ cmdline' $currentcfg | sed 's/^ cmdline_template://' | sed "s|"'$PROGRAM'"|$program|"` echo -e "Output directory: " `egrep -m1 '^ results_dir' $currentcfg | sed 's/^ results_dir://' | sed 's/\\r//'` "\n\n" -echo -e "Run ./batch.sh to begin fuzzing.\n" +if [[ "$platform" =~ "Darwin" ]]; then + echo -e "Run ./batch.sh to begin fuzzing.\n" +else + echo -e "BFF should start fuzzing automatically via ~/bff/batch.sh\n" +fi From cc7b970abd6f657eb915657e0ca9d75edf9c2904 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 23 Aug 2016 10:50:19 -0400 Subject: [PATCH 1090/1169] When calling update_crash_details on a testcase object, update the cmdlist / cmdargs to be executed, based on the current fuzzedfile. BFF-996 --- src/certfuzz/testcase/testcase_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index dfd017e..21ba55e 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -10,6 +10,7 @@ from certfuzz.file_handlers.basicfile import BasicFile from certfuzz.fuzztools import filetools, hamming from certfuzz.fuzztools.filetools import check_zip_file, mkdir_p +from certfuzz.fuzztools.command_line_templating import get_command_args_list from pprint import pformat @@ -158,6 +159,11 @@ def set_debugger_template(self, *args): pass def update_crash_details(self): + # We might be updating crash details because we have a new fuzzedfile + # (with a different path) + self.cmdlist = get_command_args_list( + self.cmd_template, infile=self.fuzzedfile.path)[1] + self.cmdargs = self.cmdlist[1:] self.tempdir = tempfile.mkdtemp( prefix=self._tmp_pfx, suffix=self._tmp_sfx, dir=self.workdir_base) self.copy_files_to_temp() From f036ad1cfc13beffdc93345c1be4fc6b1423f865 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 23 Aug 2016 12:39:17 -0400 Subject: [PATCH 1091/1169] Include current instruction in "bt only" backtrace to allow drillresults.py to parse outputs. BFF-997 --- src/certfuzz/debuggers/templates/gdb_bt_only_template.txt | 2 ++ .../debuggers/templates/gdb_noproc_bt_only_template.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt b/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt index 7f540b0..bb4f214 100644 --- a/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt @@ -8,4 +8,6 @@ bt full 512 info registers echo si_addr: print $_siginfo._sifields._sigfault.si_addr +echo \n +x/i $pc quit \ No newline at end of file diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt index 8ca8d24..7ebcc9d 100644 --- a/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt @@ -7,4 +7,6 @@ bt full 512 info registers echo si_addr: print $_siginfo._sifields._sigfault.si_addr +echo \n +x/i $pc quit \ No newline at end of file From 3d5b5b4d69c9cb58b15c77e0124736e81d4d924c Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 23 Aug 2016 15:27:29 -0400 Subject: [PATCH 1092/1169] Silently reset frame back to 0 in case we encounter gdb bug when unrolling stack. BFF-998 --- src/certfuzz/debuggers/templates/gdb_bt_only_template.txt | 1 + .../debuggers/templates/gdb_complete_nofunction_template.txt | 1 + src/certfuzz/debuggers/templates/gdb_complete_template.txt | 1 + .../templates/gdb_noctt_complete_nofunction_template.txt | 1 + src/certfuzz/debuggers/templates/gdb_noctt_complete_template.txt | 1 + src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt | 1 + .../templates/gdb_noproc_complete_nofunction_template.txt | 1 + .../debuggers/templates/gdb_noproc_complete_template.txt | 1 + .../templates/gdb_noproc_noctt_complete_nofunction_template.txt | 1 + .../debuggers/templates/gdb_noproc_noctt_complete_template.txt | 1 + 10 files changed, 10 insertions(+) diff --git a/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt b/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt index bb4f214..6c40b8a 100644 --- a/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_bt_only_template.txt @@ -5,6 +5,7 @@ file $PROGRAM run $CMD_ARGS info proc map bt full 512 +down-silently 512 info registers echo si_addr: print $_siginfo._sifields._sigfault.si_addr diff --git a/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt index 4e2cef1..53f226c 100644 --- a/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_complete_nofunction_template.txt @@ -19,6 +19,7 @@ echo si_addr: print $_siginfo._sifields._sigfault.si_addr echo \n bt full 512 +down-silently 512 echo \n info registers echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_complete_template.txt index 6abfe42..b946e7f 100644 --- a/src/certfuzz/debuggers/templates/gdb_complete_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_complete_template.txt @@ -19,6 +19,7 @@ echo si_addr: print $_siginfo._sifields._sigfault.si_addr echo \n bt full 512 +down-silently 512 echo \n info registers echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_noctt_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_noctt_complete_nofunction_template.txt index e710432..9ee5aec 100644 --- a/src/certfuzz/debuggers/templates/gdb_noctt_complete_nofunction_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noctt_complete_nofunction_template.txt @@ -15,6 +15,7 @@ echo si_addr: print $_siginfo._sifields._sigfault.si_addr echo \n bt full 512 +down-silently 512 echo \n info registers echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_noctt_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_noctt_complete_template.txt index ac620ed..e93ee62 100644 --- a/src/certfuzz/debuggers/templates/gdb_noctt_complete_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noctt_complete_template.txt @@ -15,6 +15,7 @@ echo si_addr: print $_siginfo._sifields._sigfault.si_addr echo \n bt full 512 +down-silently 512 echo \n info registers echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt index 7ebcc9d..7dbc457 100644 --- a/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noproc_bt_only_template.txt @@ -4,6 +4,7 @@ set logging on file $PROGRAM run $CMD_ARGS bt full 512 +down-silently 512 info registers echo si_addr: print $_siginfo._sifields._sigfault.si_addr diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_complete_nofunction_template.txt index da139e2..80093bd 100644 --- a/src/certfuzz/debuggers/templates/gdb_noproc_complete_nofunction_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noproc_complete_nofunction_template.txt @@ -8,6 +8,7 @@ echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n bt full 512 +down-silently 512 echo \n info registers echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_complete_template.txt index 587fc5b..4d9467e 100644 --- a/src/certfuzz/debuggers/templates/gdb_noproc_complete_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noproc_complete_template.txt @@ -8,6 +8,7 @@ echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n bt full 512 +down-silently 512 echo \n info registers echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt index da139e2..80093bd 100644 --- a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_nofunction_template.txt @@ -8,6 +8,7 @@ echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n bt full 512 +down-silently 512 echo \n info registers echo \n diff --git a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt index 587fc5b..4d9467e 100644 --- a/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt +++ b/src/certfuzz/debuggers/templates/gdb_noproc_noctt_complete_template.txt @@ -8,6 +8,7 @@ echo Found_with_CERT_BFF_2.8\n echo Running: $PROGRAM $CMD_ARGS echo \n bt full 512 +down-silently 512 echo \n info registers echo \n From 0889a8ec34ebba4f1a15ad99758d443b30d9c286 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 24 Aug 2016 16:45:52 -0400 Subject: [PATCH 1093/1169] Just display fuzzed file path, not the whole object. --- src/certfuzz/tools/linux/minimize.py | 2 +- src/certfuzz/tools/linux/repro.py | 2 +- src/certfuzz/tools/windows/minimize.py | 2 +- src/certfuzz/tools/windows/repro.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/certfuzz/tools/linux/minimize.py b/src/certfuzz/tools/linux/minimize.py index 0183c0f..126a9a9 100755 --- a/src/certfuzz/tools/linux/minimize.py +++ b/src/certfuzz/tools/linux/minimize.py @@ -128,7 +128,7 @@ def main(): if len(args) and os.path.exists(args[0]): fuzzed_file = BasicFile(args[0]) - logger.info('Fuzzed file is %s', fuzzed_file) + logger.info('Fuzzed file is: %s', fuzzed_file.path) else: parser.error('fuzzedfile must be specified') diff --git a/src/certfuzz/tools/linux/repro.py b/src/certfuzz/tools/linux/repro.py index 70423a3..0bcea67 100755 --- a/src/certfuzz/tools/linux/repro.py +++ b/src/certfuzz/tools/linux/repro.py @@ -84,7 +84,7 @@ def main(): if len(args) and os.path.exists(args[0]): fullpath_fuzzed_file = os.path.abspath(args[0]) fuzzed_file = BasicFile(fullpath_fuzzed_file) - logger.info('Fuzzed file is %s', fuzzed_file) + logger.info('Fuzzed file is: %s', fuzzed_file.path) else: parser.error('fuzzedfile must be specified') diff --git a/src/certfuzz/tools/windows/minimize.py b/src/certfuzz/tools/windows/minimize.py index 023cbdd..f81a6d6 100644 --- a/src/certfuzz/tools/windows/minimize.py +++ b/src/certfuzz/tools/windows/minimize.py @@ -145,7 +145,7 @@ def main(): if len(args) and os.path.exists(args[0]): fuzzed_file = BasicFile(args[0]) - logger.info('Fuzzed file is %s', fuzzed_file) + logger.info('Fuzzed file is: %s', fuzzed_file.path) else: parser.error('fuzzedfile must be specified') diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index d674d95..15af8e4 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -78,7 +78,7 @@ def main(): if len(args) and os.path.exists(args[0]): fullpath_fuzzed_file = os.path.abspath(args[0]) fuzzed_file = BasicFile(fullpath_fuzzed_file) - logger.info('Fuzzed file is %s', fuzzed_file) + logger.info('Fuzzed file is: %s', fuzzed_file.path) else: parser.error('fuzzedfile must be specified') From a76b6674566f115a4ac04c4d64f53d47167b1d34 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 24 Aug 2016 16:47:59 -0400 Subject: [PATCH 1094/1169] Reorder config options. --- src/linux/configs/bff.yaml | 45 +++++++++++++-------------- src/windows/configs/examples/bff.yaml | 36 ++++++++++----------- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index 7bed02c..e6c3e62 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -35,6 +35,28 @@ target: cmdline_template: $PROGRAM $SEEDFILE /dev/null +############################################################################## +# LOCATIONS FOR FUZZ RUN FILES: +# +# seedfile_dir: +# Location of seed files (relative to bff.py) +# +# working_dir: +# Temporary directory used by BFF. Use a ramdisk to reduce disk activity +# +# results_dir: +# Location of fuzzing results (relative to bff.py) +# +# If results are stored in a shared location, +# this directory needs to be unique for each fuzzing machine +# +############################################################################## + +directories: + seedfile_dir: seedfiles/examples + working_dir: ~/fuzzing + results_dir: results + ############################################################################## # RUNNER OPTIONS # @@ -60,29 +82,6 @@ debugger: backtracelevels: 5 -############################################################################## -# LOCATIONS FOR FUZZ RUN FILES: -# -# seedfile_dir: -# Location of seed files (relative to bff.py) -# -# working_dir: -# Temporary directory used by BFF. Use a ramdisk to reduce disk activity -# -# results_dir: -# Location of fuzzing results (relative to bff.py) -# -# If results are stored in a shared location, -# this directory needs to be unique for each fuzzing machine -# -############################################################################## - -directories: - seedfile_dir: seedfiles/examples - working_dir: ~/fuzzing - results_dir: results - - ############################################################################## # FUZZ CAMPAIGN OPTIONS # diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 2d194f3..2a27478 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -57,6 +57,24 @@ target: # cmdline_template: $PROGRAM -in $SEEDFILE -out "c:\some path\to file" +############################################################################## +# Directories used by BFF (all relative to bff.py) +# +# seedfile_dir: +# Location of seed files +# +# working_dir: +# Temporary directory used by BFF. Use a ramdisk to reduce disk activity +# +# results_dir: +# Location of fuzzing results +############################################################################## +directories: + seedfile_dir: seedfiles\examples + working_dir: fuzzdir + results_dir: results + + ############################################################################## # Runner options # @@ -92,24 +110,6 @@ debugger: max_handled_exceptions: 6 -############################################################################## -# Directories used by BFF (all relative to bff.py) -# -# seedfile_dir: -# Location of seed files -# -# working_dir: -# Temporary directory used by BFF. Use a ramdisk to reduce disk activity -# -# results_dir: -# Location of fuzzing results -############################################################################## -directories: - seedfile_dir: seedfiles\examples - working_dir: fuzzdir - results_dir: results - - ############################################################################## # Fuzz run options # From 7a2250da48c2138e6a0765057723a3f529b050ab Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 25 Aug 2016 11:09:02 -0400 Subject: [PATCH 1095/1169] If a rank-10 crash happens on the first exception, make it a 5 because it's more awesome. BFF-491 --- .../analyzers/drillresults/testcasebundle_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_base.py b/src/certfuzz/analyzers/drillresults/testcasebundle_base.py index ccede43..2a50232 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_base.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_base.py @@ -321,7 +321,12 @@ def _score_interesting(self): scores.append(20) else: # Faulting address has high entropy. Most exploitable. - scores.append(10) + if 0 in self.details['exceptions']: + # This is the first-seen exception. Give it a better + # score + scores.append(5) + else: + scores.append(10) else: # The faulting address pattern is not in the fuzzed file scores.append(40) @@ -357,6 +362,7 @@ def _score_less_interesting(self): if module == 'unloaded' and not self.ignore_jit: scores.append(20) elif module.lower() == 'ntdll.dll' or 'msvcr' in module.lower(): + # TODO: This is all very 32-bit Windows XP specific. Be smarter. # likely heap corruption. Exploitable, but difficult scores.append(45) elif '0x00120000' in efa or '0x00130000' in efa or '0x00140000' in efa: From d2ea09022cde0763b8984c9bb9aff018b0f8fd2a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 25 Aug 2016 12:14:46 -0400 Subject: [PATCH 1096/1169] Fix unit test, now that 5 is the most exploitable. --- .../analyzers/drillresults/test_testcasebundle_base.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_base.py b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_base.py index 3beabde..c2871f4 100644 --- a/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_base.py +++ b/src/test_certfuzz/analyzers/drillresults/test_testcasebundle_base.py @@ -44,6 +44,7 @@ def get_return_addr(self): class Test(unittest.TestCase): + def setUp(self): self.tmpdir = tempfile.mkdtemp() self.tcb = self._minimal_tcb() @@ -52,7 +53,8 @@ def tearDown(self): shutil.rmtree(self.tmpdir) def test_tcb_is_metaclass(self): - self.assertTrue(hasattr(testcasebundle_base.TestCaseBundle, '__metaclass__')) + self.assertTrue( + hasattr(testcasebundle_base.TestCaseBundle, '__metaclass__')) # should raise a type error if you try to instantiate it self.assertRaises(TypeError, testcasebundle_base.TestCaseBundle) @@ -178,7 +180,7 @@ def test_score_interesting(self): xc['pcmodule'] = 'something' xc['EIF'] = True scores = self.tcb._score_interesting() - self.assertTrue(10 in scores) + self.assertTrue(5 in scores) xc['efa'] = '0x0000' scores = self.tcb._score_interesting() @@ -224,7 +226,6 @@ def test_score_less_interesting(self): self.assertTrue(60 in scores) - if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 5c05e1350ffb1428c8e822daaae094623cdbd7a2 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 30 Aug 2016 09:25:58 -0400 Subject: [PATCH 1097/1169] Actually, we can minimize total stack corruption. BFF-999 --- src/certfuzz/minimizer/minimizer_base.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index be7b066..66ad3a4 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -216,10 +216,6 @@ def __enter__(self): msg = 'Unable to minimize: Already minimized' self.logger.info(msg) self._raise(msg) - if self.testcase.debugger_missed_stack_corruption: - msg = 'Unable to minimize: Stack corruption testcase, which the debugger missed.' - self.logger.info(msg) - self._raise(msg) # start the timer self.start_time = time.time() return self From 40489d8c306d6b9ddabbfc10e9eb4f85df344b38 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 30 Aug 2016 12:58:25 -0400 Subject: [PATCH 1098/1169] recycle_crashers even if minimization is disabled. BFF-1000 --- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 27 ++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index 7df7dea..bf9e4f5 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -24,7 +24,7 @@ class TestCasePipelineBase(object): Implements a pipeline for filtering and processing a testcase ''' __metaclass__ = abc.ABCMeta - pipes = ['verify', 'minimize', 'analyze', 'report'] + pipes = ['verify', 'minimize', 'recycle', 'analyze', 'report'] def __init__(self, testcases=None, uniq_func=None, cfg=None, options=None, outdir=None, workdirbase=None, sf_set=None): @@ -225,16 +225,23 @@ def _minimize(self, testcase): testcase.calculate_hamming_distances() def _post_minimize(self, testcase): + pass + + @coroutine + def recycle(self, *targets): if self.cfg['runoptions']['recycle_crashers']: - logger.debug('Recycling crash as seedfile') - iterstring = testcase.fuzzedfile.basename.split( - '-')[1].split('.')[0] - crasherseedname = 'sf_' + testcase.seedfile.md5 + \ - '-' + iterstring + testcase.seedfile.ext - crasherseed_path = os.path.join( - self.cfg['directories']['seedfile_dir'], crasherseedname) - filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) - self.sf_set.add_file(crasherseed_path) + while True: + testcase = (yield) + + logger.debug('Recycling crash as seedfile') + iterstring = testcase.fuzzedfile.basename.split( + '-')[1].split('.')[0] + crasherseedname = 'sf_' + testcase.seedfile.md5 + \ + '-' + iterstring + testcase.seedfile.ext + crasherseed_path = os.path.join( + self.cfg['directories']['seedfile_dir'], crasherseedname) + filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) + self.sf_set.add_file(crasherseed_path) def _pre_analyze(self, testcase): pass From 066bb31912476dd2f77745ceb1f6f25430088658 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 30 Aug 2016 13:53:15 -0400 Subject: [PATCH 1099/1169] Send testcase through rest of pipleline after recycling. BFF-1000 --- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index bf9e4f5..4e48cf1 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -243,6 +243,9 @@ def recycle(self, *targets): filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) self.sf_set.add_file(crasherseed_path) + for target in targets: + target.send(testcase) + def _pre_analyze(self, testcase): pass From 4a76c99a45a4af0af7945971ef9e7f048757969b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 30 Aug 2016 14:37:52 -0400 Subject: [PATCH 1100/1169] We need to work when recycle_crashers is set to False too, you know. BFF-1000 --- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index 4e48cf1..cdb98f9 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -229,10 +229,10 @@ def _post_minimize(self, testcase): @coroutine def recycle(self, *targets): - if self.cfg['runoptions']['recycle_crashers']: - while True: - testcase = (yield) + while True: + testcase = (yield) + if self.cfg['runoptions']['recycle_crashers']: logger.debug('Recycling crash as seedfile') iterstring = testcase.fuzzedfile.basename.split( '-')[1].split('.')[0] @@ -240,11 +240,12 @@ def recycle(self, *targets): '-' + iterstring + testcase.seedfile.ext crasherseed_path = os.path.join( self.cfg['directories']['seedfile_dir'], crasherseedname) - filetools.copy_file(testcase.fuzzedfile.path, crasherseed_path) + filetools.copy_file( + testcase.fuzzedfile.path, crasherseed_path) self.sf_set.add_file(crasherseed_path) - for target in targets: - target.send(testcase) + for target in targets: + target.send(testcase) def _pre_analyze(self, testcase): pass From 636303effe33bf92b79a58fb23bcb8131238e0a1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 5 Aug 2016 09:48:29 -0400 Subject: [PATCH 1101/1169] reimplement reboot recovery Squashed commits: [06a0a3d] add unit tests for rejected cached data situations [4b26bc3] add reboot recovery back in [37de8c1] save only the data we need to reconstitute the sendfile set [e3e59f8] partial commit [b5f8271] remove override pickle-related methods [0e82837] remove dead code [36a2511] restore call to _save_state() --- src/certfuzz/campaign/campaign_base.py | 163 +++++++++++++----- src/certfuzz/campaign/campaign_linux.py | 24 --- src/certfuzz/fuzztools/object_caching.py | 12 +- .../multiarmed_bandit_base.py | 7 +- .../campaign/test_campaign_base.py | 162 ++++++++++++++++- .../test_multiarmed_bandit_base.py | 17 +- 6 files changed, 302 insertions(+), 83 deletions(-) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index ab01c07..d823ccd 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -11,7 +11,6 @@ import shutil import tempfile import traceback -import cPickle as pickle import signal from certfuzz.campaign.errors import CampaignError @@ -25,6 +24,10 @@ import gc from certfuzz.config.simple_loader import load_and_fix_config from certfuzz.helpers.misc import import_module_by_name +from certfuzz.fuzztools.object_caching import dump_obj_to_file,\ + load_obj_from_file +import json +from certfuzz.fuzztools.filetools import write_file logger = logging.getLogger(__name__) @@ -120,7 +123,7 @@ def __init__(self, config_file, result_dir=None, debug=False): self.sf_set_out = os.path.join(self.outdir, 'seedfiles') if not self.cached_state_file: - cachefile = 'campaign_%s.pkl' % _campaign_id_with_underscores + cachefile = 'campaign_%s.json' % _campaign_id_with_underscores self.cached_state_file = os.path.join( self.work_dir_base, cachefile) if not self.seed_interval: @@ -165,7 +168,6 @@ def __enter__(self): if _result is not None: self = _result - self._read_state() self._check_prog() self._setup_workdir() self._set_fuzzer() @@ -173,6 +175,7 @@ def __enter__(self): self._check_runner() self._setup_output() self._create_seedfile_set() + self._read_state() _result = self._post_enter() if _result is not None: @@ -327,50 +330,133 @@ def _create_seedfile_set(self): outputpath=self.sf_set_out) as sfset: self.seedfile_set = sfset - @abc.abstractmethod - def __getstate__(self): - raise NotImplementedError - - @abc.abstractmethod - def __setstate__(self): - raise NotImplementedError + def _read_cached_data(self, cachefile): + try: + with open(cachefile, 'rb') as fp: + cached_data = json.load(fp) + except (IOError, ValueError) as e: + logger.info( + 'No cached campaign data found, will proceed as new campaign: %s', e) + return + return cached_data - def _read_state(self, cache_file=None): - if not cache_file: - cache_file = self.cached_state_file + def _restore_seedfile_scores(self, sf_scores): + for sf_md5, sf_score in sf_scores.iteritems(): + # is this seedfile still around? + try: + arm_to_update = self.seedfile_set.arms[sf_md5] + except KeyError: + # if not, just skip it + logger.warning( + 'Skipping seedfile score recovery for %s: maybe seedfile was removed?', sf_md5) + continue - if not os.path.exists(cache_file): - logger.info('No cached campaign found, using new campaign') - return + cached_successes = sf_score['successes'] + cached_trials = sf_score['trials'] - try: - with open(cache_file, 'rb') as fp: - campaign = pickle.load(fp) - except Exception, e: - logger.warning( - 'Unable to read %s, will use new campaign instead: %s', cache_file, e) - return + arm_to_update.update( + successes=cached_successes, trials=cached_trials) - if campaign: + def _restore_rangefinder_scores(self, rf_scores): + for sf_md5, rangelist in rf_scores.iteritems(): + # is this seedfile still around? try: - if self.config['config_timestamp'] != campaign.__dict__['config_timestamp']: - logger.warning( - 'Config file modified. Discarding cached campaign') - else: - self.__dict__.update(campaign.__dict__) - logger.info('Reloaded campaign from %s', cache_file) + sf_to_update = self.seedfile_set.things[sf_md5] except KeyError: logger.warning( - 'No config date detected. Discarding cached campaign') - else: + 'Skipping rangefinder score recovery for %s: maybe seedfile was removed?', sf_md5) + continue + + # if you got here, you have a seedfile to update + # we're going to need its rangefinder + rangefinder = sf_to_update.rangefinder + + # construct a rangefinder key lookup table + rf_lookup = {} + for key, item in rangefinder.things.iteritems(): + lookup_key = (item.min, item.max) + rf_lookup[lookup_key] = key + + for r in rangelist: + # is this range still correct? + cached_rmin = r['range_key']['range_min'] + cached_rmax = r['range_key']['range_max'] + lkey = (cached_rmin, cached_rmax) + try: + rk = rf_lookup[lkey] + except KeyError: + logger.warning( + 'Skipping rangefinder score recovery for %s range %s: range not found', sf_md5, lkey) + continue + + # if you got here you have a matching range to update + # fyi: .arms and .things have the same keys + arm_to_update = rangefinder.arms[rk] + cached_successes = r['range_score']['successes'] + cached_trials = r['range_score']['trials'] + + arm_to_update.update( + successes=cached_successes, trials=cached_trials) + + def _restore_campaign_from_cache(self, cached_data): + self.current_seed = cached_data['current_seed'] + self._restore_seedfile_scores(cached_data['seedfile_scores']) + self._restore_rangefinder_scores(cached_data['rangefinder_scores']) + logger.info('Restoring cached campaign data done') + + def _read_state(self, cachefile=None): + if not cachefile: + cachefile = self.cached_state_file + + cached_data = self._read_cached_data(cachefile) + if cached_data is None: + return + + # check the timestamp + # if the cache is older than the current config file, we should + # ignore the cached data and just start fresh + cached_cfg_ts = cached_data['config_timestamp'] + if self.config['config_timestamp'] != cached_cfg_ts: logger.warning( - 'Unable to reload campaign from %s, will use new campaign instead', cache_file) + 'Config file modified since campaign data cache was created. Discarding cached campaign data. Will proceed as new campaign.') + return 2 + + # if you got here, the cached file is ok to use + + self._restore_campaign_from_cache(cached_data) + + def _get_state_as_dict(self): + state = {'current_seed': self.current_seed, + 'config_timestamp': self.config['config_timestamp'], + 'seedfile_scores': self.seedfile_set.arms_as_dict(), + 'rangefinder_scores': None + } + + # add rangefinder scores from each seedfile + d = {} + for k, sf in self.seedfile_set.things.iteritems(): + d[k] = [] + + for rk, rf in sf.rangefinder.things.iteritems(): + arm = sf.rangefinder.arms[rk] + rkey = {'range_min': rf.min, 'range_max': rf.max} + rdata = {'range_key': rkey, + 'range_score': dict(arm.__dict__)} + d[k].append(rdata) + + state['rangefinder_scores'] = d + + return state + + def _get_state_as_json(self): + state = self._get_state_as_dict() + return json.dumps(state, indent=4, sort_keys=True) def _save_state(self, cachefile=None): if not cachefile: cachefile = self.cached_state_file - # FIXME - # dump_obj_to_file(cachefile, self) + state_as_json = self._get_state_as_json() + write_file(state_as_json, cachefile) def _testcase_is_unique(self, testcase_id, exploitability='UNKNOWN'): ''' @@ -405,10 +491,9 @@ def _do_interval(self): sf = self.seedfile_set.next_item() logger.info('Selected seedfile: %s', sf.basename) -# TODO: restore this -# if self.current_seed % self.status_interval == 0: -# # cache our current state -# self._save_state() + if (self.current_seed > 0) and (self.current_seed % self.status_interval == 0): + # cache our current state + self._save_state() r = sf.rangefinder.next_item() diff --git a/src/certfuzz/campaign/campaign_linux.py b/src/certfuzz/campaign/campaign_linux.py index fa2b20b..10e5469 100644 --- a/src/certfuzz/campaign/campaign_linux.py +++ b/src/certfuzz/campaign/campaign_linux.py @@ -182,30 +182,6 @@ def _set_debugger(self): ''' pass - def __setstate__(self): - ''' - Overrides parent class - ''' - pass - - def _read_state(self): - ''' - Overrides parent class - ''' - pass - - def __getstate__(self): - ''' - Overrides parent class - ''' - pass - - def _save_state(self): - ''' - Overrides parent class - ''' - pass - def _do_iteration(self, seedfile, range_obj, seednum): # Prevent watchdog from rebooting VM. # If /tmp/fuzzing exists and is stale, the machine will reboot diff --git a/src/certfuzz/fuzztools/object_caching.py b/src/certfuzz/fuzztools/object_caching.py index df027a2..63f5a7c 100644 --- a/src/certfuzz/fuzztools/object_caching.py +++ b/src/certfuzz/fuzztools/object_caching.py @@ -16,7 +16,8 @@ def dump_obj_to_file(cachefile, obj): pickle.dump(obj, fd) logger.debug('Wrote %s to %s', obj.__class__.__name__, cachefile) except (IOError, TypeError) as e: - logger.warning('Unable to write %s to cache file %s: %s', obj.__class__.__name__, cachefile, e) + logger.warning( + 'Unable to write %s to cache file %s: %s', obj.__class__.__name__, cachefile, e) def load_obj_from_file(cachefile): @@ -28,12 +29,3 @@ def load_obj_from_file(cachefile): except StandardError, e: logger.debug("Unable to read from %s: %s", cachefile, e) return obj - - -def cache_state(key_prefix, key_suffix, obj, cachefile): - dump_obj_to_file(cachefile, obj) - - -def get_cached_state(key_suffix, key_prefix, cachefile): - obj = load_obj_from_file(cachefile) - return obj diff --git a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py index 08e68bf..31786d8 100644 --- a/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py +++ b/src/certfuzz/scoring/multiarmed_bandit/multiarmed_bandit_base.py @@ -21,6 +21,9 @@ def __init__(self): self.things = {} self.arms = {} + def arms_as_dict(self): + return {k: dict(arm.__dict__) for k, arm in self.arms.iteritems()} + def add_item(self, key=None, obj=None): if key is None: raise MultiArmedBanditError('unspecified key for arm') @@ -38,7 +41,6 @@ def add_item(self, key=None, obj=None): # but don't trust those averages too strongly new_arm.doubt() - # add the new arm to the set self.arms[key] = new_arm @@ -54,7 +56,8 @@ def del_item(self, key=None): pass def record_result(self, key, successes=0, trials=0): - logger.debug('Recording result: key=%s successes=%d trials=%d', key, successes, trials) + logger.debug( + 'Recording result: key=%s successes=%d trials=%d', key, successes, trials) arm = self.arms[key] arm.update(successes, trials) diff --git a/src/test_certfuzz/campaign/test_campaign_base.py b/src/test_certfuzz/campaign/test_campaign_base.py index c54c36e..ef9009b 100644 --- a/src/test_certfuzz/campaign/test_campaign_base.py +++ b/src/test_certfuzz/campaign/test_campaign_base.py @@ -14,6 +14,8 @@ from certfuzz.campaign.errors import CampaignError from test_certfuzz.mocks import MockCfg import yaml +from certfuzz.file_handlers.seedfile_set import SeedfileSet +import json class UnimplementedCampaign(CampaignBase): @@ -21,10 +23,11 @@ class UnimplementedCampaign(CampaignBase): class ImplementedCampaign(CampaignBase): + def __getstate__(self): pass - def __setstate__(self): + def __setstate__(self): pass def _do_interval(self): @@ -51,15 +54,18 @@ def _set_fuzzer(self): def _set_runner(self): pass + class Test(unittest.TestCase): + def setUp(self): self.tmpdir = mkdtemp() - fd, cfgfile = tempfile.mkstemp(prefix='config_', suffix=".yaml", dir=self.tmpdir) + fd, cfgfile = tempfile.mkstemp( + prefix='config_', suffix=".yaml", dir=self.tmpdir) os.close(fd) - + cfg = MockCfg(templated=False) - with open(cfgfile,'wb') as f: - yaml.dump(cfg,f) + with open(cfgfile, 'wb') as f: + yaml.dump(cfg, f) self.campaign = ImplementedCampaign(cfgfile) def tearDown(self): @@ -110,7 +116,8 @@ def test_setup_workdir(self): self.campaign._setup_workdir() self.assertTrue(os.path.isdir(self.campaign.work_dir_base)) self.assertTrue(os.path.isdir(self.campaign.working_dir)) - self.assertTrue(self.campaign.seed_dir_local.startswith(self.campaign.working_dir)) + self.assertTrue( + self.campaign.seed_dir_local.startswith(self.campaign.working_dir)) self.assertTrue(self.campaign.seed_dir_local.endswith('seedfiles')) def test_cleanup_workdir(self): @@ -138,6 +145,149 @@ def test_keep_going(self): for _x in range(100): self.assertTrue(self.campaign._keep_going()) + def _check_data_structure(self, x): + for k in ['current_seed', 'config_timestamp', 'seedfile_scores', 'rangefinder_scores']: + self.assertTrue(k in x) + + self.assertEqual(x['current_seed'], self.campaign.current_seed) + self.assertEqual( + x['config_timestamp'], self.campaign.config['config_timestamp']) + + self.assertEqual( + len(x['rangefinder_scores']), len(self.campaign.seedfile_set.arms)) + + self.assertEqual( + len(x['seedfile_scores']), len(self.campaign.seedfile_set.arms)) + + # verify the data structures + for score in x['seedfile_scores'].values(): + for k in ['successes', 'trials', 'probability']: + self.assertTrue(k in score) + + for items in x['rangefinder_scores'].values(): + for item in items: + for k in ['range_key', 'range_score']: + self.assertTrue(k in item) + score = item['range_score'] + for k in ['successes', 'trials', 'probability']: + self.assertTrue(k in score) + + def _populate_sf_set(self): + self.campaign.seedfile_set = SeedfileSet() + + files = [] + for x in xrange(10): + _fd, _fname = tempfile.mkstemp(prefix='seedfile_', dir=self.tmpdir) + os.write(_fd, str(x)) + os.close(_fd) + files.append(_fname) + + self.campaign.seedfile_set.add_file(*files) + + def test_get_state_as_dict(self): + self._populate_sf_set() + x = self.campaign._get_state_as_dict() + self._check_data_structure(x) + + def test_get_state_as_json(self): + self._populate_sf_set() + j = self.campaign._get_state_as_json() + x = json.loads(j) + self._check_data_structure(x) + + def test_save_state(self): + fd, fpath = tempfile.mkstemp( + suffix=".json", prefix="campaign_state_", dir=self.tmpdir) + os.close(fd) + os.remove(fpath) + self.assertFalse(os.path.exists(fpath)) + + self._populate_sf_set() + self.campaign._save_state(fpath) + self.assertTrue(os.path.exists(fpath)) + self.assertTrue(os.path.getsize(fpath) > 0) + + with open(fpath, 'rb') as f: + x = json.load(f) + + self._check_data_structure(x) + + for k, v in self.campaign._get_state_as_dict().iteritems(): + self.assertTrue(k in x) + self.assertEqual(x[k], v) + + def test_read_state(self): + fd, fpath = tempfile.mkstemp( + suffix=".json", prefix="campaign_state_", dir=self.tmpdir) + os.close(fd) + self._populate_sf_set() + d = self.campaign._get_state_as_dict() + + d['current_seed'] = 1000 + for score in d['seedfile_scores'].itervalues(): + score['successes'] = 10 + score['trials'] = 100 + + for sf in d['rangefinder_scores'].values(): + for r in sf: + r['range_score']['successes'] = 5 + r['range_score']['trials'] = 50 + + with open(fpath, 'wb') as f: + json.dump(d, f) + + self.assertNotEqual(self.campaign.current_seed, d['current_seed']) + successes = [x['successes'] + for x in self.campaign.seedfile_set.arms_as_dict().values()] + for _score in successes: + self.assertEqual(0, _score) + trials = [x['trials'] + for x in self.campaign.seedfile_set.arms_as_dict().values()] + for _score in trials: + self.assertEqual(0, _score) + + for sf in self.campaign.seedfile_set.things.values(): + for r in sf.rangefinder.arms.values(): + self.assertEqual(0, r.successes) + self.assertEqual(0, r.trials) + + self.campaign._read_state(fpath) + + self.assertEqual(self.campaign.current_seed, d['current_seed']) + successes = [x['successes'] + for x in self.campaign.seedfile_set.arms_as_dict().values()] + for _score in successes: + self.assertEqual(10, _score) + trials = [x['trials'] + for x in self.campaign.seedfile_set.arms_as_dict().values()] + for _score in trials: + self.assertEqual(100, _score) + + for sf in self.campaign.seedfile_set.things.values(): + for r in sf.rangefinder.arms.values(): + self.assertEqual(5, r.successes) + self.assertEqual(50, r.trials) + + def test_reject_cached_data_if_newer_config(self): + fd, fpath = tempfile.mkstemp( + suffix=".json", prefix="campaign_state_", dir=self.tmpdir) + os.close(fd) + self._populate_sf_set() + d = self.campaign._get_state_as_dict() + d['config_timestamp'] = d['config_timestamp'] - 1000.0 + with open(fpath, 'wb') as f: + json.dump(d, f) + + self.assertEqual(2, self.campaign._read_state(fpath)) + + def test_reject_cached_data_if_no_file(self): + fd, fpath = tempfile.mkstemp( + suffix=".json", prefix="campaign_state_", dir=self.tmpdir) + os.close(fd) + self.assertEqual(None, self.campaign._read_cached_data(fpath)) + os.remove(fpath) + self.assertEqual(None, self.campaign._read_cached_data(fpath)) + if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py b/src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py index 1ffdccc..f3b3fd2 100644 --- a/src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py +++ b/src/test_certfuzz/scoring/multiarmed_bandit/test_multiarmed_bandit_base.py @@ -21,8 +21,10 @@ def tearDown(self): def test_add(self): self.assertRaises(MultiArmedBanditError, self.mab.add_item) - self.assertRaises(MultiArmedBanditError, self.mab.add_item, key=None, obj='obj') - self.assertRaises(MultiArmedBanditError, self.mab.add_item, key='key', obj=None) + self.assertRaises( + MultiArmedBanditError, self.mab.add_item, key=None, obj='obj') + self.assertRaises( + MultiArmedBanditError, self.mab.add_item, key='key', obj=None) self.assertEqual(len(self.keys), len(self.mab.things)) self.assertEqual(len(self.keys), len(self.mab.arms)) @@ -92,6 +94,17 @@ def test_next(self): # empty set raises StopIteration self.assertRaises(StopIteration, self.mab.next) + def test_arms_as_dict(self): + d = self.mab.arms_as_dict() + + self.assertTrue(isinstance(d, dict)) + + for k, arm in self.mab.arms.iteritems(): + self.assertTrue(isinstance(d[k], dict)) + for attrname in ['successes', 'probability', 'trials']: + self.assertTrue(attrname in d[k]) + self.assertEqual(d[k][attrname], getattr(arm, attrname)) + if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() From 173e44f20883db496aab4577ea2147b5596687c3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 1 Sep 2016 13:32:37 -0400 Subject: [PATCH 1102/1169] remove dead code --- src/certfuzz/campaign/campaign_windows.py | 33 ------ src/certfuzz/file_handlers/seedfile.py | 29 +---- src/certfuzz/file_handlers/seedfile_set.py | 47 ++------ src/certfuzz/fuzztools/rangefinder.py | 15 +-- .../campaign/test_campaign_base.py | 6 -- .../file_handlers/test_seedfile.py | 13 --- .../file_handlers/test_seedfile_set.py | 101 +----------------- src/test_certfuzz/fuzztools/test_range.py | 41 +------ .../fuzztools/test_rangefinder.py | 23 +--- 9 files changed, 20 insertions(+), 288 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 2c8d388..74d8f54 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -36,39 +36,6 @@ def __init__(self, config_file, result_dir=None, debug=False): self.debugger_module_name = 'certfuzz.debuggers.gdb' TWDF.disable() - def __getstate__(self): - state = self.__dict__.copy() - - state['testcases_seen'] = list(state['testcases_seen']) - if state['seedfile_set']: - state['seedfile_set'] = state['seedfile_set'].__getstate__() - - # for attributes that are modules, - # we can safely delete them as they will be - # reconstituted when we __enter__ a context - for key in ['fuzzer_module', 'fuzzer_cls', - 'runner_module', 'runner_cls', - 'debugger_module' - ]: - if key in state: - del state[key] - return state - - def __setstate__(self, state): - # turn the list into a set - state['testcases_seen'] = set(state['testcases_seen']) - - # reconstitute the seedfile set - with SeedfileSet(state['campaign_id'], state['seed_dir_in'], state['seed_dir_local'], - state['sf_set_out']) as sfset: - new_sfset = sfset - - new_sfset.__setstate__(state['seedfile_set']) - state['seedfile_set'] = new_sfset - - # update yourself - self.__dict__.update(state) - def _pre_enter(self): # check to see if the platform supports winrun # set runner module to none otherwise diff --git a/src/certfuzz/file_handlers/seedfile.py b/src/certfuzz/file_handlers/seedfile.py index e2a812a..65d87ac 100644 --- a/src/certfuzz/file_handlers/seedfile.py +++ b/src/certfuzz/file_handlers/seedfile.py @@ -36,7 +36,8 @@ def __init__(self, output_base_dir, path): BasicFile.__init__(self, path) if not self.len > 0: - raise SeedFileError('You cannot do bitwise fuzzing on a zero-length file: %s' % self.path) + raise SeedFileError( + 'You cannot do bitwise fuzzing on a zero-length file: %s' % self.path) # use len for bytewise, bitlen for bitwise if self.len > 1: @@ -50,29 +51,6 @@ def __init__(self, output_base_dir, path): self.rangefinder = RangeFinder(self.range_min, self.range_max) - def __getstate__(self): - ''' - Pickle a SeedFile object - @return a dict representation of the pickled object - ''' - state = self.__dict__.copy() - state['rangefinder'] = self.rangefinder.__getstate__() - return state - - def __setstate__(self, state): - old_rf = state.pop('rangefinder') - - # rebuild the rangefinder - new_rf = self._get_rangefinder() - old_ranges = old_rf['things'] - for k, old_range in old_ranges.iteritems(): - if k in new_rf.things: - # things = ranges - new_range = new_rf.things[k] - for attr in ['a', 'b', 'probability', 'seen', 'successes', 'tries']: - setattr(new_range, attr, old_range[attr]) - self.rangefinder = new_rf - def cache_key(self): return 'seedfile-%s' % self.md5 @@ -81,5 +59,6 @@ def pkl_file(self): def to_json(self, sort_keys=True, indent=None): state = self.__dict__.copy() - state['rangefinder'] = state['rangefinder'].to_json(sort_keys=sort_keys, indent=indent) + state['rangefinder'] = state['rangefinder'].to_json( + sort_keys=sort_keys, indent=indent) return json.dumps(state, sort_keys=sort_keys, indent=indent) diff --git a/src/certfuzz/file_handlers/seedfile_set.py b/src/certfuzz/file_handlers/seedfile_set.py index 2006d28..afb9ff4 100644 --- a/src/certfuzz/file_handlers/seedfile_set.py +++ b/src/certfuzz/file_handlers/seedfile_set.py @@ -11,7 +11,8 @@ from certfuzz.file_handlers.seedfile import SeedFile from certfuzz.fuzztools import filetools -# Using a generic name here so we can easily swap out other MAB implementations if we want to +# Using a generic name here so we can easily swap out other MAB +# implementations if we want to from certfuzz.scoring.multiarmed_bandit.bayesian_bandit import BayesianMultiArmedBandit as MultiArmedBandit logger = logging.getLogger(__name__) @@ -21,6 +22,7 @@ class SeedfileSet(MultiArmedBandit): ''' classdocs ''' + def __init__(self, campaign_id=None, originpath=None, localpath=None, outputpath='.', logfile=None): ''' @@ -43,7 +45,8 @@ def __init__(self, campaign_id=None, originpath=None, localpath=None, hdlr = logging.FileHandler(logfile) logger.addHandler(hdlr) - logger.debug('SeedfileSet output_dir: %s', self.seedfile_output_base_dir) + logger.debug( + 'SeedfileSet output_dir: %s', self.seedfile_output_base_dir) def __enter__(self): self._setup() @@ -94,7 +97,8 @@ def copy_file_from_origin(self, f): # convert the local filenames from . to . basename = 'sf_' + f.md5 + f.ext - targets = [os.path.join(d, basename) for d in (self.localpath, self.outputpath)] + targets = [os.path.join(d, basename) + for d in (self.localpath, self.outputpath)] filetools.copy_file(f.path, *targets) for target in targets: filetools.make_writable(target) @@ -121,39 +125,6 @@ def next_item(self): return sf else: # it doesn't exist, remove it from the set - logger.warning('Seedfile no longer exists, removing from set: %s', sf.path) + logger.warning( + 'Seedfile no longer exists, removing from set: %s', sf.path) self.del_item(sf.md5) - -# def __setstate__(self, state): -# newstate = state.copy() -# -# # copy out old things and replace with an empty dict -# oldthings = newstate.pop('things') -# newstate['things'] = {} -# -# # refresh the directories -# self.__dict__.update(newstate) -# self._setup() -# -# # clean up things that no longer exist -# self.sfcount = 0 -# self.sfdel = 0 -# for k, old_sf in oldthings.iteritems(): -# # update the seedfiles for ones that are still present -# if k in self.things: -# # print "%s in things..." % k -# self.things[k].__setstate__(old_sf) -# self.sfcount += 1 - -# def __getstate__(self): -# state = ScorableSet3.__getstate__(self) -# -# # remove things we can recreate -# try: -# for k in ('origindir', 'localdir', 'outputdir'): -# del state[k] -# except KeyError: -# # it's ok if they don't exist -# pass -# -# return state diff --git a/src/certfuzz/fuzztools/rangefinder.py b/src/certfuzz/fuzztools/rangefinder.py index 9cda110..030e8dc 100644 --- a/src/certfuzz/fuzztools/rangefinder.py +++ b/src/certfuzz/fuzztools/rangefinder.py @@ -23,6 +23,7 @@ class RangeFinder(MultiArmedBandit): 3. a probability distribution across all ranges as well as a picker method to randomly choose a range based on the probability distribution. ''' + def __init__(self, low, high): MultiArmedBandit.__init__(self) @@ -36,20 +37,6 @@ def __init__(self, low, high): self._set_ranges() -# def __getstate__(self): -# # we can't pickle the logger. -# # But that's okay. We can get it back in __setstate__ -# state = ScorableSet2.__getstate__(self) -# del state['logger'] -# return state -# -# def __setstate__(self, d): -# self.__dict__.update(d) -# for thing in self.things.iteritems(): -# assert type(thing) == Range, 'Type is %s' % type(thing) -# # recover the logger we had to drop in __getstate__ -# self._set_logger() - def _exp_range(self, low, factor): high = low * factor # don't overshoot the high diff --git a/src/test_certfuzz/campaign/test_campaign_base.py b/src/test_certfuzz/campaign/test_campaign_base.py index ef9009b..c12f5ce 100644 --- a/src/test_certfuzz/campaign/test_campaign_base.py +++ b/src/test_certfuzz/campaign/test_campaign_base.py @@ -24,12 +24,6 @@ class UnimplementedCampaign(CampaignBase): class ImplementedCampaign(CampaignBase): - def __getstate__(self): - pass - - def __setstate__(self): - pass - def _do_interval(self): pass diff --git a/src/test_certfuzz/file_handlers/test_seedfile.py b/src/test_certfuzz/file_handlers/test_seedfile.py index bc740a4..e322a75 100644 --- a/src/test_certfuzz/file_handlers/test_seedfile.py +++ b/src/test_certfuzz/file_handlers/test_seedfile.py @@ -8,7 +8,6 @@ import tempfile import os from certfuzz.file_handlers.seedfile import SeedFile -from certfuzz.fuzztools.rangefinder import RangeFinder class Test(unittest.TestCase): @@ -28,18 +27,6 @@ def tearDown(self): def test_init(self): pass -# def test_getstate(self): -# self.assertEqual(RangeFinder, type(self.sf.rangefinder)) -# state = self.sf.__getstate__() -# self.assertEqual(dict, type(state)) -# self.assertEqual(dict, type(state['rangefinder'])) -# -# def test_setstate(self): -# state = self.sf.__getstate__() -# self.sf.__setstate__(state) -# # make sure we restore rangefinder -# self.assertEqual(RangeFinder, type(self.sf.rangefinder)) - if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] diff --git a/src/test_certfuzz/file_handlers/test_seedfile_set.py b/src/test_certfuzz/file_handlers/test_seedfile_set.py index 4df125e..2ebce05 100644 --- a/src/test_certfuzz/file_handlers/test_seedfile_set.py +++ b/src/test_certfuzz/file_handlers/test_seedfile_set.py @@ -33,7 +33,8 @@ def setUp(self): self.files.append(f) # create a set - self.sfs = SeedfileSet(campaign_id, self.origindir, self.localdir, self.outputdir) + self.sfs = SeedfileSet( + campaign_id, self.origindir, self.localdir, self.outputdir) def tearDown(self): for f in self.files: @@ -101,105 +102,9 @@ def test_init(self): self.assertEqual(self.outputdir, self.sfs.seedfile_output_base_dir) self.assertEqual(0, len(self.sfs.things)) -# def test_getstate_is_pickle_friendly(self): -# # getstate should return a pickleable object -# import pickle -# state = self.sfs.__getstate__() -# try: -# pickle.dumps(state) -# except Exception, e: -# self.fail('Failed to pickle state: %s' % e) -# -# def test_getstate(self): -# state = self.sfs.__getstate__() -# self.assertEqual(dict, type(state)) -# -# for k in self.sfs.__dict__.iterkeys(): -# # make sure we're deleting what we need to -# if k in ['localdir', 'origindir', 'outputdir']: -# self.assertFalse(k in state) -# else: -# self.assertTrue(k in state, '%s not found' % k) - -# def test_setstate(self): -# self.sfs.__enter__() -# state_before = self.sfs.__getstate__() -# self.sfs.__setstate__(state_before) -# self.assertEqual(self.file_count, self.sfs.sfcount) -# state_after = self.sfs.__getstate__() -# -# for k, v in state_before.iteritems(): -# self.assertTrue(k in state_after) -# if not k == 'things': -# self.assertEqual(v, state_after[k]) -# -# for k, thing in state_before['things'].iteritems(): -# # is there a corresponding thing in sfs? -# self.assertTrue(k in self.sfs.things) -# -# for x in thing.iterkeys(): -# # was it set correctly? -# self.assertEqual(thing[x], self.sfs.things[k].__dict__[x]) -# -# self.assertEqual(self.file_count, self.sfs.sfcount) - -# def test_setstate_with_changed_files(self): -# # refresh the sfs -# self.sfs.__enter__() -# -# # get the original state -# state_before = self.sfs.__getstate__() -# self.assertEqual(len(state_before['things']), self.file_count) -# -# # delete one of the files -# file_to_remove = self.files.pop() -# localfile_md5 = hashlib.md5(open(file_to_remove, 'rb').read()).hexdigest() -# localfilename = "sf_%s" % localfile_md5 -# -# # remove it from origin -# os.remove(file_to_remove) -# self.assertFalse(file_to_remove in self.files) -# self.assertFalse(os.path.exists(file_to_remove)) -## print "removed %s" % file_to_remove -# -## # remove it from localdir -# localfile_to_remove = os.path.join(self.localdir, localfilename) -# os.remove(localfile_to_remove) -# self.assertFalse(os.path.exists(localfile_to_remove)) -# -# # create a new sfs -# new_sfs = SeedfileSet() -# new_sfs.__setstate__(state_before) -# -# self.assertEqual(len(new_sfs.things), (self.file_count - 1)) -# -## print "Newthings: %s" % new_sfs.things.keys() -# for k, thing in state_before['things'].iteritems(): -## print "k: %s" % k -# if k == localfile_md5: -# self.assertFalse(k in new_sfs.things) -# continue -# else: -# # is there a corresponding thing in sfs? -# self.assertTrue(k in new_sfs.things) -# -# for x, y in thing.iteritems(): -# # was it set correctly? -# sfsthing = new_sfs.things[k].__dict__[x] -# if hasattr(sfsthing, '__dict__'): -# # some things are complex objects themselves -# # so we have to compare their __dict__ versions -# self._same_dict(y, sfsthing.__dict__) -# else: -# # others are just simple objects and we can -# # compare them directly -# self.assertEqual(y, sfsthing) -# -# self.assertEqual(self.file_count - 1, new_sfs.sfcount) - def _same_dict(self, d1, d2): for k, v in d1.iteritems(): -# print k + # print k self.assertTrue(k in d2) self.assertEqual(v, d2[k]) diff --git a/src/test_certfuzz/fuzztools/test_range.py b/src/test_certfuzz/fuzztools/test_range.py index 3f57622..db5d33d 100644 --- a/src/test_certfuzz/fuzztools/test_range.py +++ b/src/test_certfuzz/fuzztools/test_range.py @@ -6,7 +6,9 @@ import unittest from certfuzz.fuzztools.range import Range + class Test(unittest.TestCase): + def setUp(self): self.r = Range(0, 1) @@ -22,45 +24,6 @@ def test_init(self): def test_repr(self): self.assertEqual(self.r.__repr__(), '0.000000-1.000000') -# def test_getstate_is_pickle_friendly(self): -# # getstate should return a pickleable object -# import pickle -# state = self.r.__getstate__() -# try: -# pickle.dumps(state) -# except Exception, e: -# self.fail('Failed to pickle state: %s' % e) -# -# def test_getstate_has_all_expected_items(self): -# state = self.r.__getstate__() -# for k, v in self.r.__dict__.iteritems(): -# # make sure we're deleting what we need to -# if k in ['logger']: -# self.assertFalse(k in state) -# else: -# self.assertTrue(k in state, '%s not found' % k) -# self.assertEqual(state[k], v) -# -# def test_getstate(self): -# state = self.r.__getstate__() -# self.assertEqual(dict, type(state)) -# print 'as dict...' -# pprint.pprint(state) -# -# def test_to_json(self): -# as_json = self.r.to_json(indent=4) -# -# print 'as JSON...' -# for l in as_json.splitlines(): -# print l -# -# from_json = json.loads(as_json) -# -# # make sure we can round-trip it -# for k, v in self.r.__getstate__().iteritems(): -# self.assertTrue(k in from_json) -# self.assertEqual(from_json[k], v) - if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/src/test_certfuzz/fuzztools/test_rangefinder.py b/src/test_certfuzz/fuzztools/test_rangefinder.py index 591d800..c44c097 100644 --- a/src/test_certfuzz/fuzztools/test_rangefinder.py +++ b/src/test_certfuzz/fuzztools/test_rangefinder.py @@ -12,6 +12,7 @@ class Test(unittest.TestCase): + def delete_file(self, f): os.remove(f) self.assertFalse(os.path.exists(f)) @@ -74,28 +75,6 @@ def test_range_mean(self): for x in self.r.things.values(): self.assertAlmostEqual(x.mean, ((x.max + x.min) / 2)) -# def test_getstate_is_pickle_friendly(self): -# # getstate should return a pickleable object -# import pickle -# state = self.r.__getstate__() -# try: -# pickle.dumps(state) -# except Exception, e: -# self.fail('Failed to pickle state: %s' % e) -# -# def test_getstate_has_all_expected_items(self): -# state = self.r.__getstate__() -# for k, v in self.r.__dict__.iteritems(): -# # make sure we're deleting what we need to -# if k in ['logger']: -# self.assertFalse(k in state) -# else: -# self.assertTrue(k in state, '%s not found' % k) -# self.assertEqual(type(state[k]), type(v)) -# -# def test_getstate(self): -# state = self.r.__getstate__() -# self.assertEqual(dict, type(state)) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] From 8481b45d29be943b353a33a3028a967ed6d6bcd9 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Thu, 1 Sep 2016 16:52:42 -0400 Subject: [PATCH 1103/1169] Rudimentary git update support via python-git --- src/certfuzz/file_handlers/tempdir.py | 6 +- src/certfuzz/tools/linux/updatebff.py | 118 ++++++++++++++++++++++++++ src/linux/tools/updatebff.py | 19 +++++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 src/certfuzz/tools/linux/updatebff.py create mode 100644 src/linux/tools/updatebff.py diff --git a/src/certfuzz/file_handlers/tempdir.py b/src/certfuzz/file_handlers/tempdir.py index dd82f71..848802d 100644 --- a/src/certfuzz/file_handlers/tempdir.py +++ b/src/certfuzz/file_handlers/tempdir.py @@ -16,6 +16,7 @@ class TempDir(object): context. Make sure you copy out what you need to keep before leaving the context. ''' + def __init__(self, suffix=None, prefix=None, dir=None): self.suffix = suffix self.prefix = prefix @@ -31,10 +32,11 @@ def __enter__(self): self.tmpdir = tempfile.mkdtemp(**kwargs) logger.debug('Created tempdir %s', self.tmpdir) - return self.tmpdir + return self def __exit__(self, etype, value, traceback): if etype is not None: - logger.debug('%s caught %s: %s', self.__class__.__name__, etype, value) + logger.debug( + '%s caught %s: %s', self.__class__.__name__, etype, value) logger.debug('Removing tempdir %s', self.tmpdir) shutil.rmtree(self.tmpdir, ignore_errors=True) diff --git a/src/certfuzz/tools/linux/updatebff.py b/src/certfuzz/tools/linux/updatebff.py new file mode 100644 index 0000000..b705b9b --- /dev/null +++ b/src/certfuzz/tools/linux/updatebff.py @@ -0,0 +1,118 @@ +''' +Created on September 1, 2016 + +@organization: cert.org +''' +import logging +import tempfile +import os +import time +import shutil +from distutils import dir_util + +from subprocess import Popen + +use_pygit = True + +try: + from certfuzz.fuzztools.filetools import rm_rf, best_effort_move +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + import sys + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.fuzztools.filetools import rm_rf, best_effort_move + + +try: + from git import Repo +except ImportError: + use_pygit = False + + +logger = logging.getLogger() +logger.setLevel(logging.WARNING) + + +def copydir(src, dst): + logger.info('Copy dir %s -> %s', src, dst) + dir_util.copy_tree(src, dst) + + +def copyfile(src, dst): + logger.info('Copy file %s -> %s', src, dst) + shutil.copy(src, dst) + + +def main(): + from optparse import OptionParser + + branch = 'develop' + target_path = '.' + + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + usage = "usage: %prog [options] fuzzedfile" + parser = OptionParser(usage) + parser.add_option('-d', '--debug', dest='debug', action='store_true', + help='Enable debug messages (overrides --verbose)') + parser.add_option('-m', '--master', dest='master', + action='store_true', help='Use master branch instead of develop') + parser.add_option('-f', '--force', dest='force', + action='store_true', help='Force update') + parser.add_option('-y', '--yes', dest='yes', + action='store_true', help='Don\'t ask questions') + parser.add_option('-s', '--save', dest='save', + action='store_true', help='Save original certfuzz directory') + + (options, args) = parser.parse_args() + + if options.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + if options.master: + branch = 'master' + + logger.debug('Using %s branch' % branch) + + tempdir = tempfile.mkdtemp() + if use_pygit: + repo = Repo.clone_from( + 'https://github.com/CERTCC-Vulnerability-Analysis/certfuzz.git', tempdir, branch=branch) + headcommit = repo.head.commit + logger.info('Cloned certfuzz version %s' % headcommit.hexsha) + logger.info('Last modified %s' % time.strftime( + "%a, %d %b %Y %H:%M", time.gmtime(headcommit.committed_date))) + + if options.save: + logger.info('Saving original certfuzz directory as certfuzz.bak') + os.rename('certfuzz', 'certfuzz.bak') + else: + rm_rf('certfuzz') + logger.info('Deleting certfuzz directory...') + + logger.debug('Moving certfuzz directory from git clone...') + best_effort_move(os.path.join(tempdir, 'src', 'certfuzz'), target_path) + + logger.debug('Moving linux-specific files from git clone...') + platform_path = os.path.join(tempdir, 'src', 'linux') + + # copy platform-specific content + for f in os.listdir(platform_path): + f_src = os.path.join(platform_path, f) + + f_dst = os.path.join(target_path, f) + if os.path.isdir(f_src): + copydir(f_src, f_dst) + elif os.path.isfile(f_src): + copyfile(f_src, f_dst) + else: + logger.warning("Not sure what to do with %s", f_src) + + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/updatebff.py b/src/linux/tools/updatebff.py new file mode 100644 index 0000000..25132c9 --- /dev/null +++ b/src/linux/tools/updatebff.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +''' +Created on September 1, 2016 + +@organization: cert.org +''' +import os +import sys +try: + from certfuzz.tools.linux.updatebff import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.updatebff import main + +if __name__ == '__main__': + main() From 47bc273405f440ae4808cd9181b211bd788d45a1 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 2 Sep 2016 10:35:26 -0400 Subject: [PATCH 1104/1169] Don't require python-git for updatebff.py BFF-1001 --- src/certfuzz/tools/linux/updatebff.py | 90 ++++++++++++++++----------- src/linux/tools/updatebff.py | 6 +- 2 files changed, 58 insertions(+), 38 deletions(-) diff --git a/src/certfuzz/tools/linux/updatebff.py b/src/certfuzz/tools/linux/updatebff.py index b705b9b..9a676b2 100644 --- a/src/certfuzz/tools/linux/updatebff.py +++ b/src/certfuzz/tools/linux/updatebff.py @@ -10,38 +10,22 @@ import shutil from distutils import dir_util -from subprocess import Popen - -use_pygit = True - -try: - from certfuzz.fuzztools.filetools import rm_rf, best_effort_move -except ImportError: - # if we got here, we probably don't have .. in our PYTHONPATH - import sys - mydir = os.path.dirname(os.path.abspath(__file__)) - parentdir = os.path.abspath(os.path.join(mydir, '..')) - sys.path.append(parentdir) - from certfuzz.fuzztools.filetools import rm_rf, best_effort_move - - -try: - from git import Repo -except ImportError: - use_pygit = False +from subprocess import call, check_output +from __builtin__ import False +from certfuzz.fuzztools.filetools import rm_rf, best_effort_move logger = logging.getLogger() logger.setLevel(logging.WARNING) def copydir(src, dst): - logger.info('Copy dir %s -> %s', src, dst) + logger.debug('Copy dir %s -> %s', src, dst) dir_util.copy_tree(src, dst) def copyfile(src, dst): - logger.info('Copy file %s -> %s', src, dst) + logger.debug('Copy file %s -> %s', src, dst) shutil.copy(src, dst) @@ -50,6 +34,8 @@ def main(): branch = 'develop' target_path = '.' + blacklist = ['configs'] + certfuzz_dir = os.path.join(target_path, 'certfuzz') hdlr = logging.StreamHandler() logger.addHandler(hdlr) @@ -77,32 +63,29 @@ def main(): if options.master: branch = 'master' - logger.debug('Using %s branch' % branch) + logger.info('Using %s branch' % branch) - tempdir = tempfile.mkdtemp() - if use_pygit: - repo = Repo.clone_from( - 'https://github.com/CERTCC-Vulnerability-Analysis/certfuzz.git', tempdir, branch=branch) - headcommit = repo.head.commit - logger.info('Cloned certfuzz version %s' % headcommit.hexsha) - logger.info('Last modified %s' % time.strftime( - "%a, %d %b %Y %H:%M", time.gmtime(headcommit.committed_date))) + tempdir = git_update(branch=branch) if options.save: - logger.info('Saving original certfuzz directory as certfuzz.bak') - os.rename('certfuzz', 'certfuzz.bak') + logger.debug('Saving original certfuzz directory as certfuzz.bak') + os.rename(certfuzz_dir, '%s.bak' % certfuzz_dir) else: - rm_rf('certfuzz') - logger.info('Deleting certfuzz directory...') + rm_rf(certfuzz_dir) + logger.debug('Deleting certfuzz directory...') - logger.debug('Moving certfuzz directory from git clone...') - best_effort_move(os.path.join(tempdir, 'src', 'certfuzz'), target_path) + logger.info('Moving certfuzz directory from git clone...') + copydir(os.path.join(tempdir, 'src', 'certfuzz'), + os.path.join(target_path, 'certfuzz')) - logger.debug('Moving linux-specific files from git clone...') + logger.info('Moving linux-specific files from git clone...') platform_path = os.path.join(tempdir, 'src', 'linux') # copy platform-specific content for f in os.listdir(platform_path): + if f in blacklist: + logger.debug('Skipping %s' % f) + continue f_src = os.path.join(platform_path, f) f_dst = os.path.join(target_path, f) @@ -113,6 +96,39 @@ def main(): else: logger.warning("Not sure what to do with %s", f_src) + logger.debug('Removing %s' % tempdir) + rm_rf(tempdir) + + +def git_update(uri='https://github.com/CERTCC-Vulnerability-Analysis/certfuzz.git', branch='develop'): + + use_pygit = True + + try: + from git import Repo + except ImportError: + use_pygit = False + + tempdir = tempfile.mkdtemp() + + if use_pygit: + repo = Repo.clone_from(uri, tempdir, branch=branch) + headcommit = repo.head.commit + headversion = headcommit.hexsha + gitdate = time.strftime( + "%a, %d %b %Y %H:%M", time.gmtime(headcommit.committed_date)) + else: + ret = call(['git', 'clone', uri, tempdir, '--branch', branch]) + print('ret: %d' % ret) + headversion = check_output(['git', 'rev-parse', 'HEAD'], cwd=tempdir) + gitdate = check_output( + ['git', 'log', '-1', '--pretty=format:%cd'], cwd=tempdir) + + logger.info('Cloned certfuzz version %s' % headversion) + logger.info('Last modified %s' % gitdate) + + return tempdir + if __name__ == '__main__': main() diff --git a/src/linux/tools/updatebff.py b/src/linux/tools/updatebff.py index 25132c9..4bc7eca 100644 --- a/src/linux/tools/updatebff.py +++ b/src/linux/tools/updatebff.py @@ -13,7 +13,11 @@ mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) - from certfuzz.tools.linux.updatebff import main + try: + from certfuzz.tools.linux.updatebff import main + except ImportError: + # certfuzz likely downgraded to pre-2.8 version + raise Exception('%s requires BFF 2.8 or later' % __file__) if __name__ == '__main__': main() From bd473a41a1ce3aa4c015a232150de11ea15b2248 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 2 Sep 2016 11:19:45 -0400 Subject: [PATCH 1105/1169] Fall back to urllib to download certfuzz zip from github. Add windows support to updatebff. BFF-1001 --- .../tools/{linux => common}/updatebff.py | 49 ++++++++++++++++--- src/{linux => }/tools/updatebff.py | 4 +- 2 files changed, 45 insertions(+), 8 deletions(-) rename src/certfuzz/tools/{linux => common}/updatebff.py (69%) rename src/{linux => }/tools/updatebff.py (82%) diff --git a/src/certfuzz/tools/linux/updatebff.py b/src/certfuzz/tools/common/updatebff.py similarity index 69% rename from src/certfuzz/tools/linux/updatebff.py rename to src/certfuzz/tools/common/updatebff.py index 9a676b2..ac93899 100644 --- a/src/certfuzz/tools/linux/updatebff.py +++ b/src/certfuzz/tools/common/updatebff.py @@ -8,7 +8,11 @@ import os import time import shutil +import urllib +import platform from distutils import dir_util +from distutils.spawn import find_executable + from subprocess import call, check_output from __builtin__ import False @@ -37,6 +41,12 @@ def main(): blacklist = ['configs'] certfuzz_dir = os.path.join(target_path, 'certfuzz') + platform_system = platform.system() + if platform_system is 'Windows': + platform_subdir = 'windows' + else: + platform_subdir = 'linux' + hdlr = logging.StreamHandler() logger.addHandler(hdlr) @@ -78,8 +88,8 @@ def main(): copydir(os.path.join(tempdir, 'src', 'certfuzz'), os.path.join(target_path, 'certfuzz')) - logger.info('Moving linux-specific files from git clone...') - platform_path = os.path.join(tempdir, 'src', 'linux') + logger.info('Moving %s-specific files from git clone...' % platform_subdir) + platform_path = os.path.join(tempdir, 'src', platform_subdir) # copy platform-specific content for f in os.listdir(platform_path): @@ -112,23 +122,50 @@ def git_update(uri='https://github.com/CERTCC-Vulnerability-Analysis/certfuzz.gi tempdir = tempfile.mkdtemp() if use_pygit: + # Use python-git to get certfuzz from github repo = Repo.clone_from(uri, tempdir, branch=branch) headcommit = repo.head.commit headversion = headcommit.hexsha gitdate = time.strftime( "%a, %d %b %Y %H:%M", time.gmtime(headcommit.committed_date)) - else: + logger.info('Cloned certfuzz version %s' % headversion) + logger.info('Last modified %s' % gitdate) + elif find_executable('git'): + # Shell out to git to get certfuzz from github ret = call(['git', 'clone', uri, tempdir, '--branch', branch]) print('ret: %d' % ret) headversion = check_output(['git', 'rev-parse', 'HEAD'], cwd=tempdir) gitdate = check_output( ['git', 'log', '-1', '--pretty=format:%cd'], cwd=tempdir) - - logger.info('Cloned certfuzz version %s' % headversion) - logger.info('Last modified %s' % gitdate) + logger.info('Cloned certfuzz version %s' % headversion) + logger.info('Last modified %s' % gitdate) + else: + # Use urllib to get zip + zip_update(tempdir, branch=branch) return tempdir +def zip_update(tempdir, uri='https://github.com/CERTCC-Vulnerability-Analysis/certfuzz/archive/develop.zip', branch='develop'): + import zipfile + + if branch is 'master': + uri = uri.replace('develop.zip', '%s.zip' % branch) + + targetzip = os.path.basename(uri) + targetzippath = os.path.join(tempdir, targetzip) + logger.debug('Saving %s to %s' % (uri, targetzippath)) + urllib.urlretrieve(uri, targetzippath) + bffzip = zipfile.ZipFile(targetzippath, 'r') + bffzip.extractall(tempdir) + bffzip.close() + os.remove(targetzippath) + subdir = 'certfuzz-%s' % branch + bff_dir = os.path.join(tempdir, subdir) + for f in os.listdir(bff_dir): + fullpath = os.path.join(bff_dir, f) + best_effort_move(fullpath, tempdir) + + if __name__ == '__main__': main() diff --git a/src/linux/tools/updatebff.py b/src/tools/updatebff.py similarity index 82% rename from src/linux/tools/updatebff.py rename to src/tools/updatebff.py index 4bc7eca..c3d038e 100644 --- a/src/linux/tools/updatebff.py +++ b/src/tools/updatebff.py @@ -7,14 +7,14 @@ import os import sys try: - from certfuzz.tools.linux.updatebff import main + from certfuzz.tools.common.updatebff import main except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) try: - from certfuzz.tools.linux.updatebff import main + from certfuzz.tools.common.updatebff import main except ImportError: # certfuzz likely downgraded to pre-2.8 version raise Exception('%s requires BFF 2.8 or later' % __file__) From e12c041fcc761ddf8c62dd0cc05ed1ba523e689f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 2 Sep 2016 11:22:28 -0400 Subject: [PATCH 1106/1169] stub unit test --- .../tools/common/test_updatebff.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/test_certfuzz/tools/common/test_updatebff.py diff --git a/src/test_certfuzz/tools/common/test_updatebff.py b/src/test_certfuzz/tools/common/test_updatebff.py new file mode 100644 index 0000000..fc47475 --- /dev/null +++ b/src/test_certfuzz/tools/common/test_updatebff.py @@ -0,0 +1,27 @@ +''' +Created on Apr 10, 2012 + +@organization: cert.org +''' + +import unittest + + +class Test(unittest.TestCase): + + + def setUp(self): + pass + + + def tearDown(self): + pass + + + def testName(self): + pass + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 7f6b059cfe5aa8ca02115349df500937e1670b25 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 2 Sep 2016 13:19:06 -0400 Subject: [PATCH 1107/1169] Remove unused cmdline arguments. Delete old certfuzz directory if it exists and using the -s option. --- src/certfuzz/tools/common/updatebff.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/certfuzz/tools/common/updatebff.py b/src/certfuzz/tools/common/updatebff.py index ac93899..32d2765 100644 --- a/src/certfuzz/tools/common/updatebff.py +++ b/src/certfuzz/tools/common/updatebff.py @@ -56,10 +56,6 @@ def main(): help='Enable debug messages (overrides --verbose)') parser.add_option('-m', '--master', dest='master', action='store_true', help='Use master branch instead of develop') - parser.add_option('-f', '--force', dest='force', - action='store_true', help='Force update') - parser.add_option('-y', '--yes', dest='yes', - action='store_true', help='Don\'t ask questions') parser.add_option('-s', '--save', dest='save', action='store_true', help='Save original certfuzz directory') @@ -79,7 +75,11 @@ def main(): if options.save: logger.debug('Saving original certfuzz directory as certfuzz.bak') - os.rename(certfuzz_dir, '%s.bak' % certfuzz_dir) + old_certfuzz = '%s.bak' % certfuzz_dir + if os.path.isdir(old_certfuzz): + logger.debug('Removing old certfuzz directory: %s' % old_certfuzz) + rm_rf(old_certfuzz) + os.rename(certfuzz_dir, old_certfuzz) else: rm_rf(certfuzz_dir) logger.debug('Deleting certfuzz directory...') From 2d9db6b1adeb98eeda347c7991e2d091718d06da Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 2 Sep 2016 13:47:16 -0400 Subject: [PATCH 1108/1169] Mark common tools as executable --- src/certfuzz/tools/common/updatebff.py | 0 src/certfuzz/tools/common/zipdiff.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 src/certfuzz/tools/common/updatebff.py mode change 100644 => 100755 src/certfuzz/tools/common/zipdiff.py diff --git a/src/certfuzz/tools/common/updatebff.py b/src/certfuzz/tools/common/updatebff.py old mode 100644 new mode 100755 diff --git a/src/certfuzz/tools/common/zipdiff.py b/src/certfuzz/tools/common/zipdiff.py old mode 100644 new mode 100755 From 5a8bd0113f2e4103d9feca7a7cd04bdf60417edc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 2 Sep 2016 13:54:13 -0400 Subject: [PATCH 1109/1169] Mark common tools as executable --- src/tools/updatebff.py | 0 src/tools/zipdiff.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 src/tools/updatebff.py mode change 100644 => 100755 src/tools/zipdiff.py diff --git a/src/tools/updatebff.py b/src/tools/updatebff.py old mode 100644 new mode 100755 diff --git a/src/tools/zipdiff.py b/src/tools/zipdiff.py old mode 100644 new mode 100755 From 5c6c91d23defda042caf95b7fb62db00cdd93d18 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 2 Sep 2016 16:22:29 -0400 Subject: [PATCH 1110/1169] Allow updatebff.py to be run from within the tools directory. --- src/certfuzz/tools/common/updatebff.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/tools/common/updatebff.py b/src/certfuzz/tools/common/updatebff.py index 32d2765..ab79645 100755 --- a/src/certfuzz/tools/common/updatebff.py +++ b/src/certfuzz/tools/common/updatebff.py @@ -39,6 +39,10 @@ def main(): branch = 'develop' target_path = '.' blacklist = ['configs'] + + if not os.path.isdir('certfuzz'): + target_path = '..' + certfuzz_dir = os.path.join(target_path, 'certfuzz') platform_system = platform.system() @@ -53,7 +57,7 @@ def main(): usage = "usage: %prog [options] fuzzedfile" parser = OptionParser(usage) parser.add_option('-d', '--debug', dest='debug', action='store_true', - help='Enable debug messages (overrides --verbose)') + help='Enable debug messages') parser.add_option('-m', '--master', dest='master', action='store_true', help='Use master branch instead of develop') parser.add_option('-s', '--save', dest='save', From 263de510856b63d609da71d25d764935fe047874 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 2 Sep 2016 16:36:51 -0400 Subject: [PATCH 1111/1169] Readme tweaks --- src/linux/INSTALL | 22 ++++++++++++++++++++-- src/linux/NEWS | 1 + src/linux/README | 28 +++++++++++++++++++--------- src/windows/NEWS.txt | 1 + src/windows/README.txt | 13 +++++++++++-- 5 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/linux/INSTALL b/src/linux/INSTALL index 6a38fc4..6bea3e8 100644 --- a/src/linux/INSTALL +++ b/src/linux/INSTALL @@ -77,7 +77,7 @@ changes: removed when an application is built. -===== Example installation on Fedora 16 32-bit===== +===== Example installation on Fedora 16 32-bit ===== To install BFF on a Fedora 16 32-bit system, for example, the following steps can be performed: @@ -249,4 +249,22 @@ sudo zypper install fluxbox (Right-click desktop -> Fluxbox Menu -> Configure -> Focus model) (Cick the following options and ensure they are not selected to disable them:) (Focus New Windows) - (Auto Raise) \ No newline at end of file + (Auto Raise) + + +===== Installation on other platforms ===== + +BFF should run on any UNIX-like operating system. Depending on the features +supported by the OS, certain BFF capabilities may not be available. For +example, CERT Triage Tools exploitability determination only functions on +Linux. + +The minimum requirements to be able to run BFF are: +Python 2.7 +Python Numpy +Python Scipy +Python Yaml +gdb (7.2 or later on Linux is required for exploitability information) +zzuf (patched by CERT) + +Patches may be required to get zzuf compiled on non-Linux systems. \ No newline at end of file diff --git a/src/linux/NEWS b/src/linux/NEWS index 543df46..f6aeeca 100644 --- a/src/linux/NEWS +++ b/src/linux/NEWS @@ -13,6 +13,7 @@ BFF 2.8 (FIXME) - Support for OSX Maverics, Yosemite, El Capitan, and MacOS Sierra - Removed X dependency - "null" runner mode for zzuf, which only detects crashes + - Added updatebff.py tool - Bug fixes BFF 2.7 (September 23, 2013) diff --git a/src/linux/README b/src/linux/README index 9682ef5..3ee976f 100644 --- a/src/linux/README +++ b/src/linux/README @@ -50,7 +50,7 @@ VMX. If you chose to unzip scripts.zip to a folder other than c:\fuzz, then you will need to modify the properties of the shared folder in VMWare to point to the new location of the files. Alternatively, if you may unzip the BFF scripts into -~/bff if you do not wish to use a dhared folder. +~/bff in the guest OS if you do not wish to use a dhared folder. The fuzzing virtual machine is preconfigured to automatically begin a fuzzing run on several image format decoders provided @@ -150,10 +150,10 @@ rest were all masked out with a fixed value, say "x" (0x78)? Then you'd know that if you saw EIP=0x78787878, you may already be a winner. The minimize-to-string option does just that. To get command-line usage of the minimizer, run: -tools\minimize.py --help +tools/minimize.py --help To minimize a crashing testcase to the Metasploit string pattern, run: -tools\minimize.py --stringmode +tools/minimize.py --stringmode When minimizing to the Metasploit pattern, FOE will use the resulting byte map to create an additional minimized file that uses a string of 'x' characters. @@ -164,9 +164,9 @@ Metasploit pattern enumeration: Especially with larger files, you may notice that the Metasploit pattern repeats several times over the length of a Metasploit-minimized crasher. Given any particular dword, it may not be obvious which instance is the one -that you are dealing with. This is where the tools\mtsp_enum.py script comes +that you are dealing with. This is where the tools/mtsp_enum.py script comes in handy. For example, let's say that you have a crasher.doc were EIP = "Aa0A" -If you run: tools\mtsp_enum.py Aa0A crasher.doc +If you run: tools/mtsp_enum.py Aa0A crasher.doc You will end up with a file called crasher-enum.doc. With this file, every instance of the byte pattern "Aa0A" will be replaced with a unique, incrementing replacement. For example, "0a0A", "1a0A", "2a0A", etc. Now when @@ -222,6 +222,14 @@ tools/debuggerfile.py will parse GDB output to create a hash that uniquely identifies the crash. +===== Updating BFF ===== + +BFF includes a script to update itself using the code present on GitHub + + +tools/updatebff.py + + ===== Fuzzing on your own ===== When the UbuFuzz VM is powered on, it will automatically @@ -237,7 +245,9 @@ version of it. Rather than using Ubuntu's package repository, this will ensure that you will be fuzzing the latest version of the target application. It will also give you the ability to have debug symbols, as well as the ability to use a non-optimized -build if you like. +build if you like. When compiling the target application, be sure +that the "-g" flag is used with your compiler. This ensures that debug +information is added to the target application. Create a new snapshot of the VM after the target software has been installed. @@ -277,7 +287,7 @@ timeouts. "runtimeout" is the maximum time that BFF will allow a single invocation of the target application to run before terminating it. In the case of the "convert" component of ImageMagick, it is reasonable to expect the program to finish -within a few seconds. Depending on the application you are +within a few seconds. Depending on the application you are fuzzing, this value will need to be adjusted. Especially with GUI applications, the goal is to allow the application to run long enough to process the input file, but not so long that the @@ -289,8 +299,8 @@ UbuFuzz VM. It should begin fuzzing automatically. If the target is a GUI application, you should see the application launch repeatedly as it is being fuzzed. If it is a command-line application, you should notice increased CPU usage in the htop -window. The first iteration that BFF executes will also -display stderr to the terminal window to let you see if something +window. The first iteration that BFF executes will also display +stdout and stderr to the terminal window to let you see if something is obviously wrong. If you need to tweak your settings, such as by using a smaller progtimeout value to improve throughput, you can edit the bff.yaml file and restore the VM to its previous diff --git a/src/windows/NEWS.txt b/src/windows/NEWS.txt index cfe70ec..f15fbf0 100644 --- a/src/windows/NEWS.txt +++ b/src/windows/NEWS.txt @@ -9,6 +9,7 @@ BFF 2.8 (FIXME) * Code changes: - Convergence of BFF and FOE code into just BFF - Simplified configuration + - Added updatebff.py tool - Bug fixes FOE 2.1 (September 23, 2013) diff --git a/src/windows/README.txt b/src/windows/README.txt index 50e559a..7d56fbe 100644 --- a/src/windows/README.txt +++ b/src/windows/README.txt @@ -172,9 +172,18 @@ runner: runtimeout: BFF periodically saves state of a fuzzing campaign, so it will by default continue a cached campaign if bff.yaml has not been modified. To clear the BFF cached state, run: -tools\clean_BFF.py +tools\clean_windows.py For additional options, run: -tools\clean_BFF.py --help +tools\clean_windows.py --help + + +===== Updating BFF ===== +======================== + +BFF includes a script to update itself using the code present on GitHub + + +tools\updatebff.py ===== Digging deeper into results ===== From f224ddec4b58de004fd0eac373d89c94173a188f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 7 Sep 2016 15:13:04 -0400 Subject: [PATCH 1112/1169] Exit campaign interval loop if the current seedfile has been exhausted. BFF-1002 --- src/certfuzz/campaign/campaign_base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/certfuzz/campaign/campaign_base.py b/src/certfuzz/campaign/campaign_base.py index d823ccd..804c29d 100644 --- a/src/certfuzz/campaign/campaign_base.py +++ b/src/certfuzz/campaign/campaign_base.py @@ -506,6 +506,9 @@ def _do_interval(self): logger.debug( 'Starting interval %d-%d', self.current_seed, interval_limit) for seednum in xrange(self.current_seed, interval_limit): + if sf.md5 not in self.seedfile_set.things: + # We've exhausted what we can do with this seedfile + break self._do_iteration(sf, r, seednum) del sf From 49626bf782c253029ab602422fd9c668bd9ab875 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 7 Sep 2016 15:32:18 -0400 Subject: [PATCH 1113/1169] Include calltrace runner tool. BFF-1003 --- src/certfuzz/tools/linux/calltrace.py | 136 ++++++++++++++++++++++++++ src/linux/tools/calltrace.py | 19 ++++ 2 files changed, 155 insertions(+) create mode 100644 src/certfuzz/tools/linux/calltrace.py create mode 100644 src/linux/tools/calltrace.py diff --git a/src/certfuzz/tools/linux/calltrace.py b/src/certfuzz/tools/linux/calltrace.py new file mode 100644 index 0000000..b7103b0 --- /dev/null +++ b/src/certfuzz/tools/linux/calltrace.py @@ -0,0 +1,136 @@ +''' +Created on April 28, 2013 + +@organization: cert.org +''' +import logging +import os +import re +import platform +from subprocess import Popen +from certfuzz.config.simple_loader import load_and_fix_config +from certfuzz.fuzztools.command_line_templating import get_command_args_list + +try: + from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file + from certfuzz import debuggers + from certfuzz.file_handlers.basicfile import BasicFile + from certfuzz.debuggers import gdb, crashwrangler # @UnusedImport +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + import sys + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file + from certfuzz import debuggers + from certfuzz.file_handlers.basicfile import BasicFile + from certfuzz.debuggers import gdb, crashwrangler # @UnusedImport + +logger = logging.getLogger() +logger.setLevel(logging.WARNING) + + +def parseiterpath(commandline): + # Return the path to the iteration's fuzzed file + for part in commandline.split(): + if 'campaign_' in part and 'iteration_' in part: + # This is the part of the commandline that looks like it's + # the fuzzed file + return part + + +def getiterpath(gdbfile): + # Find the commandline in an msec file + with open(gdbfile) as gdblines: + for line in gdblines: + m = re.match('Running: ', line) + if m: + return parseiterpath(line) + + +def main(): + from optparse import OptionParser + + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + usage = "usage: %prog [options] fuzzedfile" + parser = OptionParser(usage) + parser.add_option('', '--debug', dest='debug', action='store_true', + help='Enable debug messages (overrides --verbose)') + parser.add_option('', '--verbose', dest='verbose', action='store_true', + help='Enable verbose messages') + parser.add_option('-c', '--config', default='configs/bff.yaml', + dest='config', help='path to the configuration file to use') + parser.add_option('-a', '--args', dest='print_args', + action='store_true', + help='Print function arguments') + parser.add_option('-o', '--out', dest='outfile', + help='PIN output file') + parser.add_option('-f', '--filepath', dest='filepath', + action='store_true', help='Recreate original file path') + + (options, args) = parser.parse_args() + + if options.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + if options.outfile: + outfile = options.outfile + else: + outfile = 'calltrace.log' + + cfg_file = options.config + logger.debug('Config file: %s', cfg_file) + + if len(args) and os.path.exists(args[0]): + fullpath_fuzzed_file = os.path.abspath(args[0]) + fuzzed_file = BasicFile(fullpath_fuzzed_file) + logger.info('Fuzzed file is: %s', fuzzed_file.path) + else: + parser.error('fuzzedfile must be specified') + + iterationpath = '' + + if options.filepath: + # Recreate same file path as fuzz iteration + resultdir = os.path.dirname(fuzzed_file.path) + for gdbfile in all_files(resultdir, '*.gdb'): + print '** using gdb: %s' % gdbfile + iterationpath = getiterpath(gdbfile) + break + if iterationpath: + iterationdir = os.path.dirname(iterationpath) + iterationfile = os.path.basename(iterationpath) + if iterationdir: + mkdir_p(iterationdir) + copy_file(fuzzed_file.path, + os.path.join(iterationdir, iterationfile)) + fullpath_fuzzed_file = iterationpath + + config = load_and_fix_config(cfg_file) + + cmd_as_args = get_command_args_list( + config['target']['cmdline_template'], fullpath_fuzzed_file)[1] + args = [] + + pin = os.path.expanduser('~/pin/pin') + pintool = os.path.expanduser('~/pintool/calltrace.so') + args = [pin, '-injection', 'child', '-t', + pintool, '-o', outfile] + if options.print_args: + args.append('-a') + args.append('--') + args.extend(cmd_as_args) + + logger.info('args %s' % args) + + p = Popen(args, universal_newlines=True) + p.wait() + + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/calltrace.py b/src/linux/tools/calltrace.py new file mode 100644 index 0000000..80d12ca --- /dev/null +++ b/src/linux/tools/calltrace.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +''' +Created on April 28, 2013 + +@organization: cert.org +''' +import os +import sys +try: + from certfuzz.tools.linux.calltrace import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.calltrace import main + +if __name__ == '__main__': + main() From 1f58c98d3ed9601f98af1f70c8c68813c2cfb05a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 7 Sep 2016 15:32:57 -0400 Subject: [PATCH 1114/1169] chmod +x to calltrace.py --- src/linux/tools/calltrace.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 src/linux/tools/calltrace.py diff --git a/src/linux/tools/calltrace.py b/src/linux/tools/calltrace.py old mode 100644 new mode 100755 From 59c39463110656df6ceaa7ff8b52c141cc099a43 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 7 Sep 2016 15:33:53 -0400 Subject: [PATCH 1115/1169] Add stub unit test for calltrace tool --- .../tools/linux/test_calltrace.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/test_certfuzz/tools/linux/test_calltrace.py diff --git a/src/test_certfuzz/tools/linux/test_calltrace.py b/src/test_certfuzz/tools/linux/test_calltrace.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/test_certfuzz/tools/linux/test_calltrace.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From 072dd8a8340d72f3b808eb23e00dae8d3cf8a355 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 19 Sep 2016 14:27:26 -0400 Subject: [PATCH 1116/1169] bump date in license file to 2010-2016 --- LICENSE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 8fd941e..a5c1029 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,7 +3,7 @@ subject to the following terms: # LICENSE # -Copyright © 2016 Carnegie Mellon University. All Rights Reserved. +Copyright © 2010-2016 Carnegie Mellon University. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: 3. Products derived from this software may not include "Carnegie Mellon University," "SEI" and/or "Software Engineering Institute" in the name of such derived product, nor shall "Carnegie Mellon University," "SEI" and/or "Software Engineering Institute" be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact permission@sei.cmu.edu. # ACKNOWLEDGMENTS AND DISCLAIMERS: # -Copyright © 2016 Carnegie Mellon University +Copyright © 2010-2016 Carnegie Mellon University This material is based upon work funded and supported by the Department of Homeland Security under Contract No. FA8721-05-C-0003 with Carnegie Mellon From 883a0c64360f65e90fc33ea69663d32cc60884f0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 19 Sep 2016 14:41:46 -0400 Subject: [PATCH 1117/1169] move setup.py and virtualenv stuff to experimental, as it's not ready yet --- src/{ => experimental}/setup.cfg | 0 src/{ => experimental}/setup.py | 44 ++++++++----------- .../experimental/setup_env}/README.txt | 0 .../setup_env}/deploy_virtualenv.sh | 0 .../experimental/setup_env}/pip_freeze.sh | 0 5 files changed, 18 insertions(+), 26 deletions(-) rename src/{ => experimental}/setup.cfg (100%) rename src/{ => experimental}/setup.py (70%) rename {setup_env => src/experimental/setup_env}/README.txt (100%) rename {setup_env => src/experimental/setup_env}/deploy_virtualenv.sh (100%) rename {setup_env => src/experimental/setup_env}/pip_freeze.sh (100%) diff --git a/src/setup.cfg b/src/experimental/setup.cfg similarity index 100% rename from src/setup.cfg rename to src/experimental/setup.cfg diff --git a/src/setup.py b/src/experimental/setup.py similarity index 70% rename from src/setup.py rename to src/experimental/setup.py index ca888e8..0871f75 100644 --- a/src/setup.py +++ b/src/experimental/setup.py @@ -17,16 +17,16 @@ 'mtsp_enum', 'repro', ], - 'windows': ['clean_windows', - 'copycrashers', - 'drillresults', - 'minimize', - 'mtsp_enum', - 'quickstats', - 'repro', - 'zipdiff', - ], - } + 'windows': ['clean_windows', + 'copycrashers', + 'drillresults', + 'minimize', + 'mtsp_enum', + 'quickstats', + 'repro', + 'zipdiff', + ], + } def _ep_scripts(target_platform): @@ -62,15 +62,10 @@ def _entry_points(): eps['console_scripts'] = console_scripts return eps -#def _scripts(): -# _s = [ -# ] -# return _s - setup(name="CERT_Basic_Fuzzing_Framework", - version="3.0a", - description="CERT Basic Fuzzing Framework 3.0", - author="CERT", + version="2.8", + description="CERT Basic Fuzzing Framework 2.8", + author="CERT/CC Vulnerability Analysis Team", author_email="cert@cert.org", url="http://www.cert.org", maintainer='CERT', @@ -78,17 +73,14 @@ def _entry_points(): download_url='http://www.cert.org/download/bff/', packages=find_packages(where='.'), install_requires=[ - 'pyyaml', -# 'couchdb', - 'numpy', -# 'matplotlib', - ], -# scripts=_scripts(), + 'pyyaml', + 'numpy', + ], entry_points=_entry_points(), include_package_data=True, test_suite='certfuzz.test', license='See LICENSE.txt', data_files=[ - ('', ['LICENSE.txt']) - ] + ('', ['LICENSE.MD']) + ] ) diff --git a/setup_env/README.txt b/src/experimental/setup_env/README.txt similarity index 100% rename from setup_env/README.txt rename to src/experimental/setup_env/README.txt diff --git a/setup_env/deploy_virtualenv.sh b/src/experimental/setup_env/deploy_virtualenv.sh similarity index 100% rename from setup_env/deploy_virtualenv.sh rename to src/experimental/setup_env/deploy_virtualenv.sh diff --git a/setup_env/pip_freeze.sh b/src/experimental/setup_env/pip_freeze.sh similarity index 100% rename from setup_env/pip_freeze.sh rename to src/experimental/setup_env/pip_freeze.sh From b9654791d521d3e41c0b0e6aa8704176204e2dcc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 19 Sep 2016 19:57:03 -0400 Subject: [PATCH 1118/1169] Save .calltrace file if stack trashed. BFF-1004 --- src/certfuzz/testcase/testcase_base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 21ba55e..459d04f 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -128,6 +128,11 @@ def copy_files_to_temp(self): if os.path.exists(corefile): filetools.copy_file(corefile, self.tempdir) + calltracefile = os.path.join( + self.fuzzedfile.dirname, '%s.calltrace' % self.fuzzedfile.basename) + if os.path.exists(calltracefile): + filetools.copy_file(calltracefile, self.tempdir) + new_fuzzedfile = os.path.join(self.tempdir, self.fuzzedfile.basename) self.fuzzedfile = BasicFile(new_fuzzedfile) From 0118cc91104141c5870ca8a1d80b6e8184d9ad63 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 19 Sep 2016 19:57:22 -0400 Subject: [PATCH 1119/1169] Remove unused imports. --- src/certfuzz/tools/linux/calltrace.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/certfuzz/tools/linux/calltrace.py b/src/certfuzz/tools/linux/calltrace.py index b7103b0..020f164 100644 --- a/src/certfuzz/tools/linux/calltrace.py +++ b/src/certfuzz/tools/linux/calltrace.py @@ -13,9 +13,7 @@ try: from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file - from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.debuggers import gdb, crashwrangler # @UnusedImport except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH import sys @@ -23,9 +21,7 @@ parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) from certfuzz.fuzztools.filetools import mkdir_p, all_files, copy_file - from certfuzz import debuggers from certfuzz.file_handlers.basicfile import BasicFile - from certfuzz.debuggers import gdb, crashwrangler # @UnusedImport logger = logging.getLogger() logger.setLevel(logging.WARNING) From b55582a96e371b805b18d2439bd457873ed28d94 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 09:26:54 -0400 Subject: [PATCH 1120/1169] Warn on Python versions that don't check SSL certificates. --- src/certfuzz/tools/common/updatebff.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/certfuzz/tools/common/updatebff.py b/src/certfuzz/tools/common/updatebff.py index ab79645..9472277 100755 --- a/src/certfuzz/tools/common/updatebff.py +++ b/src/certfuzz/tools/common/updatebff.py @@ -6,6 +6,7 @@ import logging import tempfile import os +import sys import time import shutil import urllib @@ -151,6 +152,14 @@ def git_update(uri='https://github.com/CERTCC-Vulnerability-Analysis/certfuzz.gi def zip_update(tempdir, uri='https://github.com/CERTCC-Vulnerability-Analysis/certfuzz/archive/develop.zip', branch='develop'): + + if sys.version_info < (2, 7, 9): + logger.warning( + 'Your python version (%s) does not check SSL certificates! This update will not be secure.' % sys.version) + logger.warning( + 'Consider updating your python version to the latest 2.7.x version.') + time.sleep(10) + import zipfile if branch is 'master': From 6080b403a05302cd20f6ab9d33cc5e583cb2c4da Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 20 Sep 2016 10:25:48 -0400 Subject: [PATCH 1121/1169] update news --- src/linux/NEWS | 5 +++-- src/windows/NEWS.txt | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/linux/NEWS b/src/linux/NEWS index f6aeeca..cbe2ec5 100644 --- a/src/linux/NEWS +++ b/src/linux/NEWS @@ -1,16 +1,17 @@ CERT Basic Fuzzing Framework (BFF) Significant changes -BFF 2.8 (FIXME) +BFF 2.8 (September 30, 2016) * Virtual Machine changes: - Update from Ubuntu 12.04 to 16.04 - Updated version of zzuf * Code changes: + - Improved feature parity across Linux, OS X, and Windows - Use of configurable mutators rather than use zzuf's bitwise mutation - Drillresults is automatically executed at fuzz-time - Simplified configuration - - Support for OSX Maverics, Yosemite, El Capitan, and MacOS Sierra + - Support for OSX Mavericks, Yosemite, El Capitan, and MacOS Sierra - Removed X dependency - "null" runner mode for zzuf, which only detects crashes - Added updatebff.py tool diff --git a/src/windows/NEWS.txt b/src/windows/NEWS.txt index f15fbf0..633e012 100644 --- a/src/windows/NEWS.txt +++ b/src/windows/NEWS.txt @@ -4,10 +4,11 @@ BFF for Windows was formerly known as CERT Failure Observation Engine (FOE) Significant changes -BFF 2.8 (FIXME) +BFF 2.8 (September 30, 2016) * Code changes: - - Convergence of BFF and FOE code into just BFF + - Rebranded FOE as BFF + - Improved feature parity across Linux, OS X, and Windows - Simplified configuration - Added updatebff.py tool - Bug fixes From 4fc5f396026a57d75303c0b44e8dabb422e0f060 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 20 Sep 2016 10:26:05 -0400 Subject: [PATCH 1122/1169] add detailed 2.8 release notes --- src/BFF_2_8_release_notes.txt | 263 ++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 src/BFF_2_8_release_notes.txt diff --git a/src/BFF_2_8_release_notes.txt b/src/BFF_2_8_release_notes.txt new file mode 100644 index 0000000..5de084f --- /dev/null +++ b/src/BFF_2_8_release_notes.txt @@ -0,0 +1,263 @@ + +Release Notes - BFF - Version BFF 2.8 + +** Bug + * [BFF-178] - Updated config file should trump cached config + * [BFF-262] - FOE null runner heisenbug / unable to get md5 issues + * [BFF-294] - BFF on Windows should cancel any remaining timers on finishing campaign + * [BFF-311] - Standalone Windows minimizer is creating zero-length msec files + * [BFF-312] - Minimizer giving up before it should + * [BFF-464] - Issues with OSX installer results directories + * [BFF-485] - tools/repro.py doesn't support OS X + * [BFF-512] - BFF should handle KeyboardInterrupts more gracefully + * [BFF-515] - Permission denied when doing a zip-based minimization to string + * [BFF-521] - Probability out of range in certfuzz.scoring.multiarmed_bandit.arms.base + * [BFF-533] - Pin_calltrace doesn't play nice with minimizer + * [BFF-534] - certfuzz.test.fuzztools.test_rangefinder.Test.test_get_ranges fails sometimes + * [BFF-742] - reboot recovery currently broken in develop branch + * [BFF-743] - git-based make_dist.py zip missing "results" directory + * [BFF-745] - BFF creating bff.log in ~/pintool on Linux + * [BFF-748] - Remove Android code from main development branch + * [BFF-751] - use subprocess.check_call instead of subprocess.call + * [BFF-754] - BFF OSX installer isn't including crashwrangler source zip + * [BFF-756] - Allow batch.sh to take arguments + * [BFF-764] - Calltracefile is not defined in minimizer_base.py + * [BFF-766] - Don't install Python on Yosemite + * [BFF-778] - BFF should check for zzuf at startup + * [BFF-792] - tmp_reaper isn't cleaning up symlinks + * [BFF-793] - gdb output mostly empty on Ubuntu 15.04 + * [BFF-794] - drillresults broken + * [BFF-796] - zzuf won't compile on Ubuntu 15.04 + * [BFF-798] - drillresults can truncate addresses with 64-bit apps + * [BFF-799] - quickstats.sh is broken + * [BFF-800] - repro.py doesn't work + * [BFF-801] - _cache_app probably shouldn't set use_shell=True + * [BFF-802] - repro.py can't find config + * [BFF-803] - restarting the watchdog only makes sense for UbuFuzz + * [BFF-804] - zzuf seeing signal 9 on every gnash iteration + * [BFF-818] - Windows hook isn't detecting crashes + * [BFF-819] - BFF should verify filesystem when checking for already-existing crashes. + * [BFF-821] - drillresults is broken on Windows + * [BFF-824] - UbuFuzz should have python hcluster + * [BFF-825] - UbuFuzz needs X to be able to be zapped + * [BFF-826] - BFF removes bad zip files before it can run them + * [BFF-827] - zzuf copy mode file I/O is redundant in the new BFF architecture + * [BFF-828] - Minimizer should be zip-aware + * [BFF-829] - BFF isn't touching the watchdog file + * [BFF-831] - quickstats.py is broken + * [BFF-834] - Raised errors in minimizer kill the campaign + * [BFF-835] - drillresults shouldn't complain when the debugger file it's looking for doesn't have what it needs + * [BFF-836] - minimizer_plot.py doesn't work + * [BFF-837] - drillresults can use way too much memory + * [BFF-839] - Drop fuzzer doesn't work + * [BFF-840] - BFF attempts to minimize non-minimizeable fuzzers + * [BFF-841] - Linux BFF increments stats twice + * [BFF-843] - When valgrind is disabled, callgrind annotation still runs + * [BFF-844] - BFF raises an exception when the first seedfile is exhausted + * [BFF-845] - BFF doesn't have the capability of removing seedfiles from the set + * [BFF-846] - Debug mode hangs with winpdb stuff installed + * [BFF-847] - Stopping BFF campaign doesn't stop killproc.sh + * [BFF-849] - Total stack corruption: 'AnalyzerEmptyOutputError' is not defined + * [BFF-850] - UbuFuzz needs x86 compatibility libs + * [BFF-851] - EFA is incorrect for LInux ReturnAv + * [BFF-856] - BFF Windows minimizes regardless of what bff.yaml says + * [BFF-860] - Crash recycling isn't working + * [BFF-862] - BFF verify mode never finishs + * [BFF-879] - Many tools don't work anymore, looking for config_linux + * [BFF-880] - BFF incorrectly includes the program name in the run line of the gdb template + * [BFF-885] - BFF stops fuzzing - No such file or directory in output_parsers + * [BFF-886] - BFF doesn't use correct gdb cmdline when minimizing + * [BFF-887] - BFF doesn't work on Windows versions newer than XP + * [BFF-888] - BFF doesn't work on Windows - DummyCfg + * [BFF-889] - BFF minimizer tries to delete files from underneath itself + * [BFF-890] - BFF OSX isn't using the latest code + * [BFF-891] - BFF doesn't support El Capitan + * [BFF-892] - Crashwrangler doesn't support El Capitan + * [BFF-894] - BFF stops fuzzing - in-use file + * [BFF-896] - pyyaml installation triggers cmdline tools installation (asynchronously) on El Capitan + * [BFF-897] - BFF unnecessarily tries to install python packages on El Capitan + * [BFF-898] - BFF needs to look for exc_handler_elcapitan + * [BFF-903] - OSX installer isn't including crashwrangler source zip + * [BFF-904] - After fuzzing for a while, the campaign stops seeing the KeyboardInterrupt + * [BFF-905] - BFF isn't getting register values out of CrashWrangler output + * [BFF-907] - BFF hard-codes the directory of CTT based on ~/bff + * [BFF-908] - UbuFuzz 14.04 doesn't allow watchdog to be started + * [BFF-910] - BFF can leave behind the bff_watchdog file when a campaign quits + * [BFF-912] - BFF keyerror on verify run + * [BFF-913] - drillresults doesn't work for verify mode BFF results + * [BFF-914] - Drillresults on windows only scores the first exception + * [BFF-915] - BFF doesn't exit cleanly when seedfiles are exhausted + * [BFF-918] - BFF isn't keeping track of seedfile try counts + * [BFF-921] - BFF minimizer fails on windows - exclude_unmapped_frames + * [BFF-927] - Windows BFF fails due to NotImplementedError + * [BFF-930] - drillresults thinks that the PC isn't in a loaded module, when it actually is + * [BFF-931] - BFF keep_duplicates option doesn't do anything + * [BFF-932] - Heisenbugs are skipped, regardless of config option + * [BFF-933] - BFF cuts off cdb before it gets the details. + * [BFF-935] - Swap mutator skips zip files + * [BFF-938] - drillresults output is in unix format even on Windows + * [BFF-939] - minimizer on Linux should have option to keep other crashers. + * [BFF-940] - _safe_createzip() takes exactly 2 arguments (3 given) + * [BFF-941] - Watchdog file not touched during standalone minimization + * [BFF-943] - drillresults on Linux is missing some interesting cases + * [BFF-946] - No such thing as a heisenbug if the winrun hook isn't used. + * [BFF-949] - BFF leaves OpenOffice processes behind + * [BFF-952] - BFF on linux no longer hides stdout + * [BFF-954] - drillresults analyzer only lists last exception + * [BFF-955] - drillresults on Windows is missing some interesting cases + * [BFF-957] - BFF is broken on Windows - incorrect number of arguments + * [BFF-961] - TWDF disables itself after enabling itself. + * [BFF-965] - drillresults as an analyzer indicates wrong Fuzzed file: path + * [BFF-967] - BFF shouldn't try to use exploitable on platforms that don't support it + * [BFF-973] - Calltrace parsing can fail + * [BFF-975] - minimizer_log.txt missing from output dirs + * [BFF-978] - Minimizing on system with ASLR and no symbols never stops + * [BFF-984] - valgrind core files being left in BFF directory + * [BFF-985] - debugheap option is ignored on Windows + * [BFF-986] - Minimizer fails to parse total stack corruption + * [BFF-987] - watchcpu key error + * [BFF-988] - Minimizer not including bytemap + * [BFF-989] - Reindroduce keep_unique_faddr support in BFF + * [BFF-990] - tools/repro.py on linux "-f" option doesn't work + * [BFF-991] - tools/minimizer plot doesn't factor in exploitability + * [BFF-992] - tools/callsim.py doesn't factor in exploitability + * [BFF-993] - edb doesn't work in UbuFuzz 16.04 + * [BFF-994] - git edb Goto Address isn't working + * [BFF-995] - BFF pintool has too many invalid_rtn function names + * [BFF-996] - BFF minimizer keep other crashers is saving something other than the new crashes + * [BFF-998] - gdb doesn't always reset frame to 0 after doing a "bt full" + * [BFF-999] - BFF refuses to string minimize total stack corruption + * [BFF-1000] - recycle_crashers option doesn't work if minimization is disabled + * [BFF-1002] - BFF appears to remove each seedfile in verify mode multiple times + + +** New Feature + * [BFF-177] - BFF should support pluggable file mutators + * [BFF-261] - BFF / FOE minimizer should punt on minimizing inconsistent crashes + * [BFF-328] - Valgrind (and subsequently callgrind) should be optional + * [BFF-436] - Debugger output on OSX should embed a "Found using BFF" message + * [BFF-472] - Integrate FOE's campaign/iteration architecture into BFF on linux/osx + * [BFF-473] - drillresults should become an analyzer + * [BFF-475] - BFF should be able to use virtualenv on all platforms + * [BFF-476] - Integrate FOE's runner into BFF + * [BFF-477] - Create a zzuf-based Runner class + * [BFF-478] - Add Analyzers to BFF on Windows + * [BFF-482] - Add verify mode for BFF + * [BFF-484] - Install ncurses-hexedit in UbuFuzz + * [BFF-503] - minimize.py on Linux and OS X is missing the "-k" (keep other crashes) option + * [BFF-505] - Testcases should support multiple persistence options + * [BFF-772] - Update Crashwrangler to version supporting Yosemite + * [BFF-773] - BFF (FOE) doesn't support Python 2.7.6 or later (Popen) + * [BFF-782] - BFF should use YAML config + * [BFF-789] - Use lldb in tools/repro if available. + * [BFF-809] - Include edb 9.20 in Ubufuzz + * [BFF-810] - Include jed in UbuFUzz + * [BFF-811] - include ncurses-hexedit in UbuFuzz + * [BFF-812] - include hexedit in UbuFuzz + * [BFF-813] - Disable ASLR in UbuFuzz + * [BFF-814] - Don't use UUIDs in /etc/fstab + * [BFF-815] - Include cifs-utils in UbuFuzz + * [BFF-816] - UbuFuzz needs the cert-patched zzuf + * [BFF-817] - Set password for "fuzz" user to "test" + * [BFF-838] - Minimizer on Linux doesn't support the feature to keep other crashes + * [BFF-848] - inetc doesn't check ssl certificates + * [BFF-852] - improper results symlink in UbuFuzz + * [BFF-853] - Include bff-dashboard in UbuFuzz + * [BFF-854] - BFF should not use gdbinit + * [BFF-857] - Disable Dr. Konqui before fuzzing + * [BFF-867] - create certfuzz.reporters package for pipeline reporters + * [BFF-1003] - BFF should include pin tool runner + +** Task + * [BFF-746] - Unify BFF and FOE licenses + * [BFF-808] - Use CERT logo as UbuFuzz background + * [BFF-871] - Update Jenkins for new network environment + +** Improvement + * [BFF-207] - refactor/simplify bff_config.py + * [BFF-210] - BFF should use FOE's campaign / iteration objects + * [BFF-269] - FOE should rename msec files for exceptions beyond the first + * [BFF-361] - Refactor certfuzz.scoring.scorable_set + * [BFF-453] - Unique Campaign folders in BFF + * [BFF-468] - Replace fluxbox background in ubufuzz with a CERT/SEI/CMU logo background + * [BFF-469] - bff.cfg has ctrl-Ms + * [BFF-483] - Update BFF installer to include Mavericks version of CrashWrangler + * [BFF-487] - _look_for_crash shows up way too frequently in debug log files + * [BFF-498] - Remove clause 3 (advertising) from FOE license + * [BFF-500] - Include edb 0.9.20 in UbuFuzz + * [BFF-502] - DRY up linux and windows drillresults + * [BFF-506] - BFF's make_dist.py is overly SVN-centric + * [BFF-513] - Eliminate need for debuggers.verify_supported_platform() + * [BFF-514] - replace optparse with argparse in certfuzz.bff.common + * [BFF-528] - BFF should get FOE's rpdb2 / heapy debugger + * [BFF-529] - BFF main module parity across Linux/Windows + * [BFF-530] - Rebrand FOE as BFF + * [BFF-744] - Deduplicate license files + * [BFF-755] - get rid of stop seed in bff config + * [BFF-759] - DRY up iteration_windows and iteration_linux + * [BFF-760] - linux drillresults should cache parsed results + * [BFF-771] - use !exploitable 1.6 + * [BFF-774] - Gracefully handle unavailable network + * [BFF-785] - BFF debuggers should be instantiable without requiring an actual debugger present + * [BFF-786] - Get rid of configurable debuggers + * [BFF-788] - BFF on windows should pick the appropriate runner automatically + * [BFF-797] - Update welcome.sh script + * [BFF-858] - BFF should use something other than stdout to collect gdb output + * [BFF-863] - Unify config file options on windows and linux + * [BFF-864] - Linux and Windows are inconsistent in their use of self.cfg vs self.cfg.config + * [BFF-866] - Move certfuzz tests to a peer package of certfuzz + * [BFF-875] - Create testcase logger reporter module + * [BFF-884] - Allow or warn for the presence of IO redirects in command line + * [BFF-893] - BFF runner shouldn't need to be configurable on Windows + * [BFF-899] - remove minimize to string from campaign (keep in standalone) + * [BFF-900] - drillresults should support crashwrangler + * [BFF-902] - BFF should retain double file extensions. e.g. .tar.gz + * [BFF-916] - subprocess_helper should cancel any remaining timers on KeyboardInterrupt + * [BFF-922] - Use a fuzzdir on the local disk. + * [BFF-923] - Drillresults analyzer should use the BFF debugger file parsers + * [BFF-925] - DRY up LinuxTestcase and WindowsTestcase classes + * [BFF-934] - BFF should exhaustively iterate through the end of minimization + * [BFF-947] - BFF can die between tmp_reaper and watchdog file being recreated. + * [BFF-948] - String minimization isn't using the bytemap for the freebie + * [BFF-953] - Replace or remove bff_stats.py + * [BFF-956] - Eliminate per-crash logging + * [BFF-959] - drillresults force mode should ignore existing .drillresults files + * [BFF-963] - Update dependencies installed by BFF installer on Windows + * [BFF-966] - Give debugger-invoked iterations extra time to prevent heisenbugs + * [BFF-968] - BFF should save core dumps for crashes + * [BFF-970] - drillresults should support analyzing a string minimization directory + * [BFF-971] - BFF should clean up empty crash hash directories + * [BFF-972] - BFF should use exploitability in output directory again + * [BFF-979] - Remove testcase-specific logging + * [BFF-980] - remove minimizer_plot.py + * [BFF-982] - Remove killproc.sh + * [BFF-997] - drillresults should fall back to getting the current instruciton w/o "=>" + +** Sub-task + * [BFF-417] - ScorableSet should not rely on its objects to inherit from ScorableThing + * [BFF-418] - ScorableSet should support use as a generator + * [BFF-419] - ScorableSet should support multiple selection algorithms + * [BFF-420] - Eliminate ScorableThing code + * [BFF-421] - ScorableSet should initialize new additions to have the average of the set as the initial probability + * [BFF-479] - bff.py should become an entry point in setup.py + * [BFF-480] - foe2.py should become an entry point in setup.py + * [BFF-494] - Remove or modify xcat reference in nsis_mid.txt + * [BFF-495] - Refactor build/dist/__init__.py to parameterize svn repo location + * [BFF-507] - fix make_dist for linux + * [BFF-508] - fix make_dist for windows + * [BFF-509] - fix make_dist for osx + * [BFF-531] - Remove mentions of "foe" from files + * [BFF-532] - Remove mentions of "foe" from file names + * [BFF-739] - Create script to generate epydocs for certfuzz + * [BFF-749] - LinuxCampaign should inherit from CampaignMeta + * [BFF-750] - LinuxCampaign should inherit from CampaignBase + * [BFF-752] - replace subprocess.call with subprocess.check_call in build scripts + * [BFF-753] - replace subprocess.call with subprocess.check_call in certfuzz.fuzztools.subprocess_helper + * [BFF-758] - Integrate iteration_base and iteration_base3 + * [BFF-762] - Refactor drillresults into OO code + * [BFF-763] - Create certfuzz.drillresults package + * [BFF-775] - create zzuf-based Fuzzer object + * [BFF-872] - Confirm that ByteMut works on linux + * [BFF-873] - Make fuzzer selection on linux configurable + * [BFF-876] - eradicate "fancy" config objects in favor of simple yaml -> dicts + * [BFF-877] - unify config sections From 366930514286249e0bdff08578125c89597b3b5d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 20 Sep 2016 13:14:02 -0400 Subject: [PATCH 1123/1169] move linux minimizer plot to experimental --- .../stats_and_other_tools/linux_minimizer_plot.py} | 0 src/experimental/stats_and_other_tools/minimizer_plot.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/{certfuzz/tools/linux/minimizer_plot.py => experimental/stats_and_other_tools/linux_minimizer_plot.py} (100%) diff --git a/src/certfuzz/tools/linux/minimizer_plot.py b/src/experimental/stats_and_other_tools/linux_minimizer_plot.py similarity index 100% rename from src/certfuzz/tools/linux/minimizer_plot.py rename to src/experimental/stats_and_other_tools/linux_minimizer_plot.py diff --git a/src/experimental/stats_and_other_tools/minimizer_plot.py b/src/experimental/stats_and_other_tools/minimizer_plot.py index 3ec2590..dbf2959 100755 --- a/src/experimental/stats_and_other_tools/minimizer_plot.py +++ b/src/experimental/stats_and_other_tools/minimizer_plot.py @@ -7,13 +7,13 @@ import os import sys try: - from certfuzz.tools.linux.minimizer_plot import main + from linux_minimizer_plot import main except ImportError: # if we got here, we probably don't have .. in our PYTHONPATH mydir = os.path.dirname(os.path.abspath(__file__)) parentdir = os.path.abspath(os.path.join(mydir, '..')) sys.path.append(parentdir) - from certfuzz.tools.linux.minimizer_plot import main + from linux_minimizer_plot import main if __name__ == '__main__': main() From 59a18c8b1edc3d87563e8aaba3dd1cd378ab789f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 20 Sep 2016 13:14:15 -0400 Subject: [PATCH 1124/1169] update experimental readme --- src/experimental/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/experimental/README.md b/src/experimental/README.md index 1ab773e..26d9627 100644 --- a/src/experimental/README.md +++ b/src/experimental/README.md @@ -5,3 +5,16 @@ These are things that were spinoffs from the main BFF and FOE development effort ## aws ## This was a student project by Shaun Blackburn. We gave him a copy of BFF 2.6 and asked him to make it work in AWS, which he did. The resulting cloudinit script is here, along with some slides and a readme for background. It worked in the spring of 2013, but we are not actively maintaining it. + +## setup_env ## + +We started to look at being able to use BFF in a virtualenv, but since we usually recommend dedicated VMs for fuzzing this turned out to be less useful than we originally thought, so we didn't proceed any further. We might come back to this in the future though. + +## setup.py ## + +Pretty much the same argument as setup_env above. We might revisit it later but it wasn't necessary for now. + +## stats_and_other_tools ## + +The new architecture we implemented in BFF 2.8 broke some older tools that were originally useful as we developed minimizer but weren't all that useful for people fuzzing with our tools. Just moving the entry point script out of the way here. + From 375c88ac4706ab24360e00aafb1b065ed19fed22 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 14:42:04 -0400 Subject: [PATCH 1125/1169] Don't change CWD into the target app directory for the first cacheing invocation. BFF-1006 --- src/certfuzz/campaign/campaign_windows.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/certfuzz/campaign/campaign_windows.py b/src/certfuzz/campaign/campaign_windows.py index 74d8f54..b5cd20d 100644 --- a/src/certfuzz/campaign/campaign_windows.py +++ b/src/certfuzz/campaign/campaign_windows.py @@ -81,14 +81,13 @@ def _cache_app(self): logger.debug( 'Caching application %s and determining if we need to watch the CPU...', self.program) sf = self.seedfile_set.next_item() - targetdir = os.path.dirname(self.program) cmdargs = get_command_args_list( self.config['target']['cmdline_template'], infile=sf.path)[1] logger.info('Invoking %s' % cmdargs) # Use overriden Popen that uses a job object to make sure that # child processes are killed - p = Popen(cmdargs, cwd=targetdir) + p = Popen(cmdargs) runtimeout = self.config['runner']['runtimeout'] logger.debug('...Timer: %f', runtimeout) t = Timer(runtimeout, self.kill, args=[p]) From 9f1f513065f52fc14a976db147026d60e630e1b5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 15:31:32 -0400 Subject: [PATCH 1126/1169] Heisenbugs need an output directory, too. BFF-1005 --- src/certfuzz/tc_pipeline/tc_pipeline_windows.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py index e2a79a9..b7c3dc6 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_windows.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_windows.py @@ -80,6 +80,7 @@ def keep_testcase(self, testcase): elif self.options['null_runner']: return (False, 'not a crash') elif self.options['keep_heisenbugs']: + target_dir = testcase._get_output_dir(self.outdir) return (True, 'heisenbug') else: return (False, 'skip heisenbugs') From ed07b67cd0d064dbdadd7df6ef9163ab55063d03 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 15:42:45 -0400 Subject: [PATCH 1127/1169] Don't set the CWD in winrun or repro either. BFF-1006 --- src/certfuzz/runners/winrun.py | 6 ++---- src/certfuzz/tools/windows/repro.py | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/runners/winrun.py b/src/certfuzz/runners/winrun.py index f0caace..25d897f 100644 --- a/src/certfuzz/runners/winrun.py +++ b/src/certfuzz/runners/winrun.py @@ -230,8 +230,6 @@ def _run(self): bounded by self.runtimeout ''' logger.debug('Running: %s %s', self.cmdlist, self.workingdir) - targetdir = os.path.dirname(self.cmdlist[0]) - logger.debug('from directory: %s', targetdir) process_info = {} id = None done = False @@ -240,10 +238,10 @@ def _run(self): # set timeout(s) # run program if self.hideoutput: - p = Popen(self.cmdlist, cwd=targetdir, stdout=open( + p = Popen(self.cmdlist, stdout=open( os.devnull), stderr=open(os.devnull)) else: - p = Popen(self.cmdlist, cwd=targetdir) + p = Popen(self.cmdlist) if self.watchcpu == True: # Initialize things used for CPU monitoring diff --git a/src/certfuzz/tools/windows/repro.py b/src/certfuzz/tools/windows/repro.py index 15af8e4..6f8341f 100644 --- a/src/certfuzz/tools/windows/repro.py +++ b/src/certfuzz/tools/windows/repro.py @@ -103,7 +103,6 @@ def main(): cmd_as_args = get_command_args_list( config['target']['cmdline_template'], fuzzed_file.path)[1] - targetdir = os.path.dirname(cmd_as_args[0]) args = [] @@ -133,7 +132,7 @@ def main(): args.extend(cmd_as_args) logger.info('args %s' % cmd_as_args) - p = Popen(args, cwd=targetdir, universal_newlines=True) + p = Popen(args, universal_newlines=True) p.wait() if __name__ == '__main__': From e422dea7134e84d8469270c3b7e787e41f9dde26 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 15:51:52 -0400 Subject: [PATCH 1128/1169] Minor news / release notes tweaks. --- src/BFF_2_8_release_notes.txt | 3 +++ src/windows/NEWS.txt | 1 + 2 files changed, 4 insertions(+) diff --git a/src/BFF_2_8_release_notes.txt b/src/BFF_2_8_release_notes.txt index 5de084f..9b3abb3 100644 --- a/src/BFF_2_8_release_notes.txt +++ b/src/BFF_2_8_release_notes.txt @@ -129,6 +129,9 @@ Release Notes - BFF - Version BFF 2.8 * [BFF-999] - BFF refuses to string minimize total stack corruption * [BFF-1000] - recycle_crashers option doesn't work if minimization is disabled * [BFF-1002] - BFF appears to remove each seedfile in verify mode multiple times + * [BFF-1004] - .calltrace files are missing from output dirs + * [BFF-1005] - BFF crashes on attempting to save a heisenbug + * [BFF-1006] - BFF shouldn't change into the target directory on first iteration or winrun or repro ** New Feature diff --git a/src/windows/NEWS.txt b/src/windows/NEWS.txt index 633e012..3de5840 100644 --- a/src/windows/NEWS.txt +++ b/src/windows/NEWS.txt @@ -10,6 +10,7 @@ BFF 2.8 (September 30, 2016) - Rebranded FOE as BFF - Improved feature parity across Linux, OS X, and Windows - Simplified configuration + - Drillresults is automatically executed at fuzz-time - Added updatebff.py tool - Bug fixes From b9266becc03f484c6c1d7ca3a0f24803e2ea931a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 17:46:14 -0400 Subject: [PATCH 1129/1169] Remove stray comma. BFF-1007 --- src/certfuzz/analyzers/drillresults/testcasebundle_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py index 2693b74..00b0cd0 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py @@ -17,7 +17,7 @@ RE_MAPPED_ADDRESS = re.compile( '^ModLoad: ([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)') RE_MAPPED_ADDRESS64 = re.compile( - '^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)'), + '^ModLoad: ([0-9a-fA-F]+`[0-9a-fA-F]+)\s+([0-9a-fA-F]+`[0-9a-fA-F]+)\s+(.+)') RE_SYSWOW64 = re.compile('ModLoad:.*syswow64.*', re.IGNORECASE) From caa545988760114607730131462251d5a2d771df Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 18:13:09 -0400 Subject: [PATCH 1130/1169] Don't stop parsing msec files if a bad one is encountered. BFF-1008 --- src/certfuzz/drillresults/result_driller_windows.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/certfuzz/drillresults/result_driller_windows.py b/src/certfuzz/drillresults/result_driller_windows.py index 9b84b20..566a87e 100644 --- a/src/certfuzz/drillresults/result_driller_windows.py +++ b/src/certfuzz/drillresults/result_driller_windows.py @@ -7,6 +7,7 @@ from certfuzz.analyzers.drillresults.testcasebundle_windows import WindowsTestCaseBundle as TestCaseBundle from certfuzz.drillresults.result_driller_base import ResultDriller +from certfuzz.drillresults.errors import TestCaseBundleError logger = logging.getLogger(__name__) @@ -58,7 +59,12 @@ def _platform_find_testcases(self, crash_dir, files, root, force=False): crasherfile = os.path.join(root, crasherfile) with TestCaseBundle(dbg_file, crasherfile, crash_dir, self.ignore_jit) as tcb: - tcb.go() + try: + tcb.go() + except TestCaseBundleError: + # Nothing useful in this msec file + continue + _updated_existing = False # if not self.testcase_bundles: # continue From e6b1044d6d63b4d31eafdbf9a6bcb2e45a8cfba5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 18:16:20 -0400 Subject: [PATCH 1131/1169] ExceptionHandlerCorrupted is an interesting crash too. --- src/certfuzz/analyzers/drillresults/testcasebundle_windows.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py index 00b0cd0..e940c5a 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py @@ -30,6 +30,7 @@ class WindowsTestCaseBundle(TestCaseBundle): 'DEPViolation', 'IllegalInstruction', 'PrivilegedInstruction', + 'ExceptionHandlerCorrupted', ] def __init__(self, dbg_outfile, testcase_file, crash_hash, From bd9180fc904f7c8729c13d555f27469032be218f Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 18:37:58 -0400 Subject: [PATCH 1132/1169] drillresults on a 64-bit platform needs to check for 32-bit and 64-bit loaded module patterns both. BFF-1009 --- .../drillresults/testcasebundle_windows.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py index e940c5a..c3cd007 100644 --- a/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py +++ b/src/certfuzz/analyzers/drillresults/testcasebundle_windows.py @@ -106,27 +106,30 @@ def _look_for_loaded_module(self, instraddr, line): :param instraddr: :param line: ''' - pattern = RE_MAPPED_ADDRESS + patterns = [RE_MAPPED_ADDRESS] if self._64bit_debugger: - pattern = RE_MAPPED_ADDRESS64 + # With a 64-bit debugger, we need to check for both 64-bit and + # 32-bit style loaded module regexes + patterns.append(RE_MAPPED_ADDRESS64) # convert to an int as hex instraddr = instraddr.replace('`', '') instraddr = int(instraddr, 16) - n = re.match(pattern, line) - if n: - # Strip out backticks present on 64-bit systems - begin_address = int(n.group(1).replace('`', ''), 16) - end_address = int(n.group(2).replace('`', ''), 16) - module_name = n.group(3) - logger.debug( - '%x %x %s %x', begin_address, end_address, module_name, instraddr) - if begin_address < instraddr < end_address: - logger.debug('Matched: %x in %x %x %s', instraddr, - begin_address, end_address, module_name) - # as soon as we find this, we're done - return module_name + for pattern in patterns: + n = re.match(pattern, line) + if n: + # Strip out backticks present on 64-bit systems + begin_address = int(n.group(1).replace('`', ''), 16) + end_address = int(n.group(2).replace('`', ''), 16) + module_name = n.group(3) + logger.debug( + '%x %x %s %x', begin_address, end_address, module_name, instraddr) + if begin_address < instraddr < end_address: + logger.debug('Matched: %x in %x %x %s', instraddr, + begin_address, end_address, module_name) + # as soon as we find this, we're done + return module_name def fix_efa_offset(self, instructionline, faultaddr): ''' From a9e80f7520b9364f75fe3e90bff314b24ccb60cc Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 18:39:38 -0400 Subject: [PATCH 1133/1169] Release notes update --- src/BFF_2_8_release_notes.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/BFF_2_8_release_notes.txt b/src/BFF_2_8_release_notes.txt index 9b3abb3..fea6fc6 100644 --- a/src/BFF_2_8_release_notes.txt +++ b/src/BFF_2_8_release_notes.txt @@ -132,6 +132,10 @@ Release Notes - BFF - Version BFF 2.8 * [BFF-1004] - .calltrace files are missing from output dirs * [BFF-1005] - BFF crashes on attempting to save a heisenbug * [BFF-1006] - BFF shouldn't change into the target directory on first iteration or winrun or repro + * [BFF-1007] - Drillresults fails on 64-bit windows + * [BFF-1008] - Dud msec files shouldn't stop drillresults + * [BFF-1009] - drillresults on 64-bit windows needs to check for 32-bit and 64-bit loaded module patterns + ** New Feature From cc804d038c8dd0a6035c714bd59374522fe943c9 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 20 Sep 2016 23:56:33 -0400 Subject: [PATCH 1134/1169] Document urllib HTTPS proxy limitations. --- src/linux/README | 9 +++++++++ src/windows/README.txt | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/linux/README b/src/linux/README index 3ee976f..14edc06 100644 --- a/src/linux/README +++ b/src/linux/README @@ -229,6 +229,15 @@ BFF includes a script to update itself using the code present on GitHub tools/updatebff.py +This script will first attempt to use GitPython to retrieve updates. If this +package is not available, it will fall back to using the system-wide installed +git binary. Finally, if git is not available, it will fall back to using +python urllib. + +Note that urllib does not support retrieving HTTPS URIs through a proxy. If +your machines are behind a proxy, you must have either GitPython or git +installed to use the updatebff.py script. + ===== Fuzzing on your own ===== diff --git a/src/windows/README.txt b/src/windows/README.txt index 7d56fbe..08ee807 100644 --- a/src/windows/README.txt +++ b/src/windows/README.txt @@ -185,6 +185,15 @@ BFF includes a script to update itself using the code present on GitHub tools\updatebff.py +This script will first attempt to use GitPython to retrieve updates. If this +package is not available, it will fall back to using the system-wide installed +git binary. Finally, if git is not available, it will fall back to using +python urllib. + +Note that urllib does not support retrieving HTTPS URIs through a proxy. If +your machines are behind a proxy, you must have either GitPython or git +installed to use the updatebff.py script. + ===== Digging deeper into results ===== ======================================= From 8310b7f6eadfb95b161573f24c2026b4e9529fff Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 21 Sep 2016 16:45:05 -0400 Subject: [PATCH 1135/1169] Don't overload UKNOWN exploitability ranking. It's not the same as not-yet measured. BFF-1011 --- src/certfuzz/testcase/testcase_base.py | 2 +- src/certfuzz/testcase/testcase_windows.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 459d04f..a625241 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -48,7 +48,7 @@ def __init__(self, self.debugger_template = None self.debugger_timeout = dbg_timeout # Exploitability is UNKNOWN unless proven otherwise - self.exp = 'UNKNOWN' + self.exp = 'UNDETERMINED' self.hd_bits = None self.hd_bytes = None self.faddr = None diff --git a/src/certfuzz/testcase/testcase_windows.py b/src/certfuzz/testcase/testcase_windows.py index d5abe6c..56b7d93 100644 --- a/src/certfuzz/testcase/testcase_windows.py +++ b/src/certfuzz/testcase/testcase_windows.py @@ -27,6 +27,7 @@ def logerror(func, path, excinfo): 'PROBABLY_EXPLOITABLE': 'PEX', 'EXPLOITABLE': 'EXP', 'HEISENBUG': 'HSB', + 'UNDETERMINED': 'UND', } exp_rank = { @@ -35,6 +36,7 @@ def logerror(func, path, excinfo): 'UNKNOWN': 3, 'PROBABLY_NOT_EXPLOITABLE': 4, 'HEISENBUG': 5, + 'UNDETERMINED': 6, } @@ -100,7 +102,7 @@ def update_crash_details(self): # Reset properties that need to be regenerated self.exception_depth = 0 self.parsed_outputs = [] - self.exp = 'UNKNOWN' + self.exp = 'UNDETERMINED' fname = self._get_file_basename() outfile_base = os.path.join(self.tempdir, fname) # Regenerate target commandline with new crasher file @@ -132,6 +134,7 @@ def debug_once(self): return # Store highest exploitability of every exception in the chain + # raw_input('exp') current_exception_exp = self.parsed_outputs[self.exception_depth].exp if current_exception_exp: if not self.exp: From 312810b0a2c12c72aac4b5a2eb92a83c98552c12 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 21 Sep 2016 16:46:15 -0400 Subject: [PATCH 1136/1169] Release notes update. --- src/BFF_2_8_release_notes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BFF_2_8_release_notes.txt b/src/BFF_2_8_release_notes.txt index fea6fc6..23f8c86 100644 --- a/src/BFF_2_8_release_notes.txt +++ b/src/BFF_2_8_release_notes.txt @@ -135,6 +135,7 @@ Release Notes - BFF - Version BFF 2.8 * [BFF-1007] - Drillresults fails on 64-bit windows * [BFF-1008] - Dud msec files shouldn't stop drillresults * [BFF-1009] - drillresults on 64-bit windows needs to check for 32-bit and 64-bit loaded module patterns + * [BFF-1011] - BFF on Windows is putting PROBABLY_NOT_EXPLOITABLE crashers in UNKNOWN directory From adc133c1f02edcb12036033fb803635e648ca898 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 21 Sep 2016 16:51:49 -0400 Subject: [PATCH 1137/1169] Save old certfuzz directory in case something goes wrong during update. --- src/certfuzz/tools/common/updatebff.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/certfuzz/tools/common/updatebff.py b/src/certfuzz/tools/common/updatebff.py index 9472277..b649b9b 100755 --- a/src/certfuzz/tools/common/updatebff.py +++ b/src/certfuzz/tools/common/updatebff.py @@ -61,8 +61,6 @@ def main(): help='Enable debug messages') parser.add_option('-m', '--master', dest='master', action='store_true', help='Use master branch instead of develop') - parser.add_option('-s', '--save', dest='save', - action='store_true', help='Save original certfuzz directory') (options, args) = parser.parse_args() @@ -78,16 +76,12 @@ def main(): tempdir = git_update(branch=branch) - if options.save: - logger.debug('Saving original certfuzz directory as certfuzz.bak') - old_certfuzz = '%s.bak' % certfuzz_dir - if os.path.isdir(old_certfuzz): - logger.debug('Removing old certfuzz directory: %s' % old_certfuzz) - rm_rf(old_certfuzz) - os.rename(certfuzz_dir, old_certfuzz) - else: - rm_rf(certfuzz_dir) - logger.debug('Deleting certfuzz directory...') + logger.debug('Saving original certfuzz directory as certfuzz.bak') + old_certfuzz = '%s.bak' % certfuzz_dir + if os.path.isdir(old_certfuzz): + logger.debug('Removing old certfuzz directory: %s' % old_certfuzz) + rm_rf(old_certfuzz) + os.rename(certfuzz_dir, old_certfuzz) logger.info('Moving certfuzz directory from git clone...') copydir(os.path.join(tempdir, 'src', 'certfuzz'), From 34ccdc579caa113789abefa50b6f8aeef9c36058 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sat, 24 Sep 2016 18:20:01 -0400 Subject: [PATCH 1138/1169] Allow string minimization (to x) during campaign. BFF-1013 --- src/certfuzz/minimizer/minimizer_base.py | 5 ++++- src/certfuzz/minimizer/win_minimizer.py | 2 +- src/certfuzz/tc_pipeline/tc_pipeline_base.py | 15 +++++++++++++-- src/linux/configs/bff.yaml | 5 ++++- src/windows/configs/examples/bff.yaml | 5 ++++- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/certfuzz/minimizer/minimizer_base.py b/src/certfuzz/minimizer/minimizer_base.py index 66ad3a4..040058d 100644 --- a/src/certfuzz/minimizer/minimizer_base.py +++ b/src/certfuzz/minimizer/minimizer_base.py @@ -39,7 +39,7 @@ class Minimizer(object): def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, seedfile_as_target=False, bitwise=False, confidence=0.999, - logfile=None, tempdir=None, maxtime=3600, preferx=False, keep_uniq_faddr=False, watchcpu=False): + logfile=None, tempdir=None, maxtime=3600, preferx=True, keep_uniq_faddr=False, watchcpu=False): if not cfg: self._raise('Config must be specified') @@ -136,6 +136,9 @@ def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, self.other_crashes = {} self.target_size_guess = 1 + if str(cfg['runoptions']['minimize']).lower() == 'string': + # Assume that a string minimization has a larger target (structure) + self.target_size_guess = 100 self.total_tries = 0 self.total_misses = 0 self.consecutive_misses = 0 diff --git a/src/certfuzz/minimizer/win_minimizer.py b/src/certfuzz/minimizer/win_minimizer.py index a88b9e7..b05aefc 100755 --- a/src/certfuzz/minimizer/win_minimizer.py +++ b/src/certfuzz/minimizer/win_minimizer.py @@ -18,7 +18,7 @@ class WindowsMinimizer(MinimizerBase): def __init__(self, cfg=None, testcase=None, crash_dst_dir=None, seedfile_as_target=False, bitwise=False, confidence=0.999, - logfile=None, tempdir=None, maxtime=3600, preferx=False, + logfile=None, tempdir=None, maxtime=3600, preferx=True, keep_uniq_faddr=False, watchcpu=False): self.saved_arcinfo = None diff --git a/src/certfuzz/tc_pipeline/tc_pipeline_base.py b/src/certfuzz/tc_pipeline/tc_pipeline_base.py index cdb98f9..e17d822 100644 --- a/src/certfuzz/tc_pipeline/tc_pipeline_base.py +++ b/src/certfuzz/tc_pipeline/tc_pipeline_base.py @@ -196,16 +196,27 @@ def _minimize(self, testcase): logger.info('Minimizing testcase %s', testcase.signature) logger.debug('config = %s', self.cfg) + # Default to minimizing to the seed file + seedfile_as_target = True + confidence = 0.999 + if not self.options.get('minimizable'): # short-circuit if not minimizing return + minimize_option = str(self.cfg['runoptions']['minimize']).lower() + if minimize_option == 'string': + # We are not minimizing to the seedfile, but rather a string of 'x' + # chars + seedfile_as_target = False + confidence = 0.5 + # build arguments for minimizer invocation kwargs = {'cfg': self.cfg, 'testcase': testcase, - 'seedfile_as_target': True, + 'seedfile_as_target': seedfile_as_target, 'bitwise': False, - 'confidence': 0.999, + 'confidence': confidence, 'tempdir': self.working_dir, 'maxtime': self.cfg['runoptions']['minimizer_timeout'], 'keep_uniq_faddr': self.cfg['runoptions']['keep_unique_faddr'], diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index e6c3e62..eb0d061 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -92,8 +92,11 @@ debugger: # Number of times to mutate a seed file before moving to the next # # minimize: -# Create a file that is minimally-different than the seed file, yet crashes +# True: Create a file that is minimally-different than the seed file, yet crashes # with the same hash +# String: Create a file that is mostly 'x' (0x78) characters, yet crashes with +# the same hash +# False: Don't minimize # # minimizer_timeout: # The maximum amount of time that BFF will spend on a minimization run before diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index 2a27478..d1a65e8 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -121,8 +121,11 @@ debugger: # mutation range. Default is 1 if not present. # # minimize: -# Create a file that is minimally-different than the seed file, yet crashes +# True: Create a file that is minimally-different than the seed file, yet crashes # with the same hash +# String: Create a file that is mostly 'x' (0x78) characters, yet crashes with +# the same hash +# False: Don't minimize # # minimizer_timeout: # The maximum amount of time that BFF will spend on a minimization run before From 5c15b7da0a148fe31a094413f8b0284faa7d1b63 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sat, 24 Sep 2016 18:48:35 -0400 Subject: [PATCH 1139/1169] Fix unit test --- src/test_certfuzz/mocks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test_certfuzz/mocks.py b/src/test_certfuzz/mocks.py index 259cdf7..0e61111 100644 --- a/src/test_certfuzz/mocks.py +++ b/src/test_certfuzz/mocks.py @@ -152,7 +152,8 @@ def __init__(self, templated=True): self['fuzzer'] = {'fuzzer': 'bytemut'} self['campaign'] = {'id': 'xyz'} self['runoptions'] = {'first_iteration': 0, - 'seed_interval': 10} + 'seed_interval': 10, + 'minimize': True} self['runner'] = {'runner': 'zzufrun', 'runtimeout': 5} if templated: From 97fac3cd182dd730890e8c10ea7ef5f2db85f9c5 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sat, 24 Sep 2016 19:03:54 -0400 Subject: [PATCH 1140/1169] Add string minimization to release notes --- src/BFF_2_8_release_notes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BFF_2_8_release_notes.txt b/src/BFF_2_8_release_notes.txt index 23f8c86..515a0f0 100644 --- a/src/BFF_2_8_release_notes.txt +++ b/src/BFF_2_8_release_notes.txt @@ -136,6 +136,7 @@ Release Notes - BFF - Version BFF 2.8 * [BFF-1008] - Dud msec files shouldn't stop drillresults * [BFF-1009] - drillresults on 64-bit windows needs to check for 32-bit and 64-bit loaded module patterns * [BFF-1011] - BFF on Windows is putting PROBABLY_NOT_EXPLOITABLE crashers in UNKNOWN directory + * [BFF-1013] - Include minimize-to-string option in BFF campaign From fac285f629dfb82e294d43c749dfe98a986b5f03 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sun, 25 Sep 2016 20:31:39 -0400 Subject: [PATCH 1141/1169] Allow verify mode to perform string minimization. BFF-1015 --- src/certfuzz/iteration/iteration_base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/iteration/iteration_base.py b/src/certfuzz/iteration/iteration_base.py index 3991f9b..701feac 100644 --- a/src/certfuzz/iteration/iteration_base.py +++ b/src/certfuzz/iteration/iteration_base.py @@ -48,8 +48,12 @@ def __init__(self, self.r = None - self.pipeline_options = {'minimizable': self.fuzzer_cls.is_minimizable and self.cfg[ - 'runoptions'].get('minimize', False), } + minimizable = self.fuzzer_cls.is_minimizable and self.cfg[ + 'runoptions'].get('minimize', False) + if str(config['fuzzer']['fuzzer']).lower() == 'verify' and str(self.cfg['runoptions']['minimize']).lower() == 'string': + # We will perform string minimization in verify mode + minimizable = True + self.pipeline_options = {'minimizable': minimizable, } if uniq_func is None: self.uniq_func = lambda _tc_id: True From 1a802cf06edffaacbf9abf144c5e02ff7771da41 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sun, 25 Sep 2016 20:32:12 -0400 Subject: [PATCH 1142/1169] EFAs reported by GDB aren't valid for SIGABRT. BFF-1014 --- src/certfuzz/debuggers/output_parsers/debugger_file_base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py index 320895d..d82ee63 100644 --- a/src/certfuzz/debuggers/output_parsers/debugger_file_base.py +++ b/src/certfuzz/debuggers/output_parsers/debugger_file_base.py @@ -416,6 +416,10 @@ def _look_for_signal(self, line): if m: self.signal = m.group(1) logger.debug('Signal: %s', self.signal) + if self.signal == 'SIGABRT': + # If we have a SIGABRT, the gdb-reported faulting address isn't + # accurate. + self.faddr = '0' def _look_for_faddr(self, line): if self.faddr: From 48a76a6fe763e0aabe95a492ef6a1b7bd6b0ce55 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sun, 25 Sep 2016 20:32:28 -0400 Subject: [PATCH 1143/1169] Yes, we use faddr. --- src/certfuzz/iteration/iteration_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/iteration/iteration_windows.py b/src/certfuzz/iteration/iteration_windows.py index d36f1e1..b667603 100644 --- a/src/certfuzz/iteration/iteration_windows.py +++ b/src/certfuzz/iteration/iteration_windows.py @@ -48,7 +48,7 @@ def __init__(self, ) self.debug = debug - # TODO: do we use keep_uniq_faddr at all? + self.keep_uniq_faddr = config['runoptions'].get( 'keep_unique_faddr', False) From f1338b8c5594be7c51c23bd0dde3d124b1643639 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sun, 25 Sep 2016 20:32:41 -0400 Subject: [PATCH 1144/1169] "String" -> "string" --- src/linux/configs/bff.yaml | 2 +- src/windows/configs/examples/bff.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linux/configs/bff.yaml b/src/linux/configs/bff.yaml index eb0d061..79d4d07 100644 --- a/src/linux/configs/bff.yaml +++ b/src/linux/configs/bff.yaml @@ -94,7 +94,7 @@ debugger: # minimize: # True: Create a file that is minimally-different than the seed file, yet crashes # with the same hash -# String: Create a file that is mostly 'x' (0x78) characters, yet crashes with +# string: Create a file that is mostly 'x' (0x78) characters, yet crashes with # the same hash # False: Don't minimize # diff --git a/src/windows/configs/examples/bff.yaml b/src/windows/configs/examples/bff.yaml index d1a65e8..0fc891f 100644 --- a/src/windows/configs/examples/bff.yaml +++ b/src/windows/configs/examples/bff.yaml @@ -123,7 +123,7 @@ debugger: # minimize: # True: Create a file that is minimally-different than the seed file, yet crashes # with the same hash -# String: Create a file that is mostly 'x' (0x78) characters, yet crashes with +# string: Create a file that is mostly 'x' (0x78) characters, yet crashes with # the same hash # False: Don't minimize # From 21a3e510124257664884c8caeadef49f48706bb8 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sun, 25 Sep 2016 20:33:00 -0400 Subject: [PATCH 1145/1169] "VMware" -> "VM" --- src/linux/README | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/linux/README b/src/linux/README index 14edc06..c025526 100644 --- a/src/linux/README +++ b/src/linux/README @@ -9,6 +9,9 @@ See the NEWS file for changes The UbuFuzz VM requires VMWare Workstation 7 or later, or compatible virtualization software. +If you are using virtualization software other than VMWare, then you must +create your own virtual machine that uses the existing vmdk disk file. + The OS X installer requires Snow Leopard or later. Xcode Tools is recommended, which provides libgmalloc (Guard Malloc). @@ -19,7 +22,7 @@ UbuFuzz: 1) Unzip BFF-2.8.zip to c:\fuzz or another folder to be shared as "fuzz" 2) Unzip UbuFuzz-2.8.zip 3) Open UbuFuzz.vmx -4) Create a snapshot in VMWare +4) Create a VM snapshot 5) Power on the VM If you do not wish to use a shared folder, simply remove ~/bff From 0746b5c12aba1ed365620bb4941fcfe581b54efd Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Sun, 25 Sep 2016 20:59:26 -0400 Subject: [PATCH 1146/1169] relnotes bump --- src/BFF_2_8_release_notes.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/BFF_2_8_release_notes.txt b/src/BFF_2_8_release_notes.txt index 515a0f0..0c4c9e0 100644 --- a/src/BFF_2_8_release_notes.txt +++ b/src/BFF_2_8_release_notes.txt @@ -137,6 +137,8 @@ Release Notes - BFF - Version BFF 2.8 * [BFF-1009] - drillresults on 64-bit windows needs to check for 32-bit and 64-bit loaded module patterns * [BFF-1011] - BFF on Windows is putting PROBABLY_NOT_EXPLOITABLE crashers in UNKNOWN directory * [BFF-1013] - Include minimize-to-string option in BFF campaign + * [BFF-1014] - faulting addresses reported by gdb on SIGABRTs aren't valid + * [BFF-1015] - Allow verify mode to perform minimization to string From 30bf7a24a74778c802b890dfe8b97639a70f2f17 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Mon, 26 Sep 2016 23:20:12 -0400 Subject: [PATCH 1147/1169] Set self.fuzzedfile = self.seedfile if in verify mode. BFF-1016 --- src/certfuzz/testcase/testcase_base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index a625241..2d935a8 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -114,6 +114,10 @@ def confirm_crash(self): def copy_files_to_temp(self): if self.fuzzedfile and self.copy_fuzzedfile: filetools.copy_file(self.fuzzedfile.path, self.tempdir) + else: + # We're in verify mode. Set the fuzzedfile to be the seedfile, + # since we didn't mutate anything + self.fuzzedfile = self.seedfile if self.seedfile: filetools.copy_file(self.seedfile.path, self.tempdir) From 1846566a7e33dcede7718f7c6739d76ad5e93d55 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 27 Sep 2016 08:20:43 -0400 Subject: [PATCH 1148/1169] Don't bail on campaign if we can't check hamming distance (size changed). BFF-1017 --- src/certfuzz/testcase/testcase_base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/certfuzz/testcase/testcase_base.py b/src/certfuzz/testcase/testcase_base.py index 2d935a8..1bc4602 100644 --- a/src/certfuzz/testcase/testcase_base.py +++ b/src/certfuzz/testcase/testcase_base.py @@ -197,6 +197,13 @@ def calculate_hamming_distances(self): # one of the files wasn't defined logger.warning( 'Cannot find either sf_path or minimized file to calculate Hamming Distances') + except AssertionError: + # Some apps change the size of the file they open. BFF-1017 + logger.warning( + 'File size changed on disk. Cannot calculate Hamming Distances') + # We'll use -1 HD as indication of unexpected size change + self.hd_bits = -1 + self.hd_bytes = -1 logger.info("crasher=%s bitwise_hd=%d", self.signature, self.hd_bits) logger.info("crasher=%s bytewise_hd=%d", self.signature, self.hd_bytes) From 5f20f1944cbac740ba3d957ae004b401f5fd2b9a Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 27 Sep 2016 13:30:03 -0400 Subject: [PATCH 1149/1169] release notes rev. --- src/BFF_2_8_release_notes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BFF_2_8_release_notes.txt b/src/BFF_2_8_release_notes.txt index 0c4c9e0..091ac9f 100644 --- a/src/BFF_2_8_release_notes.txt +++ b/src/BFF_2_8_release_notes.txt @@ -139,6 +139,7 @@ Release Notes - BFF - Version BFF 2.8 * [BFF-1013] - Include minimize-to-string option in BFF campaign * [BFF-1014] - faulting addresses reported by gdb on SIGABRTs aren't valid * [BFF-1015] - Allow verify mode to perform minimization to string + * [BFF-1017] - Some applications change the size of the file being opened From 5a9cf28b1b77448003c969b383d44011f9ae1501 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Tue, 27 Sep 2016 16:37:47 -0400 Subject: [PATCH 1150/1169] Modify README files to be more clear where bff.yaml lives. --- src/linux/README | 2 +- src/windows/README.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/linux/README b/src/linux/README index c025526..d99f984 100644 --- a/src/linux/README +++ b/src/linux/README @@ -264,7 +264,7 @@ information is added to the target application. Create a new snapshot of the VM after the target software has been installed. -The bff.yaml file contains all of the parameters for the fuzzing +The configs/bff.yaml file contains all of the parameters for the fuzzing run. This file must be edited to suit the software that you will be fuzzing. The bff.yaml file is annotated and should be relatively self-explanatory. diff --git a/src/windows/README.txt b/src/windows/README.txt index 08ee807..291ee44 100644 --- a/src/windows/README.txt +++ b/src/windows/README.txt @@ -141,9 +141,9 @@ crash (hash). Once you are comfortable with BFF's default ImageMagick fuzz run, you can try fuzzing an application of your choice. The first step is to place seed files into the BFF seedfiles directory. These are the files that will be -mangled and opened by the target application. Next modify the bff.yaml file -to suit your needs. The bff.yaml file is documented to describe what each -of the features mean. The important parts to modify are: +mangled and opened by the target application. Next modify the configs\bff.yaml +file to suit your needs. The bff.yaml file is documented to describe what +each of the features mean. The important parts to modify are: campaign: id: This field is used in determining the fuzzing campaign, and subsequently, From 9b2d57a043f0cf9b8ea6064ceee5ea01875e0619 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 14:19:12 -0400 Subject: [PATCH 1151/1169] Copy example bff.yaml from github on update. --- src/certfuzz/tools/common/updatebff.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/certfuzz/tools/common/updatebff.py b/src/certfuzz/tools/common/updatebff.py index b649b9b..1d4b352 100755 --- a/src/certfuzz/tools/common/updatebff.py +++ b/src/certfuzz/tools/common/updatebff.py @@ -105,6 +105,20 @@ def main(): else: logger.warning("Not sure what to do with %s", f_src) + if platform_subdir == 'windows': + # Artifact of prior FOE roots: bff.yaml lives in an "examples" + # subdirectory on windows + git_bff_yaml = os.path.join( + platform_path, 'configs', 'examples', 'bff.yaml') + bff_yaml_dest = os.path.join('configs', 'examples', 'bff.yaml') + else: + # Copy bff.yaml as bff.yaml.example + git_bff_yaml = os.path.join( + platform_path, 'configs', 'bff.yaml') + bff_yaml_dest = os.path.join('configs', 'bff.yaml.example') + logger.debug('Copying %s to %s' % (git_bff_yaml, bff_yaml_dest)) + copyfile(git_bff_yaml, bff_yaml_dest) + logger.debug('Removing %s' % tempdir) rm_rf(tempdir) From e84a9fb8bcc9318a4b43278eca6f3106aed8d9b8 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 14:19:33 -0400 Subject: [PATCH 1152/1169] Recommend turning of network when fuzzing. --- src/linux/README | 4 ++++ src/windows/README.txt | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/linux/README b/src/linux/README index d99f984..5924dcf 100644 --- a/src/linux/README +++ b/src/linux/README @@ -64,6 +64,10 @@ provide more useful crash reports. ImageMagick was configured using the following command: CFLAGS="-g -O0" ./configure --without-x +It is recommended to perform fuzzing with virtual network disabled +to prevent the fuzzing target and/or debugger from communicating +unnecessarily. + ===== Analyzing results ===== diff --git a/src/windows/README.txt b/src/windows/README.txt index 291ee44..4b225a2 100644 --- a/src/windows/README.txt +++ b/src/windows/README.txt @@ -24,6 +24,9 @@ Run BFF-2.8-setup.exe in a virtual machine to install BFF 2.8. The installer should detect and attempt to download prerequisites and configure your environment appropriately. +It is recommended to perform fuzzing with virtual network disabled to prevent +the fuzzing target and/or debugger from communicating unnecessarily. + ===== Running BFF ===== ======================= From 1162652c61beb828507bf083f24ea5a0f14a7010 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 15:12:45 -0400 Subject: [PATCH 1153/1169] Linux variant of copycrashers.py --- src/certfuzz/tools/linux/copycrashers.py | 65 ++++++++++++++++++++++++ src/linux/tools/copycrashers.py | 18 +++++++ 2 files changed, 83 insertions(+) create mode 100644 src/certfuzz/tools/linux/copycrashers.py create mode 100644 src/linux/tools/copycrashers.py diff --git a/src/certfuzz/tools/linux/copycrashers.py b/src/certfuzz/tools/linux/copycrashers.py new file mode 100644 index 0000000..253f1ab --- /dev/null +++ b/src/certfuzz/tools/linux/copycrashers.py @@ -0,0 +1,65 @@ +import os +import re +import sys +import shutil +from optparse import OptionParser + + +def copycrashers(tld, outputdir): + on_osx = False + if sys.platform() == 'Darwin': + # OSX + debugger_ext = '.cw' + else: + # POSIX + on_osx = True + debugger_ext = '.gdb' + + # Walk the results directory + for root, dirs, files in os.walk(tld): + crash_hash = os.path.basename(root) + for current_file in files: + if current_file.endswith(debugger_ext): + if on_osx and current_file.endswith('.gmalloc.cw'): + # Don't mess with any .gmalloc.cw files + continue + crasher_file = os.path.join( + root, current_file.replace(debugger_ext, '')) + if os.path.exists(crasher_file): + print 'Copying %s to %s ...' % (crasher_file, outputdir) + shutil.copy(crasher_file, outputdir) + + +def main(): + # If user doesn't specify a directory to crawl, use "results" + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + parser.add_option('-d', '--dir', + help='directory to look for results in. Default is "results"', + dest='resultsdir', default='results') + parser.add_option('-o', '--outputdir', dest='outputdir', default='seedfiles', + help='Directory to put crashing testcases') + (options, args) = parser.parse_args() + outputdir = options.outputdir + tld = options.resultsdir + if not os.path.isdir(tld): + if os.path.isdir('../results'): + tld = '../results' + elif os.path.isdir('crashers'): + # Probably using FOE 1.0, which defaults to "crashers" for output + tld = 'crashers' + else: + print 'Cannot find resuls directory %s' % tld + sys.exit(0) + + if not os.path.isdir(outputdir): + if os.path.isdir('../seedfiles'): + outputdir = '../seedfiles' + else: + print 'cannot find output directory %s' % outputdir + sys.exit(0) + + copycrashers(tld, outputdir) + +if __name__ == '__main__': + main() diff --git a/src/linux/tools/copycrashers.py b/src/linux/tools/copycrashers.py new file mode 100644 index 0000000..0390bd9 --- /dev/null +++ b/src/linux/tools/copycrashers.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +''' +Created on Sep 28, 2016 + +''' +import os +import sys +try: + from certfuzz.tools.linux.copycrashers import main +except ImportError: + # if we got here, we probably don't have .. in our PYTHONPATH + mydir = os.path.dirname(os.path.abspath(__file__)) + parentdir = os.path.abspath(os.path.join(mydir, '..')) + sys.path.append(parentdir) + from certfuzz.tools.linux.copycrashers import main + +if __name__ == '__main__': + main() From b0a0e37d157e04ff7b1cbf9a9aed69e574620584 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 15:14:23 -0400 Subject: [PATCH 1154/1169] chmod +x --- src/linux/tools/copycrashers.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 src/linux/tools/copycrashers.py diff --git a/src/linux/tools/copycrashers.py b/src/linux/tools/copycrashers.py old mode 100644 new mode 100755 From bf251f8c2f77f00bc95010cfc13076d00ae4a3c4 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 15:14:36 -0400 Subject: [PATCH 1155/1169] dummy unit test --- .../tools/linux/test_copycrashers.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/test_certfuzz/tools/linux/test_copycrashers.py diff --git a/src/test_certfuzz/tools/linux/test_copycrashers.py b/src/test_certfuzz/tools/linux/test_copycrashers.py new file mode 100644 index 0000000..1efab39 --- /dev/null +++ b/src/test_certfuzz/tools/linux/test_copycrashers.py @@ -0,0 +1,22 @@ +''' +Created on Jan 23, 2014 + +@organization: cert.org +''' +import unittest + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testName(self): + pass + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() From da763ab8d90e9726fbccf22393a850cf67c0514b Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 15:16:05 -0400 Subject: [PATCH 1156/1169] Add tools/copycrashers.py to README --- src/linux/README | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/linux/README b/src/linux/README index 5924dcf..8a8d899 100644 --- a/src/linux/README +++ b/src/linux/README @@ -228,6 +228,10 @@ tools/zipdiff.py can be used to compare zip-based files. tools/debuggerfile.py will parse GDB output to create a hash that uniquely identifies the crash. +tools/copycrashers.py to collect all of the crashing cases +from a campaign. By default it will copy all of the uniquely-crashing +test cases to the "seedfiles" directory, which should be empty. + ===== Updating BFF ===== From 9288967afdb06e8487e04b1b491aa333c6569a05 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 15:28:44 -0400 Subject: [PATCH 1157/1169] Fix typo --- src/certfuzz/tools/linux/copycrashers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/tools/linux/copycrashers.py b/src/certfuzz/tools/linux/copycrashers.py index 253f1ab..bd06821 100644 --- a/src/certfuzz/tools/linux/copycrashers.py +++ b/src/certfuzz/tools/linux/copycrashers.py @@ -7,7 +7,7 @@ def copycrashers(tld, outputdir): on_osx = False - if sys.platform() == 'Darwin': + if sys.platform == 'darwin': # OSX debugger_ext = '.cw' else: From 429ffaafc5f973bbd937b29fe6cf6e6c9bc3b913 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 15:35:53 -0400 Subject: [PATCH 1158/1169] Use target_path in updatebff to allow it to operate from within the tools directory. --- src/certfuzz/tools/common/updatebff.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/certfuzz/tools/common/updatebff.py b/src/certfuzz/tools/common/updatebff.py index 1d4b352..df4591e 100755 --- a/src/certfuzz/tools/common/updatebff.py +++ b/src/certfuzz/tools/common/updatebff.py @@ -110,12 +110,14 @@ def main(): # subdirectory on windows git_bff_yaml = os.path.join( platform_path, 'configs', 'examples', 'bff.yaml') - bff_yaml_dest = os.path.join('configs', 'examples', 'bff.yaml') + bff_yaml_dest = os.path.join( + target_path, 'configs', 'examples', 'bff.yaml') else: # Copy bff.yaml as bff.yaml.example git_bff_yaml = os.path.join( platform_path, 'configs', 'bff.yaml') - bff_yaml_dest = os.path.join('configs', 'bff.yaml.example') + bff_yaml_dest = os.path.join( + target_path, 'configs', 'bff.yaml.example') logger.debug('Copying %s to %s' % (git_bff_yaml, bff_yaml_dest)) copyfile(git_bff_yaml, bff_yaml_dest) From 9b3d3eb36c2cfd0da888385d36639ebafdf18edd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 28 Sep 2016 16:08:29 -0400 Subject: [PATCH 1159/1169] move epydoc down a dir level --- .gitignore | 3 ++- doc/{ => epydoc}/Makefile | 0 doc/{ => epydoc}/epydoc.conf | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) rename doc/{ => epydoc}/Makefile (100%) rename doc/{ => epydoc}/epydoc.conf (99%) diff --git a/.gitignore b/.gitignore index d109de3..032429c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ doc/pdf *.pyc CERT_Basic_Fuzzing_Framework.egg-info dist_builds -doc setup_env/bff.env src/linux/COPYING.txt src/windows/COPYING.txt @@ -24,3 +23,5 @@ build/distmods/osx/installer/*.txt .teeproject .externalToolBuilders *.pkl +doc/epydoc/html +doc/epydoc/fontconfig diff --git a/doc/Makefile b/doc/epydoc/Makefile similarity index 100% rename from doc/Makefile rename to doc/epydoc/Makefile diff --git a/doc/epydoc.conf b/doc/epydoc/epydoc.conf similarity index 99% rename from doc/epydoc.conf rename to doc/epydoc/epydoc.conf index d164758..7cdfa66 100644 --- a/doc/epydoc.conf +++ b/doc/epydoc/epydoc.conf @@ -4,7 +4,7 @@ # The list of objects to document. Objects can be named using # dotted names, module filenames, or package directory names. # Alases for this option include "objects" and "values". -modules: ../src/certfuzz +modules: ../../src/certfuzz # output # The type of output that should be generated. Should be one From 0a6a2b998b89aeb825d74c61054a74cb15dd08d5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 28 Sep 2016 16:08:45 -0400 Subject: [PATCH 1160/1169] add blog post pdfs --- ...2010-05-26 CERT Basic Fuzzing Framework.pdf | Bin 0 -> 119636 bytes ...-22 CERT Basic Fuzzing Framework Update.pdf | Bin 0 -> 100781 bytes ...ERT Fuzzing Tools (BFF 2.6 & FOE 2.0.1).pdf | Bin 0 -> 140394 bytes ...Failure Observation Engine 2.0 Released.pdf | Bin 0 -> 157499 bytes ...-11-05 A Look Inside CERT Fuzzing Tools.pdf | Bin 0 -> 110794 bytes ...nge Features that Use Oracle Outside In.pdf | Bin 0 -> 204052 bytes ...ing Ubuntu for Interesting Fuzz Targets.pdf | Bin 0 -> 147426 bytes ...ne Weird Trick for Finding More Crashes.pdf | Bin 0 -> 134260 bytes ... Scenes of BFF and FOE's Crash Recycler.pdf | Bin 0 -> 325529 bytes .../2013-10-23 BFF 2.7 on OS X Mavericks.pdf | Bin 0 -> 107879 bytes .../2013-11-26 Hacking the CERT FOE.pdf | Bin 0 -> 111017 bytes ...03 Feeling Insecure? Blame Your Parent!.pdf | Bin 0 -> 216104 bytes 12 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/blog posts/2010-05-26 CERT Basic Fuzzing Framework.pdf create mode 100644 doc/blog posts/2010-09-22 CERT Basic Fuzzing Framework Update.pdf create mode 100644 doc/blog posts/2010-10-25 Updates to CERT Fuzzing Tools (BFF 2.6 & FOE 2.0.1).pdf create mode 100644 doc/blog posts/2012-07-23 CERT Failure Observation Engine 2.0 Released.pdf create mode 100644 doc/blog posts/2012-11-05 A Look Inside CERT Fuzzing Tools.pdf create mode 100644 doc/blog posts/2013-06-04 The Risks of Microsoft Exchange Features that Use Oracle Outside In.pdf create mode 100644 doc/blog posts/2013-08-15 Mining Ubuntu for Interesting Fuzz Targets.pdf create mode 100644 doc/blog posts/2013-09-23 One Weird Trick for Finding More Crashes.pdf create mode 100644 doc/blog posts/2013-09-30 Attaching the Rocket to the Chainsaw - Behind the Scenes of BFF and FOE's Crash Recycler.pdf create mode 100644 doc/blog posts/2013-10-23 BFF 2.7 on OS X Mavericks.pdf create mode 100644 doc/blog posts/2013-11-26 Hacking the CERT FOE.pdf create mode 100644 doc/blog posts/2014-02-03 Feeling Insecure? Blame Your Parent!.pdf diff --git a/doc/blog posts/2010-05-26 CERT Basic Fuzzing Framework.pdf b/doc/blog posts/2010-05-26 CERT Basic Fuzzing Framework.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6ffaa577c597c0262a9c4dc3d766c6d159beedba GIT binary patch literal 119636 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

BEG@5?>oX6X1yB;d z!Xo{4w?OJcYY5;i7oqD8vs_ORI}tI5B#v3=u*HC)#qdqf{G!5Nx(2;-F_KK!Out9?^u>KEb-3ZY1(dHWmUV1|iPko$*iELa;MItCdno@YA^|CIQ( zEzad4N?SNH78l*&&jqayoR6o$*HH9TE;!44Nc}KOb&U5(BJ8!)grwm1I_J!FZX_c+ zFYW>-OO&+P+}L3{?QrIPVVkhu&6F3oCVd95&6cqLX`P$l52)p0^|dM>Q6qoO*r9DE z;PR~`;U>y84cqb>>1EDtLhArTxKCuv^@F{V70;+0Sq*Kax5?uCG=m0aOSpL=2%?fF ztH@708;@+56Pqr7PC*o>1xTz0sKWOZ+;A8EF51I80(=ac%4SO#0POX`sPf++p4Gbw zcjCNbOPXx2y&5mW~LP8+Qa!-ET6&RZsW0DrL&l?U;cJ;wrpm$tp>Gl3A6x zc4wf|6IRHh^~EPT3t1?#t_!JHm&Jv{|mHWS|YJc&`aO=-e4_LiqX zzKJnhat@v%)qlp4XW2jj z0j$p4)xlqa%jpJWeLPfQ_lu2=(Sa+WitdnaKzld$urOAVP(74)AJm`*dLM|4z(T=i z`{@-3>ewfME{V0qxqWs9Jb2*s$#FMlGe!Xh`X5Ly;9P=lxSRw1Mal9+2GM-~s!^hJ zaT^j&@s5aK0|i>5eLo5N<0?!f1EWEon?>{ygJ6^xJbO*t41T(+nT;5~ZYQv5|IipV z2C#0YEES6I$+gr<{^c6ryTm1x#$u~K1io~%ax)APC%tcJetLZrK{;;Ls!It@t zmh-Vq&NJAF1a*+{%9n|FeAge}e5W{NG^A|8P71B^LcJ;11;f2JZYX zpv!-~`QM-V|BKvV`vqctYxG}K&i@<5^S_ci?05|So$Rc{0QtXAJb3@F6b}O)!~Xz2 z{}0CVUuFMYyZ_eA{u>rw>zCmR2_p;Kgx1wJ&s5SDZmP|GpliK@x1u8S0O_j;re(H0H{&#e>FHbXyh%vrmFA+ z0&8SsRCoc!qC=kcA+gL$>6Y!2dMWvnmX#&l{DK|0j_$ipWo~Y6X0Ks28+?thwNY3| z3Z zU7gm_#f608TPPzT;b{UAysfQ`{R5I69H(SrTffW~0?R7aKX*(ou$JIU=!D)b2w_i9 z1A*`r%u;|i1EPrg4@0Q{o0V@{CK`I@T<(nL`KL|!&*Nc`Fo|)YJCnvyE*Zm)s z^^NmlPY^UafqODnK=Na_TtCYk0za@y;g8TI5di`=HZ3(8oqy1?0x8O@7(g;NQ6iu{ znS+{#&?yWYlG8}--A{r4$iu?}JhMh(KmeczrfeK9JhOXp?^;h6E~?9bB$>$jUk^Mp zi4S2(S7fzW-@$;tdiV(WV{mM8P=0@)^aX-|N!(C^FQo#BL;C5*;9~c3{bQ#uOZYuH zXk#}6`Y$gtnQ(H(j@3BXEIxrc4Gst#9vHN?umA(SnwrCFZ+j~*nJyKTGBrzS{{+|j zW^f5;z!55qsXqpNZ_nn^Nv)D-6MN0Z_Yo(~&g;jQzLdc3!ezlxnL)?`24VSQT_H0F zk$#MQb~%oQZsvb}3Y4F`tpVmm)!mURE-)q^pIA{vwMju?QX(1_B%q*}sm*4aAh|o6COkT;fS)XoEiCCGA_=}A3gjgam>JE($-&1VGAO&=-}#yU*%{x@1v4=| z-ak8knOQWZ4ppFN&|Riw6{bQ_i(pT!gn$V@B1c0fFGf{}plBIUV2~XqWkijl{_(mF zddl_r$t@uXP*QtpO#Y{+>ni^%KOa=TCiZ)ln+SUtFaDblj)M$=5CXkE&l#`@P7+@M z--7=`GHzFn3H?1%h{$s*uM$XGI3nBhq^PacjMxAZU?p;ZrLLK6s);`Fmg-dsC}~lH zgicXXU4wl`ol+@$(mOL)hsKNkr&7a$1W|2#kV=vgNHU{AAP*$O8U+(kMMXnRM_x%m zLrF_PPD?7#y^a4hRuPdpj^+v#;f!%t56J5|A#V@Q4lt+?mTwOpso$Cx5d(x@8YY6U zCm#+F7+X+OJmPOr;*|mEldJMa2bnfA6|HA?PXgPlFv2+jj0*zjs=agcr=F_+bR6bI zLFirVh#Psnx3YgUmupc`MfwCC=a@mJu^KuZ`oH61(oJpVYn=4?`p^0K`uX_rcwZ`| z@iVBX$l7&Th{{p6{GUcQI8*~60J2*E|1T3LlEL8F>ydh0q8d7UqfvM92pW0qi3v&y z^6GM$Cl}?mjeA9btb{=8hkdSyeeDBpp*8&UOg?3sScW<2SWL5&UHoWA)M9r^hc6}& z|8px|XPH`U2S2tU*}V!AOCc1%V{-fxtQc#D_d&2>Q<;V}6y&2>NppL7_cI-GO3DF} zJ)zxPVj!V`w;~@uaQM04Tl{f%Xi=vdtD_qG0g^l`D8>Ht%TNjH>!g5x+< z3K;M#;JA$+Pl0j7@BEU0g?n(SYrG$RXnla@`mE_EJ`BJ-)CdCqoB|^?)Qy8vN?4ee zjXOJbr-i<@!p67m=1rlZXi+M7G)mvyy6+d>!e@>pc`|4}JldWohSq0HEP1lZeCXxB z4LXfVc)wgs@72_A-PBL{qGy7UR@RAHz)8E~ZOyD6_sSSUWV6`}?4E^kr%USd3(L#P z^Ya6S8%F-Ur`*03g@?xK>;;OMf5EZUhz0B6%fgc|&3`2eye{yKu8GU)JXduRPz+k@ zx8C;|Oe_rjJ8?wTrCRdpRJ-d~s;{wC@oLcezLHo%g)-i$V)p9Ndih9Nud!9~X;AnY zin^CYz30`Q^k&I=dbQXV5k0?3K3y74wJg$iws-M|fj(%s>3;e4ClvMh=-Uf4oxzEU z`dd+54VpII6=2;Cr&Y*XQyTh+MU8IINW_6ab%(p(GK$wPWGg^hBoGmF8IyuI&4dC7 zOg&ZDncv!2*B2 zoueqy{rI&m@CMuC;q)$WB)ZFUdRNJBx}>_guDl#2G&hG^AZb?+DojM^9m3iWNbMA9 zfi|{QB;Nt*2a$meg)Xm-j}#7CU*M{xMF&ARV3^iS*7 z*THZvt_qD$lhVVK!W3#l(M&N^j?{@YEa_D=$yFrL)Z(Cu2*;~M8_5Yw4u{9>VqKu< zw(rAg+>3Q;GBfiMGc$8jQVl97=(7SkhRB|D&##cM|1oWU-SZJ^NkU0cm zhZ3>O(A{YwR!&XsrtUaMAn^3@nT4OxwN<* z*sC->PS`PiJ23!Ut67Tq1NXIAZm>LV z9yEIl7-n~VWIX`}aOy%ps*j@kiAR6%u{iUpc=$Y#lKSfA>iHSfZkHyn4~@64v~XDz z3!W|jHfPvhZn~xb>?H_Z; z(~1jcUFtY+ii8q_ZhfDL*s`{{EX_z(zvq#(d z%LmBr-}k58{z=FIFK_zi#)#z4%f=P2Pvhf`Zsph8*=CF@sBh=i+mOH#=|d51am1Vt zm+qq-muD5J{hdI+m!^cQy_-*EK?l&ncC@vH8}LL%fhcS?mwV_yorPJcwH1*YMlC;F z+_Q1O8K*d0-pMJ;7}cP_prB|t7XPadg%GtEF48W60zTjDyp$ar0Wwx{WVsDr>Nu6%*lifRg!PB*4vQ9R0?I&j&F$SSmM-;5XX=zEi}IsE^Wi~of7V>&(u6rHhXUz2 zB8_o~Q3(5MJ9r}nrDVbj^&~HsW{nY`%8;wkCxS& zSKj`R*4{(gcnRaI=J&MOv4H2ilHj45v688YU3^bfNrD&!JLO@PKAkLj2G&6Q>g=hK zx1h77rR9Xqnk>W?5LrJY#XB)B0>=8+I{a4#xY8r>bu&=k3S{ zA}_D*+d;!pPV=yjZ^carr}qv`i&wMU_uW&zEJ}IKBoa5;j8+F0=LQ_NHZF@2)@r27 zXcE0Vx(*|T!bKI`H3h{Rr1=e%{a4BZHWaZf53&NaS1J0~%XTZJ%nYm@lZWW*=(x?z z+^7f%i@LhLK6Uz7_=SWXAqjr@8$W`5Fap33GO#*C5Dr4Uapqou=?M4Yqx#$Jrpuz~)wSW! zJcFO^&)j9EmwK@fJ<{liekDrgl)Fh`0!fyM+0EC~;I*;N&Pi^H#U4B$h&c+6VA$H& zY{NjA>MuLESK2f_I=tc%0sX%3Tu3^n_t_ATgs#TrpN&l=9W)+C;tLbk@v92=m+BQO zX{9a9nw>1FE$l$I^Pct7SC_AQSl1CA-K($X=hG9(l$;rsEo}02^zof@N^h2hyEBOs z(w2=JYF4l2`ERrOR~Oy$@u9-D_n?X+dc~zM?KXV(Jej^~0$32i;l3_f$6wshFi?=> zpD2~o%;J)YcBk82zfK-G0Q@+uB6NW`XbSYB4-Yu{-hl0k?6@9aA6(ggV!TI@Xg))H zj$hY#Z9`*b1!+Xe#YDPUWU^(*8v3Xj_K3=vxJGA8>rA49SLNQI?)u@Vx`y>VhnATq zy}H|;-m#FQzJ~atf?30AUv|>=c?n$`1q*U!jj@6|shZmNZuUeHY0HFIO!S75lVxbxt8+T?NS} zZ?cDctBR_Rhf_-#Z##EI>uyNg|?)H$o&X(t?1o+;S<|I zKx;goo8unPQzC#Q-C*`v6?8H?Cd)h$ixL){0vcmF;&cYWR_r#+5fOx=R+|~RXf)B> zet!HgC~1uz<-tl*^Q%ZQr8fvE7T;J_2o;>b`=9*ZsL(>>00OU2w^=}_`4ASAeF`R& zOTOCOU^YM}0l!>gK5)cLY{I3vISp;3tjdK!5Q&1%As-qlUD4!}b&h`Z#nek?bwu|T zUe6X@(*jagPr4fuQIqT%s>L`&y%E=h)z(ldZzYw!=t(wO9rpl1NfkcFWKmQ9oBiO( zywBdT`_!@e)4k8xzWm#KIOkHvjgdYPO2#CpsNDgR;Se^efojQ0}Ns}D?rhxVMc`)28hRm zsMn_-CPRlLW_HL5TGCKJ&%uXwOc4I)0PUc{1|jv?boocqrm;jjImcVO^dT5LWm>34 za_&0i;K~wk(VV8!E-Ff9>>-vO(Aqj+F%r*HB#4r6TpI15Eb=)~^sO}Evnk2G+gn(i z5SiTjtq<{teoHvceJ>jZ!BVp=Nz3dsSD%qON*Pi`*A`ZyrTY(jo)Fwwli*$6lOT~$ zzTYMMoR1KZks8ey-f7S(6hkPVV1&0>9sFC|<6DUljjOg8Owx%E+oAB=X``1e^KTGY zrH{d0D>Yype13 zRyUm=`Nz4lf~-#);e?`8;ufu10*4F&od*1z8UWqam06aC4y9g}eDK(Y54{1p+8y~f{ zc2?}wQS}3RoeMKL4?T^7FR6dk;PCFNt+Y%}jgh+BxOM+kC1d9r=9lnJ^^14+rW?;1 zqt=k~LAoczB=;Z*0HjT33Cp1HapC?uJGla&R^VYIny5}?1>SmE!Xfn~6(o8!bao{) zW>s{VB<#K8p=9exp_gFN;^y#uVMc{pZcj~553bew`*-vyvc#@nd_*HSKwYjMG`SF$ zl1F2sUnEaptyWo|2b^<{l3(0#z)JkVX`0+wfvE#T3E|q-hGRGW1TUiyVs7Ik_(OEe zBQrC*zT0tY(j0^=2x)&@+WCZ+Th;qaOtt@HQ~If0Qxw$9qNWvgrBSfhAx(Y((c(lLd6HTHQz=u|ROym$Uua760SOLVAU z5LM*TS%^7g{rnWnCAj{<2i@?)dtx^nhIFGBy`8t^&)iS5l1n{l{X|b;kJ`capv8Nl z`El847-f2i{-zvV+wXy@@0gQ|;&eiq<5yDm&;)H{cnO{Elw$=OCubA3jYp)D%{w_T zn@&X5?Ko7!nQ7R?#s{3Mxo8d=L#+uII5n_LIu_Jt4~fYf zj-VS?o}Xazp-&@lWMy@AVMqODc-!GJPmW#*n2-j=UiEQ zgdT%CQK+3tXVzqT+Z>9Z)W{Al`pP^%Xjq$3gOm#0v`7EiNIZAPA}_|D>*AiQr6VoI z=2k>FEzAeU{ zgTHLY&sKYBTc*FLdEXm(-}Cw0is_Fo-QQmC*TS*-Cz_7ea~(`Fw{NFrUz0o@q`Rg0 zqFIurp`Br4<)2>mxr>9^JtE|LKnb04Vf~TMB_Kn{)!|Hh!}FTte>X3cnE`HL!@wuv zk`WR%h5OZyLltHUq27t4W(}M~JTltH3);&OJtYZA%N0c-4Luf|sPm$;AiBBP^%b4% zTR0SYbkE-xmU|oO=9iuuJG|XxNhxidOti)qnL{{QMBHHxMQB-(saQ;BnvSKLNo}K0 zyqpT`n>bB98x1^bP3p)GEX)txoF9)S{m^XNx*&7#*w9Z%XkJ{xchOZp%n*h?(dx{7 zy`N8KcW-ZRW4ocDAM^VC<@&;}={RKb`6MCJIztWrz}Ld{5NAVE4QloRpVk6B1Ixw> z`4x-+7RjlV%Aa;@?gS=0zx>t8#DgCt zeJ%)#uR&S&**^DXQ)^vB@~$C08BL1a4P#E2sQqqOQYXr8(Th zu4rYIZ}zNu{8n2E96N8KL_bjO6D01#MsJ_QE6NHeK*%rwB^wxAZrUElG(H2`Rm$ zk-ga3&`zt6uG`Q)+xRk3Dq7cxkElqAgx`1*NpZzMFe`zyvzdBs7-2-9C!? zN2;g{YZ}4w9&c({2fgfU42CMgZZ^b1BZ1k;s8j9Ot)1f0Ql*!N=T58eNp5G)>gGCA9Hnv@usUxerg2YlH<)R@H}5_jSV1@JIu3%X>nOwCb17+5JmJm+q8y z=ZO$qPGLk6|m#F;Fv_2<#p`j1!VTgiwd@0HBg*&Nd~ny=%>e#BaR zh~Zm?YJKvC0f}WGr=ynrSS(VxliVF(7EztT1X{6sM}tEV-JOqT-&ce&>@aOqj5Y{L zFFuHuoF7Q28IR|;lricQ7ytotD2^CtQ!~pzjJ!RFkQT;fFL1C*#P&vC2>3q+Ux}i? zFCaoewu8y0i3skqjKI<8aMXvtx;4$-?Te3Z5)-Boe`87{SU??lyXWq=JZ^NaXS{pn zTb9CJDj8Wg7*#bESUVbYCYQhOz7KtU8=rY|w%&4fs=jV+Y`(AFK4m)!c5I)#n>Qbw zioHD9Zd_c?t-K%Q?CnI0bp_L96>2r6*%5E5+u*7(BcF04A-dGFU4FdIBs_*QBJj%nFs-Rp(Utmsr(%R5A=>G= zxV$|}C6V8-mBE)7JKdK-^Ufged*Z0r)~3Zb9Ft+?;yMP&_8FNs$L!mo`{1MLd&Xq_ z#_5=vnb=xueC?cK^=eGhT(+H8H@UJAFoE{X`PMv}Cx1q(qM7 z8S+sOa(DI}9RmYT88(Cg*E`aClnT&vMw6^bj~QIGIL#0GnZIv3SHcr2NeCFK-p$F) z2wKqI+3R#)$@KhK?m2KQexw08hkDs79F})d<EjoHfhum>6D;ttMHx6DIX5`Z!gdrWo6r_V=EZrP3+OMs<87J*!INh zjm*mwWFy?05{D(b!^?#={_<(y7}Nsg4C=KFXJpj=tz*v&(_&Lolj7o%67sE1rRmqh zL*VmQwqP*itPuJEPjFl{hNFqab+`o%g5scuaVg*|I0m<9IskD4m0l>SUw<>dB!A|# zf>5ylyFfauAuH@+;4DLx@kV=U<6L^}O1xtuZY}`_Zje>h9>ruAG&mgFV#0nmI|==3 zdHPb2x7t2F+_j73=S}UyMD*G%&!&u6-xKd+PHFC0?Ak(gU(4}yDc-LlSRXaPO9PRM z!f+EZpWBHBP!t(xMa7+CEO0x&sycqE`tzv&aPNLGIM{N<^!WJL#Kg3uFJ$q|+ziZa zn3QB#Zja9cv42#->#5h$zu63NQjfA?bv^AXl^imX3j_`OaVR{P|8_rcm+4c3z z`WALmQy(TBW5}ecf;ov-Q{Uv7sOO*}NqbA~v51ob5=$Vk`!CLTFPb7SmcjPK zVYVX?&XAAMEhQ<_=)EDUoRWj07ZJ=A+QUj#QyOTODczoKHeOe6eH$Mi6gx2xbfwXd zTYe}8!^KZ$Wm~R9hvz}TBak{{0Z=@ieQB_AB=uWd|ti2xl%5S1=&A_XrWDz1c6kgE6t$Ew9WuQ=nEh9BPEXiaFPXSHldp4&z`HVC$NICPo7- zajFlILE0mcNUC+Id}kFKmbAN<)!B5CXK^m*v@x5V$nV~RTjR=m+uf`U12h%ub*(!! z%_9wU8Vx-{74uW9!y_E3$*;!j#zb7#yyP#J%0G+V%8D)|p&}YqYG(2T*h~3@Btt_* zbM$GGfY?ICc$Wu=+MY8@WNq89jW7VQ==_fmXah7J72%rr1m;diho=NKhEQz_&t)Lj$!BzDA~GjpI2&4-+26fK4PBU_q8y8ZBwu)lAd!^Iy%oKf zj_1RJ7~oHLnobagBFPIvw<3(>!!m{L#G8ZrKv8+r$^CWtCw8kbuJ#B5R=8X5rIy*XERVschRVB>&7y(CppL*PkLB9Lq))_Z!o*z6ETdZ5AZw2VCW)5E;8Gq@ zi9hey$z@$sWmHD&+eB>IOyomAYEe#PUr*pzOw++jiBugBF%nlET8xx34{mK8Zz_!- zTuijg&b!XdJo&a=buq>>t&Cqy>=VtXjZG;@7Qt#ml?w<{=D`Ey$H>bI%l6wFyK-M8 z(4nvOJbaNnci6psPpl~3k4!=p!QlVIf?uh{ak0Vy>s9Tl*$*KEoWbVsHSV>7OXHn@*oz$#Q{Ol4Ju zqfvr%#P6@9VLRi|+0~F(Rj^nU(AIKrIphEtaFFd?rp~gR!(^7kZQkf*(&D0v+UQ$W zY^%1~-94X%*U$T>mWq~^l9G~&j*c)(`5I&92}Y^>;FV3p9nOXX%R!>?92eKnQt6aRpOQznYNV`c!nxBA z(@w}MU>$B|!f9deYh9S?9+i>5$TbJM=|saIVfLpAye*T=bVs@+QFH0 zjK*JrwCUXUUDlR`58l4pn#i8wApN}E2Z@lEJ@vVr)!&ug)d%YWETF1;|{Qvdr2xt z3ZI*I8Wnu6xr&r_gU3xoKBlt1Ne9VB$h4`E!X$^mu8_8xmTw8}G-`+RPB-0_fe}+ujkTHUoBDqw=;GC4;7?xGMsFPI#XF4u~uQEJ+!ULX=M%osxp#W4}cu|bk zQ+`@(6w}FPu2}Bq`A^ZiQ(HY!P$h_kUW$`(+56*p`06Q%SmYX-+PpnJPQ<8 z|AuHcfB&Kiy&U6V=X=x-3_~H1GW;HBbzqBRB70P%Lp#1{QT1j}{{tJLn)BFX!-86~ zWV~%5)iamb&ZyPNtX036)HIt~I~zZ%jiiNCKK7WRJ^9y6+Oaxx{_vl)TkQmluqSTOlnk5YE_A;SBL7BM{abcb}eYvx)a;J2@GplEBM+AxmOxSg0YhJ zUwbuI(=RpAX=Lou#w?ICCIB|yY_4x^wl>&%8rD?TC>qI?KIVg0eKWN6f^`ws&uv6+ zd+0%UFbJ^|apFNt&a2(Sd*W{cQZ*i3%M$3}Z^JarO8o&YA^G79cit^Vf(s_2zZf2i`F@JVhn|^MyX`|tn=T3 z;O`R0PeBa)_h8t;NWd`%eurIgRl?g7#DnDe!OqQVOY7tYgHS^=R1EM^qg`=XUH z%m7GYWM8V=E(K=`Fs2$GN(X>Kp!(b9>*rS+uGN2asKlNtW$w#lZ>yr7tfL`XGvPe& z`%5MzDMLq4)AUr;DcW&oMra&x}USQJ~}uzHaa-<1SsJAp|iAB{Y)O;*BJ4} z18lAX1Jfb`3S=u%B(Vwp#LM9Ynw!Z|FJk?rGTO?|xgfU%5YJEcr9h^eGweIHsp zlF1W~x|4#u;oTX<^*EcBL?ADBTY{i^N3=b^9N&i#!TYp{t`U%nonJ(y_zWKMErslqst zZ=oVsV_IMPZ?PWK9sq+JVqYxuJmoXWy~n}w@%!-9Rn^v6Ty?qC&U5E%b^oEHJA9xp zylMpDZBlsB^Pwk@FziXXPYd(ALD|pV;oH^Tzo+fuwtw+?`-gY+g?;fs%S=DvXeFI= z*2?*w_KDizIlwuD4vO?zOj4*-O(o-8^V)RYPxl9+u-6AYU>PshSFw;hL{)qyn!0*? zWPgApJnlZdrwO=8n*~kax?D=vu#yguXxg6w@~-O=CJ0(-M*l?UXe=V5M+wgWN03>R zg$3S-AE8W3M!~Feva&9>TyLq<&OnvVz;w!`ZYm}-C}8+Dk$E(8+#LsBYG$=%3^dK4 zRClVK_HfR7Jva=Fzn$ORSN81>*3BPIH^!E;{p6d~Ov;5M{;@9P+}T+k7k8PKvDmGo z?xs}0b*Ah4rmqpkaL4>uaA2auG@|->%g7rWwSsE<)63J>D(?JR$}XqD`=gQ|V_m{y-oo|= zf6=>$?*xs-fk5uO+3N&B>bl(Jt;_bL$@%||btlc)5X;F2nhL#I^`hDP5|Nw3MY8 zylgK|Pj`jDx~is(NX~mjAbeuR=Wi^;YbfTQBYtKm24EsO{iXMjhG;KGyIE)Z2{-Er z0KbXEYpJxOo#d>x$nM${B4#Ux&t49du?#XbW+7Fw0y1qA|HFf~S0V+aP%l06dCc15 z@iUuuKq?b9xXGEpnwLo`n|=DWN$U0d0edxLbPtHT?g9rs7?UdpPdFcrR0weyOaer5 zYb+%`i@1BbB(Qbb`yGp=FW?Ck1!BlQ`fD42sN|S`oil=s;aa*iQtm}^_Fh8HRa)9r z(vclv)@nY+wt2`ffoAL(K8E66DSVg=6`uQMEDl zl4FeZJLyiPWOAzfjO*zxzxv6gMGi(Sr}Zr|6^wBWd`!2Rv7D^gk9K-fC$fY8Gf z9nOu>s;`OcuQU+0*uUbGQYuJzUj!koG+3Z&r=C!Ff%z>dDQcm0yGMg+;Eq^7#E4({ zfF;EUUak!V4Rf=%gJ+00N5k$UXH0$BkXaF2gW@<>mA4DGTZJsu449^6Q3bQ(*`~$E z=wq#*1k2|5-HB6N#Kt*swK5c~rc0lW4BPDJCygprr+G~<&IlwPVhljcXZX(&-ESGT zU+24hE74wVtDEDIV@;3|kY10607^h?y#njrV@;9|{ywLZ9>VM%Y>QklY;w~+3~E3Mp*n+2enX0<4rRUD>_s0i6t#KxIp?WP05A_F}X#(Bt3(8yLnEvo&y zMn@IRqh$w%J0>20uEGhm=L4*OGKp@wJa@BNba_AfK1`BPj-e6j0y^vrQH-*AecvC? z3Kd9}{=IR?r<`-~@O^%yr(hDK@E3fAJNY$0JOw|43Gh(T zp!x!!p{V3dEgpSONXn1mLR?V4U}-$!Zsx#juZU6P9ybfz}4tf!(pQgjrCOfSFJp` zpw?_|Pr#{I3HAE|utGo>Yfhu7KpsOi^CLf4aY&5c!9o;+ho-m2iw?iyJ>+0cWdz}` zI5QpP_=X}H-SzOC(evgSdfepejLg^&fCV|w$euU8$j82|8Tp_HY&h7&GVLH)wW>D6 zTAPqs9(>YB0HnExuDM5EwOfdNY&M6^nx>!2O}MTddrn>rSzi(5WF!7q9co7f)i80N zgaSss@QG}LksN%)4!RLPrti>d#p!7+VzPeS)xm_$Q58zMP{D7&ISA=fhDCs>B{$$! zSKD_Ef~*FP&JhSM{-221h%6c0sLt+(Cpml30k9!Ht1G;ZiPz=x7PAKTb;=*;rt51SD}X4owzoUTJFtC37C3pY<0LMoD)dg}MCtg1vu zA*k!!8bJ2i-!I7FE%{~Vul++VD@8;;uCNmo&}YK+Jw6BHSap4`b~|3v-!z-G3aoTRKLx~e{0LCh za~B3ZCk*7hjp(=M!Cm!Z2{lb;9+x#*%_p~(PPC9nxtad?xTpYVNKmi&pO2fJux!qX zJzh>h@a7yUVxFm?S5w0%D~WKN1eID%#$*+=rf$?$-X*#Gi%Mz>CfDsXrmMWXY*ZWM z@p1Smh|wFT zzEvJ{7dPZ4F0ED6VgzNQ^HwVUZZ7VFS>BVGR6}*CmeEGM@SsOzb?>Y;I%{VPWv};-1AO3W$ew1OsB}nI0e){5>&8Dvv#&#@tIgkgtq*%_J4*hU z8NR&PDCK#Pk4$cK*y5l05Syv7p^34zjfIJok=OL>s9*5TOg)RSgH`1%Zic>rjg^V{ zII>-%hO@dlgaF}wo+%{(=DM98UTLcXQHogOoq@neJ{-u3=&JvvtPWK3YpGvL3@NO4 zJ$;M5rud;in!@BUy|F|X@xtWf`e9{Xh-ow~m-l6FTER`ITJha>IW(_(wv0TIP~X@> znFG+$8NmIdo^yE)q@99SlKgalou^HG3dOK|;&s93eL`XondW|1`@G;C$~K?Pp`Fj6 zT(PfczNKQOw{+yOLAj=AdQXOg!H{0MM*3Rq|HaxnMrXFITcfdU+qRR6Q*p(v*fuIj z#kP%#E4FRhw(V4K-&(cTUi;khopZk*x6L;7%rTzv47{!NIo>&XrymC}U_{Em!$nN$ z^^VOzt?!153Rf#ymnKo?gD`LQjEqU?V!;IgbP*O*Ln}{r8ldZ)HVZD`Nfg)W)H~a~ zaWD1OHS?;c7A2a(mU@NPQAwqG$(u_{Ycz7H*B6K37YyOqUyHzr7pMLBPCBND6_O)R7YtM|Jig8E-@kjxGH|_w7HlBbg0k;A& zcf#dndTwjn#bVacbkfmA(#c8!YM%j4Dr*Fm6{$3?2&^S4s-uYE>j_S8M8B~*OPTc& z9h4T?F43kt2u1K9l4O%M0j?*KAA2tfE)m4+Ktp6CyLiUrVNWA2&Iw?&{u+)+^|0}# zq2YleAc=(m10kSE9zC$r`25vO@n?+P6P<60w2vUo6daNehc@zPTxcw>zRhcH`I4fM zz33R%enmr=iy>u`J*%hDGS4@?;YDg#5EZjpFY;n;@0vCscVD@zQ&I-Rg}T001B8}1 zRF85R{Xz!wGCq$oxEkoQi797_UrvQjJEY>tu)?UIbqjE$hT*kD{u5ZKIGL=4Lq3RG zf=owop9e4{-J48_W3PcBaSdr+WK)Hx?9ftCLHt_w87{ zP(t@DpgF1#XJ8!3XJ3wC1Z-yBD_}A68(0CvIcIxF+=y=*u|&tOA#G{ zGRBFr`#TMtSuIEcx9^A_lB{G^A@#ty=g-Z=SD)>Xn9!G|~;HOnv{ zLp^H~)C7$huvYtu9J7i+X~6XDIcZmG16^MQ&=rVFO+a>XHao7*P32cXjSi)0*7Pt2R&0&oXNLO+kQd5D*Jv6n)KwRw|>WRn+uM+nZP3-P>Z z4wF?Koo9=HcL%q9p@4BAh4~LYk1DDsi`d6RzK_vnA+Nx0HL0CzUURr8bonk5`!TvQ zpuVHBZVggHQZrl&QwJ53R&USX&hq!yYa$+-p>sd`clB#@AjI+g%` z6-XN0K4AV_(X%6?QpM3e#oRA+ThPrZ$D#%TKb0xshWl9x(7ySFs!!~~2%^|EdC+MG zYJ!M4o6mnpGro#vxNA10t5Ot$=p76v>Obx%%&Zi~hM2cG@!|tFlE2R3Og7JeM zx|$xLf6?*mXI|#%uC`?!{#EG0M$c6BPbxhPD`_pBXxm59+?_$%9~G*iktz_;s)5ny z;BpDz`=Y-LjWqBa?ez4#O^5>Jad9%MYX+i;nV6cYaB>sqmF&@Rz>OhoF$AQRS+c89zw(kTB-u3i9F%u_W5nI?UBl`tE4t?wlG5 zhanoTG+eVSreh`1e-lyTi){}xngO9O= ziKsrafVH*ZNDqxL4GI+V4qtN`V~jd28%dz%`PSKw`bO9LD>#@?sPKrpS$9i(qX`=X zCP^(w$kZNHviWeq+&Jp}7;HX3lk93FVl+c`cNvz8vCzo$g=3`d>{$?^RKUXBUnAaB zIf~wdFj4+cDc-NbMA(FI5Dw2|MrP7D@CmZ$<~jXogciXVZ|p5Zd#6y)KzHmCO$sc zU%Supo8M>~@3_NVZ$|chKw&0yBv0?dD~eDhRVEc_MXU0>mCjbw(UEicpEdjs_g(3_Leg zf*?==1QLaBubzhGS%MpqBuo$&_`~8#>5zR6c1MboDaWQTk$#8~oo90_<+i3B+;wJ| zQ|7nAO0Sx5wt11*UE5?_y=YCW{EATf=7jCV#N+5nT-}M8d!@}on5<%`?6Rl~Te&8C z0q1(&Q@Fc==q4^|OurG{CVMqubQS+?=Bwc7-;a|j97{u`e z{PX=Ccc7{ez``Y}g!aL4Cv5%0dV2!wjfS8H`zG9Aa2?Q1L^-11R<~i)w`_Zv53bAI zJcD8sk!NRaB05!7kxpXw%jK?+F~ztN#hQ`=xReKp{P|i~{KUg{ucj+O_S z_F<`MGtIR`0@MA$5h+Km;svtl6Fbz+UY(v_4;xRi>FYYwtt}a%Fk#ExVJr;#H7_zk zr$bqbz>lXPL=jdJGn%E7^~?C-LPlN5G*34iqWh}hQVkR{Ue zDUuUXM9|GHnV)Ckf%<_6lL`|-8)cOHFHB4?^seI1kIybo_ak&jkwXQ;FCQUqg9&!` zXb=%>k+}Dv-c%ztX{=x|Hy_A!1j0|Q#912~m=o194?{VRB05$?x%6EQ5V>k9w&TR} zF`7U)n@U6^7RjjQGjbTyXHTl1k2J9=H}R@o+Z7%>YoFhq-s`SiJh`}TrJr}(HoSN@ zZNBfX^vB+UPdsQMC%y|aM7u9sja4fR!biYsSkYAkz(h)!mmY*OlYS0V)P~io5PF*1 zvN*Fsj?tbZD))7W_Sn~YxFdN+1Og@x66ObGXi4*O(QT%}Mzp@3z$sqXOB(0vMP(Bw~|#%^V|xX~3(+80LyzDzIb@C`*BG zg(5MV@)ATLur!1!yUg8}Pfbf*YeZezW2j+Nh6-l-TvCy$Bx=QVX4gdWB(ou8IDDmF zL~Ip}$ohaLeL+Pa5+*-ud`Z@(5AgMYNb1lA*0r=f+&VKl)ngpU`Aj67PiSJNr@x(I z)M9#5wIwjPm7-BwR-Q^%Rs&e{$&Z(iy8V@ZUvm99*Xvo|A6#LPW|!h*SFOb%nZbC6 z)AJ%K@hl>FJ-xz>ZxxP(aMS8#UGwyiQ|r6u)MN0s_yTjGi3u!9U)`$Ck%d?Qe^928 zdQKQQNT5`)A}PZNA2X>QjJO|>zaEqf0wk@zw*0mMYT6>>cGV-V+dQJB;qrZM;JlE4$`SSJki}{?k^oX|BTBI~J zZf_vsp~dCowyDJk!ML(l^~T$^)Zi|zimsK5=cApcSIxR)V3ef#5$@#F>frcvH^B{` z$6h8XvVAFEVM*Dt7-keaf3YO>6wE1ki~WH{rdl3TJtv%7Y7kJR`~Lg;{pQF1mhGm1%g)s?>!5rV_e=NmXx;CT_?2sNnGW{;+CFK(iu!9}J8Vxkf}%`MBk%K+mFj~SZ_ zOfh&Gu>Lp7$mB}XFDbTq#cJva2$U}bC=gichl+>>(e4@(s$Fcg>kvS zy?vN~ij51!NcA$LFR(Oabz*q0XPpK0&^GM5!a&gX8@a9sZO`eAo7`6uLThm!^SHLJ zFZGM>Z;_nu=jDwb=k=f;H!JCYDfuDW>N%k~Ec|>FnxsCf4)hub6(ap*yRmKJDfEWw zT*NhqcR>K^0*e;=xEBCz-`0d%RGq`e8bVawR#6uxX&xfMPN2gsY})wbebW1gKnMn- z9I@`p>HP-5BM`-S&0wfQdN8feBHbtMu5O zaug927{a~?))E9H#3%^vR*Gh zp(${ZBOO77x`r@yFsa^=*btxd!&9tbu6&^*ehU3?;G zT#J@Hy~3)x9)&8LE^D?DnKI>&4E#QF^ISwshIaLzA4bjQ_Bn-RT!F(JJ6AHyze4tf zFa1i+FZ64ZSlCS7@S`LYj+1O8M1gR?FN>>E^)EaQnKp}woW$C8vRoF zq}N7;iJ&k@VWaz{(8Kl1tnZeAUHA2H#3Ya~Ij$&a-utrlkUOBf`s%{p$UhL*a9ET6 z4~#^AYk4zsF|o1zO-b~dX!>s=oqrgK9RDY2@ZSR0&H&YFfcCYtxsfA43;d5nH_IPI zZk_*RAo@?OmOrjV^qusrY)t|B;Q-BW$G=s|RgE1S&24Rn7?~N^{xkpIj1<0FR>Ug8$(+o za~nV?vigRKw$}PK|5(J$9UPrR%=8_I*f@w7W%U1b!wleF`XdAWyV@K~OfbKF{r|H4 zUwHzQ_#dVHbN9~`K*9Y#qWW$B&;IzU>i@N?e>d4bjq|6mSF#|E(qZ4w}88- zF3#bTFM5kxPiJ@i@0KmM;2x((qd-;UAC9WnoExSOKIe_f9TuNiq!M!#g8|^;&mRW z4E{~-DD)%mvqibPj7zW0ONWhLhhuVt*oDGrq`n>vlfh}bVvXQL zXyYrz;``=PZDMq@70{g?7P2(feZ9N9>+X0yonY6LL**gNpzhjvJbI5}qWtb~P2Q50NzssZ;3!rTTB z5lfJ$jLCNt#SM`md(vZ^C0D;az(pi^!)^RNz@c%!%aFx1@QDG2lCb@Q%bH)mJrvsU z0W1Y(Ag!C^wkPkAu-%kDHhuc>wo*K`V{!DT^G)IHBP(f*-XX)AE=Nzu?vh;!@X7 zja5msN4`X{O^^mT38GcW=pt0d^bGPEP3z4&?8kg~)z_mB6P+;iFA6IIKO`FnBdDQ- z%3-9SXwD_O*WZO@!fBz&t4*nL>uB>pMI@)oqf@Zt`b4DSJPZxpICP}TW~DOPDo!enJFEgB9t7iU;S4j8Mbti&~bGZaqnr|XF!2Eqk6vkPAo z@XzWGkiIRHCyy5zqmRwOx(i3)Bn(FuYKhcZWmp}3p$F?(>Rf3$q3%c{j6URJl&~&g zd^;~FA+R#Bp~hc8hv68^^jG6fLa2gT0~Y{!hj_)MI@=YS!SFXBc1LdSztP51&>gVX(i4QNLga$N1D69a^=b4? z@50{#-D2$O^yv|x3fvLC&QpuYM0YbIB<)P3;7V;AR9i0SV7XKFKX5^#;Omi1u+udS*6I-QR$XL6|UbIq%wm% zZB~1H?ySdPTlRa`rB3R>Woh!ObA3nS`_=GXiq)Wr6 zmwQ0lOG{U`2sYNCa12)kV?&5Gwe(x^oN|aM$GbS^OkW!7IKW$8n@vnfMK0=UjgIus zB6NnEgt1IRo`ZQ(yU>V!%CFu|zpbDz+hK~s@~Kk z?B0xS!(fLCZaD4VhZ;e-DBe|3%IKL^NRy$ZtX;z{W;65(g;2K+@&j}U5)Uj2gwmI_ zYqd*u3*iblr}Y>W=>x1OJuHVuUL{F(h;iSd=|>uWum%kyDzbMa?^;_k^fh~OvPxAh zBtbN9!tvEJsdYhtHFD>rRU}^f+V|vT)#Hg=+*OU+Q%>*1&t#S5aijYx+G+C0)?xCq zBO{P2tE`9@ZydAhHZ2b?C8xvBq9`Sc%`b@d5REpZUey7~*o!q-pOv7~&?^s6QqHGH z&4Uara@xT2KoxmYw^_cP+jw<=ImT+sSgmz8I1?A|`n5RwNih_&3FkRmJv}n06H#IbS>>IoMc{X3vi}+o1t;6{HbES5NHUa#%H&eD$Sua-(=8=ig zw84Uging34cMb8@2JO~~ja}vv3qey6IJ?F$@DShR;Um+dIJ&-7?n_(&Dh+V*QepEs zOZ0uAdod?7fg-_*t9kW2zlq+Xk*IfVGPDa-bEazZxU%HqR!moLUA~K3DbS5vOT)|xk1z7kw_%buM zDVu=^Zd7w1mk_T$P!CjV5zR)X;W{g`_U=E-7sjJMu}@BFc76;gz0ziUAQl5p;{10^ z{THnG4K-MpIN1M&)PHeY{*6EW;M4yL;3_9CA|oeEE979V4`9dm191MqW-zS3fx03Q z<3BL<+!OcIwm4o{~fGZ0p*Z&J{vI2bn-v5S|fE0jhc6N?`?|+{Bw)~yuU*~^& z;sk)>-#Z5Ysd4;8?8yY+2L0{l_r~=v@4ufi{g-sV^Zb?O-=6;E`QK%;17PgmvHbS< z@9Tf%WC5Ii`~TYxI0JnBuFvnB|IPup{!`X(>+g+)VHZ5ryhSDIaxXX zEA;;FZ(08{^!{%H;r|G|IRMa`6+nXuShdRpfZj|T|0DF~W&)t=f1o!rU}*dUy*Zfy zfcp>h{td!qjP-xN75yG-_~m^zs;6S1(du>sy0{=Za@>f7A)(8N|geN|cKf1GwSx-g^{C>}H>}i+UIc{+OuHuQw7bXS{N!Y$ zqPVOQ`KxY{3Nh=Z{5)C6#e2u*WA^6V+_cL4x-;LT(&T)*MgNI=pRi_#XdSn-2|ei! z=8`ZJeO9fB3YnwLJ@Suqm;M^sWTOe4aC`6h#PCI(SZ+iODD3BnSNGZjlZj!CeE)+y zB1gH=*n>&k8Vl=#wGZZ*Lkz~uSuH}VLHD;f4a;xY@@`&xKQ*Kej8`Vh4i86@Dzp>1 zJ71}0>PJ<_POaq?Y~RlhKm9@uQ-!{vFCjrh<<}P_CpY3DzHxNmP_UagMgO+T$eWGB zGW+#jUS!G#M3~^CEGML>Qh@8%457=tb_ojTfJDR({>m=rm+ca0qXa4MrTbp?pH$oE zhu)!HMrq1R7>|L`crh=rc~CMZi)lj$m+>(9XAAoJ@KE$ntw;zYctTUiekqIMNJ%7C5?o<9@ihfsEXm5EsA6857NW0L z$QikPW5i=VW7$LZhz6uWxe{4POoh6%8BzB@1o%TyB0Gu3x0Sml=3Z_rXo=1=*x8qG zWhQc4TcH$LCvzChT*tE>ABD4w2brY7(@mo3GO@&CpqVO~meqlC+Old$w~D)wAwnDr z4JH#8o}n8QB?JuTDPl;HFhT2}lc()jjYcQie4-i-kdpaf!~k8?O15I*oDjuS&BiN~ zK{nQ*zn+S-ux{d*U_)rn{EK%v&<;t+;QQb;=L2VrFaEw`R1=OqZ;wzwFBBz`(-~Ei zy!~t9TjJ6(WjcYoKur~uw57m19e49@$1W1l28yX`RLKddcj(|q`jnkXNYIcJN*%a2A!D4t7q)79sOU?k8wXc ze)*vIR?bc-Bc7rlZx#FY1nh-^435@AB8SGN2ZHZM5Hqqm(&Iy4`ZT0tqIXI_bu*CKJMTIhDrVgy+r zEoXOp(XLdY; zyt_2obY)D}JG!R?h7Ehi(sg9Kwr9LH@q5fr2xIm@2eA1el;1K%@rqhWX#0JMWJ1*k z?STf%Jeq)Z3XClXamREGjY=e{5KkGJTpH!NvD>TBrq_Zmfp*Stk_cIGJnGAELco>b zBS11K>fJiZS!n;L@qT2aWsYL1SzX2_!-cIb=WNfr|8-^u*TwgLJMthM!=C@Lefi zq_BMZQQ_un>C>{IYT~QvrMr? z8YksMCd*?fd0rPgOdPs`;N>CZVE2QgB-=^j%|^=BR?s}o`Z&i((+g7e_J?eDzN7M+ zfN+W7thG=UhD}R(uJ03{y9 zGHG{;%=ItnulsYK4c`P_==n8RJqX`yv)JY?scCbm~ZvUpCDk zLxlWXAgspP@I$d2Sk4KC>cY1@>P8QtSFj_=C|3F-#HZy7+tx1sXJ?=O1xnTdDHj?(CdLfq2k^A$ooAz&;>_ya zfupPqhI|}LJF>M~z^Z!mLwz-YSo$r+%|})yCrfOObuZ2V+rn7*0-J>80Wlk;tp@96 zvQ;X6=vmfm7cGe-^#6|UjDI8@KJeE!(t4osm{@-uP(kJ;#J!ihVEs*_YzbvcXY02idWU3?>&1O&eWXbe> zLEXB{4Fj1;pv1e;H^K@l@P5}Emo*)za=d#jD3-u^*$%j^*THKMlDE73jO;*EWCjI4 zwT{HD>EhFm52&x;89OQ*Xsh2!Jr@y2;az4j+f``7@TX(Rsx9tE=KIuY^_&hmQ9hxS zYmO(Api;Ume}B!DlvPY;PrF^HAO2BNKYTLGdg6L|Pi0N8?o*TNWo+UoE73x$vMM77 z!4pMw4KS#-B&w6~=K2$tXqJ%_R8V^jW0~f9`e|mkN_&Y}3yTm6?XSZ8OBZMX4H6W` zdK%!FLMN0@!h-H$Py1ZHukr6F+g$QWurq`1{PN3cL{9kazA>45?kzri@(6LsN0Qq0 z;b)}h0kwj8!y-of&*;k$*47YGHWW&BR8$}6_eHD3seD23x6BSu){Nwf@xNDm-F+H@ zsFC!*RI?^gjgt_Mku;)ij{F+;RTgKs=my@1sX0JfENME)$?N6ePUo0%sB&1~`0<#) zfC88WaxnLfb0m9~F#f%1$Hw8)bHax4ng|>1V!X_(TH$v=M2F$|BY&BJ zc#*nra=SDY0fmR3c)V-IJ(0^}YJk-YVv6{9wKW3hu$aoWSay$_*z@4!ILa=EO^N z!5OkSIP+F0sQnl@EYs%W_QiK_taD=w7xfk5Pqf+7#zj$u*6Oib?lF8a-V)_l<^dJ? zojHV4lS@<_@%%&Y3~y52n%)whVDmaM=!hF*O@}*+;}LS>kByxKbmTc_d5_=jkobk8 zYmu#RwEE;e-xlo8gx`F|Q`%3wfgh0_p>NqOd6}av`BeCx$RN8ZN&!m1@#E1oUV^E{ zYmey}X|QPTQaRVeq8B;0xi0E!_^yWsu2;rIzWl5U-#*kE?;B)8{`VPHI9~eMYN8av z2@tVm7`v2C1H|fHuBfWY<;OU+k$4ovKxK0F&(hPS<1-w1-|{<$fuE}~2KbU`9;sFn zGe>Giq(>w(#Xexqt^tI((@Jx&(<)7y6_V`DNcF$_hP%<-HvkVlnbo8Ltc^^)F2<#Q%gF;KiZd{nAptw^Fq zm-!m-#+G!-GQGOIWA5TKYNRz3W7$+K;tnKB^POk-6%{?SUPND7oiusEN$Jz~sxl}C z_XGG;v#2YR2xB`FI+bw}8xtm-Db`WeWm^!|zx$VBS7hgBm)Xv2=5CgLpPYg_@=QG= zW&{G6d^=Jw&r>EnCvL1|Sw=)YK=e6GHL76C0#;m>YYEhmSk>sLQRR$)&qE!O{@#F;jNt4IJ!Rzm=M5NU9Jsl!Y6$v?x*jDYj277@! z>S5EA>WrV!^QQy~gOJXS#T_DfFr9(t+Yr7eaNMUIKF^P+pwx|{^hJM5D0LERp?iVQ zBC<7RSdmM4WfNDEye-LZ8jh@!^bmCQ^ZKkB-&+&~7zRoJr&FnlcHNQqFy7)R)>9|&kW zz<7_oazj2j;9b|!LjCW8E6%g(kj;wK3SaQeEH+>_#1s)XAq~_dS=3j!n{U%Z?j9E}GG|Ai13l z9~O1+dDX0p(Rcw{;F8?zcB>@6T}rs4G5=}-e6Wo$<`=ZA3IF0)%kk4@1*+WRDNv<1 zNEHEFTj8rJkDcH!_**!t*c%M&PW7wVR~xd7czhU$SAj~((Q^kL<<@Zv=JW`NcDYz` zaY=hwCmBaFY+JdJ>!T+7w`X@J3>cG4wW*AV9J4X`RWj1o7LDQ;Nojcc$|ntJ`dLNZ z#kilbG>Y?`T#BfXWQdVb;ygzY!}mj*96T#Xk}lz|Zn2vbH>!J%j`l+N1DxNT$RyKA z*@u}e4{XcL3D!GPE;!P~#U#dA?-?mtWlfC6hsfKfjay4xxzMo>%CTG0k^Rv{WFUUz zS;oCnG4xyKtB;p84@<_MT8cwDI^w=yQXUs`PcKXMa2Eua35$mgk~!zad0eLqfn0G( zI!GJ2+Al6>@hY>la_gt!X7ZL+3s8^u@LR-1fo1XX9Je2&Tn;L&;qfgW+*&_E1>EHW>x@u!G8chKi}DVa#|hWB_NkRR*1A9Y5m zwxJA#@9|es2$Y9{^pd9c`|+WoHN_$RA>L>UXkK^>6SOawmB)J1M=a4A{|G_BeBG{wwlY~i51 zmS={(HV>J-#hLJ1XnUQr27Iv&gdNXJW+PapG38@KE0B)>S@#f44YZJ3 z?kSs)8X3|A1(qUzBsAhfsO$VWze{?DJ;n(YLrUh+E#;<_V)~dpTtPGf>MbS|nj&eWIgd+zi2YIsV3!x1(6L9`fFCSfS7}NR8V_pA}NGWyxIn zXzYp*J|8E|NcLns0FU$HWApMI{A7g@UNMG>9vML~lP9<<$$P zis*(!d510UpbrvYjEdT%Q-7XKm$c|&wFN;&NQ^PxdW4UA)Stt&7DCKj;`3%8wj?!m zI8BbWOw69)Z->8R!D*w`fqkI7(BMVQi6)$F_$(+X-mK%vwhpM5J(pklMz6-c6bRad)^D3v?n1?@#E^$Ww^1b&78^_lCWwqOP*vwpZOj?lXWuHjLT9jL!+->eJM?; z$U&=1tBz4ry;eENZM_L+s(R)+Mg#MzWBx&j3~)v zkDnz6K??J`odpZO)Q5}SCcl5HMWuXP@o_S~=8(}F4g(^F$-vtDe z%uQbLcV+K00cKj+l-LVj&=H!E8XiP^9U~81 zCHUz3%=pmN>|4oZ(L8+vEG`xCGZwos5T6#h0dhQO1}eo`ykWo1J3M6$jHuk)-A|sL zkjGjRp}rsKJr4>Vo?)K5xNL6M-hkVAy3Mai`aU5M!J+utEbFUU|9CGDgW5v23Mzp0RDd>)jWeT~9KRqD4%@#b03aY|}(9Wj$pA9EO4%bh>C+bj=Z} zbBL>RII43}jK?RFOKoNQUcl(WOzM+qy;zwAbCHkaQ+7CAXJ}eNpHNWe3{&88^eQ)C zDOGmA{rv1)-x$Y3^ZXi9C~7m8J9|tE0Zh7_`ha{X*ZO!#Ub<%)T#5K$a+c2ey!4YO z`Ixx@@5O_W4jcdH{Gr3~Ng4jJ(m6sfpxdxj(3|lZ<9dbWMsfG{u2wQ);Muu>zVzW+ zb(-i?RAM$*KpMtY^=fF`Y!DL=l8~>r)z0&IgZC&_VZuLw(#uhzBF;s)Ksuxz;?0lA zVU~{C-?DgLs;;Hwg)NVr#5`-F);?ut4cG^KivA?aCxgo-gI%~4VU9d2)2o;fn2~+@?WzxChyk?H~IN5N|Fg|$K2nakyRX9D!%XE(qg>bod~DX9#J=*0hozUBzUFgUX||>MQ3kGtu{!>W zQ4{r@?>2w52;sJ!TGw=h>5sJ(Oq^~b z-n&%2Hi!o4k)z12d!loX@0>#YBv*}%Mm`!=i^u~VoQ3!~__c31L2eRrbQqI1O#<&k zufu8Xd^>@zHe)80L-$%NyfC^reOw%`siyt#gp~80-uY}hJ^Is!H#x@>-Ak+U4g#3+`@7@KbI;>5O4&&^Cq9SG3Y`=m+8JFM%c@(` zc-O}9n&*+dC#Z$Z^V^!nE7~I;%SjYs^0$r^~$@7K9a*3hpvyWky zrHtjMGpmx%YDZKsVm*fCv+KzwvX7H$7{|&n7}P1GxbsbK1}M>Aq(Jp;`3!O(U@s%PvssH`QM;5v z3$NEvtEeL`({+QBQ3Yi9HgTEiX^Yh;C2nst#F|+<%ak8ZcJWBnlU9c*m2JKs_sorE zyB59QZ87ki6hxa}SNR(=zTF&8;v{?8&qj5!a$NPLZcdkOJmc{`OgFxK@>tw0L3O>5 ze=|GlDL7MAThp46^iXrpXSFM4Cg8KFdpkF9lNg z2H&9&*RdcLm);zYbqv~E??GwT3GL}?hfnnJ7)AK7MVylcLF3?ltdr1ix^yOosDKDn zvWHBR_L31(w%C((zN~T7nj+&9xesM+wHaMwUGfN!iz8v~<|p3Wmmn2dN^Qr(T-lQK zx|<1I#4_yfmpjmtMH{L-wKa^XnLMu}N+ol%wNlxHY*hz@^4`~#Bp2}aTrNa5zRdBh z^>mqWhD=BIqH)inX%XBe20#d|-RNB!71p>xkbxAX0Fnut-l&7-dW$R7y%#?gT?FeP$4*nRs9DFD2 zeTcq4FuFVR+NAb!cyLTQOYQr`fA^e4J9X-R1o#L-lf?Ei_sazHgk2qc^A{!pYvcXY zx(^>2%_YWoal=$LUds3v5R&<@G7ERn<-#*XGHQ_JU4$DdNt9+V?KQHhFO;D>5jl3a zv0%EOB=A8CgUFyU@6Lon-$~uv5q_mD(R>z_>D|47gg^Ag-uv5XvO<8x3441fFb=Kz# z2)JkZ8?Qu#r%J$%4YztdnB_7<$dN}$R7I_hK7TJ2dzN7m0wu?XG&ey7>+3O$5Amhh z4D~|7^ehVG4h->Uj!enecR{Ac2>pT^GXiwB1A6#~hHW4JX2p|lnkc(iCdwBQ0;$iJ zx{5La>GTsUhtjCy=a^oVzQ2@_H#V)!msP=(^(_-2|F!{#`2EKesZp_{@H53v+$Qu5 z^U}$~mSvXK>E#ftN^pq_LJgs1sy#urSho1)tm}gp$rsa`hD+4^1&iFN>n6B4OpQO7 zK-GhR!+j5V`EK9%ie}O<@)gfp`tDk0b`l}O<{%5uJ1cM zZ4zhf{?wIxU(oeQwPt5KlxD&Fu`~5B{t*l7B3ab?KAENtGt7<9$$EV z^XsuJp{NtGyx0sD`W;DI2i`CMthR3

dFOo8gH`!R$dsqwDayvizhY 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 literal 0 HcmV?d00001 diff --git a/doc/blog posts/2010-10-25 Updates to CERT Fuzzing Tools (BFF 2.6 & FOE 2.0.1).pdf b/doc/blog posts/2010-10-25 Updates to CERT Fuzzing Tools (BFF 2.6 & FOE 2.0.1).pdf new file mode 100644 index 0000000000000000000000000000000000000000..983458c744ef922185804756c9ae87714cee3055 GIT binary patch literal 140394 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 pCwlNP7$uni+5DTp>px@eOcd6SwT{l4dUJZL)O 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

pEL0tRYTQ&E_#SXQ^$mPO;>cb!;pFSh$ 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É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 z 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

    n++=Jn1+I9)$6GO;hW#@ieg< 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’BL5!^1gd2xUFr8yqIWs4sOL_0j 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

    9Kjybi^ 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

    xQm% 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≥KD$D|rl7x}m9k&U`a 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%0D7NNNFKdZYH0BAA!X(AQXrA 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ℜ^Vy8f*RtU7A-|E8 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 literal 0 HcmV?d00001 diff --git a/doc/blog posts/2013-11-26 Hacking the CERT FOE.pdf b/doc/blog posts/2013-11-26 Hacking the CERT FOE.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9b518dc8f8cb42e28363b9fb096111c50d0cdeb5 GIT binary patch literal 111017 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 JspzVL{tp=RT8jVx literal 0 HcmV?d00001 diff --git a/doc/blog posts/2014-02-03 Feeling Insecure? Blame Your Parent!.pdf b/doc/blog posts/2014-02-03 Feeling Insecure? Blame Your Parent!.pdf new file mode 100644 index 0000000000000000000000000000000000000000..415048f34f8e742e106ce25cc7bce765f4969db5 GIT binary patch literal 216104 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�r<3V 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%0DEYmos>rNCrx~odnE; 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≪5BK=SocCxm)M+ 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 U#-GV0VIn6^mWP|0hv&!P8&$O{z5oCK literal 0 HcmV?d00001 From 4d3c2fd56ed30488d0b2fc5a83e216b914dbbd21 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 17:57:06 -0400 Subject: [PATCH 1161/1169] Add dwarren to AUTHORS --- src/linux/AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/src/linux/AUTHORS b/src/linux/AUTHORS index ac7fd20..8e3846a 100644 --- a/src/linux/AUTHORS +++ b/src/linux/AUTHORS @@ -1,6 +1,7 @@ The CERT Basic Fuzzing Framework (BFF) contains code by: * Allen D. Householder (adh) * Will Dormann (wd) +* David Warren (dwarren) CERT Triage Tools was written by Jonathan Foote (jmfoote) From 8de0006e44511396f42fb0be328a1793644faaef Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 18:00:18 -0400 Subject: [PATCH 1162/1169] Add AUTHORS.txt to Windows --- src/windows/AUTHORS.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/windows/AUTHORS.txt diff --git a/src/windows/AUTHORS.txt b/src/windows/AUTHORS.txt new file mode 100644 index 0000000..031b6c5 --- /dev/null +++ b/src/windows/AUTHORS.txt @@ -0,0 +1,11 @@ +The CERT Basic Fuzzing Framework (BFF) contains code by: +* Allen D. Householder (adh) +* Will Dormann (wd) +* David Warren (dwarren) + +Contact the authors at their (userid) at cert.org. + +Crash minimization was inspired by Dan Rosenberg's FuzzDiff +http://code.google.com/p/fuzzdiff/ + +BFF Windows is based off of FOE, which was originally created by David Warren. \ No newline at end of file From 6ec036704240d1c38d9a8e3e56718be312536381 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 18:44:03 -0400 Subject: [PATCH 1163/1169] Rename problmatic PDF file --- ...03 Feeling Insecure? Blame Your Parent!.pdf | Bin 216104 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/blog posts/2014-02-03 Feeling Insecure? Blame Your Parent!.pdf diff --git a/doc/blog posts/2014-02-03 Feeling Insecure? Blame Your Parent!.pdf b/doc/blog posts/2014-02-03 Feeling Insecure? Blame Your Parent!.pdf deleted file mode 100644 index 415048f34f8e742e106ce25cc7bce765f4969db5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 216104 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�r<3V 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%0DEYmos>rNCrx~odnE; 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≪5BK=SocCxm)M+ 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 U#-GV0VIn6^mWP|0hv&!P8&$O{z5oCK From df2f5b84fb2ef9079399404762396d195648bc06 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 18:58:12 -0400 Subject: [PATCH 1164/1169] Add dwarren to AUTHORS, add Authors.txt to Windows --- src/linux/AUTHORS | 1 + src/windows/AUTHORS.txt | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 src/windows/AUTHORS.txt diff --git a/src/linux/AUTHORS b/src/linux/AUTHORS index ac7fd20..8e3846a 100644 --- a/src/linux/AUTHORS +++ b/src/linux/AUTHORS @@ -1,6 +1,7 @@ The CERT Basic Fuzzing Framework (BFF) contains code by: * Allen D. Householder (adh) * Will Dormann (wd) +* David Warren (dwarren) CERT Triage Tools was written by Jonathan Foote (jmfoote) diff --git a/src/windows/AUTHORS.txt b/src/windows/AUTHORS.txt new file mode 100644 index 0000000..031b6c5 --- /dev/null +++ b/src/windows/AUTHORS.txt @@ -0,0 +1,11 @@ +The CERT Basic Fuzzing Framework (BFF) contains code by: +* Allen D. Householder (adh) +* Will Dormann (wd) +* David Warren (dwarren) + +Contact the authors at their (userid) at cert.org. + +Crash minimization was inspired by Dan Rosenberg's FuzzDiff +http://code.google.com/p/fuzzdiff/ + +BFF Windows is based off of FOE, which was originally created by David Warren. \ No newline at end of file From 4a7675dec56263b3039760d85e2250bf60760df3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 19:04:39 -0400 Subject: [PATCH 1165/1169] Rename problmatic PDF file --- ...03 Feeling Insecure - Blame Your Parent.pdf | Bin 0 -> 216104 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/blog posts/2014-02-03 Feeling Insecure - Blame Your Parent.pdf diff --git a/doc/blog posts/2014-02-03 Feeling Insecure - Blame Your Parent.pdf b/doc/blog posts/2014-02-03 Feeling Insecure - Blame Your Parent.pdf new file mode 100644 index 0000000000000000000000000000000000000000..415048f34f8e742e106ce25cc7bce765f4969db5 GIT binary patch literal 216104 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�r<3V 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%0DEYmos>rNCrx~odnE; zK)49ib=}P}oS>fl?Mr*|@gbZ_^uwuN*_lrd(As<-ZxWT#war5da%nSF@i|U}WI`IPbp?f2aJsQ}=d$Dc@O!Ww#cIP@^lug)AAC)9G$XI5IDh% zl-_l8J2Xt7-|jlCR4MTHv7>k}B_@OF;UsQ1I-i6xTF$_<=eYeGR-n-D+d2D$yTxaz@bGZWJjpP5CSK;4JfuWJljmn~TrAvF2I}vQ&!(K_)p_xK z;sXMI`cz#{p}&shAlv0+4x10$f$VTm8B-jDAtT%w$pr*Lpu6EvAOu6A0Qx$D}^n{T$TGN2z~gGLn#)vbSM>;~v9(xhTd zZBc9&s7PD{!iXu5@Y2MJ50;D}F=vQUNC{fh-^?SB_v%qTY?;yyIE~(bY~MMH$EC57Eh9`4!7$j#Sz>1>(#uy1_3iFPzLCo9HU%#;n5_ zI_ibvg{7J&lA9qy&5kjN!f%7*C6|RP6=hY@Q^==n z7^suOW?FZov4vRkjk>(tuE%vh#MxNt-(A_3%zNSv&9mc0T&&jDVT&E<$u*bEpUutB zAmg6-wy3LWNax4BK((ramL1V+MKOK>3BM~U{sK;<_k`RO(N9lpi!F|Zr|b{;-xxx5 zt&Dt0O=oq?=$>M&t-7UGv@Ju2nF*bJ&#o85m1V*RpjOht#x<<@8HOM;Hi(`>M)Dgc zZa8>8^FwbqHHTgkl8WZG_Ute&wMS-uIdj^u0OSwI1*4-JVq04-gchfW@?2DJx23U0?5~rbWd`=P)%h zWlyK%AkpmC{tiw&S8BxN`)Kwa&<@lgT4*`uagpo<>Z} ze>5`rWxAnjk}}3E_xzmDshnqBW86h`xG3cG%Cu-u*}IjxB4Hu`l7;lu!vPt?-&K zs%H_ds8EH|az)g<@d?5&9+1e!$t9>bWIV|q47E74fAZ59pT@;2Dn4H$hMLhn_+2;dF2b5b>#a!ID#=yg7R16 zW$cJD0@{O5kL}@p+&@#a52b{vh&GawZGSJY#aRQJp+$S5K+vg(J^9gE!ZP#O$vSFr zv1**PF^At^Lft5l68ba!9(O%beIMX@gkE1610&rbrhhcmy)w(4A3;n zG0zx{gv)$G#7Avoqa!9J=J_%>mgVN;?!ig6`Pw^XpqrcIvM$dpmyW|X<7N{=k=*`6 z*cRnG{EX3@(j!p=fmokL)q!lm|wRj31?h+^>vVI-+Ny3x& z<_9WY5a8MqEvF*J(ge2_6hldpg@hgd-O1)eKeZ+02(GY=6^(LjrDyl7oAxc*%Ms=v z)(@iixi67B_sA}-Og^3;Z1DR;2mQMAnB=!rDDNXVGtO7jPQH+U9-h)|0kO&SYTjN7 zPJ?x^!zU(TG!``5F9-f%>9VNLQS%Xral*!09XjHW;iFTthNdccW9dwv=v*$R4k-Vs zsiokvoa7e<)c)a}Ik{b7AC!6VHyHWF$sX?5fmt=I`U;M3wpx3sQ(<%*PBS(zwNFLo zFp?}voDLy%;|WUTIVm?-yzE6-+4I!eu?}iPSgLoAdnepm)$*GtU@6Z4APsXp<_xZ_ z<(Lys9Gl`!$p5ZAwYPZ^IBu4)@u(cx=RypBTzv#^$aHD#Ysok0dB`Np>ecNgHqGQ0 zloXWIUfDP}%*tHrf2@$2UibC+CQT+6U2z|>zqh~|xofyV`HCO?>GT0(6Kip%^qXu# z(eXD4F+1kGl2$dd2(T)62cpEC&ZiB-iD-FYsc&h}56=rMBKw zNu0`lyyK+&i7Huolt)UmYE=Q5&NhsAe8L)&n2SQlO4q}e{T*$T96wjowoGc9weLIM zLLcR-I3=^%*N1{lRP>4TB`GZ0H4Rew^mcz}zyv!N{K?vIi#0j91qN+;;FrVMif@TZ zcGWDiZAfih`xctKMD=0F2fYtHzffa?Bg+I7{EGcHPQKm}t={<6s-Tm6&yx&`_{gZ9ZC_mWTg+L|@!JL0;ry_WcYq2NxF^3nABEaEKWtwat9kA+47=K={V)bHr6AjA+jJV3fmiXY;Ir+@FIq<+%LGV@cVKi=) zTH1QkkX6XHyecde>h0Q?0G^VbXvr<{%;KE_j>uEbh^TPbRDTl=+%_8RxC{d4Yq<%d z&q6!*FYI9hqs$+3o7w;RiwwZ6!9<9F!*L$+bUK>QM@v3II&~Ch1XM0VONMqXi(^+U z!JzFpFxfp@y1eg8Y$y6vxDmkW(XRduOcwWk;pcRi~JZ2LA1dLDF6V%SKH>_m5XL?0N+a{=HM{pGU^`-yKNJUpBYJ%+2>8J!5%b|Si1kZzkN-6@WI@Y;G}NflL1Iy24JdWRXW?2sdfx4eV!sS;ky)CC*oYSFOpMOA`= zM3*^YL8~15Tl!e{1fq)>?~XH!%LSTurXF!w*Kq%%|Nr6at)t?4wyn|bMuRp457Gp8 zCund>NN{)eK!SVY?(Xhx!QDfGdvN#QE??(&&bi-x@7?jfH{RfnX4I}-wW>zd-gE6$ zbIk@U&w90f3!1A35>gS|^%b!~!%tO)m+FbTZYLz3ODbV~yY%AgW7p!pkktfG-gx2= z+K9!c@A$nZCdu5}e(hr|*uNp8Lp`tUUDyhoK)JNH0m!txj2*r;8Aa7jx!cwjRBic8 z@VW<^n9Y|jP|6);h9oyPph2C2b5TwD!^p=8NF#*h_1i;2%_qs^#YJAF#&`vDgck)dbRoQzSzxJ;RHOE56nPn$gX1N^|cM zzYs0G&e)=qJ2?7cGub=4I9js`!MZXwt57=v)YhEtNeFrdGB|y6I+Dpl#JamoIJxi4 z?j(*#c$94D5mQZ?Tfa3qtf&RxW{_8+pDezQ;iw2<6OC$2mFd*2aE4(su(Q00S}9fnDpSrtng?;-Klr zcEOaCV(Edp>l0?EaqDl!PI+qk*%w&e#&=?rhyWCc8EyeDYW!_byi#31A^QvbkIMu) z_CPjvyqs6LlW3EMa`!&~K7QmNMS!r^nYld0Cs@;s1C z+LdKe0oymUQ&YF;vH(_`6uqCj)H&86>=HPHCn4t`0CIdEZ`(JFusdG!On^(F*;Xig zh>T$&)?Bj1_TG@`{aKyTogo`TeDA0AU>Di3%X+j(R(xocZK4E_O2a0_Jm$E9`1n|3 zcE|j#b77U#OX*2)$n8PpjQVISQzSf7W7YbooTVKPa80S1MpD<2paQ73K@{FzFPHlR z0h}WM>Q~Y)NCT~PWBWGHXi&qBj;!s0Sys#pd+#2yd~JVp2J+sdjGR2E@)zO@`B2E< za+pYL7Zw$M6#w}X4I^pH_~{1xvf0IJ@oTu3F+Hmb|14lnMPV=gm4AqNYk+`{dNi{! ziof^Fx;Ftg4HBVOh82#vqaMIBfLFdr25HDv5FkbYNBKp_4di*K1!hAT>S!~sh^exV zVt0Gqy(dG}4*jw&EAm!p=Nc`__#0I&BVpdEzjZeVI~vI$8->${nH9vSk4_Bbu!CEH z0Q{lSV7Np$W@(UxFnq1Z`*{G02-`f+$|mI>lX`&2FD8DjZ+sB(EF9^4~N^NXLBLfppz#0l_tEEFD7{+gwmQ)8BBz{i~Qt|)x)XaQ!MB3HW2LEr)@}A z6FD!^CPPg9ISxlAkP=^!k}jp*M_I=5tq!#ybvt?7FrTs1Hv$b|UAaoiEQlTzW~`8W zJS{bj+y`O_(S^^vADV?-gNM?-2};kx0!=Fbpt*QuN3eXbjQtrN?v^-iQwyHUwmRPG~-I*VSF!k?V?3Rx9d z`JGaoTHZgp#vzOPVIpbW#W|rlod%f(dFw_Lr7>j_^m&?j`~y5j-@k^Qz2DTLt`5nN ziIs^M{-iWNy`rq&GFCr^X0>BZ%~{6PI+$C%q@htRIh)PfD=_4d=bCoMfGQZK9+rW( zjHkzV%&5e4$VjF^r-7huQ-4r>-@DkCYNcc3IT&cUX;e2_xV+O)QNpz9)Oqa5pQ=;6 zjMw_Pb?h4B`t1Yt0~?ZHkV{ZI#?8Eg6K$)%=M2{2-g?vMS@=XG4{srkL91D_k8a|-bcKD-Qm#TlN-P(oa+uT?r~XmUs@ECTN%p0nhzRKLWG)F%md zi6n`SN$QLmbz73*n&FT_K|HcIxbBor-%4&Kmp=w=9&Mm(o-lK0C)OP;erwn+85ygL z*6Y^zN>BQRjARx367ssIWLviKQm@&08-6aXHRvIBcYt9x;!Nk(s;Z97+G3-4b#@-$Y2S(4DnAX>I?LJ>Gbxj-9|}LQb}qM4x4w*! z3||aCcyXC>IXjwlI6VhGc5|IrVN?XnR%-ul{_L#N-LihwaTsL-v5{Ul9g!S~FnP23 zC3Iz4AzPukL*Kma&(45ov#7#)JJYBZhIPL+uJ!SM_v~D4S#If_wDMQ z5;rR%I}ki~?{ywnR<2t-w^~g|Pny~c=%jo8ev>d!mgVLA&~9cp+;?gI$wtz1G@gpjkxtH*N}Y8%s?)f@kdhyMYu{)M4`@hUqv z+kfHJ{{(@wU=bNHF;N416Jy9f7*-LY^Iw>j^MAm!T>rwf|EB=WFP#VbrGc1d67T+@ z!~zppCj2U$^4B!@{GTP%K5V3?e5*8EO^{W1OA;C@bKud@`P|{o)9*I8%=c#eKf^pdWtSI65oDNWs8T0;#Y`xWN4l8>4V?}B%$S2UtLI+dGTkfhPh zy7!jQ|AS`Wp@u$H$+ycp$IQ=_N|i5Fa*K*ZF*b!~9;=`IbgQOD_H(~`K0Qu+TB@OV*K4d#Dw>CbOccb z8nK|;W38;WOT&-Yr)^$8fhv=b(VvlwKGSm-*c@prc%Yn0(Sp4g9QtfN@&Xc9-_Jv| zX0P~w&BGGR+p~~E4V_;_t_maBLi|n(?63F{-$X{C3h;YnKBrK7^Fd`3EzW;bv0zS8 zRI_~gjGJ5}qnQ1ZTFsVtce3hcOA|pdRp329M^!qGW6+&`?(L_{J5Ro$aYQ1)GZ6RZ zo_C9smf9rwrY+X)&R974NsGs_RbvHRi_hF7FYfmnE-xBhu!D|SM%0H?g)DlBCwzzdrY9&Syu?=Yvj;BL>_k1hr|%eq`e=Bg&DbpW_&I zG>Yv=u{OYlA2_mbY5$7Uw4iSsCOW+m6W`zFGspR6k(2p@#3u#CA#s@x2tObnd=_3) z1xGH1bLG}E7CBDU4*Sd0JNU7tpr~;4dt;&*m*q^Scy&*^p(Q?m1W1t_3_<^#s zQt*L1fu0E$8O@4KieUC#twcjIUUgMGy;+jR{FT#eg?Xw6(Kr&4x$9#7tK&`QFUy}X z^r!i*v4btQI(X$IE zgV6NI8$+>uROUcC5bg(G%-pjBC1L%gMk@QP}y$rE%)TD+4jn#F1*3$?XC0!il?jJ31s>2$$^d1-bHsv5 zS}4a(ESjiz1)P8?f@A!)hqy_))+y~?J z3b-y$b6r8>1bh?a5nFkLmbwm*R6=IrjbR2g$lfms^j+_RsIb}0)c*I9trMi4M)C4> z8UaM_*Wi+n#r<{8Or*y*_T0kv7%lF9ZlUaypkXodOGh{!##JDK4qT)ZwwZl}${f&R z%Iv%Ey}m~`*xTILaXRU76X>8b{XkzmQAv0VnLBl~^mf$!1=c%Qq+-F+a}K%fX}d>Y z`Z*%OjL|4c52Sm;Dgxs{!s^})tlaf|+3^~`##v*`fZS4D z*@yBAl5*&$7*T|uv9v2K!EfJuJWD_}r@URv+{nwmEQ)>HLF->S0}4{nyfr5Laz_#Gq&qebtc0Kl0A!>hdM=nf|rSG@5SK=6FWAu z6V^O(s%A2;-rS-nq6L3W&>270i2%|?)@OVwd{RdNn1$Qa9IYh<1MVm5TR=#^?Nh))xH z6xQ%miTWf<*OdubrHiDx~DhMZdgYa{ncExL;u-FmKthI(*Y=4w5mO;@lwrm(; z7}zl-gE_q^AJCC@Of=LLEL-{8u)6OxR~W9}TQd3el2A7A^+Qhtq;&*E$jxOsHCDIM z0=<7_gPgDTJMFl7odMUvE0Ag|g&lIOiXmM1b1fP1JZ&dZ=(C^=2gy;kC8sZ``=Kd( z9+VD+2v4%|iA>V!S&WHTiT%7-i90d02t_nC*Pye@fquFC5CbywXYbWC^OtxJ?@u-r-n9 z4!F+}Yw1{VEFHZ>(`-NWTbv^wpr2?+1Pp?Afxj; z&Df|8rd~JEP93M~R~%X?ZxM&!^c|sRtuha1uM2Y0?DM#bk$0!~Zy2*mjk2By^cWvk zVxKgs$p${ptLtm6_SGt9%rt$3n6xDFJ!r|+3LR$b6I6G0k*5UAcgzs(Nt?2d>{@4e zJmAI3SUH(JJoJBCza~XW>Z0V`$c@%yiw_i3Qh!3yppfe7Yl|XrPQjfjNDouzvJ@Mr zr_t@P;gHXBH-(tZMZPW+GV^~Ijzy4#MFFC6z9KQSju4j;^bjHrOog;P$n@~rtB(sE zWyg|E{bmzbvqEGbz2r}zuDLbs986mDlMm1!{65wcw;5_0(q&GIk}GtyukvQa4H5O( zO^+lf|I+bs6kcN^vCX#!KcGqB>>UUcapzRLDaHV|F?U}Lrr zDLpD)4)L74JHO8^mJjS-7(;sc3wvQBeOPppp>hfC9W>c6mPX^y?hPH%hF$_eUy34G zJ*`}pguA!Ac78Z1sd4S@%npX|$?V2C1~xQ6oQj$yL7g@MUznO&@q>>An>+Sv(q`wM zRYPg}XwO|H14f1yLo;qs+PCXfRWDXroJaY9?sed)@1o^{;N zatpFg06-L?L_5prwBQ=Ab!R63w%~qk2Xlmv*>+}-7+=WcbU+SqB+4v~fpL}4=5dOc ziFaqB8>={(>(f1I@kpd973!EhlA!@ZPL8@h3s;{niqf_+0bM~4e4#0_Gj^C|AZ~8o zbDr;n7O4GGw(IW4Y!oEDUu&xK@@#$#(J2~q1>H7m&YVs9KOC5`Zt0D$50QlsLcpPzltdGYZ?UcCoK9P7O(&y0sCb zFETBqu<-AvNA4362fLPQhbB|`A_{28@Rhy-P#Anzd(}NZ-)}&StwI=G%q5ZlAqB9# z1-~-HYOHM+u_pdiJOOg$?#;dnps~117r_|u6ZQ^flxOP}d3OE5!9V0Zc7%tzc!gn6 z$3n?A|Gs^~fkpP4zFh!k3og#m+kt2;KkQKiigpnzaq~PaOhG@M#OGzoThfV&?Eyb< zhScS{x&)E{fdjw%yzS1*!txq|njtwUIjabT-2z%8avntf(=C}OLi8I#MMH5p(-d)= z`S*E4HKLrC&X(wNAW6QoM=YD&^Gu}Q_d?DQZKPi~wU%Xnx_^x}jErXPRaFnsmw9c5 zGQ6**Q$Uwp#@YKoWP4eD5!s&nd!sJ5l8cwS1s6}Xvr2GFpg+N$j6>Mp$x{1Ns<2S- zJ&9p29ZG8RC_QzR{4t_%N|wc|a3zU&5gBESZ}G33Vsr{)2nkp%k+6KR9SFGj(m1U> zKSSg*lVwm3Wr&FLW02hwK7EK?lMa3MBXHoH+#&YxW{->uO2}q_%oIPUQDNfMnkitv z?&<9{#v#N{uHda{ibF$1so-VXoXK2%MMU{|nif(c@?!Y<__}bqVfEY>M8;S#9<;nl(aCL0`afWRr9By7bkLL(j;kBUYVGUfz##RjFKvd@!YCso@k8E*4 zydp`mpjX6p{s(Ktp_2vrMkj4@a&>HtdTFxSh&S%=QwX~gF->ZAw4kO)oEidu6 z-K61?_u(u$HEiFlT+awJ)YcZ&J&cS=P?;4`jUfTvB14iI&-!*yAf*`cHIlbnO9k=P zWLua)3DG@vUmWogmniJ*n|Du+TR&Zs&zbo#eYG5M$&Ef2MH$A~h@sQE!C;`g7iWlI z$lH~O0bM2J4-|~^^1#lqcoI?PWCogWt>6y^s?e42417YCTiZE>^76S5|3W{%m&OwI z#SAybyCD*FXpt0cneMTVu(y+d_#_^Dw@{tdp@+6eo;zG;0 zt*Vm`=4C6vdScoXIce2~wpM(w=E8B`;aRQ8Ne=t9B_ro^lyA09BiJAPLsadEhOIlL zp%wImaFW~eWszHK{Xv>%0c!-U(jfbdJpa%lZoCMGUFoL`1nfBw{laY_JjIJT<0+cn zI5i@jB#_J{FJp7i2h^^y-~Yox=F-74a}a--I6j$e!FWqv`9_`taU}Be1673o`k#U~ zYbaQ`-WfGEAtmHWT8&$jZ&$n$U4r0LH(aiuDmq5Xc}V3rJwOVr8(Mu=z5X{-Z5Rxm3UReTmpzJf!pd)Q&brrbp?jiblWPfg0=Azcy?6gP`Hz zNgX`N7fo&AvxG(pZ&Q`JJo6i${pE|z{cz77RRA3y*7C-*o#NTCFOw?#L2Dka2JZ@< zuKV0O?4KAilu-e$s>;|MiydBhdNc|QZ?otOk~_buc~{^LC)vAF7)(1i(!E{`UfT<{ zx70h%21M6%pZVD5BG&|lD4M2?fwz)5V~OSo-v_Tq7zLS9@L7I-i}R|KSE~r1RB?a1 z-uDELW@yG`xP*dzy&$z6WOecF`bEz0NE&+tfpEi?ILG8=OIpb@@K^z zuENGue|+OOGD=est@sjPNi<2U{$~6rMB*!D4-|*hnN$0Z$&2(`(xe=stQZbLY-tIp zk2=91`%9?Am=k#;x6aTR3V!}cE83Hh7AZzTW<`?+LZt1T;XtyZ)MXR(yJyv+Uty)+ zsApWpx|8#x*h+G!6{AIemC6f+v9k zEcDs(UFl%<`UG$0bYh$@1Euc>Bc>dV<?*Z ziZ*WSsAS3}ls5WNO8goPnH!f(eE2xQ6z4p_bmOYfL#%X4YmnEO&ryB&SdZ-Cva!?3 z>&cevW#>lfMyRFg1=Qs2jXoqLbuk8b{@^U@Mb7f1zmQ-wNW_sqL8zw6E(Kbr0@>@J zBp)iaVeMRZ3LB*{x<<0BSDZU-%|y(G2ugb7J-m0qinGn7$M@*@$ZSVPfbwzx#Rs$4L|}R___T_XK=hK@X1u zgFvH+eUj0zArd_PjKX%`sc(fYW9_^&)LPZWUgr23{%fPHN5t1`d~CONB4=0+8A*4& z`Z3rS9k-{d47c(gE$Kh##L2X*WSd87tKUSufi$x7M6r$=q+g3 zYpT`blUa8seJAeGvVbN&FA%h+p3vhtHYRnDodBD!H{kfN-_4iAT_sMy1yU`Aw55M{ zMM5s0Z-cx1(p!BZ9Tu_UN03sZ~jXD$jRr z<(nsr80H8^@@O~Qg=LzElJD|Xq5hVdF%13dNSLH$t(bl0Z4t5VX4Bb&seS(R6&~e9 zbuHl-R-t4ly)mcZM9Rt!NLO6&JL(8r3jC#hMc$rB5>DY3azwOT$g5x zrA%if{BZ@?i+k)y(h}$aDI?lpQYnXjXfJc!+9tSlq>FLhCfVE_uXUK{eh~?e}+B3D?9QoRws*yfCS3St{e!jP+LietPg+PcWY`5?< zvC1(!tRk=JLhK+mLf$K$r`{F~YlvrZ9+#MWmM?Nh$|sPx#&d-&vHXkIhrg3>rPZl+1p#+- zlt0GX<_SEqlIDW;-UOXVqu{0>ubi;d)j%CdwC@SVB51YU(&o|(h3#Tty~Z~SF5WK2 zjGVYx6Jq-BRGy9FdH|m#S~p4I5K9{H9Fn!&SyE!YNtQ64E#{3dL*jIB6f@v#kIb2N zNHTRgf9peZZBsXh6*IVpYy>(E{|`}|zk!#(Q52+hA} zI`aQ;a^wxZ{bkbpy$mDpD4N*YI({=Uv4^m8|EomI7DmJQ;s7i78{>gh`D9{jVIXSj zf}-`8^aBeNLEiDQ>-^>1{F^rP&z*n!|4R^(|7L5XWa0pUMUUjgC1DI96BigU=pQ~1 z>`>D`zheLXm4WE|cQO#<@9kptY>>YJC`FX_?_p=4V7B~ItgOrn;rI{dNSPhN_V-K` z%r<3ZZV21I<4(%TY!J5ps>1<+8T#)!?@-uy{`voJoBtsz{q0T0h6Bb<`d2j>8%_wv zznXGrQ=FW;)~aArRRN6j;0N!M39L^5bt`;3ZVtj5C{tk_XFtZ5yinc^70D8A_`!5 zw)D(Bx&l7f!p!9Sdp~X?nJw934wbN?#nttUL1}WB*IJ zT^$Y~lJCRkf6K2+)1XiDw|g(QIO=RM*ePdOli+^98K65Q|3~Is9By2=!3foiA^v*U zNkn|@s6oA2SVXw1hU$YqOxeC-eBwldXOq!QRwTVYicYyU*gf$F&j=2Kf1P%XC~jzT zmjc~p`8R8WbOJB*^nz`if5hQk^L@gF#j!$i;~s*w%2`G6`k6zGaHhaZv_BO3^7evQ z=W8v8j}W#tf8dYlc0VEjC9D#h#`sK@>vRVevON@Nk#=9Sg8MYwsWF!o?m+M zs$tVwrQJ9!yiVsT_FE{TIGjZ;@Z7CQs2>>m3K;_#3mFH8jn++`QbamuUi53A;pd1+ zi8Gu~vk78R;nviGTKr)`kQ2iDV)8&hJnAH8*aTy?@!7_38iH(cBXzfiusT>C<=sYn zc%9L)Vg~!@Xv0UxO07bxfrWN2!gRA~mZ{ffz3lBoiMWUqy!O&i}dOh>aTxGU7}rXFXA!enyiy$= z?KDM@hiueAt0CEo`QK-lt@Mu1!|g2}ES30`A6ngKlUH&E5{g=OXL#hw*f=)ZuIn}D zoJzQgK2{vsIh8dy$R>%Awb^?%YQDSra{GHsNY~q~qNzw2N(M%THwGg3Kx=CTN8>e- z+4CBQ*Ig<_0F=BRnM~MUfkN?_h~C0qW;xsp7e#5deA!(az5yT%$aaVXK(X>z!f*I^ zI!8f0zR*n|@Lh)PZXv1Tmth|KtKz!7pL~rO8h;|gFGg3c>{garGlikTfL|E3@E-OG z(h!J{C^Q6Q`WS>JB3yF{0i1cCZr|v#nq6rmEq(s;qk+YDbkF{}K&i6U3oE`5Y`0oq z=6!!{rRQ}z+PS*qKDbxOiR$9CHt-Q%~b>9MdJQ+Cn-pu zB`C*TKly-2+%XC?y1MG5)*S(2UA%cL!gN(pqElF)oh$zRu$$G))goRz-;?&+a=$-q zK;$M9^WRw=Pi|+?-0H~pgF9Skz{pMO76W!upj8q2!^xUdnA_}CI>HiHF=3V!%b_jr zyI;gMyjdNF!P`p$6`k8lZvDa|%}=QYi5LA=V>KCS=w1^qOKet6+aBu&If?dzj=LJ8 zHforS=~|UUUwE@p*?xYjY(8}uY5sB`KhkU{N;cwpn4Oq-k@*o(W3(SX-GsO3=KaE- zcxPa6L6cX?G6K_lNnQBJ7RjCDD>q8A)f-i zyv6Nt-L`|T`X&Ibs%dxb_q4W z#|MCp@&B?>{-=G!^6bX~&K_8YI_QcB$Mggo*8$vLW;6h*J}!6w2ms~sjl*bD7<0dP zwpyn_-e}f%#co#^O9uCOTUg5#DRmvdi=^#76?Mh^&K0Jg3Rxr5n#F<_u=~0S0iX87 zrUZ4~!tYLT44&cu{7q1xj3YPzjYK*?Eu#{^SfdayR4WUNHc12N`>qKg2qy|dQBYtq z>yMoP_vr#{-E5_iI!FDHKowafeY_3K95f;~-}P%`haVD+4#NNee@sRboqp1U zKm^H!Q6Cnm41glS>!%Rtvjou@*&h(1Y|Cp}0J8b&ru9$Ip0F#{q>WAq9d7=59lGEhJ^0HQxs zzg-KCS9>901Vdnd(T(o#7H6vH{q(;KbA$nNh1@xlrNU^S$oG7g8UPUh6Fe8Mqg_0{ zZ*IGi{Aq=zxFTmX4bjlx8*#`ytPk-raJ0OLF0v>c*W$wjqa@hQq1#s`04kt~9Gf93 z0hjCJFd_W@H}xos!^0UN=n?+$79NP8gbF%0&$7GA0R4j#zyqceF%RufUxZT*i<7EA zAeP-ZgN_N?;7yX=QUxjdGe6I@cL$ZBmW|Pv4KJ)wAnY~>AyKv-XYn^O-D zFCs9qG(%rf_uuz>E>Hyy1T5wD$NL9VSQPm9L)KD0y@83-OWyOA+5RbsqoI~jlc^AJ z_grZ*_>?kPP^ZQnuedN3;x;0>FBzPmT&8LA>i|i#oOP+RGR^)|krKbZR?##yku(?j zJbAXrliOt>baH#-B-Qbk1Ci+U+gQFwjH3EhyT?`KQJb(YjCvhQX;sH9w)@9Xl^Qvw z0iQ92+*#2}%_nlN4rU2CEc+_)vl{AS)4*MpELw%KRUBh3u$4Vtb42Iyr*ii$QA0AR zy}Y3;snK>0Q4Rsd`aYaKi${;Dteu-GQp3EeF?e1|;cv99!GFbu@9eP#_0nEP{hb*I zgqTE1BEy@b#Ef{9_%wl8ov&xAj+aGtg;rfs5~ACK`}#$en)E+k9n4Jg$^BU|xoqxL zN!gk}Muxk;L0E!xF?YUH#%X0LmzhQgve;Z$u_ciM*0DU@?}r^DKJ$DT%6{Hc8O1$$ zSATToGlBg4nwhB4^S9ENQ0v*_#VAZoo|4bH{0XsTDkQ0A*p zlA;f2@*LRL38~a>Ry=L?!vkNpx3w!4gep<%iK?W)JoRL^JIbF*5ZCQXI!fNJJqTh* zn2`JJ46QT$knxC*0@&|tCJEsB3_+ytvB+hpr9(9UWuE5+uT$Sy`6`yj`D-fM61qh$ zjx*4rHz){^d0St}&~W#*_p&G5M-w+@k?_|a{@#^UDZj8FVKV~;IRwz@))ht=5acsJ zi6D8(B}O&R_SfS+;LQMttO{Uo`$s}Q39*pVPpzsyJ0Fg?I;h3@ z@G(kFuP0>9qt|y~PpAvW8NKo7O3ms0<2bvWZ#n_~soSv4yON&dS%Ym9;yo1k;5~G= z!HYDkc^Ng&T4s1Qc*p@r9+3U}yMPY?zvrTHB|IN|ga+ct?JQGNUY78Tlxp)+ABWgd z0Zs5($?{iR5cuWycZ;hH`?OO^Xvtpk#BO#xB2az5$EN=ks1aI*E6NGBK<^6xFkWoj zfz3wnXiCrgDHI!-i_UsE1aL$kx8i^pU}b~~^&WUE*E`FNj*jEYb}+panU~%Al|7aq zfG?OgnMfXh>B3(Rg(~9f*u5V>?y(ognFTu)KPylKVZPx*t~V2wWbEf)+Ekbu&A<)- z6eGxGBG*9y@WPyZe`n?@F-J7hKH?2OVwKQRx6^U>pk|*gky=;$4*%kaHdMz4gdh*+ zfB;&Lw-kd?>D-9`Op%(>D8u*VHV0_rspbcW$&+qbF(DXeXlw4rKM515_+#HGEq&dK z3n!#ISa$}xxIolJO3)8gfoa0#V7obzM#T&lQNZVvD^w{?umo%ZpoD6&t|uupUpEM4 zB4~(Xs?{TQPx%}zr$TsODu15cDn3Of;^)8Kkn!K|<-raGg=_Kp4%gEqZKOQj-_?!1 z5ya*|Zj>H)ZPZox3Au|g`{S;X!jio>vX!t{C9zzoFp9Pt;#Pm-ie1myV9jZ;ff#lG zw9HgCfO{GNX;wy*sk9NTF_0{+85{n^%hR2l((vk4)Y+05KhS}Hhu}A3ws^>fHryeK z^CwRd**XBcy}zN`cEnp3`eOMU0c2X@_KgpV4L0cfvy=fFf7&_UIYVJQd* zv}0Hki~zY#WhPy%sW>K6=BI}%YHBE2H#}x#nc7NLuLiW#sWja&RzB>2>B1jSN=#0I z+30eB@f?D&QGLgpZ5DM>9PQAr@RW`vCZiVpjvR3ARR@qm&c-m>K_1~fX+m`P=K;_H z_UD~@eMmM-KLJ^rO~Z@s+R&;@(ts8+G>2Cre!~DJLySa7*YV>V1SXc->~uvJg~LU+ zx&=g;hSo=`&WkeD!*+##LdNkjH%?=*qB7b}vGd`Oksl?-{YFj(C<-1V#w&N_XaDZ> zHdJ9cffS<(z(fe1O5$=W{Bkya@GcseC>Vg#_2y@KC#pPB4{QzhH-YU&dhT$p&$y8= zuiGXIxQ}Hv!{j~W!g#{fLzCFmkGtugl%lG9SocxL*hSP_Ob3?9bqw}H^fJW4R}t1c z!Fclq0sx1ud)z7JybrR-U13K4hLhYoMJ`H53-9C#5kSw}JK7%0p|c8Lqh|Nvw_qap zH62zQ1Z}IyCM?P01%FlvxR@#7QF4IT09w02?>%ylOZ03enjdZc%rFyc-N2zV-j1DF z3GoQR<88!@QJdV>h6pk|yuTH(L?}+BRwH-j!ClGB-$9F0iaQE?gsZ-V!$sbt!yiF* zq4H^>^r1gvSXj<<>J zvh8z42mq39=6JVJ6}`=8Fc+J`y>=$MuPkdBsi!M24thWL(Zbz^y((q%lP}`8toAStw7eU;w;OS#K^_r6sDmh zpj@6@oFk6e^6GC1X(S_0crz0g6I#DQX)7voBpi?@Z6=^ai0~kRnOJ*jU8@auX~swc zjaQh%mnw3jWewBO4@~!tl8`PrjvjpU{8U6naCD6BNPy4HzX1K$g`z1i z(-O<{{Wuf>bl$yZjQkIl6Xw*PfC@t;cwrk43+mcp6 z?}8{bSWUA0#ea*ivZ2+u4C$yy2xL+$dNd4%+e%7+KG5kl7RxGlkg^Rj6RbJ^VptJ4 zAtk>Q1}E~>OH?@_M6Bdcyzeegg#PAqwp`hw(4~yFcKpc}b4Zp&*J}`~BnCeS=;}3i zr<+X!1vK-!dP(KOpsYup4^9x2;-x!f+cq{ns~~~`U&fLl#}WaAt(b)TxBG6c=pSGx z>*K>v%!|MQ?J>O2Ex=D&vF*l+p?s$pcWioP7z$p~sDmxcze6iRq&>>#frs3#nx@xr z3>aaib=xZtB6!!4?+8=71bD?3!;kR1+{{&ZKi&1dcnMwZRv~%6Y*Vm0A1|6z=|KSZ zF@Hb2A*;dRl=K7utgpXhdXcjsuM(fv9?W2+^ojzfqi*GLI&8arHSO2d^0kd~dgBrQ zG}DLFetCRZ9ea7Wa2M_(>-3TYpThbWi3EXwi=Htb|4#2NQT86rTbOMu$<7t7zEzSv z>EoQ1qQKC3-tveIAtdh#Zp+USz@H!N%nf&_jO8y@5}8yC#$BGJehmKU^Z?uoz1&5S zz#J2GBhg^k)EHPmL2Bw?;j@?2vgZ@mKwl66PbZH0R14RHekv9Z4^sc8=qqFMx56%1 z`4U5r=PFhR0Q2Ppd)!uU9A~W&roKt38gxVp0RxzZ|9(F7FY^JDcagrl!s&q-H6U$f zTuk9oe-kIp<)!g62(1bqA^yf6Q2t_ex%kQJ;jXJHXm$M~b&~=$%p%_ZSoBtQ)C?%H z;oU?FT_aO{+-0>Q&$j!?!>W<}ItkCG_vlhv(C-?e(*Z z`P@2-?(K7(nIHo`GE`G>SG~jI4yOO%cT)tDdlY(kw70v<|F9vI%XZi|CP>qO+i?C@ zRstp!tH3J`OU(B2Wu`P?7&8Nw_BXjG(~bVp@;Z7*{LVBMv^{tS7 z;mgx`^$uv&SHr*QQfJikR1h{MS=vr}ZdSeZ@}*;%VlzP@q5v=6FORopnJN}P`A?eQ&QhZM zp^{}k{!C(mvQ@s|i|2L|Cg0U3uJJ?Z<*7UCZSbE;9tm z&phk?EA8$`IG-8VP`F?&Eu`)G1PcCYZN~tnXF94b;=>Au0Q0Va#juc-`;7q&$XNtpYbgAhpg{3?`61WhL zMYjzmZSwSR^Jl&$&+Y0Unnki}$(7ft-qf=-4D_EmM zu>dBV(dkldGnxPD{h0)oc+bO0D`xY7=y;b8oX`8c^}orRyAkl^H9ljV6eHUUbDEFq zAI?@3pDsJElJGgun&iCvO+7ea@*}=m>76enbLag*8AD~$222YTrtJf`myzu|fC)kx zLb5#0hnv1`Lc(ulO?U%T&-zA_#9g@2b(UeOx-1>H8-rxtKYP_s4cC@ioy{<|s#;I@ z_O6rkybPA#CgJ<~9_GUax94%T(Xa3b(>?WsrTojrqeUVr+)3rO39TV}??LbmKeY{( z^SWCN-}h0{c*V1n)w>ACQq%_!c_(XL2Wa_83cto|{x$Oi^@{A$pVys`SQAe9B+rcCFVFC5NJzOD~K!yf}1X6lfh8;ujj)wV&s@YkF5K6J@CM8 zN5xWQI$C09G86a9Js-Xt6n*0Qi0UXeVWVWFLRpb9e&-W3@mWY0``e3?3p-exG>4iI z`4si}CsK?Jay7i`669kg;b*SM_9j8KvJdUzu>F}`{YUb7tWhiwQ{aW-nas_WiN{h< z92LGgSNq9WSh(Ho3XEwrTRseCwOQnhoCNzR!DKh3n2=#P`y*jb9~@E-SnVm_A9%-8Y~CF=jet7kIoxS_-qiTaOVqjOw$Z{?!4Q1 za|&#NWdrY-e0@gd{rD9f#7I%Q3R{3L1vt8}1$dm2)!Xp{6AQGbEB_)AgbK`2j{G1% zz82)C^>t%}OyF*Fn?>-^`=Wa@2%}u;@#TD3N?slU{oy4dyU9Rj1aue)q7VBX z!xc#!Z2DKPP3R@$&x9e;TOrc?uLeGVOzr>0*jtB1!8Yx~yL1RHf+z^Mgp`zoAi0!u zNJD<(2_J3!Ap08gw|#@Nc*ssN3V4y zJr%2MfKRej9yTbcTcMGJaAfDgVY@8a3U$ARV>u5B&rXsq7juOWdv{`JIDqh-aoR*d z>cz{VK{M#XOT%W+rZ-o=3SN2xH=eFn7xAR6;okB_6`QndjF$M->V64?;0+9v6F{{e zx!iiBHS40M_f=-EnRF8f(PNess}}+D*p|l_fH~+Vclp5UaJ4%Pw`QZ?3>O+E9sAnG zz)(7n3>pStgiXZodZ(}t`SMrfSH%n%37{IR{>r+gNDu8JXZ77iQWgDI+fBNw&g!A- zI)D-Z?Xz&nQ+=r$Z+7o6Y$Y(!xli!q9fpYZ=4%UK1_zMkpyZCPwmn{84)(h%Smyrt zJ~{cTtv(ajUI+&WP!Jd zv338t2PI9=LH<}3L^#6a0rnTF&9EQ!<85)zYAJqZJq|L5KGbZ^AubENdX>0@6)XAf zU_jY6a5usjrqp4UM(Mx63*Qn8{@S^gn3@*}`$=%Qzp!SX^6Jc>H>6W&D>=}VAaC7i zxfja$#bN`MjjL6e#mek)yoe74f8EpU3WdWd8`NGQ@V1`n4qZLazjehECFS6Gk8A_G zR=~4+dx2HUqbTS8O{bY!_rn>#eJ5yQyM|1olOgp3w{(`l`RBC$tO0=-qyFa_Jdw{t zX9lQeR0BGs=G@h8%eZePhAaOx9I1CQal1c~Y3fdimzh*OiQS{6q?R?zERRCphVhU`^%~z{mdd`a12KYD5 z=YP72x(%Inh6rY4yyC1couF0=OsmL)vSH19XwkRPhz-nzR`1O$6qYyqU_pp8M(RK` zvB?mn{3=^RsQ1i4RVE?*x;7F&nF?QUvzGBp!xu>nl}Jv`PsjR04kmxjObOL%2g2B4 z$vL;vk*_QQmh4`I#H!WEk9SW~&=~F;-}ik-SG|{VzA39`-AFCq69uD%T%{X}%cK~% zGHV2X4-c7mZVt8l{^DVU?p5TVZ|3U*kMp48i3qy$TqpId^R0z(;x%0?ihME-pz80* zgtyMM_Cy7-kKZR6G~9T_=JPn4DEH9`iN?zuy4Ea~L2p8+*&%_C*UWSfL_!3wOhSK= z;Y?jqE?<9@{ryYhrP?akk=RR9{KONnk(%pY&QBBg>pp)yo!4WRC`CX9>HLrgURVJR zbF~x(Mz1Di6$Q=dVMdzIoOZy??`rRE1zHBi#qwc!LrRjLqtX37lmX@g56Cg|j#B{w zaaLQ$E!c;kv8?xoU{S4ie%+88=tU7g$MV0VXUT#H3r6%r50eq=Nfj$gGy_o;(9BH_}!U{X4My8KF7Sg6)#J)F39G zRO6s+3>640#3Yu2an@j5?i-!_hT}gw2Ug?wpK?#JK$Q~(&o3&jLO|yvjFo+|cJYiZvLnJ!UL7Ri#6CM}?kKGLxI5$bKE?ko z;lr0psq*hwjfe15jzh5~5Z7U-90)YDWnr*`$zgSjf)8T*Ezb>NgauFzkLrFDWlz9; z6~xH#*YJ#oK$yAb?;^hVrb78z}qDUCLG`vtY2Fh{VM_1aVO94Z4i4g+@IhhjF^vR=e zud8m7>$<5|6@HsUf;ZHUvNu(v$uTI~A;eU;2}1&<8_=5yt->+#unZl(q}IVl3n3K8 zYXyj9$U;=rM!7>z!BVgOx-Q@9VD@@q+0%EowaMR6x$A!&C!dskSGU)|6WRxde5o`1 z{^;QlG!M{4#0-3RNc#rcXza$EeD$hVM=x@T}lzchbd8&^mM(#ut zK7S8O^_Uojtm#{8RB4p>`$9UhcolH`AOdAnKeC~p-ovp*Z;+XiXxWL-qT0`1ucQ;* z_=Zar$)>Xt@bmK1tBz~E-J*nW7c6VGLo!wkFcLn@TfZHzJIGOH6Mg6VJh;T>8Y;-aqFmGtRnHK-Fy_`!vm%EC zm2mOysuA1HoX0CbOPpGFR;-VgeHn+g3fKM2u8LyAxC7|2XcE+q1CHtUKD#9Qv;n56 zCc*d2059e0BoZQ>xIUhk`iZNlKKDu97N+Tbb?N$|&u;xN9Fi0*dO9zNxAy8WVQiJj zwvvXHS|1`2i83yaT`anR0fl<$2?l;?UZYJ23Z1Jkc+|B(rx8E^oz~~t5N2zdyR{~L zE#9!^W!aTmZ-(>rZj@j?nb|%Alv+Bc=N4DUh;lA3KyybWagD8UdGOeNvQ@ir3~jne zX=GfARMohoUWZRn@T_(riP`UAo6xUU+@BO;QM8vULhb zxb|QbU&6!^^^QvTsIJ=Xb}eygkBk=?83izq)6kfnN17!9 z4mY~4XnyA*+_%SZIh&w;!MUK_PMmR2pQEAT0(Yv&fOhP2m__&bka}+;{)qFv z&j+`9&RhuQmLBu2OC^av7T z_wQ5Xcr1g^So_@;8bB2zf)IzMSH`ihz#ylsB-)r(vY`M5Ls>j)Op*5|$J-hR2j5Gi z^k2(W5zF+omD58vJ{kXz zhc6GprFH#C%!HD@6$1thxB_}*v!d+yA@`OV7rPQiOFbyZ zs<(^DnfP!?S&+t4#fKSl!B0{5NLas!E7m<$v#FKSJSe{OhPs7=6D_&N!;sEP7^AYM zYY~pd>Qe@KT>M$XBo+ zuU91NL*_for|uT6O$C!J-&cwsD;D86*my~EaEKS@b1Iu!c*mL`IS6i`ZQryz6bMt9@eA0G(H{CipV6?z`)t6{B$+f%4XSk2(F2#PC=yxmklwedc?#UhFUsz-Ov^O5l*Q0*)jl|j&*}yaYOD5u5sYeJSa&G z7Yb%ABmgnT@5@?5-ioED^VX_UF`IR^Bqi3^4Z|&EDr8gJ&inA;P@&%EeaXvH$755} z&ZWM?A9ibc)@-}CXvZ)}O#6DH_anV85f-t+q>Jl458bDh!z^veax}H_%cGi3@XGKy z2J>{NbJd)+WE!=wHDbNLcw8d)40RMN!@MKph~x)39!9fe*vJBMtWfO2);@FNUqtHc z;g^w)H?ul~ol$E#Y&|-xw10H-5)RN!O$Bv}oFD2 z75ma*wO1xZ$Z+I^SWpBh(K2ncH}ivr-7uqO9w;0u%oNC%RO6uS(tt29lWpq&Cm&w43>*ju!htc~t1 zcaJ|~%=peVM6-yg>u|H<`H1b>4hFP0gvyo=qTr=iAK7%W7Lk+s`yN54>IMq0%*aS! zGBQi0j#o^`4ZUVlDw!Ku08!b^dux=a7;*TG?4;Lf){z~)D@;_{^@Yh>^qJr>gnw3a zxEUxc6m0MaO*GLL;22qi;zxgs24B|v*O#R+kJQlS5(nIkD8Pld%@;j|nBjO>r2z|6 zc42{@>qF~6bwAO(0#Q9ZMj~&n+GLl8KLRE4OOC?=@?O%HjKTErz?8q_I~X;8Lj=U( zE2qy}!}q{=AIV|*ui#VLCOiPSs4+6(%?(=KzerPATH{!TBu`Y~a5>B5v9BLyBX1C3wX(tax)`6mbQpYa&yxRHV9ToD zJ#AqY{v;8?(j+ENP8duo#)3mqD=KKPXGpNk%|y`LKuj52OeNrDEdi@2XI*ssh72Zx}VM6PfIS zyu{gQ-Xo#Z$&2p+xv4}b6o_MV?}@Cq8u{JzAOQ{hKC3LZ25~rqTc`4Ah3cvH)#H`h znN~Un!uHI~Q^j8`zI7heo!gxqh%I@O#=kz+*go9z#8w_Iut{sc=hZ3iW$&-8;c@+B zcUZJL>C}ByyPWB!Zp+JkdsvwpDB9kZF+X~d`ACF(5WjKWkLJZKThIDi#EtGoEn{Ui zlCPZQUliPrCV=*K#xAVjE`M_UJWyAshk;P7tKFTj>jo)f5Ps+9W_)A*%wHp7bQbZX zjdQpl+dncBlH6Xn9e#-s8pfukO3cqq4<1}{Ug2@J%`xw$x)xP&Hr-T#chFL}n}fPW z#19?s(a{ifeGl}!UZJa@BNh5d@MHRPwq+=*5+BTuNb{=d58#`oSM3w3m`jD3cEqPh zCeTX8`7+h4Pd<#*yyh{=TMGJI4N!sdWx9jMHyYU7ciOtYUKTn(Ep+uv9`UzdXzy$f*ohqU3D%Dz zU@;gwTK+&kDs%Sf>5|o${XE^O;YLdP{*xRGsfg$#E)DIx=-bPqg7TAYRq6%tfxb8H zKFQV_FqdfZc&V8q5KUlP=y`34&hnXXU@pcZUVTifA@LLBK3i2a0u>eOllc@y6w=w2 z_^~L-R?W8E6lgq1eGTn%U2`HbA5;#f;%(I_ZgANW$HltIS+_>z6tA8cyj$pgr{|Wi zh+v@i9F?mT+t|Uzx@eX0HZCqr+ER}b{n5d9p<(?wGLCqi*1U*g{ItV8b?u##-ATUC z9th?Yi1W^qX}#G>$3OV(e!=A!Q19>B5>)hId$bnGG8gZX!DHf! zsrmsk<+{nXa3_g)XT%SkiH3OIHVVG>t^+G-rDilY~{M(l-B4w-Sr*?WZxWX&te;S)EnnxcqSJ}yk~O@9Wh0TQH01m z45&>(RrhNd)K{=&E@Cks#AWA#+I?MZCKMM{;^WYjU8*pq0p#~6kC^UzO9JeA&Cgp( z9C*dm+4H8UU)#UW;o{It`Vx5g<_6$}a^LvDss-Ljt!~yVO?5#r^?l8^AKs{SpJr^` zwIhGmlV}Re^43t<6J|=Iuk>~wGuiSe)8y4{XNyvDFj$o^f?ecxZ{+|hTdgC zVLffW?0)^LtKeb|en`k-ceb^m#L^AP71R{HO*9OVbbyJ)igW-UF;w%*i-nXw*Al2I z(eLL6#4>{GEjw@VqYM^BSqD6Z%OaSn=5IU`Ai7bx>AAg68VKQ=xRYNAO{q#@-V4Z|kl<;Ln-0cw zC?>ncTU6?q&Cx4t_3CQ*Z!A=NjHI6)JAS^3b?0sHBNZdH`a-YmjOn$Kir`M{2*vjw z-=uPT^*;QNtJ(4TOFj)SKe127)Pb~egI**Akhf%FbK&F}i$@4_&T5pz=B%6buDx|T zYk0NK7HN_phlh1tklfer|FFU|3HwjJ)RmGNF6+}fYM@j*_7o69(aYzkV4X22VH>*i+ zdl!amrN)R7)EwJH{kTV`lJTuzLG5<1g6@L@tAtgL>(q(Aq_pa1^=a`6fN)n{eeqNd zqTIOFP+gE!rp%X{J!XSL#Q()m%>2R8nhrW}k!)$`<@PLNLxl6&zAMA_o!r zOU7cxJq-fU7FRH!EG|jvx$0I9PZYkWqdg5iSQpye!NO2eFzmn4hv?^p@QY8nb*Ju( zmZ1G9xhn0W9OFL|Qww%rg zIu^Zk5sTt_Kw7=;YGljuhBi>5;*y>IvnZBVpViZ!{@%n8Tr<`Z{?D4=n!M@zlBf2n z3rtcbUIJ_SnVJi2X_kC#-eXs0n33S{Y;yh{Uh4Dl-P#Z<=O@bhAclV%@kSjTCO(JZ zp%hiAX=N>$a>7ZCpZ&XI@eASq?*rsdd78v})n z##7n+FB97jyRHhB@xg{GMnXnPxZq~7-Ux){_pQifYCY;vwXKU#o9NH>P*{VF;%e+) z71xAOuYVL@WeTRUA@6d?Apu&!)Ym!RF=l$Joh7Gp3(U>Si z^YmQ@m=2ZD*JBvnljsAzGr$0p6wD{T`M-N+Sp%HXT8m%Axg*lCG_6p+387UEfu@UK zZeDN0yI(xWZm8zA+%7D4ZJNFcp}qty@5I5HKE>wq#w!d`9#0stYcVM^k;tsZnPy!6 zkW`Wd3=wb-5bfW0t2f~n($hd6Ev5fg-yDK0UVQHiY<5|-8dg&A>v60DVG&)ant1h{ z5+X&<#tocIxd(mV)_N9YCi(~yT{!B^)8c6ir`hTS@=R&MsD!UwsSlX$CgUW-*9VwNTn}BG ztVDvA>_pkdp{vNKf) ztOh0*m-9D}g#0~SC;q5e3Y7JoJ3O$^$eeIZNQP%8p|e*{<$kOz7ZcDd>jg6$5uh(EXjXPJM_bh+~YokJ#T&Y?^vK= z`odoJRUEmDsy`&APy;w08mH5g;Qhx)JhGG-BPW`F4>ShZ{?~Vgf3`939)Ems!Z|qE z{Ti6IBdVIvcfg}0(JHVv;0^T$_7bN3R|6PQ&-UJ#{`N@L%;PoUFMu^K1msEo!BlJO zfE=c6Lg1;th=K!uw<)lf37t%FCZ-qTe~huf8bT=E)ll-~^gPv_tSf^@ZkCVcM18e~Xi$$=hXzigg*OgnO6;aOg0s^{tbV+Gg#%L@9q zoGA=Os8m*Yi5|pzB}jaC|4t!1lF$Bkz+}NI6@x?nJTXQWN0#kcqMl*?zdSKlRNsL- zLa`;KIAPYdHnYB!Zn0`?`zdaV4fU+hfsg54uUY5)lhOCD>_7JpD<6Kjvh)A4ZEAzJ z;HeL;95=i^uaP0TH_8@!aIWzg)vd=EwACXviBJ5A^&Pz@TE&KQybk*=9$6HguDFd; zOn$Hxi}MONZ_I^{0Qv;ajfO1qbd&_#(YYJr1P*74leR0B`FgR!&(?Mzj3Kz@`1vxc z>a_p;`Dc;;QIxHVT|cNibs5(FABw_-KjNnx_ML;YKHnkob}w z$J?DVW_^izh$Z$C32gHvW&SgjI7V0~sw*v=Ss#;zWJ`D@>jOMwBbPY64+ENQ@2dWp z(@OT@_2V;{Q(#q&0%?n^_&Gudl%rZ32*Q;`@9jTWo^-0J1~1ck1zJ|x1@`VrSLzJy z#;q}+;zW#1^ROXO!CAo%2*jJo8I3h!Jrcnvn3DLEVm8gJADAVJ??(g@j!!Au3hfi^ zh_jKU@<1_6q`!K2>qfYxh8r*8u@3dsv9=x5@7JLdCLiJF7^|z>)6a=ah zW2b>Rt*g54pP}{zf?#;9aaGr|wSJFf+1wn(AP07gNE>8HxJvZI5Hm#x>|+1feZhB% zty>SHJ*#6R>^d)DV>}@bRc?;nFfl!+69Z90Cru^^2K0xG3beUL>k0o|9&MhxwG48V z;F}ni=z^q$&#%%2{UrI3;u0OKSTc+I*WFq# zD`{~><#&B=gE-}UIV3$$SoUd6lB{xyC*y36eHMeQF7qT&32(bn$3Hun^*9pWbgVt@ zu9zh_n3r6A*po=CIt_{u-4_jNC$hCeupKaDxAz7%?n;TBOm`-%o~?K|BKdk^<~#$N z2j8@s=2*h*(J~y;OVB-)|8uCuR_H^$5{Dg~qd7G-BaxpmsE-G2v ztFt!iwGl_+$Ytsm@7Zi)`RrT_UoY&fuNr;oPYdueQ0&5VFzyhdy6LtIvFV-I`=OJ1 zx-qHzjP&W(mt1o>;-{Irc69`kMJR?^EnNQb4WVT&)xr=eSvR_Zr zoNxOb-!?O87Qn=yu}mL?ybRcUF10~% zgT(-2M9MyGM4b3c6ZF0PbE909M>1-rQ?G{&SOhnP4<@11i7*snsd~QDGBff*y#5W= zJ;$PqPn_8l!M(pjV+PK4@8qVeQexQq=!bV*&Rm>gr~}yr&^qowa(QL-TSd~ZJcGDV z^L&%jW1+(exwZB4g{W@RsERt%5X_q=(?WFXxZO0bdb2feD`70&`sMQIv?)~dXX6tr&gZtTc7u4VqN~PvuBHTAHrWQWx=*9@s-0~Qf*=Bm zGi|(0-Vw%>c#{;^7;0<8nMkKA+`{A;@Mjs<8Fxzb*gZVE{ru> zN?I69W7C4IMT@L)UW1}*ytb8+;NDx5^#XfyG{6fsI|{g0WbF#=`+UV>Vi zddPB3gs8|p?B=LCxmm~no(F1K2T^bko!@F>WmaHj^-3<>o`L{tzEVC-ybm$;TOh4* zHz>-qNdH-y2v`~<23x8tIjwGV_`faUS%Pd_cx=}Q=l1wFFymf?h8@^PX~k9UUi)E) z(6xIHwg!8^Zu=+GFy1ERaiEgFZpX6?^sE3Lca}*JWB(hi$+cE5{SNJ@k^LaiHQp*y z7E=O3CU!o=*>x_{J7VWo@MIO}<*j;c^ZoM!=k8G&Wyob!=r@wlD+6!TNZ7x4ETEzD zi`bAc{Q`)y2x0=xXDE4FznGEWQ!{TkO^a1*xtk~AMm5|=%r#ex%g<4XO`YB>_9nJ+ z^gH$6N+K30#=LvBK;g&4lVT|?P{;?#L|pqA_UkALfq3-2AFs4$f6-jJbIDm*tNm4w zs$GRYO{{Mv1w1Ule?So8q>ZIJMp$PZe!xEaorv?hE~ROLja z4BG>QLg&L9J4}?6D8u9rqq3tZyuewcR0n4dHzDfP1SWT>0gsy1WFC#KZBI}_aUAnHo}a$eA!-3m z^}Z|h{AA!XD2|fPc>UE$v3jPmm44Y%q4LI=wzpS9#lrd^!q~5g&OJ?{+`p-52BEj8D?WWhN))#}WPmt63Gyv6RL8n}{bKt%DmBn(E{ z5NWq6MK*21x<<_y|Id37cUXB;hje^Y<7pv0gg<*N_q`zww8+h=5sAd>>X)h&I_6O%1}#k$S-aci$*m+bN0<4+36CJ}ze=2fGUhRrWt8vzFmdx9f& zCCF(E(VyZynMxgb6m?Q4KX*ka7fF3j#N%OAszgP8$>3Hb#Lc7DjNa6*ADEBUiR0>3 z=J7a7VFYv~tbHEbiyX7;QlJzX7cojGeh!Vd;kuOEqEQo$tJLQA&2JglmSs={@<4GM zv9YWg;tc)#@}=hPLuUK6krA1cL%~QBneJkrLfbrgt&=sL?GKQF06%xOaM`AzA_KZZus22 z(M%Qu6-c=GJvzf(hA?s>DJ{$`vON)WI>Fg37)(d9wI=whGt2}t@uNC&WAeHM%PDU? zEw!WyNVkv`Gr_lRMALOvF>YEi-xMnLLN}T4Me#>)F~R@m&cDIg3Bs8syRHBzN`!MF z)Et^<>cc|0H2{f0hN2zet2KJZJL>DHR^<7XlFL%OkR(rMkRM@w2?H8nulht9^A6Er z43!=yKIjgG--!W>DI+~Sy7XuvWR|2dF)40j2+=pko5`yWE=KT%54f7U4-bD_C@ zHw;bf48uZKS?a?j(u#1V1(prKmRlz~HQ@Dw0nfwpq1;B6x4=VKZ^-g1n}vY$(VER- zKoFT(r~f(`5hHFJvkiGrx5==7s`TV{(fTRzkz?YPqa~pc)8*YB1KnVxCFGG;usQut zJ4=vz(}gGILJ7Z-@e4e5vDk;{IKE22=S`fcM;4woSGcquw6be$k8A{(yi5gBm1{oajx))WWDv3C)zS(EM z-%C8lbopd%)WT_J{e5F2_boEOAU2NAE_Wv~?Jk?n1b*v0h}{N*&X2{M3V+B&7>nuM z2klT!LV~?Ea6yK^M3*A4+9wLieFzCkI4PQ}vE;~UI!`!~^Hjk|sbHfkaZFul zx#{FSb4{#Tv$pyM`*)WX>+UN{>^c(L=Z`hnQ9|88sw+c~Q$3LwP(4#=HLrbIovG27tJ2Xx)PE;Do)z^ndE`r1VJ;5ZuZS z#7hQB**7>N6{@ZhycKaVKxY?3)1A`)H58m06hMk9Dm@eGEc{^pJJ3_SP=cc&<(=pH zw)w$t-{;){%4$@!Cs!StTJ*YPdF{1yh>}c8kli`^HKy9m^HZnK5&x6-PWZ!n+iz~z z?0C8VSRd{gtvi=(4y$8Mr@5e`BaiAXB|7BnNXV#TuIVqk{aK{c08FiN_}`24&_R*k zlM73XlU9G9`3m-PuI_D>ql4&ir-+b~BRPjXVcE0I&ct;aDi3$b+KZU&X_mPPF7s9g z=f=6+43pW)kjaX%LL@u;OJ-0=JApii>Zlp!0Wo5-D}2zfhvm++bt!^!s8~|Nw*Y? zUA0~vrV!F)J^8iAwsRQd#i}_`@m4FqktI&rJVz*pOz0X;C`{>M!UudlWE=c?86aDY zcJ-DZIAJStRDD+Bau9f4(;le9mD8txnMt0d3z7;yWrblgd;$oh(5RBi()P>_Bx_W1_wW_@?~qz^9(U2FbllO&&fHP!|HwaMhxo3b}` zHF!I=%=#j)YER{P2>e4WYC1O&WGcle`li!X3)IhECRV{ih_BDfmkFAdAipU_;_q`_ zq63k|?@M8-C)-!sQYOf4@Ua=Z9Z+fHgE0A*Hsne=i93mk`J~R&Zx2dFaw(Xm2nSY; z!oRTgRyu5Cias4SBkx~n+`KkNgmFRoEq&~mxYjQ4^5+|CM37T^tgvRXyoY0v9MxzSjt4rd>@U&f&8#QmRA&ra?JertTVq>D7Im7o65 zTWn%-(`R;7^%rv_Yhz|&zV5xJ-x?c-w8|H|;u1btPex(CyDda7Q(5@ZmP%lnJMfy* zcf2By0>~q>8VRs;1L^l>=M8v&r4`2KJ-F`tS6XV?*ZO(-5LuK)OA`727>JeAuy%c- zB2zT59Zri@B_ne9XIWmD?LQ$DqZ5z12g*NAlGRbHU?{9FoDamR*;&}XF)A?DBZ%+sNEN(X7Wa8WhtaE& zxsv+Z92GvZ5VcBT9{|)==+7(E7YR5wW5X`B$^4hus+Z|%=&r@NFo8NI03|xDz5kiP z{hVTwRI62L{F+O}BQkpAbBc+~QlE^;*8zsPOJl9w zWnHNiN%yZ&S>sytaKxm0NBj+)e_wbII@6C*i&Ao(EYXWZ`KuxE@C)fg*3I;- z8vT{@m0l8wbjQ>pO0jK)N4ET18VjdmiwP1DH2_RNtcEWU_a_?ctk!L*tD$YD!5`b1 zHvc-~ZUY6|JXIO%5_8~GCTb{U(&Jm=WGBPzjA_g6D7h9cl550krv zUXl9W6=_s{K1$;RPk>_7#S?(A)y+x^XWdPYihs|}b6cZ?y8<)}9=eqsCmkmV!JYi3 z%r)o8QePy+@C^Nc*)aT(+tgI=tI_|m0!!zJ_Pt9#8XHS~?e+*M#R}wVnQEDa^^onP zzHli)_PD9l33T+|DkQ<<PR8%+Sx0*(NHA9gupDz*Q$CIf#z5J@+(==9%Ph!k)Y zP*m|T#2rZgsCaL+Mr z|6^uCvvpD=`VoJHBC%4gK~E{`KvZOx^5LH=v+`Nk^VFn+7dR$QYNP=8%T4H?93vZ5 zk`3MG+@S{y^@Yg_?HY$SodA+_r4@;G8X)JjvY_z55KudWALRe6?3L-w>^s*#DWW;* zZwmGoXvkpsZhzoJ?>_*>g*j>sa(!=R-3aWT?uGs9B4hiiWC)fOMvAS{HLU{B<~_cQ zw4nI>!Jd^?8y0>tyigrPzfA_9S-=i20Nu4E0Kl*~d42ev0IQa(XLrZFB%i zaIzEV)m0G1>9jJm`z~#h9R1IAr({3LxmCvY^NQ{D zbD>VZ*e1mwMDL>twYesavRQID*5`-Mt#;J{84}n3qOpWWaI(%MEx}%@sgt0#j~I`p1+Ld=@c&{p4R7 zPIiQuEk{^qNx`mArE$xv_vdQWmn34gA5qL#|^(WyQ0IA)pC7WG7q;n{`5l zZa?%LV+JZp^cp~VPCK`trs6W$08qAH@BSzoi=*GJq|?7lBKqZ)5(6*yph9qvV)Q{{ zcvHi}uQ6O-vUP{PJaYhPFFn4VvZ?TlD~aK8{6f~_l~1J(U)j&gE(LN6n-uxq*w9cR|G3o%|fLC5P;AedaZ>4yRhQMOd{yd8HInUZUgK_@77z_Y+ zUXpG1$5E37@8Yuh?;r!X^FB7#Za?Jznx37&#V1s$9s)oxp89{RePdtw6Q%;_zcdek zr;VH8k^VU#VENa3t+5XGIi~){@}S7HeC?HZVHr4`*KKB~Ktk+Af^FwGy@M$dUN9v7 zX8kVOUZA2M#UfGhw_A-yOEjEJ>P!Hz=Fct7NJ|v@?qg7lw`GJ?R>pN(S$GPue-RR1 zX9-dENIAL}*lvE25Hqi>-uBhDsZ4E%aa2`vRntNY5AuDpG|1qXJ4rO;z~q;vx6;U1 zXvjNBo4}mqKXP&kP~aBl|~fOeL)?HHYi`<`V)=?zHi9Emh}7 zK3^k#fFzL+S!-rzTUJw>+d+?w*G6*uSF~?m6VwKAS;dIcNJ%f}MIK7Q#OFOrhJtrF zciVg|DVc0#>j&Y7jHFazZi>;ywOZT)q}atz^+!K$1SH)1G4iSF%b68t_4LO4^J&zL zBU^jGjKnUq!?%pxl}|1|lW4ifZ{6s@nr;+3;m}`xzyIW=&21%Xi#hqy@=y7tOIkzt zMDpHtc^>86O>T{eK6fwp`g^rZY90k1hPG=((};XhzSRxv#G zSv*htck3~W?{BMP99}b6XGSM~(g$6u(f}355SxLF?%LPI`fKOC)fEb^+$Itf?#_A^ z#+F34G$tmr#-3qXmSIXT>HHzh2}O+2$z%*i!RaPHZSytC&<%&g7vDI$}#&be4U@=L#K4arbO)qxtSp^B|}6Ph)dHJ#$f?1iTO_~-9} zH|`1yKU(FrS<|;uQUpz7iHAY^GlZ#{&j^|9q*iS71Zm}7-atwD(x?9S`8nBFO^ z&=;vsq2It6v}8$%_<+{=jjxG5!CdI9rEt3s{gvPTqw=76*+=1?(r>kwL=B(93+#$- z7Mo{pG*>8s`sSyuZE;`zn2J$GK2NM2Glu>gB)sY3GQS2%Wf&Ki8IbKKy$SyrlLW8z z@?BaJy#F^@Bu$8Z=!qr#_pNWgn+@oRnAoeV>wJ(+lnj>e55D3Rn^?OU08g)5v%bQv zFIWR*wm9C90ec_d-$@c}F(pIsvt%9k)lHzZ^=!;keJE*6$6R$gF9a%`w(SZiXj`LH zo$QJP;AECKaUUX;k}D|aUe=o*<*-m>6&>eA8Jm$4PXHWd^d4Pc%0!8J{bIWu20%e{ zKdh9epi{sh1SaITqk| zLmRS!dqCvW8z0}^xdQ6${2Q&?%@fG*TFv=2d`S&JfuPjEl=SB%3}|1CQ~&NOP;!WR zPc;ppF?CMz#${6J~FM`=Gq5;PTq!|q)C5NwMd zbZV2$jhf~j861YFbZ*PyI4PVGD_oWaz3L#65Z8E6?<+hE(GNIA!0MwePKOpJT0nw-h3gQ+aKmk?I+!!D9a!FS6UNoatDEht&DnL!)@g z3HpE3R5~=jr7T`;GUubHc${}Ecg~^l zE{o*FDh4!Rwt|%(y6wd>6mS5%Rx~ROE`8JHTbT_!nJrvA>C)7=eCQH+9M`#S^xi z=Yzhj&JdxQ!@1=jtJzH78g2!Wv0$X3%dtZ%OlMAfiB=>tKq<6#S9JSN(0vD8VQ;wM z>f)LX{8YtStoQ&@j_ZQX5Yrk^vMYEgt^U@D{lw7+y@Ux2qOxbhW=n*65ISTk=&yK# zKhFcAW{)}f^jqm2Gji@SChA39`Hb`oBxTflVsx4PO|0qrAK)m&yURA-yNi$1K~LI; zpn*x*&6it`G#X$`&seO{L_ij1TipAw1+1^<`>F$CWjGrTV z7c_7!;;;v;O+mlh*VdbXMwXY24kpU3yZ!3nf8=(UV){^Cx9VlZw=5iJD>ZMC;nIMh zXAuJNj`iJe;p^phljC;|KvP{o)MC(UA(~KpEwu>q|T;h2N{U z?~pO!)q8)onH=LO2#vy+<<2hf=zYzOhc&%@>}k7HTt(v1a@O*#04v`miPaHQb`E4J zY28a!!Yg0)+|~OeK%zsAcU{c!(hw$>rLCG>>K#dOegr1Q)(!?K!E6ZVoA6i?mxSGd z0|_z9{?wqA!_60KTKbV;5f|DzS-D_4p)dTN>h)elH-s|N9ZMhO9c9?ojJ?GUtLFy2gjXW>t!+I?8}W*}z1rxnF=4<^M^>P9 zZS`5ZGP?qYG$uyr#_CYm7F-FjZ4hzDIZoMkkInejm?HcSF z2K?_O53rI8x%tQ_OLFAAB?coC9#(Qr+_mmpP?zTqzrV~+SdWP*>MW42 z-r`>UEBu>g1sS?2rsDVFz1}^x2ypB6qCQcA6>_Wfg0$SjKWy9 z^Svxi`h^xE!1qMey&7J>qmixF(m!q)_=4fK6j7)_wgvI+120J4jL9f0LUd#%BPisP z^+3+kC1vDof072I$2HKZVY8NSr1WuzdJJeBVHSPYadj+ajomLHYBtql-U9KHJJoLF zJrkB{7v7>$9B(tvm1FGMnU+kl!?$*gMYM+pP)|r_GANex%L>HSv|j|Zx7%h!oYpg{ zBHhcNQ;vpv7@ZI8y6V5;N@?lt7U@P{DCtg-mhKJ#>F!SH z4(XEm56|mF)`z zq9P-gF~$sW14qbaN>OeW7m#oQ)p{_hdhxcK+u$8E)K%l>YvSMn0Eu*h6xH;GJ{LGb zgL`oHTxz&LQUJb7;n?dmDs%ud`9UBg=Q-{E4Kb`71^#`XoE-X%DCp*P74{_QgxffI zzGO+8UEelw6!3*e8gP@oH>OVgP7F(PbWGOl)S-7iT6rk~*e(VVFYznlt9Vr%s;02= zC-|(HyNd2TV$tL>_m(WXZNvMwgLwH%85(5UM&fv4nwjA33$Ur3xj-`8BQzjztreeb z^V$)@3vlbhb~wpSJi}J{M(=ZoC36NAK|-;4b5+q!5myqNvd};hYgEaK_uXdz1uK^$ z@^qv3$>xGoqTs7}hF6{E^`9HVnNU2&tAknFr^g4KbA6wO=|bh#i6+0Kn*j($^Hh7p zg+jfSzg;0XJ`b1bBeeu3m6W9w6(8WOX}kQ)R9)ojJcF%-2K@@j=bJX{h^9s(jj7-v zOan0`Dq}FKeiSIQ-S1brCygKw)y1pZ#Gm1{ot`EjfJX2aA0@p#0f`Y}ZZ4W&mM8s! zh-33Bi4DjcILvgLTppM<^;exbd~67Z?tL$;5T#ZH+llm2y7;D`ct?+X&{P6tEsq0sA-Q^ z9VX4QSd`BU4Sx``A2hp*>~tKTrhH@sQbJ{@7kB+v&(f z`yX^R>uitX->gB?fBZ*1Q}Vpx9sb^Hac@XuPN3j_?O zX-RqmCOvPqMx#Xd33NAAit!JUV}Brm$0qX?07yu;CYK}O%{5YN@AF}vVztsx0}RQ@ zJh^MKy;}fMGQ}KNDy&MG_&2`NQ&v2%(o`o?aIs8k>NBN9Tbm^hD0fcZZ2 zt?J7p7|lhIYOz|^(|&RJ->ajrmIGg5R`Vs|dZdB&?)LK^wft1>cSrR}jOrMuD;0)t zNfXGq*h!m9n8G)iKzDX&l*Wd5P~xR0$bvev8!2u6-+9~~viJUrGMXZPysW7sYyTlB zN+&jzn~}5mRgUr(F&2h)2P_J$Qy{Kat+eBBShXqII;%`$SB4M2fD13TRaf#Gjl$UH zn9$Z=oS##)nIq3fU7zz^p9JZC+3=H*(osEo$ib=F`G2hHlY2|vcQ()t4FvJ8hTRHiltRNs|`-k7Jm(}jvzHehF} z8b;)OjkWJxs{okzA}T!mcx)da{;SAb!C;+1y7lk0^1@|KVz{gLa%Gx0c!B&@I0%3n z13RF~*5RwiO>6F|`TSkL9;yNlh`LO)k2js}$3o^qgywCaB>l?l^UGOH_(+`sFg%wZ zi9>W^KYuEKp@+bZ?;AP54!;^pmFcuMp+Z`zUW(7*bAO7YcG(+F#A&w*)xiLuIy(YT zRPbb|!;RniQet9wdn7pQ>4H)^;|-Ua-RHEw-@I?6e0v~CaZM}SKE zi<91b&k?`~k`PN-%;t;m9~2A#sp7qpl7P!Yq5b|ts|p=*@(|@=fej!vPX%VScvb^N zR8)P-6sg|oE8hMam&6)_9bdEk{;=uTh!YLkmL+EOF>e}_jTlg+88iuC-8KFB875PR zCJJ!7+(1h1vw~>{`jQ=EXGl`tGzJ@hZ`tVm;L)rpDJag8WCS2!;C)`GS(8a@2e8~9 zc`I=JooP-i@iV#G&veAF-ZAFtW`70nWEC4`tT<`0hp;E85Z$@ea|>T4{FuB)r-UA)&oDO1YNEG=IyBlA6}my&oM#&`Pk^8&Dx~mS zEv$p-!35vU!})J(#6T*Ft1AY%lR`6rntqX0cV#%IefIgowPp&4<#1`Q$A{_0;_T_# z$0jX~t~)W`f@(L6wF-B%0VeznjULj5+@JPvxZ}V* zXFC89wSZTxu5sb<;cmke47me9T;1nQlh%H-tG|Pnp-%j>)QEHHvruC>L_<^p0X99x z3T~-xQ({>43pE+5=^_MV=xB_eT6c|FgB#RO`?T1+D;WDm1x09zLHy7Q;1sH64}UMm zRhwmf6Q`_H7p%L7R1e@kzYS17aoiaRd~5d#`sr++mO%kv7;)KDE~;EP=S15eqgc5`f{O^5Wy0f6^#1NqNWWK5OIlzT;AFaz8g?tK?8_ zbl8e{CAI!gu7$}hoeHQ7YDFq2quKSpT}TL!eBoB&*Ds5&3NdeV@T%Ad+Cmk7NJb3Z zl-G4gKwergreA&m7<}hfbg}y1kqo-;uzLO`47dYTO)63Y179`ctKTxnwJ7m19L<|o zQ(slJZsC`TE5?R-rGI77XTI?SvGK6BEpGIT{OMM{0|24e zY0KKJB)<|@;c7$Nm^Hjl-}vfWtj5yXdoG^%cQMmDq2SB3mJ>2muM6|WOk65jKKH){eYG^xOtc5U#T+SCm{7tk551^Ra7yJb{ zI4W^PU-8oi5QRmG%daX&q3wr1l~w%9zeWc<1-xqi*dCp(Y=~UxI3BgkEFZK!c|LtR z*IrpNj^N%-Hjd=pA@l?|Yw~bmuAHqXUL}#JPVL4U^cie)FFgX7`^v%nQt6& z6mgTG&ARGTS01e5ZsHA6!iEaDept&F+AW|Gk6~=!eHFPSIc_@0Li*#m*?t)%TrsQ%C8*Nwu-(F|o-$8f;eHM~U3OLar06 zyzQ6bu?N|q#>!wpx0(M)EC3^ey{J^{t0puT4lnw}_RCDHYDy`t-{pOqV)pbhc#G0m zTueU-ZM9rD?{PbcKk@O)P^}2IDx4kht462k6iFS>`q6m(aaq%$!$AL}zSkZR_Tysx z7vq~Cj3vu_GN~y3o8=~T)R*TD7rjyYv#j2X0mI{FdT*~|qrSMoAGFI0?}Dg_mC*0(blu>Y5P{xwZ+jnh5EtR{W zxml+BY*+fEyFlbNcwM(OpZ><}wc&U9mn`Vh^F|;=d%0psa51*=jrzMllOLE5HE-~5SS;8c58c9 zYtO1F7Wbw3BH5gJ!(?6};iK3|DpeIU#wx7LJ6-~6Jr_Fkw=}l)Utf08Y=9kiEoFpQ zU?>!iC&nkOcRhU)B6xwI=NYT9^ZL!Q`mp)HDr+a4*hTv*8+Amlt=Dh`( z3rW-8iMsr4@2zz(+?AK*;^b)n9c7p_?_sF zPi|d?lxeRbw!3`NQ6Cat^N88CRb-u)B{L$FHykp$emPzg3pzY*fMH?~LOHi?_6A4! zYj^3BzG9G_-Bt^Sg*Dz!lwTY*34qicYh@bU!R)QeGL)4^XUHd08<_Vyi%&AmusI)j z(E)!brw&vhFvR#pi8cY5i^jb#o21gAiH9J+KuwEE$(wIn^m?^ib_UHXs_Qi<#>V~| zG1n)Zr!Uouwip8}C#i9;e8RrtDb6uaNIy$>}tf`kVZI64lp;2Qyr<+u0U5oJqu3|LKMVO^uD>{6Ft&<#PuHylVvISFZAYSHXvp51?M7>2B|%q z?`Ew_77?88H`WMJUCT7SAK>eDuNbc~vSxI)>WIc3H|~&YxCwO^OlTZHHYW=6Q%!W+ z7Mj#fsInffw?#o2klgwnb?@ecC~BXRhujXUzaf*nFxFouVK)e(>p3@>XO%&32MV_> zxjg?=B9%$?S&X+Md1-`!R*y*uDprt?G$6k2f0FO}H{4Fzr_OIc4qIo(>kD>~?_fP+ zBz9#kALeTW!_c5lM*H1ptH_`(Zab7rB9^u?(lO?NUc&#j28cb1SXeiQwITNPC|X(N~ZIi zIQa<=vaf#E)V0BocV;2C{KkMz@&(dHFkgbvy$3xk{hg@vb(zw8J`9aw2DE7Bqm+6& zINj{-#bPittzZ>K3X_RwfO%d0@fnPe>B2u{f??c}3_Wd%^@OXz1)?Q;tr1*u{XlcO z4P=Hx`qQfn+9p#D7xn&oie$K6YCqXitHSJ)pGA*?b(e_~nek0U($RMX`U7aNKO)EJ8@4xQM~l zx_kYaob&wL+##h{S8EB)#0h-AwiEsIbjC!Pysir^4h-o;K52o)9h&zgobHyGo+zF2 z+49M2A0o@9){VS=kzdsM!2F1HtGyCRLz(&|W);4GMBuGf3%|#4%MIwwd?~L2>YvSMtKQChO+Q0N8v-0Bg1P?VvvYi;(K;HaJY)_k7NTYNB*u zGMcPN!6=uMV6Ns`WrjlP!VZ-6q+&M?tPFhf`wkIf78B?1vcB{lmem0j4_{?DE%ru|8tI&d;%Eb0@GO%iW*)RRw!9-?G*# ztF_hA7%9ZQDRF(z;iooSF`uD;C}YfANDx;xPLQcrG-+aH)tMSB(|SNc*UrXK6>j7GhB zX~`iWU~jP5rW_CHxP_Z)xi#Z*vUm-kC1B^%l&|VZ7Y<2dq=pwGPJB>eQ^!D(dD>I( zN#VsCOkzdCx|iemEiOo_$B$DKf1HUBLND*l%9KRX36(5XZyzXzIHP zc2^NK`g&4J0}oaPuK7uapsij8nn7LAb?eX*=EDG5Hp}ze@?vj3983R5u33}&^QYbL`96-Y)VgN#i*!>#z&jwRbSSq^Fsi;sM*-ZdJmwi%V5tBm^l1j! z@+bTMT`w7Z1?uwPWr3c8TuZgSj=r-_^>d^4taVO@L`!e`xZMs?>48j6S)hKcG3q_4 zj!)E{#_)y1xf}R+bQmN;wt9k}{HN(?5R{K{E<#Hr&XoF_KrCyj_{<&6eUxG6(g$-P zufyRbkQLQYk#{f^r7i{#(AHt@4R`_F$}!D;p7gnNjC>XF`<3?7OrrUAw?0fb?@SDi zZb_5jGqE|B-FL|R;`IQO`Yt-^RQd$BQVS((yxy7YzQZ;E(BAd$UOtYA_I+f%OL=Gx zDshK(0^;eQ-V#Z$LKx@LY6d$&m4^=0^%usrPgOHSQc%xgpb2CklnmD%yYkRU@rLdP zTAK<-SaxnQ+c#B@_>-C77E?d3ZnAnbq4KYny0Bl6ZD!~~6D);`AYJ4DM=DC)5LDCv z^ZfHGZd&gCHYp!;cL)_-Mi>KMjDw!Ck0G%svY(O?Xi&`EQKJdKeLXd;k>l{FPrDT=04S{%tqW4t<>Z)%tYdIZ!&$r!mq4$<^3As^_fv|Vq?n8Eb zhO!lr_aC`loOe^pnt3Oq5V5a|S#|w-O4H78-#2_rrEonAK*`wUNN=WZ>&T10`C**80)RsxJYvaY!Q6RZMNHh{TTlgLwNb|mv<8{Cn z?}H!BScd6R_Xs%3Zk3b5oc;@!y3l57i@wK(Y6_93O+WXf9!9@9;got^wMQDA-v}qe z1Q{Slg088!K}L`e0xYvap_5SU<$c6bmugs00+IcxN%I)J?FVy!@Qs$+i z{HjB?M++m@L-vl+{g%|M@4g{Zjg8xS<9O}KoW8Z7`DZ2i`usspDJbdUX8K}Jpu~rfX$KuUZ=bH(PPWL89z5sASFk=aAvbb zol}N>)A~9c<57#+7O%p=lwR+mUEcA}S*6_`pO3rq(x1nZr3+XlQp)vpDM=PLMGp8~uHcpC$od-YUuY-eOJ6o-Pi&eI z%p?cPEdI_(^=eq!-l;WP1)Jkr89oLrd|Tf6Ae92+OLJR-zwj?iLc%#A`J*djcJW>> z>Eun80*m`1?R|^8GXZons7d_^+5(ys_Mj}C^Fc;M!}6uItYx!rHl)QBVYEzc8g~Wn zo$faL=8tYs(b+M`PXAoxBqJ$m#X?H{h3)P3(9U6#ty2$C417Q*cBMYiI&!7rOU@`F za`JFMhNg8#xK={NVQeI*6Vf!GC){}-$E-Qh8HDL`yOor?1k_RKqGto(mci1bKs}Rg zvitiT5duHwgP9020X6)(NJ?^aD2T=rY!>cloL1*&C*lG z4ono}gVUE{ocdi^24`w|lY#K-R5A}HOQzlV^c^80f#RWtjXuuK`;jWryp<0igrdH} zTZYgaqMg>aazTbLi(n;EB?@(@QF^OeLVX_QPT9$zhPU!!aFfa%l6z@MN~ zNZ=xRP}bBgWv8XKMP{uI^tLv|oHvNgo;=q?!j!#P)57~SOdqQyNbplP%1@?R8dL=F z67b=NE1lrVC+m-)BgJ#s+40)Bp>~A{+>|Vt1vZpltX%k1gMr$DVt52&N`~t>XAJlko_IIBECv!rSg=8e@El`Un zAHqo6n?(LN6c`p*mJ?{FPqjp+JxF6hr@Md3MPPW}xiVEr9lMB)r4epQgr*_GyD zb>Ck>V|*?;_a~nP)ch0VPS8f1H&4K^}SKl3tBikzwf6p zd~~{mxjb0kbd-qYTUcO(!h(IP7@0JFiSYy97q!k3f2Z#eIdO^DvP6M)mjuiZ z=&k+H=F&aYwH9+*b9HOoxbGpLpg0?K&dc^X>-i29kTzU&PZWi|srJ8FGR#e8csUl<+NhW>?L!bN*z4THAz|$AMeU6*@-v7Do%UK)0@cA4;jo z*D>+h4zas%nuJ8+2(FMUSr5e1!y9hw`PYwPm=}(TE=)E$?ws|k@#MlocfCVby>?sX ze9E-eleKeS2DI|HqIrZsII_TBUo$uy46Lw&#Fx62(JZi zm)!OJN~5^a@8R`Xi+?>>q+X`eFV2VpQ*PM=Er&M0&(9Qcp7D0WGup$GJXlznGRg;A zUD21>5wml7^lv~GsAW@&P%NvS_YlO#}I}Yh@~Dx zx}pDZCODUKdKuz2hfcCedzW0D_pU2OCrLpgCs29DtD@?sk${M{xZ&dyr`3fDD4VQI zuO-D&`z6d;et&FE*;Ya?hnd!HVIhhE^&_3Nz?+sgr8^qUDPEzB)+kIbBGP}dK#Og) zDd!&+a_NU-n)zyL!bYkG`=fn%ZS_W#VKz=q9VohYEZhyVX*H9#8I<;;K9|L)v~Kma zgN@+y$zI-soKFna5kJ3lP~*vKUyCYljvd|C1S8z!_by;A|2-A4JKOAk(pzvm8Rl6b z<##Fw<}&HSlK+g2bfvZ_!H>Wvr+qhG7jKU3RU*ewq?b*QYf#${x2p=1Xx zIw42eC6tTK;;ZG#q8cvu`zV%6Ckv21+_3$p+}3-h20XbUQ1rWpkt>VYZ}Op?7Ff!FG|eR8vH55q7; zS;qDKaR2e38F#+Qm$RD!k7R^t`K3|KkgzHq((JDD?jg|c=uh)+O&WW z49tn5VUVOk)<@n3SkAU5YUl9hchi977zN$Z8!w!gVK^C0Q|K7-)3{vVW8!4{&(Cmz zi@nXC7@7xPTP-W%tu&=&)Ze7?Io@Q9wtC&+woyYx&nVlHM4Kzl6A|`G`@&>5GTo*_ z)TyM+z;$F(BZzHaobT}TjVWHdZ~nJeYON)(c>_XZ_Hz_p@YE}Mdz%dxcI&$e7!z>> zXFSh9roh!toM8d75@77rUPDcJ9SiKf)lTUDygv!fQLZGI`+yw?Ppv1B8_wZ#Au z3qkqIRg^7w?lG36xaIt(4B|knW}W}S3Mf_@4o(k)`_4{Lqnsba723ZB(PEI*S=irl zn-)u=x-6@(!9wxmI~=x(&abG7)!y#z0A@tvn|1)n55x}uMX8N8!SF3V!x;q#&%n+Y zLaqM8#G3zQVthK#t)vP(zoO|&7RxfqVV{Y%6q9mZbn0C2f)O8cuzAw?KzUvXZO6$_ zQK2;d*B8P6Frb9Xx(&@;V~O9~N48d#lOXOpmD2xiouX9l(dp|@hRQ7W*I`QwkU%fRhJ`;X^OgoX88`0R zgFj=_jQ~MhPR6BZV_VKHfzJN#_k<$R-UOT5fwObiHhHrD7WM7)VPh0`RO z39zZOBh>7+Sff{#RGvwTvv!D^3$D_&l(7+v0SzD@YwG>5q#dF%W-My8)k}`>1#5P#; zJpGN%2&b|aNcqIJzxiU+M3lHTiMS0ud&+4b1M|9Ei$F#;KCRyS@yjjY-h|ACSlr}? zGf8%3VTfC2mu3nPXZAsP6!uMui{@F&BW_f$2villdb1FyHJvT`cHj0 z&3UUbz>t)=CO>m%7ENnO?1gFp2xm-NrYr@vD>ooEM$b~UTM=$Yvo8JDk&dGgk7r35 z*XYzlc2~iCZ-sTpP_g$-HA#}ye4q~ zZxtGH99?n^FqCOJVSYvLjG*K~B8eEyWT%s)?iMyjo4IEXP5 z>fG#`#h9MfgSV7X&+&6-OZ;5GPHrEF|5+SFf8bSs2rh@~&Vy3}kDsV#!fsiS;)RTx z*KiG!JblOHPJDjVA%JT8YVG?gcSkGBSGDH2s))wR%~S+1lb^v^=AB(3k$P@aMomiT z*|etGvEyz9TxMUsA*@GGC{N|G*`7K+`5~N=vT?TSl;Ukiz#N{s^?aXe`d#cz#s~o$ zFC1yoaR#vk+W1<)0%F);%tgR{nE+7W1KmW`3~i`}-|fSQ}ZUnC$W zFQ>%K3Ule=j^bb+#X6tGqA0Wq9MPY_?N-gVfG*)8do+=&>t~`JE9&E@L=f~+g;OP^ z?5>R>v7;G#hxs#maA(uH3gZAd-H7}xs!lumvTTJi4U(;b$x_wk%@vp`fT~qT@#FJ4&V4 zHz8Tl?s}VuujpZLT%QK&XvF%#HOE`133La-hssco;kr7)E46qb*;Lc~fk6^Fd2Qw; zD;v6k1f`fOTG)Qj)asYYPM}&R`?qR|%vg*zZRod& zu4(a8k-hVgF#TgxN6|dy8s6<>i+K*Jy`jlB5ryTMJU2R76D$psi7`Ahe_1G7%<|u@ zHw(R|RCGZ%xNhMb;oX>RedNXk$VStpsF?w^?xJnU_4}sP@GyJ=3f>1+=>tCeaCff!;C>ZdLTlW9lp0Ru zk9f9yDTq+hnk`1bxKD27qa2oo2uQY8z!u7VJvdFul9}7lm0LXLB2pJ+K6~@@CKm;V zuJ7uUjEwBQeLLcXqkmFjEJwAes0RTaBRfp&zwg$rLhh(ah52yMz!=M$IX)=kC|4(@ zkk)BKzP|W8`aIv18*aE&%UO$Ky`ev&E~xn>d*Eti$_MARDDfY05BI(A18o2fa`xij z@Qdz5(~&peGxs>{CfITje7Zje^gFQGPNvAio`PQ&c7=+pfrGYM%t-cnjcH4N_vmx& zW1t2zii)tqW*<-!P@b^Lx!Zjwtsd|leN0kL6JpkNLczLJ#Wzcucr)q(Fp`LHoZ;&Ir5Z#e8?^j~%E{14ENT5kJf{mQF*bF)QWIv+|t08eo4BeC+3 z-n&DhF2StjSAYsvrJNDOqi$lI#s8k|JoS}t{{p*hz`ztHw6C*<*qviv7x0Q z$^>nA&fqf^n9NWC=llpNdkc4633Zx8_cFKca`!0UDf7?Dc~VQ4Tm&=^pQNQ~0jkC+ zc}+Yx$_7wz1YPGPI^gd`XpGvlL_%LvnvHXdBbh;;=6k3kv6|D<$h#zB)%!_QWO+dk z)rB$!g*=aapP=q;J|q*U<=ZbFeI&%FR$>sCY-Ft|ftnc>3g~}+^4TgtoS<{hhsHIC z_ub;kej&oDfr2ktcp9>iq{Yq28irS*a*%lE)G{em&jOPX5ESuTEL&dSsOudWd8oT)(oFAL>)=R14qz57R=j>p%7+C+ z`x-^nRE4_ESh`6l837)@!t-U+{P*t%STolLe)6lU4u5^`i`ZtWzxjLdz`(VGU$)4T5$$+^aiF9bG(b2u}-6dly5^`^FpuzJ%fwH1L7?sL0coN8Xq@-0w3e!XsX>PP*LDrQMlu z8rp!e7W^z)0*8n`QvLg6UR!Zq)~cLt6AH%}6%E~Lnojwi1sRN?`+%VZvpYD{aG$p1 zizMl|(=&=}+214&w_YWFd>)#!R6tFXQzqRd%v|u#>uZ7`f;SfbWqyEK-!-8dEcPYj#OEkLbapvLjv ztRV7z*~9_W@6}>Ad-+m9pYsPX{66&;${Ef3E~sShq+7?;^Z)~H}v<3wvG?3Y4ezd_%~jPwJUKIEQ-Zf>!e%kOLl@Iug4nd6s( zy)fcZ@Cx6{iwfmdgFhwtFVl6y70d_e8Oj(K4o*6Qf$~%&Uqgx~h*ce4K`ZYY8zYe8 zDywhBlUfe7dzgQ`o_T3~(FnEg=W^w_ta0Tvrsq=`h=OqRfLZGv z=?dsh{Fr8th@*eqO5@KG-uYzfQ#@z3(BwT=*b=6`<)9X(4k(Grs$3KX&5DH>*$glY z=KsX6S#rY@BGizV}xhyKLWTrqgTl6m*1 z@2aiwm)>l_zpdv3tuNP3(n^1NUs-Qi>RU(Gsw^J@L@&FZ%r)~d1i6Q;2%#tlXDNIB z`Taak$<&rIDW#hW7HFFh*qf%i5(`?8!)d6OKYIhHoCA*I2jc#LbHq5wH#g9aO#>qRJeYqPH4_P+KO9+45aM(uKYzbwgU^<6?=S4xV-$?p#u#I z2#F)vx7Pjq+K*vYUR%sc7dDx~bpQAIHC^>$Ml%ak=i6|a(eh>INURxSqq2_tk+>o< z&W`HGYbv7f@RD$(dL=|#A(pnLsjR;Xkj29wib&*=PCvYN^kMtbT0i$|^oyC}(l;%? z^~fKG;j4o}09k43@Kr#E)4OAE#Co8{Sp!(3QVk6!NTt8Dcyd;LP&aZhb~z>dK3LXm zTmp}Rro(X{ULIZ7%ekb~#335!{W9&a+L?S^LNh?dId$K^h73!ktF)q>usENTF-P~P zyS%&t>bXzea5BtPRY%{!;LZyBNbUi_C8((gY^KZeuNH-SwX51IpQsQ2JyKLccEsxWvbK|SMHKPCR_Sb=8C-ROi zO(7nl#P$T9(K}qw(_zqy+rtaXcq@KNXff5X8hXy}K$>5_?gq)M`w+O?b)3bV$;B4X zYn_k=x8E?)nMpA+gX> z;nvcM+wa_#W2R}DSAl1AgsArQZQ$Z;7q`^e_S8gPT7KvphuitKvmgScSRz$ zOh0E}8}+xVJA(hB+sdkco7(qC;!>Y@>y&5sJ0PrDry-z*F7jw&p8HWm+7bw9uk!km zpxRznGO(xHevlha&1p8&@c8$@x0uO$WKcQKlC*%@ePhUz2MLi9eF%_SHx$52ta39D zpc^sI-H7zU=H2{V1UAK>3|iI!NkDcVYWtYj9x|z_Ir8(hgbu6Sk=l#N7Kp_D1|9kgwtWV*E8V zb@S0#{+U#(weIli`gc&9K~#>>v=gFer+g^uWp2jOl zY+1$jA3s;3c(%G=u_*CpHHK3tZMPb%~_iSqV;bq3CN#_=Bt%=L>lO(C@+ zYXLZqH@zYpuexY|$?7+|oi-i+8jQ?6-x*zri5A~78T2cz8m|`9G;1TcowR8_mE`jR zBA$aV$WUo5`=Y5*XL^;t0hxHRRA1`E=w1+Mr2zpXJsA*_)I|Q6C;jYT8WZPD{ws;q zXxC!Ics>|uzX0P+ikS`{FX;cw@V&keb47|Ve{-e76oO*V2Bd9n6TLkVFc$EzXos|N z+wTKbKNjdRf}Ld>3>=2f{1>F1nHa~6RyH6xijm6(|+*M|7=9_@WDb1$Cv?`IE1~E!XFV+FQvDpVavol zKRWP%tr(+eKvSrxmV;;Wj*0IEm0%TKZEziOD6VLFQJ35(w~S@&k0f=yO!BbBYVQWv zpI(=csHgoHT1w#+z^(xjOTHm9;PkWtdKPagv*KWXB0_Ir+(5u7Z6ID-AFJ+96$N`1 z!B2hkPzpck&7H!fP)IYd+Rd)#0AOkx;5o&+#HIW(y$z5)jVC=piE)O~yf(p`gGm@e z!f7HhX#Fg@HWh z#F5C768RCq+M7Pe$T*C1tSBSf1bG!J9U_iN<;vA&dHExcNP$tlyjT;TnE4N2jcl`u zw`Ypt(b4j_cw67V1HYK&+H+TUa$$cyjCavrVbk}cP>Jo+dUD)1KF2&JR8!{yCe%Qf z_{kiQ&sC~fEjfp*NftK`jwPI8N>9bI5%+ z_RU;M4R>;)&}Kx6+%ApSpwDC~5RlC}l|j;2(}6lAWpBh+85*y$8Mu~ZPRccB23CK9 z%4*%u2q)fJesrXItUwq#FIVo z-3Oom;n^n8AWfSn+@&Q*$%^@U8u@CSs*5~eLSm9g82qwy=CUYW zJ^7%ohP-}=s8gT^WdW0yW9dV*41g5krlV^A92UUhg=U_D!;=%Qk?HiO5qkyRaVQ1P z#}2x$Ni)$;^>SfJ+UE4XP72@e$xdW)1>DstxK)4Y_m)(8t^!{xO7fWKR4W##iND1n35>4E~GcOy6yb|1}q&L&9IRa)r-YdNR|~!*Y@1r z$UiGMXX~jUV@!M8#bb8VLf)kuhDY5-Pi)Y}p}|4^hRB0A5KmOdV)xy&C2^+tK428c zxp;c}uD(3vQ{*PFk0bbHB|na9mvv$JJKBM*tRQ3jz;i`x`w(0lSf+2L?>jC^7LeLa z7MEfJBH(VKqGM!3Aw~u+3n}zf2$ld~3f*Wy<3K`(2QIDx z-xJ)iOo-U&b5ohVSJO~C@R8@S-=-+s^M!7e$*tR_X`b#fGN19e!ZOQk4Epcoj1Etr zs4KE&_3u~K84wF>rgq_A6=*N4NykF#`Tl`L=3nJDJ)7`m0eO|zDNBOFX6VM zm0v)hvnw)7o2L|n!-5jHGO^GNo^CIGZ7a7;Xxm2bR}2ZF@v7>dvz|u@Y~xT)>a^MS z%CDZTjSrL5teQHqwtwH<>)N1egFl9`wmqVe`vLYD3GnmUArrq*gEM-nZh$qZvnCZC zH{Qu_JHj%)I8CDTDv)zotir%|e}H&?SyW$HM=;@&rECJ8v+BS6pl0UjN<09^ZYQgVy?oV}4yWczp9 z`3(1@kUma9Xe%eFP6)yKv;$#iAFib3Q+L~g@SFHeLKk-eO2Ti;?ts^!bt`08Kh5!U zQ-M4`9&&x?UG~CQ1TrW%J1xMRphAX%kt;fdJ>ntk(Me3gMe{e=JvU`s34lwbZOsgwsK{c~M_ z9W_w(QV?uY0Rfmk)?w z)xVU?eI&xrai~o|nNNlJPk)e!=e)U2j-SrWnNSn1yw@FjXY3VSG~WOFxG0?5ENHtP zi-X68KF<@K&Bqy44%3X3J9W$=Y`NN*3ACxDZp6t z;gVN$>a>QMhA-(su!|lN+qq&e#(&h zWoIL&f9`zvY-kSrL*3E@c$k}$my-3psinDv6D2#(TT0dsHcqAvcDB}rPNt|%4$i=X zVz$<{4$5|h#-@}!|F*0wXXyBel9Th_Z|^Lf92HF+#B4v?+1i-eI8g#EN?KY2%>aL& z+YvW4wly(D6&6PQ&yd`}M`=iID&5OHticyADUB%OVegeaY^iA{Jdq7xF!7&G@1$@q z94rY$`5}^E1O!q3_h?xkLi`}vPJ8cBT7Dl7M~*oDsy5elap9Zyy8gCvBI20$nAa@8 zY2Nwt*o%o04f=n6k^`Z>V`fe?P{{L7G#5l^2gHn1(_w!w zBJZ(g$}?q`2;0+={r#(q&hp&sgAziF@XHfnFgrrX-g{X&qm+&>ftoV1`bG>mlPSsTT~4p_ zgxS$U>DgTqYoZA;3qR=Sq;iJg5)FO&6qionb=0#UUYoMnuS2h}&491YtZ&x9!}MqP z%eR#w3dOU&Z~xw`4kPEYT!5C@Qhz1FiEZ`2I0mz zVN_9-QAAK#gk&5x&R}|oBZAorc4q^Oc3J8?T=tgWLPSRl9S$X&_aQ2%+Ev-;s|ehd zLMPQjzU@Z&#TMNj9~SmFT2tV^;-SCrWM*4hUuedk%X3>@#Gfs5GiZwCiV``P3MB0JH?mH8^9LrroUDoqJEROn5>s58v9#(cc7W;(QM>S_t&n?*DeIs!1 zo_@o(YFsU~WK9oPzLB!Oo^HbTOmBaBWDI?1um^2WMZ9aqqreS(Dfp6|kX;cX7rGw$ zfsUKi1@usVu-}+Hk+UC4;#K8^N)qsx?sPKNvTF+xptJ^Um-mdywvMb&e;{RZuLq&F(#DBf7JZJ!skOcN-md3qyKp zKjB23u>!GM+_z@aN<7QYW;)UPv@z5c<*MHgHKfsy37q7%f2;Udi9UTX)qH>z@cYvR z#`o}faUzs7@HPl{NIs}%s5WSKF^6|Iq&E~(MUyR)10trv6~Y_nX|_6MFzs7G72GQ5 z1ZjyEt0FBec{50BWiW>%An#bepBXDZd-`NuWqX!IT6r^7V;b=4&RF$1=X%fN5`N9) zPA1U9)7LjvXkk4Rt7he4-N}{+ZPYTeSCRZ31IcswZZ%)@jv-@>rpxPa1xC3@$S}4$$mHb=a)chn+$GJD?3q1$6F6bN2>}84I=AvC9L|dxSPL@Pe|?< z(!_3NHYOxFU1dZ(AMUf5tm3y?a1-vzg>i2qki-X1&*KG26tx<^;2VnQIAiKV8-o+< zb`h0%)uVj(nHR(l?iVV_Iu#+y22JH>+f5BevKGUJ%RQa0_9`eKDxs4b(h`zW zc-r`|Y2T*!s*l_H$F{09;m+o2wb^D-(rbO|oC;Qn2^uVyYb6=2liY4zKTTu!VTu0J zqMH8dYa?MA+L~PG%G|H7jeNo7z3t!7*E~Xb7VDsNTKo9ioz4#?n~@pPd8cz6x+hlT zzDGDY{9G-H=>CnPcHH&L$P>Z&y5UYLEp2t{_hAo{+)ag-^Y3m`NPRtR&`-&3y@C%` z5RX9mBI9Vp2d?U~n3V;&c0HmGp4P!lb&ou_Kb{J74PKZ_Z+Gw$yDU}o*;ot19gKCN1xiV^7`+Ge zj;3#8OHID-QeP0!s};~C`gc;)&we+u*j zKYGJM4(sA6Fc_?b@mTV={rnLbc!^(uSbVXt3CA(e;N-HiJ=TT!IimEoV*vRap~T75 z47zcE-`cIx$*lWrUB+6wqopw-lz={GugmSldW@xM#(ML^$<*_4%nHemjt>zY_Q%7| zIW2zZCXKDO65Z`{9Uba#&Q5xfxItVTj1POie@3D;=8)m>wR#8^4$TDKW#uZw+;@FF z*W-l-adGDU_AT-J-Nd|?@ZuQpokkS2)f6o5X_KuWTU+wZQoDt{i#zX7T*Jc#X^zmX@GShr3$)RD3)Hqnqth1atnWx>+#ia5grKF zX+I&&ev25PR5pEG8s)aBNb#HJcl6si)eIeNKEjchmACWo8OlWL9#0FRi_7mD`8a;0 zwG7;S{I$o=@y(51X!v-;GG;xm!u{oNEvIGEbBooNMMsC}{K!phE3FCBtv5bRON`)iV@#Pr59&T2(o&ePx7t{8dh=ob@RClsp?H zzu#~5do$aRfq0+o9B6U2S4DC?FE*kUJlCmD_~U^`MNelNJ%BSa8_^aTD0wGJ)4v%E z*z01T-j6r!AeZYcb7Utq#;H4huFl)Bgf$KS{j~8ez$EBc9e&EL-Y00EgK<6$;30#A z$n*(RD%QU-&*Fj{4&y9dU$o0Z}2Y2v{FV*toS2t6_28)b6SA`aZ`i59W+&QV) zwGs-wKPTe~E<9Y}YzhhI)8DD0!${qC{@Py6?P@`9WVgFq2pyh~b-TTzTz?tsyIlNQ z@_TJ1&5eqkhCF+qUuM0=A)ZH)VxiGZc=1X+L*_9-wKt?k3$$AvdlpqN2c-nDU@<9l z=CIY!z)%e9iE1MTk&_bN;_%QLm6r0LHgr0kLAO}#G@qomGeO8t&|&g3lG}exGQ^Go zJUyk5k7Mf4e-_o;4AMzlwpVV!a<0eiTS2I2StwvDalqc6>Zwi9_{Sn!3ctC|Pfj!g z{w19FYHp^#_1^udPE=Oc6E4r9i2fU!>aWe(55;?feF&UHwnZik@}?pfpWN#03S2CH zaJAc7eR1+X{c9X=(tsBSm-OQNWBn$B&Gjb3y1!b&UQjv|0wB9MoSv9J zMy&>VX0irr)?Z`Ws|2c$vK-CG>D3%12DgMzLARv5A$k3bW*Z0GMAZ;M{*SS(FY9Gt zIOg}^Z^z2fGLsvn!nP#;R8R`5AX}O=8Ti-oR)zSa7V~Z```~Zw={(?1MGS%4hW4Oe z*!+KsPsD*a^m5Gh4)N;^>HocyPAmYrgi_6ye68)bn3u)24gL5-G|*%(Di{S>Ucor? zKVLdfGn4PNLu=Y!i*3R}d{mA>v{cWPPC#1f`Z0`q^K%=z+wmnNVf%vA7YktT>HxUJ z49bwL|8&rs9Hr6eaJxk}ySPJfZW$40Q7?s<^2;iB>U7LSfNhsLoY{t&HmB#_BUxZj zSZ=Z#lf@$bTnYE+ng>{MaJ``Us|dYlAO<{n-aV+L1qB5GG-gf*lUcrR5RVor07|VM zmz#qrfM_Q=Ps)t7Tjaa;;|wUBQdYL#jcL1cov@qmL#_5+v@z({p+KsP2%ZYMweK|L ztGFqkZcmn!n`I6G<}U_~%FXdKo}Gb+z|t^69;{oA4L8V_C+I6n*6!3WZ7$Xomgs!S zoey~Q=(X#IT5a~`0`YX_)4AS_81zaoB*@P=_&?c$;17 zh7pfCFryjI2MwlJ1kG`ZhfbDi08-Itt}lCogM$MRM4>bTy}cOpT5QC`nZTo01#)RV z`@kcEDIm++X=%o;nvZQRrsJuoB-#z15ibD<|5K4to{+HcL zov+!sr}F~rJ5sONkdQT&iu!W>WvNNlxakV$yLl^z zKCvpL3ZEa5Ej?G;K(%sX;ZdkCkRZtgEW=b(gaFSq-E2gb1(beyKL zMGJQTq6uGLUt~gVD7z#k10f)TC4{{lhDdf-iRP-&Fsc9pRNrBS$3;M z;$kl2!ANk(H=SZXtCY~wC!;z$1*&*aKQwM*(PG%agM!1uv2NR3?}$w&4RNF2ylx5i zOx`+8_HhilcrJ>0QgI>i6S36FnPm3@3i{aZ6^Mb6pjOJA6za4J)8_e7=M>8Z!sT}A zM8;R^GHBXTkiG4F_ELUybH4WT+m&|sb@gc^91V z+{?EPHlAkN_paUXd>xT968|LFfFhbo{v8I`2N(oU5}SE66m(p0L_|UZ+v~dS41@)q zGJ1O)4seZar1 zW|Pse&0)$}`gL#kAgRApZE`O=5X#t|NTXT|5DXeXtl~DX^0v<1o~{@Wh45pbq4|g} zHydg7mpR(A*Yf33&J@VG)os#V$(DWNuCLH3f|zG{-xDueVk(;fAsm-ns@uYDv);=8 z`<|@6sq(zR;ZRA%ZM!!Z7<+7iN|Jn4bSr;CUMG}+8;xcC7X|q5?0-gw*BhAMCS%E% ztOjY5%bc+`)3tVETA*(2z%uO)2Abk8N!58-AV3P`c9w<2TJp$6LtXAwbH!@Isa(ou zt6hUg=MG=bm$^7O)dLU?f$%FfUnMfHY7uO4lkrgWc3-H`X@Gkf(5arc)k+;6&(Rdp zY9(r~^p9Ggt|r)9giAG?@J)bHvBY02<#qFY^6r%9e>UbKem|RO(8g}vlhfQflViHT zu$HaQ@p&4>X4is5zq!9mjT7ONyfD~+JN*LKK&I80%-J2A^JP=wYa7Ev{|H%l6UbY4 zf@Ck7GTj}FTDCBB87S+KOt;AXrzQyjaW9K(NYc+u?99DF@~^vVr6`~wLJ^+->V#A( zKCNM35l;zY2m8Tq2?eBJo!m!+u^2$=~9S7bn_`1oRdO>6AwKaJtlao>j^BV>jks>ABC8K;uQ{9iM$W^xRjqm z!J5m<#Bbywz^`N=*mx8NXOL2NA7RFh%t3aIM+RVZP?e=vIjYvx9w~Uv#nvuYDkC%G zMZup$4Kf3eLSy~DppTm)6}gatiI->%tVpfz& zo)BfD5pyi0nX`s=+h(6~`x#1C7~kcn`+bKf>yp$=^M2&f4S**4u~&*Ng(z zIlGvMl&Li8tTLv(UGtLKn{UxxW@=FEsj&{h@6}`YcsLrY32(1PxReS!s#J!yGdx%n z_YOX(fBWDvgw_{TH>z%lLuq(W^!*1c{+KbXW>3Hu)7osG=7_S=y5B+Bz@8lgZP61= zy9JySFJP9IoHFlen&_|R+}tyxk~w)gm0v9}9tMHCc!w}mT9~kvn_E(04=U0<9t&#d zY54rn-B#};JF%(Gaz2u)d5J+zYy-c6!u`R)mdSX2nn|X2Qd#UW)jPggZdI+MiB&5` zO)#)38r8ybT*~S5i-Gk3l1&1w_A?nCYK*diZp>aohtt7A91Ei zCFbW_bH@~$l7ZcL-33oP)QmqH%(CMJVA382bZEIk`$<3@t#J{Gy?)1?kIzTB6!z0h zaQDrM@9^3IY;5`RX&O~RT#dLhl)+#;^`$-5%?^j_rv%hF_%0!>4oxqOu9i7(zOtGO ztJKR-7v@TQ`MrAB7lH+FiOQCz3L1UDF&I$hWogaMD}A=EOXkT@o!3esHJX(Bd%`Z+PGo*c>gn-jOpY6`f3{Y7RweKH4X z(l%DWma)J1{Ha+9zEcex+%?)*2$c1h^m1!#32b|Te;_DfxO_~yHuMG-b|_HN3a}ui zARqNLBt4-@l$y4aRYwdP$_L(0K1i>GPkqRi&G@+Ha(6VJrK^ov>Ri)*u}auNoo!94 z`9@gYuVI`}51`pFtEM^&ymN81UM*~wKxbvzn-=EpFV}mvp6N$CbqW!$FE^t*CF7CZ z8#jxqo8N<;7`(MstUwk+q*W=*{8u2@7MfND@3(@f@eul!dn+@D8IRvVXDE`r3AMDf zWw7qrENc5&mvWfRVL4r?D`Giyox)*Ft|W$Y?8q@Hp^X+;&%;Ydc{T>N2`LJLFxqaS zysa+oJ!@OtO?K>9JBQe+3GMF;EMFhTe9ELWdyqHO<|U!=bm)IHfAxTuG5*T}dh$R;e*AHf#tBD0 zPE}mc-{f@)M8U_)xx3zJR9T@yiQaEo98hQA;hE7L+H|z7;+7T0p83jb`S$yO|JEQ0 zFg)ag{V_R4c8N9A3VldYxLlf&o`AAgG;|Er^N{@lB8|7ty4K|I1kiM+l^Umae z+VC)Y--%k`)~&V_!D#Kwae6j>Ap8_bo@dDAB}#vhqL|D9yy-a7f{K>6`m?z9TIVCC zVsws~8o7z5Bbqa{I=UwGXy^Tzg{d90%`KbEJ!AVlDIPQtF6!-;aLbP}wpxSSg7Ho< z+1F=tJ~YB31>k@NNj+ZV{CYkiYENlGU{$*4r1U( ze__RvzbvyF=|49(n5Udihw*J(hBDY6Ck#7}{OZi$@J+#k@%wliZT*e+QGEW-++d&MmV`^({jZ*jw8O4#1_xt9H@(!t@XFm0}G}09KLidf! zV-8bx*?}{{H*T$;f>|-F&$w5Z$TVq85r7RSrhTDXB7oC*;4rumlv>(Lc8cl$y5!V6 z>TrQ2myAU9-cPZdQn?_EQFHq%-Cd=e2=-iiNW_fe$%mnSyt^qg7^wC|WI|p6muv|w zZD}!V-A9RK&KaF1x8O#9l5kEtm5_yghjRy97t-vnXHN2&sw1Bc5xvVa81V*;mRz zRMy0QyQ*mQ6|y2u&sr>7GivXPB6{6d1B+;{i7++IoPEp40|EcX~v3zslc<0a* zQt%!Nw9+Xhzgrka2PkMGgsWYtNDEk7oMc!yCzFGUgC+5VteH8rhLq{S>x!y8|1Zpl z>+ob^Y;H#M&J{AJn(mRL2V2M8qIx@^qYp6VLm< zG-v+b=-?ka>3@0SfM?|=6`fCh+UB%u4o&Z6k|dp#Wz#>nFIVVwfQ^^}nTCKh4fz82 zmJL=*XdZy0alF=Y7egh#N&N!lUGI$`Yyl|&%;|kT6Yimf$?s}C&G=Vrps_A+e<+`h&_;M9td%KY!{B!@(yJ#9ZYk%K!&v0R9}{RC7CD87Hxr0Nxk4 z;nvnx8qc?O$J6CJ=>&kQm(`#fDp+yhz)@)UeK*tSptHQL#n7c1hZf@T=fHySRub?Y zwzV6!(LscC?(|RZm8uzhN#u1aX4Ys*EB2MtGzNDQV>#w>0rq2jb}N+F`7&+wCVP`b zjc5TWKdN?m;m_n^x;gj(Zcq1@L8xSaQyw@swmTFfs?!sedKU{V(H`)*7H@tnvY@a! z{0V3oDJiKyBf!%Rc-smPOO+D&(B(V9_7JG;zkpIy?hiMBWkaJu!LpCPHm32pIs!Nc;H)oH=rIQ2I`*Mc$<0>i@dG@~ zAmF&w1XWbDDIn1_nM621tIk5V$zIfHSbSM~xfsStS(e3g-0kO)s-k_ZO+1~ZzL8vu z$8+#>X0}?flBCC`asaT-E?ERdU?yPEINiKOE#l82;-58niC?3Pig-B1n_ zd;rL&$!Jzd_>|gsGbL^wTO~hIp6K(099(G(m)u8vGoGt1Q@Ds=j((% z5Y&eNq;!D+ng%r+4EjLB|GpYwlv_VX?RU2=m#Zp@8xVDFB|$^r?P56o7>Z z7#aO+{PRXk%aF&rbE7qqOk79NA#{=|6^w=;kF!;-#m2qyH0=QItbbZQS^Du2zdJxr zoP^ZCz+h3diw*R7wGqHFGznj)l?aD0cC@0OL1NT(dh=}bPT9YcsWjXzna!L@Qiysp zq6BR)K$L~vf>1sd{e7<`dG_d{U7r>*Tr}X%Cnn;Inj2E~&KM1ZlUx9Y9LQ?(oMVqB zQw?Vxe_hr#$9L=l+;XGKXzs5TP%$J1LFeU*Uq$Gw2~BXysI2c`fq7D?+75_Z@ssoC z?6U5sU)KQt37z{1TQh=YrG6K{ccLnW8ekwc?s}vo9p2D9WufuSx{X|ZtB6c5Q^4EL z56aqUF~Gd}uxO;4w}CtTeIgR7+d42WGBYy)AK5UY16aS+7mN5-3+06KYtw-NAbF-K zlDTzYBx$#AECBRL;rmL1%}6BEcm{ujaTU8@s9Dkola~DQ8M;7?i?y{6wnrttml2Db z?J~pqVMth5Z`T>nte-y@^o*GL`}&5Y4)NBQ$Q`ek?W2g>;efh2X_RD#UtLC<*=HL4{A=m?m)8eIs@oGW=VV|0IXoM>C+gV&i-f;*^xbfQ34x+ z!4IWcv#I-gCxE1Q1iEG~7~&w(!Y0pp5wj`_6(1j-8kfy15&;&nL%egH4oUA~zOdeh zC2T&X{_%Cf-ZMkU_1fNtBPVNEVbEz*f{{i8oZG;G1BQJx7Jhs49)1M|5_F#~_ujOp z63hN_ta_Dkv;5Ddg3Q0`uc!}6TLo%Ag>#@c5!$pDE&Wx>zgC-;XuQIyd?Yzp;KHT7 zV4yDWqTOUs-`@D(sCjbMC3rutR_>X9SCQ6O4$!>>P@hhG2LO2NOTfiNdz1V2++k}H zE~Qy(^FJGUmJm;?|4A<)2G5|{oa%TmLz^;Y{tL$Q0x15rdACf) zU$h7SN{Fw+z$uw&hUzbf3!w3VYM%&S(U{H|AV`o-IePQw3lV_EKV`*f zioeim=X!y80@c! z0R#d-mpx*paj*9j-~#X)`&^mGKdpL&Z;tTGL{cBKv)$vz3;CiMQW^wQ8*i=d+#B z4QzDobklje2+7D|)h8uEc3-WpHiBOm6rbt}SlNzBvh(gQ*5jF(<>!u9B4ueR=n2&x zp1Y9UlNpTsVhq?NYVC}dnpDHWFW{h{k#HajRB81PbSI! z=Wt$iq;!H?`oZvS!Vj-CZEL9w97h4pS)~Jw*(#?17go;Nvj*GAG5@puLwPYf^G`~ zY{=Hg(#6B*L-k?}55OgEq}R$tLDI?P4>MP$%$0j4b!_v4M;cdcnNPX+2J|UeC&g^( zHG*}4^eW%%L}yM4%^HiOQs{A6rCfFw?O0ubXDM4Ri zrg8!-;Hk%D@j!*4QY2vS+<8B#94b-hFDjuQ*9}!y7*^gl?GlPES4R|Ei3~vq}~9qAtUJ{ zU&WsK&mX^kYrd#w8efeP`py9W#<$O|^>Osd!|lnNtK>{fJReoO+^jTO44;o|y0VOe z7XSwvl6H}EM(^r}8~Jf@a!|9a^|n$93lp%5paF{#oj`JDPOlX#tOn9*?dEJwHXU99 zB#61Gc~`dV58sB@;GPqWHzbm(g1)~%Nor!Hgz4F~xY5B;^bs@g@Zx3f80SQ9>)U(G z;e*^xnAFzD?7X3pcw;5(zsap1(|?FYl%K3)(QaXP8nYN zv%QaBr+ysdCLb97>ZD(w{4g0*3IR*b18kA%3zVNe8z!`E-R?R3#`v`~h0vH-!DYxkF{JaD0AQsEOo)w+V3;NPw`Rm0pfd9B zU~}Z=tD^*=7KVHC21^p1>aYT`Ouz1ndoqSttT-=UQtmCviYNoV%e{sHBG5Wpk5-6; zUD|a`GY(Ebf^ z&)+z~Vgi~as7uLz@~_&I5RmS~rJg!-2>#A!4OjcBvjDA=>LpAKPLvi41BCVo|3vV? zU>ili;If~C8bPF%6kQ~rJv{#2Aest%bW1Yv7pf>sU}W?Qq*{l%(EmAC_)iA#YpyUe zGZQEKf8+|k=Ig#*{ja&gZ2wPkh5s`%7>-fI#L?WyMBLuM`M>256aUR2{@=_ICjM_Z z!js-$z{`u+^9Y;T4-WDT0uGZt;939wAO6241btp2->Uy%8oo}hKPl_~C1IWYf0%^- zq?P{bDhr*c>uDslC3R)xMnAxdPKo9w&EwJeZtb*ffj`$%(opZ-@Hx-B$mPTTuXiQ zDJh*qI+7~trR0W`KE*+CbmltuBhJ@&Jb$6&fPKkbsl36hniO8$johZ+-)cT9w}l(K z658CJvK`?&xWv(09CNs68~Z;;e)=h{x=9yVg5O97 z?}E=17}2FjJ(Bp!?}`&EVvLlQ?{`DI5#RSW@;Qu|W~uo~(HMWQL0}Sn&BkZ}o?xR8 z+CfbdHgQxZl!i^^>(Z$+%&yno{;{LIxsB(8bcH0CMS-N|BC>8MTd&gCM zwq3ivEEKI2N}1WyXvw{1dSYMU(f1C0%#+(Dxj9mMO4e>1(VQ!@TW+Z;Qvx!0ZT{$` z0WJ3ivJDsH^C#Roan~-}C>gBet9NY89 zsZm_!SwrieHYC2( z{w|&N<+QHA0GG*sOqoL`;s$>HjJJe4pwsjUf()mQn+C6Jm_{XLIN6^xYaCZqh;Ix@ zY^v&0U8&@v`OcmgiWpW9Y#ny|at%!yNX()CSN9xQU>`$`X({Ouc|d@6k>m*R!x-6Oz0n7>B& z33^=@9TusOLm3o#Pm_A|hE&o&Mu{esO_kcIDe8;(b*`3*Cejd_*VxnNM@yxSuYQB= zB-!OnoI@2l;7nHzApD%rEt@@yi<3dT0@Tqu3@)QfrXPF6&ZcMYPUw=w@%H3kh~@i5RY`~ej5!Kh4$zF;NjEXBX*54LCo#d z{Z;7dOFI#r1LSE!I@LpCZ2jzhR7LE$(h(_R_%vL&XQU~Q7JQ-6HLDL1s8KzNOZ}~# zO;dH(t@r`SgGqbeO%!SV$6Bl^^{4EWo1j!)W@t&HjyQ6U>`_n-$;< zDTSER_NK;{p;0DAF2dz|?=TrG?O7~^Lz?l8u!G~BN@vv?7%#CkbFMn_hiW7urq zP=MXu`Iqy2gdly8CBc$u_gaI1GtkW1%S4mJfWQ*iJziwgWi+trpc^hlJMP0P@O<1z$-c>NZCj&Fz*R-xU^*J_qq4T418u&^odLbeg zQ{c$504Y3xVNl+|iMQ6o%`dqsWqee(t%KS)fK!vgF3*2J`~1bR@$wy!anmRqDBo4c?waP^ zJuwkI*4WjI>BwG=JobbdksEkklf2Vzf*Q#Mx*O&KNWDs_*%36SZkO`K6x*00pB}r) zGI~uY_YmKf%7Hl%+p`MXw}YqvMq>vSpcyTted@yt;V5dIHoN1g$scwym0dG%HJv{> zh_9AR)*F3ygdvlWw`@n!bNo7@Af>f&G*^xU)dx?XtuY#FPclOs z#g4mf-iU=^Q5wP$Kxs#-)jO{2E1#o%c*5EMrdMp%wc5r+W`C=9?zu4F7LdlfNF!h z+zQlN=F1v{2!nmed&3N6LcYtm`JZUS^QvSRdkRHLjwf5*vr>ik)ozd88b7M#JA|Kh z_*I$oYuxQo?~5gRf6jk5W#N}bNMKNhzwWDc*%Au&rdR8v5LlssvmHaW3^VyDmJ3Ch zO6#nBkgyikJr#zZZ}FA1mnm8`wxfzIhunns*b(w716yI-d&)+M^RO`7BL1)8N_eYX( zURph>`SS(fsQN8TeY(^|Z{HNe$VM|F&!|R43iFCs5x|^5$SqJjL;Ydccd*r1mi@&1 zy-+Ksgp(BdSB|1a7LGrtJ9qN8;v$}+hE@6*vA^Q*cA1 zMEgqj!q=v4H|FH)+m<)rm>C>6hiOJrW0? z)kXWOO0h73)%COB(5)!qeg*Voe;av!?aEa-24`bYul~@NPRNVj4bN8zyeT{W#?_m@ z%Acu_U@vhd$BJaoi<`K6mhkdEu9-V(FG&by5$Jq79MXDA`r%o|Dl*V5b|h^;QGA~% zvfl1jzhzJ5i318x`%f5TZB5r54i3$TZ_z}X$ox{X=%zJyoIAXg0b4?;1trk;xn7T8 z+0(52`q*SL@aE-hRtCK#+*%Tl6n*!Fa{On`P(wkmoc*LdJC7-`GesR+Z`!Y?XYzy_ znZw1GWc)ea%>Mqd{TCC@CwOPikR5o zxUg?D_{ocXV;c^EkuOnbK@vDcF;|G5N8yggf8fYZ7muIBK|vS%!EeW_MMuGljOX#; zImB?!cGtQU`ZGEXix3x!1fntNz6!<{NETT&aJqqfszvYq{F-zmboK$c&o&jA+@esP z_D)XFy0AwDCp=u*B#b177vJ2!xdZ12*uZkPRDNp+JjZ{*Ja|_L{ensJ?DyiwVnq@6 z-gVslzCwdNhk|#^bE8%7NnL+JFc()QM2X4gqrz}+Qm#^E9aq7! z^IaE1vsXdySJVA6H>89K>VDqLSF|r_ZjHvvSHc;j>0>s+2b+_|p|9?(Dt;BLHRd|$ zYd=su<&R}x!`FrBcsd^8|E&48qdP6VSS#2s^6R7m61<7Mppsyb?8aWIk2!fD?W!qF zZ}n$ryXlUVo%_}TDXGu2IGmg`<+17LRGbod_f}uzV1p4i*_NcnJsUO$`>5#H2-tA0 zqHGl9%!?XqP=EWRr<#=Kp1BOuXzQgD^+UMJXU~FoO?_zDKQS0stah+TP{eK4bA8)u zZ7!-&8qd64Fw=tx)s-7}otTYrqLhZ++NWj0eXkKJv^7~2aPeKCq3;ScDNPP;E8BEn zi_q6FSyB#8u~m7s0nST`)B)0n4532dJh5GJTkhjQT9r#M`I|4A;_q24>(Fa(a+wzA z-bIIUN7Z2O)O&3EgdYp{E2j4fHJyK$9S^K45rU#a=GYP^SUFr3sT(tBSBSE#D6N&L zw1qAw?@aC~uTiMn6Nu9Q4P%PosGGIJmcNJVa47bx^O+;%FgoLzOYZex>M1Zrd=p_P z);yR(jmEnpsijHAJfdju$>bD9@8%@bPD8Nv;d#sYtGs(}8hnpeeH&A%EgF<6U-7*2 zRu?tB{y3h4vF4C_cI$9lw-%-p#9A=@uo5YOU^T-OJtjqB=3_|0olZ9$%6 zpG7ae&$T6b&nfh`sbcL9R>2?Ez6fTyMzjsYgQG50j}#cylOqdhb+Rx~Vib2S@$V?) zptu%ZiA)z5Aqxi;Q)&n4syhlo5O-zf@T^S47Il8qnRE~q(wXGXBDnPnFFijumH9ZR zMEUluQG)+>JF+Uir-8k1TbI;L*-vx94O}~CAH11!w3O9pU@NdH8Ao)&yt1Pvkb_W( zRjQ(Qx}z;-C8_qu=czsfd)r{k$P*)cxl}ZNcZ$m=^wO;JZafpAghxg_Sa%CQF*Cf1 z_#!f2(27{_g<3)t^exhlR6_MF`CcwD&axC4dRw8oCbQv}%6T%411uKqvHAUPNmCVP zNDB$r4Hw;2GV={fc>M`bsl-X=m^~e6p=$mxdInR)q@i#o2huc}wPQ`Jp=uN#x=5HO z9&-_r9OUua2XZ-;Z{E``$x#(eO|ldn5nCx5Q0#o6$@{DZtGYQ%C{krcn@1cXT4657 zWZoSy>QKId7=h7TPh-1YxAdvKuYGP~bv18k#0k|ikT;!WgB$-x)DMb0o@{(d=ZweC z0VxzrJSTi9RnHUi58ilfFpACXomp+kor*&(B7|W{>cHZ8^$*}#DMPf_~ z`($1aSmssEfiVg4qhdWu?y{k4@$eqi?j+PH@#b=1&VpjC46wak1l}^=g`YWiyU&}1 z-IZ!2aCnR~`P^<2y&+@NK`?T-VAAwua5B~(@sl-yv)JBL>{4=Mf9oz!j^m#niQ;?j zi$KJ9Td2jIGMX+e*f^4(An?>)Skm{~5aO6oZia8v=b$*aid7v`x*QBtaRU{mFnpJM zL<{D?f`)#Sc!@A-2CZ_>mf{g?Y!7?WGf{K5JV|r2!qCJig0$9_Qa8y*EWsG zw~h8>S=2_mRlpy7-bCYVOuRVvu{Pf6mPmRPe2;gQxtiPvDc%-5W0b^POEMm%g4wA^ z%I9D8ZH9E&d^EDcZzl+oa-N%~e19P0SiLP#=@vG$**<#DfDGerC^XkhLQ?eATrq5r zYqB#^(D988x#w{9aC|lTeDwgoQ;O}h6OkxceduAe*N=QARnA;9CTbHV$zmmx3sG+n zxz`zZjHh~@f6*Ef9lM@UmZh7M+c#HxXZQQto0F5nl9A)<9eCT7-MW6gImlukUm7=Q ze_|gcq#2dBRlp()!gMOb6p3L#v0G7|v5uSn9i_Rp8ymV>zZN{BuOSs3Ky}~!Wr_I- zj$8UeZj%;`U4F05b^}FptN`lXG6IAgypegZdZr)!Ex~yAG$DkdA)=x8^B2X{qya}i zJwNVfS6nU~MbKp|im(hUvvV9uQ{eDiat#MwUo=P5jrtfQEuB=?{i@Q6tTcxz-SX;qeuJFMIX(`^jV*{F`}N2f-3Uyk?DTPg!rjad6=b2$M-od zWapszj2$w*=fSbp)?pMCUCMh}B@m(3eld6R+$dlsJ870xqhd#15kVov+BEBw8CiAr zj?A!>)gPw32=XnLOOOpCE~ykQX|$9G-F__%0VxDKnq^i?sb5s(+ZEH?Di~L!JSqYQ zcyBU!4NLj0gyWv=WG7g4&FfD2I4ZXhx%t>u7<{FVJ-M-g3;S*%zF85@A+9dZa#cBn zP|s||jWj}n>03hn`u6TJ?@m4 z!S9=x#N^E9tg_4t>u|93M>Qh^79fabG^U&Hon_o8?=dAy&nM^zsaXz_Ibf2M$|kfY z%f!b>@uY*z@C{I0uZ>i22wQ_i5bZK~HsAJccA77VaY4^-NXt2EgwfV^1)9rt!%_tJ z7iMnprOKwjMk8PQzg%A4e{xYGSzU#>=lFhXG>UgUMb*kCP?+@&K_>%yBX5GD-v402 z|3L+Zd&GcHR0LPMbR4)i+2#n_d(FEE(f+)*R7;{j1q_Gl4U|wrw|r9 zxs|lo8+<=qw#Oxcj$i6)BdQn)7ZuRG|HkWU=OL)0kf3GVavo$S|GGa51DY7 zhlm)a_O;KUp}P435{XB(iDfievx?ls`Pxn^3evW5wee~~H~(?u1u}aGoaTQBFuVp1 zUIPs5%xs|l2y=n{ON8Ms2aduY!W#tx`&V|Fe^_gjOupEBwKp>PLJTnA{Nc+HwgISb zK05#xUSkvhfsU++vAKbejT4+U6L5i@nT42xi$(V}B=HX?&!3Y2M${DSZH$yn9Ei1v z85Kmt0QwyhCxCV557~~;Kkvf-yh{@6{x@2k*MOYx7v{h8Isi`&z?%cKh4}TdiV8C^ z^K11$dw|bO#B8sOKYzY(5HtVNaupR2G4sEXJQXft*8eEL0)znn{XIJ|^Q&n7lw~Cb zdh`1EwX%OHf?n&Bv<4Bga{c?S|I*dhYyWojAEF_)f2;Z5AsXUh`wyO>*N^`@G(-P) zf`(sHzq%2KH3HKInJvWA zH*(|Ybg6tDUtT(FPkPMk&TUwbC2QKKT;<>O3m}rI;DFgo=CqWrKQQ%-x2yQ z<9cq>nVXxN*=bo#`k$e#t>osCJG_I&cTfm|M~$)V0+RqTWV3JKvIJSX#B_8BK1yQ~ zzztB*vkyuh#%E?~J4Bzod^E$=)MT`Dbfl#DEs_!w^K%3arm3lk>k*C%=3T+ax+`R(9exO0q|WiYKJhmp3f=5eXo65`xi2Coo^r65#QCGxs}h%UrtQjen2-!L^MJ$ro+^-0jOhfp{b;Jv`$4TSulD4nqU zPh@;40=t=qP$IW7y(7m@3IttR7$aAGx=&70+1_Og?P>6ET0B9v8t#zVU$f|}5kUC5 z)zyboUUZh8u$?HYq-qy3g85f_rtk`Bp^&KzY2Nt#UZ2cll3AqGrEr^!?xKjDoHmFq zyemN7L`g%TGlP|f@FVahIK^ZUp?Vy8YWp%6w3_|&BvicrYY8Ijd(|bi@(gSI-o6#x z_a+&5d|Gs)oER)DQ%!bCcC`RCtb$S;QyIGOD$W4XDpeO3Aw>fZJ39kONoFYn0}n;i z$lb##{m=(r2B>k`ph1Z&el`M@Ob2OFA?eM@B+5jCm1uVKDroqq0}2dG ziV}3WXv&tMIfm)MGRE{+nveI(Zw`2$p7^As!3!!6OsK)~+D@~dv$NkAR7C!sF4?8TyPwBzxVVtOH#|i=Joa zrMb9hC1M)^*Nnq9v9>tPwK0_NEci?VmDE}$FjGreS~=fYD^!ZieVm0}E_^ZntXgU& z6IjZ`EK4f`OPjMop#_I+78?^*R$5qIQB+b|R9sR}Tug&`xk31lQxh$bF6|c*=`-M9 z1AO+Aq2VBK0)(UwK;aP-!fihj8U;a34vQndq7VWE9YaS+iw&ie?P4eOcUAklC|xxy zFMGbflQ_^t66zujb`l2IadnRTU{`_8ksIryi1CoecqyWNRe_E?ccrDPn#(Xd$zq&k zSQ#)GhUTUx++8*99-7j!{L<30(vr%Oe=JnW=?jgFu-Z=vtjIW`|Fn1z7h*&KDDVM+ z{bma$#D&oA$Fg4}uP`BG$azVNE>T^}%PT4=uBtBec2&9C@>4>clY?#Z;yjDuUE_U3 z*&&*oKviy#GSN+t8)%o{A&zqwrgTExCPOx2afH=iZ;j3nAL>2<(DIw<^ zk8N`L#0SP_PP5q-73I)NX>n0`>0T)-E5jz?MBtz$14P65rG9`25z&S4AOGLQeNoXMS01+(cUQuyjaY>%NoiHKN`9fJ_UrqFAL+of%++btur4n6V zdDu{8&*n=$n=ku%x%SJ&+RtWdKAow4F;V_vykw_8{dQ;aSXuCRS?EBH&xI7%+9bCK zC%%$zXl(T1sWXR9o;YG~_C%o0r?R4?s;a7@qJqY=$A%|4gGg!p5IC<$ zgQvJn^@=q7?KV8gyHl6kOYTiai>g|^3)y9}> zrD1h(c7YBI#6de47s4F2)X;D2oneZSWG`@7{cNu6#a!{dv7D{pXy?f5!RDjN> zuCB4FsuqZ#pFKjA;6n?EMnw7vZDRvi=oHg0kg!Igatr?xGB6e}p|L-nRzsf7CDyqw~F4RTdnJ$04(D;10@w1iM-!0dEwOWt*6ED~(^RH2kpH_~X{aKi>zF(Vy>+{q_0$pPnuL^^=vq zeX{VEC-XmU_WgdX{_7hRU(J_%GMV@6YVO0~tY_m{cL!sZS|d8My~14tGLDIj#R=nc z@1Ht(_{^D8M?QQnIwq{PwhCgvFU6uec7L7)QnM#ai`!QWWa$W zNcw;eq(am}=MK>cn9ytq7II8_6tIC)qYo9;b=6gsDX9@dt;u&MYj&rLKVGQ*Y_aOA zn@wLWHGQ+x{OxMX51Va2Ze98L-nG9xnEK=WYyb6V=5L?f{>vA4{^#rU|Ndn8kN2;B zd#m}2*^1An%08YbdNEP>B^nnP; zk!h`x5urw>vaYtS($81EcC~!_TKWB%icc15KD_}rbziNtezS7^yIUPUZFl_n{>a~+ z&iv`YINF~d&iwCJcmDSEo&WuA`_CU;|7mmRtLt@NE!2E6Q}Spm=h=A9N3+HEucqG_ zh+pampK1<2pA!)5&Q(ba6bfq#({m?J9{=EjcTSypUl-uh*gzGEPJKN#_6Sk`f)6R^ zB-PJ7J0gMX!TI}I2vGwLnSjvJQXyoBRIm?F5T_+aAW|ecA!C4ZVSsSzs!B9P8Hw(< zM@qLwitbJnKb|grJX`#1uH>uL#xIu}zg=zpegklZ{`_#_=ZE7zKbrj8$4meB+wDJo zwDjL!to`}v+>dt#f4trK#X{95^QAAQi_v~OU-WQ1d#xvN@j}9UTS`xHcz&#-y93kS zj%{jq1k0PFM-QGnd5FWYYG|l!Y$SLZut5-!_GN@Lr01nLHI-} zL@h`k6yPFKAwba5OmGk)6&#kFf}E5LP-8<=K~d8As_5;}vdy8qM|0)R=gVF!m4CKe z`Ne9@mn*g3ZM6Nc+55xh;1Aow|MhU}zaCHg>7#|eesc3qA1(j+@#0T+ul;am;QL#h zUoF?Xn9X}ORrF%EydONRRuJ|ukr1e8btmiHs7ndpVw zfNrHE28~|Kx;t97b1nbD6h^%2qs5vpR%*UltN&)L`MW#kf4)EbA9siUV`ua~A6)y> z)5*VmzVzRpt^W11rN2Iy|9N-%_qTh$Tx|J#q2k3{;q$r5r!&x7{?2ITLVMCwL*!Ux zRA**zlE1B|lSnE#E99MFFi#nq9K({>(CAcsLv?FwYs+5QPYK-vD8dRV`XwI{8ruE( z0U5ng{`sT~EZCV8qChZ#T7ZQdpBAnD?De^6;n(}i?u}Qh4`)4`DSI(f@%c*QXUibd z{Po)T-*0q%yLR#C2iLye8v4(BSAV!W@W;m!KR;jjj z@%5rl7OJ1j6g?QvT)P}Q+YminA2w7R(3Ip6>!)ziu9$2X@cB=R9?v-l0JJ#sHuk(yqso227 zHafq*+yA$ZcK-VO-ZyIle^~GO?snHlv!$Odlze)<Q8Q92~_!Hvhd!u?4{1w*~W;WV*kGUpw=|^vPg$uCytB4R4X-?@ytac zOE$~U-1wM<*~z@T%snd9zaRq%(JFd{53rD)V}nkv{csTYFPT6vAf~56VZr{O3NUo~ z(1!(+y|mW<+uT%M7(R0)>t=V#?W=kBuGef$RNWh|-<>So9fNr4zuM^dcKz}XcgH?o z?fv%l=;uq9pU*bk9Vyxw%HQfMeKyna=}PA(x7(jBH+;6z{%ErJc3PQk7s4+TTU!M!hYhQ(=PgF_~UM=VUw)YVp9ym;}#UZE)g5y~%945LW~ zmkjiNJ_s4%!*BOb%3uP)5UBtQVM6pqjzD7}Iw54p+1pyr*A&N0c4kesM$EUxZ4BkE z59KU%C9ht|T|CkvbQv7xH@8@F?p@0bmv;d z&UD50WXbkq`SwJ?#$d)mTkK3->|}NPWM$G|VQgNobA+4RSIu%%*zjx&m=>okjgOfc z9=0++)!bCq+1Uv=DA?%e=pc{?qL)2vYui7R!~_r#3e5+>f)d~$iRcU>On?OnAyUCU z*b}cJFDH;m`rs5~AfQ;B7ai!h*wIpx+Ex^Pq0GN2-K{LaK091h7VpxSPnqtuQR|d?UkDY0Z z94ikQDhwLVi|Ee@Z%FWoa}xzRuw69_rO1?Nanj1CAmeTGpDjfkWbf+Bo~c+dh1(uXz{;wm&2>_D*Kg*XCqLV%zpKnN3@9V6Az(OO*; zovhQu1xlm*#eOaIb~jo`y(#)mj8!>&u`yY?0E0Vov02i@B*AyA?bk)RL}iIL_5CZy0A1i=tR5hkzb zgfJmk^rO&z;R01JUOa!{d`C{4d#Jm>(~fPYWT*sZ zz*2qua&y>~OrLZ&aj318i^xdBI;FHZq+lEoGe30G7>2lU;{6nbQI2VTvbrSCOW8p~ z1(CxgalJVKL;1Rk*#SXz3=a(hbGK4#BI2F4Hajce8+LbhUb%ARvcAxS@E72`q9P(5 z0*9Wzh&iA|yd*&Y1O;%=LMpUcXe=O!0w6RKV8LODPB0WmXyi8rr?aCmIzSlaB8v2K z402XD2~7O8>;PMCw5MH`pM71Z^MweP&P1Yg@UMTxjT^ zVKXg_RSH&LZx2ehz5o%SL=fqYLnK6^fb0>`encc>ST4yR=!Dn{GKfG!i+yOZ z5&4ny`KM$qcU?#hmFGk_+S2+mtXRMa%e<=14R1Vrz{$QpdcuWJK&&2 zyo3zFLNW+~Ay{ZrB3K9$A{88m04JxwI6y63U7e8a<*xHNv34~nJ~1v*e=0Sa!Mr#8ez=A`2K zIVn;CT!Z9{7)M^Lt03B$ljJSzED9N}44$kE8?BD1OAqpMjS6-wYkl8dwm0kNQf3@JoBRC=_ufQ&wauw1FZdJuKLRDhAjg4dGc z=$TOIqi1pnrz6?j+jG7l%Re(j6QH!r2=k7Q^ffg(VR+`amC*@L1t-Fhnd-}Yc&+-+ zkFJ5twVLQ~wT%nU)LU#FtYn2q%-V7zvy#J%jE&EpIIduvh;=_G%1y>cQ&IY-0@HcZoCsW-jNq$&(06jmWDZ0Mryiq{f0|I zM#@9G%ENW8JTBw3h;1S=J6arOyVF_F0_+SIE!(LGIQmF$L%<9B%@f7$_MD&Hc7AUC0e?O7^~x71WaIUmxe$niSNKpsR~@AFql3@_Os{H~U^pSFd%Y-tNu4 zRu|iz;@6xIa49KtydrhDDs`|l?ow9p`P7i=2>0HS(5`%4Ynu01eez^|@<4v@NMXp; z^6>8Bun;$alxNDaJmbtYy55+c?k4o(ns}Li7;7h)80j4xqUh|wc}WIv^b(?a3qnb4U+VhP*1)r|($)5~yM1|2Co67r zW?ahDHHSL4MtKh8MlUxNEH-D)wdPH>CvS}wd~~C6wL52fv|@X(X0a*bYH8$nb@b)T zfEaJNy$YWoI2q?AF86kd;2Br@XcEQz7_n)8efH?k;PCM9kiHBI^r6CmP{E4sFgj17 zKuZdlyoh4}5wH-yC7ui{#9P1w=&%F!1W%GgfTPC(rBGO?QRute--DC(_YGX09cozY zFI?&_9IuYQnjcrEv+Yg~n5>Mx(VBIqzv5PR_M?fCuNK;V-s*igSh&;_eXl3y$yC|n znVKh)ZMQEL^d*P11iDvvsw@07EwLUqF67)DtGG8&wtKDg{#ez6$?CN$8RJ#KLnXl- z8M@3MrJaJK5}RfQD8t0owcg4bW$6RqK}~LMSBsNJhX!aez(ebY>gV76=j9Yn+y|k7 z55h!2ya=4e0wy#Tl0k$X*%_}QM}Q82op>7Zw|}5}5CbwWFgVb?FqD65Bz0{x>&E4z zTOBFu-C0{hMGq!xUo1B6PLwZSNqsh3^}|NbPwU;=U74#*vD;ng+x=-zrc1tFyY%&1 z_qVJ4FQ(de1}ktK@zM438w2HABV|wU-sO#kCsS1qXNxxal4onN4AymJ2Ny;;XvH=z zN*la0UaoV#U6*>dtze-bc`7|=rYhsw$k6EM=*V6n;vt9thw%Bw%=-Hh5r_yJkbzoI zfQ2Lxu+U6M1_29Hf}&?aHGAUh8yM^!#Q2O1TpjG5ypp;;mby8ezCD$)JDsyTmHQMo za+hjf+^D=Yki0dV_xXJ5cei>zpKW>2S9GT{1NH8e)O#aY-`u|N-_Pd$OVXf4kfB_3iGD7wbQ|Uir~n)$UbXRwPZ;2KN?tx1{=JglTMLEO!+vP-y6D zd9uM*ezQLPW?lNN+WhtInz5^+*REX~8yf?kt5>fQLLdVnQSQ8FL4ZRFiA!%jRB911 z5Hd6tl0GDZh`@+i@G8h~PcS_sc?SpkhX#9xhDHVk`^JZ^tPExBj%7TaDSkeUcZ+I1 zovr(1zV4I7#>cZIxB8QxO;`PX>GJohyCrUq7=Ls7;)~g`hhzDhSF)DdVkT>YJ9Aud$&eJPbWrgE zog}eR%NVA4yR&koF!6qS!TpP655_Nzk6i^8z##>EP=E!9Xg+)XOl23%2LU2Ah#p7= zwYE0Hk5O?2L|hu&zyyU>i{9J8hX{r;e@dl;{R1OIIOEK<%Zokfn}f)XiWl<@U$3@( zy43XfO3P=94UeZvHU@J)zR~jCdhcKE&wanrx7CUFtTQ$`Ggn&EH@mZ5%+~(#{^Wmu zwDv!r-T9y2t^M!sZvO50{7<{1-*5GOw%qh|rhI!i?{;_UY<=`lad1bbUqhNtQm|Ui zITP=$xe%@!jP;*O3B6gKcK>|oN_)nwYgfm|#!(0sdINtF{SXVn;9_KnIqSXWB_E<3 z@PXgz&6dW3iy*3i8*MDu8C1c9RHPEcL@&$a2 zZ*I4LiW@T5>+X*fZ}t^GpJ@JazT^9~{?BIG?{wxZH6?9a%Gv5F*zL(%X-~R8TJ`nX zl|Mcj`}4DDyy5bf$KyY2_kOwF@?xpt(M-|KwY=*svEx-yomu|1ajtb~{*k^47nOCA zw|XEYe6ud|)4|4#o|?}_+UH9WXQ#%eCZ|v)_6qbv3gFQC85$ZORS$|7mo`@%z1p01_e%Ds zv-RJub>dCT@76oMSZRK~RJl8m3mszdGgcWsSQOfl;!_^&nh|0jU5Iqw zY)OCES@v+~{6=lwTv6KOB)Cj~3^w%JFA)I_Q4x9zK1eLM4&dmOL*>?fXGW$#Fz^;! zTHzW36~;_w5V2fB5@c`$R2&V*gh+63oQOKvIGWMXzSilE%#|ytcdzE%pDKJZQ}Sf0 z=-yz~dVAbxNqAG7Z&R#SQ>1HqglDFMAVzGFuH}`u$-5JRyQ2cm2f0lb$4!@n-fW8B z?9F~UQT}`e?^RWAjpl3)W!xRenXQTH&kwqk?%SN~mlb3m@27IraOCXM*+Ht#1dkV^ zRhuo@+jSKe!rkV3n`fqHXJ)3Rr>801yj0Hk%-}rS2OQj&D-it z+3iWY+mX?e9uUMgPjMDz;0A??9cs&Q;GUHk{npOxLqD!@l8Se|Cja?Z`({V(c%g2o zC1HCY`_WX<_DJq(SK?fA=+%;dtA$|~lYCkeJSt<{3Zgx9E&@jt1J_o0I?03HqOGoy z&940A!sOxd)P;p>*JtMD=4NJRr@@ER>1k*OaK`Q;+m)yCpwCf97Qo|Hf}rEGf&#uoU*q$XJ>viUS)Huz39PU?W563_-B8X z&yDtkrAu*ZJ;{sbqb6$u2lD+pl6)?u2R6jIl*TyaMmvZ5$lX*-XO7{;Y@es&rT0b~ z*DvI(H)k#kcFkYEzBoU3ePM2Xeim>@1sPPxhwwoQGGo;IbyaUXlydgOL*)grWkLpU zAQjqJ_N#?bnSL6=Yau^i!O^HZ8q-ezV;G2u>8Xi}%_ZGMAwvyOOMN-(eT5qxIcx1{ zJC_UB+j6f&2c|pPMc9dA-Q$}S zme3ySFkKtAbSd%rg}9Nbz`A&CV}e^#vR`eyTWO4AUbJI+h*P4MZHkL>t|{%waLGn@ z*?dv#)vElZ`KhJrOACv$*RRi^LOG~FL`#T>N1tIYl><14NI%xYB)RsSr%!LW9b_5; z958|95-);U^ivSEKtj>yCM7g+3zzt)tzawYFZOX^%@;%Zao zT6@9us`TNMqyn91qPIhgvob)=^$^+kiWr&R+Lq*?nWp?3=Syy$FWK&_ywy@XQ=d0c zp4pof))e75QW=f6_(!V48YHE50LYm(g%;b_TW(crwoYrvOYkJKJgvE!Sy#5ee^U$tAz-L zNQL+GM@_{Z+Bo>Js{MLi;m9FU0w_Ch2;!V+XUN`g5Y1 zqrLMzZNpATuPMML8rMWS;*Ke)ex_Nu`#@gz_&DEvV)s>Z%rRC)t zs6e!j(jv$#fDu(@Npk7ML#c--9|Mg-h91EZ+32x!5x+$$k(mfO`TL4xpH%iTh#~`N z!sBLVW@qN-7Urg}FDzcDP3S3(xLTLkTM&6M#xKiJQXJ&moDf_c;#})zU*)MO_fWQl zcwUasT?qE9a+ep`iE>r!JQXY7o}cF^$Z!y5yU3F41-V}G!R)Z9ip24 zihXN_YgLT8EY>kM)+s&4vAH_q=B<@mx7SwJ*4A#X-CSE9GIf=cbBUd% z)Lxyh5NFCnAy&rD=ROFNnJ2l5T2ce9r0b?2zpCh_WRJW+X;Z3mdxleeyj@w0Lso<~ zF-$u%)_wc-#_jc$HBfn3Z=tNdEEF6df^vw2=oH&$&ct~r<&bHMOj!E)g%S?A%|e(E zZy`)bORx|!z(S@!kU4j%UN6erYOVQVvi}c@SD*B?ZZ;RM)n(plOux~bx-(q7HC(dXm9@~Au+Wmw zT@+jts>$@1UCa$=O>?e@(&UC|Qi4=Tk&buPu5X~+xwXE&wpVYXP;{sQK>r|~eaS`n zGrGO!Jd|<}5t+8gY6tPsWavC1lM%E;k`DWz0w(cuyqsVmf+1Ll*HMEqKh~BVejz95 za(?(|Wzuj-%v@c{Om+PA@{}9psn?5BXUo!N%F-9hv#(cV->A)>tIS%i%v`NVS+0m* zs*Kybn7z|ouz4kK`9j8AZBl=6M0u=Zj0ZO-!tQ*wt})fMFj|}7FOSnHGm`?g@7&tl z+_gBs(Ap%+gr$5qlxt0xY!U z?oT3MA>IzP&`baXrz4_Wy}4SG5n7h)+ME;ERTR;mA2C&)Fi{*mkQRP9F0eV=yVT#Q z$jhzJ*`dhUvCiMODbV*)jP62&+fb_iLP_kc`ixr*nG4ma)77a1xe;}7?r|R6a1VY7 zR=*j#@>qvBUty@HFd@jUp*m-4bA5AbZF}nu_>g+%&c^!sI&`GRbBm-H;Oysv%%amu zZ#(p~^>WRO7>Nd;2jr1HmHM#4A?yErf}z&ZNFr2PDkP6U2JneQ2^Qq_+O0cRI_ioN z0_xKJ+p_{YGlKdHV!PAA8pFNog1jnpZiT)s6~1mI?yglnUL|f0W!_FjZrXa>6pit? zmL7aHGh{3;;#zLhXkqxJEdRm~b(}jlJy=nf>01%!oarx)^5pwF3*rM^FSJ(N-MI%W zceb~-w>Lp%3)^?rNd+6^7i2?PeR93@AKIDqd=>#`Z?!{_K}DFTfP*vv7X3^_T{!Hm za;e)sD457-Zy_&45)pd`7NQo60`WRBIvZ=Nxk>(oaV`z1zU`^Hw&b7-@qrg2bd7;t zrC#=Vp0+v84r%sw+0OR4ZnoLZ$~s?{E3tvy2|>L{A(s<_hVx^)Ga@?D0&3#ia)PvZ zVRnTv4pnJhMKLZ>?jk=IUSxnntj>9K@FK|U+}+yRxeGEoJKJ}$y>*9FD$DLbJXE4V zIeH1b^q>9H7FItH5Q;v*sH+K71W06q!1r92#xepXWO1lpx=avn0m)aAh%f;bB3I<~ z+S=;v+jp)FUMxuVtWNT&Pw=gc^=*y`xSSArA;PE3OHt${%XX3^+DcNKr1_qj+EC}N z1YKu*a9dQsV0K(@W=vmR{6J}PYkFWsoJ(eqwlvPQEZ(^w#xdGU;o~6iauJ32Ds@ic zp}zK=-TS*c+uOT4U_vSofef8o*nnpA`q?ue%7Sk0gM*oif}>BXmrETY0FqE3gK20_ zEz}$hEM(oHpX(`a0ZF2ke<~F)(PP2rY^-gp@7%ppksnl%?9-MR-j)&Gk`U6Frt8fJ z94`vL*^+j%EqQAI_jRv)Jk`0@QLxmRKU$eFR-8VP7vGm1Ggz3=T^QSt>|GeHDU7f$ zjCLuGv5)l@dTV&@c6={a$c7v0%D*<$xqI)y{d+q*_wViQ-bLNd2Z-<|K?E{Xn(evs ze)GY)aC!M99K^LZVNtRnIfW$B=g~{o)vINUK+~&*TFzlz4W?iV;Q=yG6Ty%S(ku2B z@_OUe=K99X?XBJEk*=z2-}b!FuEOL?c@cv}5m$>N@Rf>{_T+m5MGxniez!IHVyXY( z_VC9F^o0 zF1q!Cq3*Gh%Obcg$N-C8TGSO8jYZD{izsTTiG%M+Tb;R^!?n*AuDn?2zdO==^K!+?`ReiVtij^MvC`zu zw2-Pe|DtHG%ut7v0Bxi@-{02CNp{XoYT&Ff!Y9xI9YnD{qQ&W*nvgA(0w@>Va6hAfui(YO_2c>S}iyb=0O|~2S+6?M`5AX72Dge zLs;U@*3Rx$c|lxNcIbuT$V&yuy+z4=MRDT|8NDU(LuE+=1@V13u|oy1y?Ie>so_m2 zK@G{imCLip-OyGDLSbVopzL(x~o}{9%9VnDzVjx69=sr zMgald4LT05urS#tJ=7^Vz#}`@FE7|9$KNG4#68W|KGoYc$xEB+Z=d387wIa~ zIf{K91ZsgXzC$Ww8_Kz-6}*#D=25N4Sjjo3<{H}ZO}tc=QEs9{UqxDw%KF^Nx88i~ z;5+ZWfB2}Gg^`_u;@fPv{r2Bp|uWm5+-^`G6OZ~A@)kX`I~S3>fpiGj(m97+{%o}wF-+0eDvf2R0BA` zLfU=FwM+AXe(214MLBvn)I1J2P|kib5DcV3)UszU)HM+i8xbBM1Hk|b)Iw4iEv)a} zz5BcO?hg$1oHaP~?z;z$9(gM<$UY~^DZ)eQ?Z9!fWjQIVYB&pqLt@O7=U_}}j3))02V1iN5tK}{vjT8+EseI%3D+d)v>~nvceS;> z6oN5R2~0I2OF7p>#x|0141h(#JgVZI&nw*jU9ku#=doh3B*)16$!4Tj43K;G`YqXwexr znPH$kBidb%>M5=Yva5}BZb|g6h;S?mv&-_876&QQ{1t8*mWAax2HTR!VRE=^J9}+L zX4>w(yEGX(t%xVnt3hHr#0q|?93mTBP{Vk!u)*pD6<7!vI)eyF2!=?7WGI#hWU+An zK9-);assoGoxO^|G(UFy@Eflm_|1U>@4fpg2fj(J&apJY3AYi_b+VK|b)vsA#!DLM zB@S}t`r6xgX)N89X3i=TJE@_Q+`v(K%1(5`S#Ah#@l{(zxo}gxCFK#0_z2d;6#v#l zuf}+fiZHueowO)eogbl92~3PFEzK>>Ev&7q85Sb3z}>?YeB$DxpFgLRgQ*;FUc7j= zUpXYLFrZ{`37`FLOk@KrkP0C~Rxf0lrc;T|BSHr9(|Zf9GA_?r1!wp^oZUq}2xNM3*<7g?aw#CywPQGH}F-qHwnVUQzRXV3K3 zTDWP<-PPw@6em42hJJQd0a{8e*@5cHXy-QE&`9*`%?UrB|V-t>8JRbWYK5 z5HjHC1@jMTK^~D*!m|Vm1cO)6WQh0T6v&{38#7)$&Kg@OU&vzf7zW0tkDoYn@XcQz zI`rDf58qa^j8nbr@^tovI!&HVo9Snp8EBgxs7>%y#Cb_$Jte5HQ(Q9!I52%wrvA33 z!4B5pPRv*rcCJoc6YF|06E}Ihu4L)DGlM(R{P6ke@(@LNn4&e+A74zcG(KU?tfdu$%j5C|Y&<0p^85pQ@xIuTr;nf?RCJrz4#Y#0L);icj4)|8 zCNuT^JR)RhEU(xL2;*`Mxl>S>BNFr2Tx+g?DUpkm8nuOuse#Gqlc(SR;NWjg9DDQZ ziQh`C3}aoDnf`!dn;+V$O~~`hB^qt?Zt5} z;v}fWUs4g_&>Zi9pIgDZPQAH-ec8bs$=;1o_B9czwq%#~)Icau$i`}hX~j5aWn*S# zZEk92Y-D`S(8z#=-_No(WpZq+@E@CDVs30^Y04FFv|4ppSqVZT;-P0NA{*LpU?8Zt zawL~XwDDp>$D$Ag*GJfa>`z_`w)m)kw~r?eeX=pPVOvS$LYY#`<}qbTi6zs_%-ZCv z!G~v#A3S~Rz?q}(h*+la-l_~gd7e&D6hy(v)2VU;)#(B9%wTn@p9*kNe3enof^d6o zvX?5xQIzhZ$nukvgeY6$y}Hsv@fO}dZsc%&4lSXIXH#a;Of&mpIv-m*OEzb{D0%i_$$LIli*e5DnhCy_^x$n-elr5I#~E(VrbK zkP*<4JMv5?2-pfXtu7RQFcvSjmY#4>@}Rsq>a6(Up{t`$d!oHqm+0pm2w z=zXsFah)S4!IPivEy~lW3xb^~GC}H0Uuj+-e!EGMLknvY z6LUjj{OTIRjKyVGGOTUbEC^d9;~?L80w#yY5J|XP9vgfRf;Gbu8z2VL#mx1?*0jXCVh*~J6MC&GVWuu=tUP+G46+IA&d_xx`*f!G;v+_v3qlhEwAKtG zV~evOgU%S48l5wy$bb*vKrrZ%NX8dQdAMgGk?{cs8xGF4s6PIlF7A$ekvc7Uc(rz{Rs7V1=a{wgeJvjUVkfy#mqO-Zn(DaP%5s^69TfZ@uJiTb$d znzV_s_~HEEfr7wZBv-oba+-f{R`75|oUPCb;ajjQ*j#gCQv*{oGh-7oe9y(i^c<6I ziSF@5Y@r05l-N3`9bN6vL$DIdL=ri2UoMgg1tK|L#DgKQxJW8XR7-{haslSa;j?){ zL3huk;E({Uj+9EdOfC@!`CJ~dhRb3zv4KPBGernl3dH1eT(MjL@$iH!rAA852*Ggp zOe_4yVOw#{EUi!b*mGUv=4#<-Z56Oi||~eD<3xy zlH4R2zA9*@C{R@vqAdxrEe^%kbL^_4o$KRVYoi_TiPql2(6Op0U|DQSTRM+#_amX= z$19?)Rz!_e29H*Sj}-+@lm+EP`(tET9CL7BV18yGg-;tAow2gUk0CHHxUd?blr2+n z6&eJ?M)#x&AqWV>m@lLfxkRdvN#rWEt;)&O!QIOvAjmHwIxH?RCL=pFG$P2y-`m^I z*UQ_@18L{w=I!U@?eFaq;N#)tZ0Des$^0jUI2# zknt_7Z5a4bN49`vYGy=XF+Oc=W6HuRLMjpnG2%SAnlF%8D>Nd7TJ7NMqP4eo_3#V| zi3p2~^wW839c0dK4lqeu2M3wLRwUJnK%tT;<&YUXM5>VES(O&K2f@H&twk~_OvH;8 znd0PX9}=z$4E6C3^00G6@`x}eqE)Enuon*B2694+oiPzGS>}L-YQZ||s$>_0`NVlB z!W{Tmt|WQL)4kPMzFPc{U{#c3Q>=Y+yhBs6XFI+OmE_f(;X9ZYFkDQ1cWJgIaj7#M zABVWvlZ{_dxPBq&=H=Y=o`Tihyv@G!8{N4Pe)bYEA9E#}!#1}tJ9YZlX@e7nW(F)i zQwaBvh{STa1ijNp_+o41lt?OunLyzJu>js8rZPb&QSkU;s@noFL?x9em6QhL0hWd8(@!G7^JR=ak-S?*OlpyuNz;^()Cl{ z=L(*v2%oHuoNrFJaXxjmJL_hD*6q=}yCVe;#w&KN;hR3qPv`4)t`$YNBZ2MZ3NbE@ zFs+iCatyXgp$|cXRShCSPKl%<7z_F@Q;HA{dXCk* zJ|e&rz>82tGA>3OK7*u#bK(fF4g`xwutDVo8_sWoio?Q$ab(B|$HdNf3LA12W<)lQ z4BO!fSm1BNG3N2DczlMCQW4YA@{GS$RuJlu<*iQe5f?=`l|{Iggg8{kAcH)*GJ-~m zgd_VxaA8eE1hXuLxr1z1rI06@RL5DEtS4lX#8~f{O7mopDnb$SZ!-A zju#89c?xP`#aiy1iP4EOr%s+a$>g8|LcT<(vQ>!?JCCyV66oN7;*HUA(m0% zfghs9!b5Mfpd^(l#8M?6CX6i8*lF#Z90P&^LL)*T31pnDgVxT`*2CK!)y>n{(ZvqF zN+k$~4cX`s;RHaz!dW05;39d%6Y{YSxLMj5F}dbUzJ(3X+LCQ$Vrd|=G06yZ%MWv| zj&{WF#vpfEQoS0JJTGPjb!SJ6m8VQrq%SsQEuYU=?aH__RJc1<@o=W*`CRSu>-AsU z?)Y|h;G6BiZ?^h=xZnQe+QpIc1gbIyHX{+Xo zF(XKYQd|pQu0i3 z<0k6kSGsbSE@k33eD02w?2MP)o33~~+YB-v-)#EbTI=UGTYr4e_lJ9vrD;LV&KM_w zU$DQ*){!d`Xq{wOpo%2|g-WPWBQDB3F(Y6KMuH%NUe>e_Dx6s;VGATou^g)-2!K)@UXD2(S%cw# zkFj}dBqNsDSS4bA_!!X)e3;3>JrGK|T)ve;%~PrcT)vG~qmU^?F>#T&ePqTom8;mk zF4EGZkja+Jspj~Nfz0*(jC+&#l1|l~Yvr5crPOyVC+nUpw0ynP@af8hq)6weq(FgO zh{;zh#p072pmR}cgbJnb-+uLf{`KGg<=}7MK6c{p=`+Vqo;mu#(f8gz^6tBb-a2^b zz;6$~`N6Sw4NcCNSs8JKRvZB?EiLscRRPsQ0gP7&nYntnd-`}pM2DwmrA0(XI=Q;p zJK4E~%TD+XawwB4UxZ>b)p2KBWS({?5e%jzThEJtc1q20RAEv;DgHKL0s)+_6Tl?yjzy zU9-BXSfhFCnJbTGILW}z#v6}*q*Xl%L57$bS{9#$)X;soIMX;^hyx+-SbU+!KM)3i zqOk{oNaeZT+#c<3AK5$~p17>}NU}LQ-^z}v-49sp;Nb}lB7_Q>f)c8#n}`mt3>+4Q z))SUvxB-hmCMSUpt;S1UvQ0!Lj|CyMuc+>u`oW6icD z^a18(HQNQT^&$1=8rS)#;Bw5{46e=dWBuIgOE|~tN%_yWlX|eX%f;09SMvQf)zgC0 zSonFU)CoP7?HDzX%0&7Lw!<65;}{Lq*+|Qfub#_V8_v77c{O4cpY}%ArgYaDuu21a(KN!a=WhA|9pSh`a&9Xj{1%t`mzMv zj7n9&wuTKxQ&1F@ELI@DwMG9K*Jm|a$E#!Q$*6tnk-dB2F*JJPES`^%Y17?_Dn)+* zhZt32WM7Y$X#YzNkMDf<=#0?o>>Mh{wsvlM7+DqkJuWm~AYmczCBb6uFGNBv_x@5Qxa)zwVsN_OD{Rvv zI;%Snmi%byj4y^a*xg~e{NC5CvCPt`34JvINpPSgs8CZE`V>T}w=dey>vVe`{WVC} z`)bYxpV#($8$Gw%;c!SUWyBUfkrb|Z^R`|=RUJ^i3a8VGwOG1LDL4(khukb1 z=_7rcTFIMXlbKysK^f=w0rt&vX{M*(9ifjdv_j8_p0@iVKM`A<*XL~TIM3;wp5NE* z_MOpZa9h~+&4ZueVx=Ty)*GxJuA=|+OXihY9ugvg#w3M{?3Kg_)i1NUUI1~~(ZdlH zNB+ouPD%aRld+A`4(-`f7y3f}hO~^slJK8+qCfaJznM8qoQ$lje?x;<|C=xztbf6K z{e{&=!hsAY!b~Z8M1u#%ZDu zUnTYz51P}sm9)r4_-g1-vDZg9i9CE=xE zWO?jXfj`8GGKS*&{Eo=&BpKQ_5W!_x^4oOH!anP2J8X{j-kYGcFCz@&jTgXc0u%9x z1;`ROXx+9fokmeI^nI5sOHj*4kmIf3h*R$Md?x;7Ncg@@Ffiak@eOr~yq>V7ga|D# zO*RX~GR!CmQKuh?-v`jggCqgjvm&kKhm+JtQ<*Cw--!DZ3WLxX79pdn6X?e@K%NqT z>aS?le%)E8uP2Ak+>Yi)xPSjGPC66e=@{0qOptB_3lsw^_yI!iq-Z>etFx%`RBDdZ z9S3NBtZDlK3M3A&>*{p|A4Z4HSpxl-i-x2QlEFeKTx_HtZOohR&kWqOmb^=b>HmuJta>2$jy1ss9aQ2=%da0=N zsfkYi=mW#x^8@JvzSG96oHn;)L5EcwkTp4^rRTX>y`QOP6h=oiReG-JshnfF{BkFL zW7|zB4vaDYcZg;`>jINoCCArY0qN3?L}qK#`aoLeo6o9eFeXL{v&9cP&#w6&4oePPK`RSe&Gr?q^Fb=`& zdzMfy*~88Ugrfrdw3!Z1;4OkyKV~tYi{hL^b`89?Rl1~@Kb&y82E@b-;S)`!q%Y$5 z5zr~WnzGx~#tRh6BNY0-nH2sGio(Up@vkZ2H;e0UxYqv-vW)dFkk`MEWq(X6|2N38 zKT7_6tN#{R_CKJ>{-A~Z15NfffeiM)pvm5mV*lWV{kNVu{2Q)D^k3m>48k@}@9494 zLYS19p~E{&?N3Y@^Pki$Q5{^R)XwEx%r|Ffg#hQ z`j5MahBrUa2@HW7pt)GsJ z5_8sP41XkJTeV~YhP#IhE8xF@4jtJ1=FoWAR( z=PTAD&@CjE@EMB#X5RW60LaSB^sjmAZ~o#Rq{2T&$$uRHe+~#)eJi7P;@^La6-#{+ z2O_pV^9a3j7Kt?JSl>gJ(XlWx6R~r=PcDp1OxmyvfcIf+_Epf@#M0=!&oKx(eEp4a zvZ(GhLe~h+2h7yB}zUx2EetYuXlZ}{(-iP6zsKmeh{v+Z4)A`x{;w=6-7T%$a zY+UcZ>i*ZUFp}!#fiC)bSK7RnffxHkVhD^1hzcOaMh*VV%moubN=cE{1OrN8@!1*# zrHjZ93i^i+Z4T(5A!w41z3B%AUQF~A3}TW^3~*Hx;^b%knRtVlmA$)G0^Kn}o@5tq zer@d)ZS7w_OHC+h&yp2o!VEt}4|EQy@7##E-K4_2cV?G{He%#|x`x+GUX$ z%KVjk&)Y0zsb}<=$|F~W*WYi;I|`dkc<6}9s@5mpc0{Ree`E;{sM#}2poSy`$FB!l zulhnX>XcZjzVmXDX4|Yw_Np>NbPCf>$9tn*htsqkfGMLh+D9`ijv#6?8_>_+w@NCV zv~v}e*wRb29}ckxqutr5Q70saV_M}1CB&08$pBc?@CQtFnpla9+Q4Ph-2%oASO?)G z-GCD{l`sYca8XH@GSUHC zNrpl6Z{cd?l8p7!fUGcE$i|h73^29gNh`o()Ll`19pv42H7x8^8Y54@0{w6;&=SqK zlyMBYRz0ckyPIT^4A2sN7og7$U8|Es8Q`Xs^eezkDai>~hOvvHKMOrYH7pD)Lp9E4 zM1rjqN`eYdrypk4Pk?sFWDNbTPBtv0zYE7Am(&DYrWv->#|dZ>PvX`;#WYS}Wci*< zJgg0r{#c-rWComuR-}`TXN(6bK`YWrCo?L3PbL{Q)Te-9kxx?4mkel9Ou7J~!zq$U z`!YTNw_#Xhld6GXD8_M&q~8ZHDYTPV^^bvhuq;|h*+4Ql7LlYbAQ?<7)i9C1C6Ejn zjbZq+ei;zn)R?2Vu4~diPf|nh2TWIto}pS5;cDwnx2_h=;zkbj~XI7(XTMxTc!BHS)htl9(tbnP`;N1?Ty3 z6K%PM_Pcf7Y6pQ%H%M86rtzwmOIC04P zW~!l{hn9C? z*V|j%TSrw<#aSOkK$oW(?;@z>Wv8>;xcO)|%^hJql6}KYeIFjBA%aO+@^U86_f{wzDEC&&*f#y&i>ZBK*QT2 z`mV2e3ecbJ{LU@>q#sTt)Y8=0=xBGaZ9lX?Ls9m9b^DSg5m(D=!#;gzOv}qYP_4RK zpy|1P2Pd&q%WK{KyA)L7S(C@F9ZmW*+lQ1PBYIuCrk))s`j1T>8}I~vwLcx2I(K*z z$C}c%F0m86Z6A0N<(fQzJG6*9^|6 z(DKMD)H|ppEwq7h9V{y+NTI9~tN==rgo<8yg+$9Nc#|>%{0G>D}qM z={4wa=*{V=>6t%p!$kV+-Q}L+{7%nWyo?sQ*|O*E#k*)IHBVx(Dc}Uu2g!0ffFur`b8j zIr066R-{$1!p@d;{Bw3!xK?e zkSfAsu#rxflw}$qo*siMb}RU;&&sKreoGdE-^b1?JxW)^;}6~tOI@{kxbv_LJ|>;U z=d7mDDhs&F-Q5}XrEp8&?0%G8kW~b{AOuj6zT#cnRhYa6mhhTj1ds)K8icK^Y9&Y- zIArQzieSGW2K7N^eWpLSS#+*`7$87KP0&7TnZ5&CgKICz=XXpEX>cU8V(^qT)w zsN!F^$+!-t)IO@7`C1v&I=2-ZR2 zdsPHVdncI6Q0m(&c#v_tKe}9!+N8(V_ z)ZMJ&>!&N{md%!A%6Fn~fyWCD2FAkHHhx_yJ8Za33z)5HyMaj*ei>h4X6Xi88>2oz1Sm~duY+o@V>ZHg zL2z_3taIOEKZC!*c!AG#DxZTjf^v7>t@E7I-9vgH(EC?*lAn_|Lbrc7SqJ4r;)mMn zG+8HHKU-Hkhv9_dha~i6S!cb+y+`)=unKkNtGKRxj_8Wp3b6`K*#%>ZSLe&od9;qj z2^-!{S$KSUT7BZ?tfaUb{+wG^9vrmJWpJ3YciA8u6364|VS6t()!$}~R5rrFqEdcm) zxXp#1IHE8Naov*fNNr+Em4t;4_63e_3RPx^ds3rLX)4H9em3XM2sG0-QJ8@?vmMs1 zv|rwNq`E`cIm|ffy3pBqF5;KFce53znwmG~wp271Uf|Gy+`i%z;0E2CP}O>jMPrNJAiiaD z>lViwfw>^T9$nZ!Wr+WN78pm_@1Do+#^cH22C4a))c)W-((&C=bctW8Ar{~C;$uI3 z9gEzP>Y#N^&@s7;#9GC+Te3$$hw~=M&-0aKq-~tX5MTExz3YSxNBA5uwqj*-GgR{v zYv!Qo>~@?u`*l)YE#j7jUP*jf`NOaoq>d7!ZQl$Wwft${A~xsVV|R`m^s>CjT>AN& z%bGW3z}5}1d5G1HDzJe+$n#avGsFVJO^2){#GezuvLKEarE0swu(g=3wuJP?YrZc! zjZzw&Sk>#RvEg<{)1>|qV)gCK$^wo{0x?>3zyb?EmxmESC%ULVIa@nwk0+MF5oMp2 zMnpN#vW;&3OuL-Ajekk}fXh{0<}KMyq}nfIpP?njlO~+1MWP#BIpDJWE${7fMU2HZ z_hJ#6)l5kA(BdaVI*b4&Jl|Q#*Eg1!w;rk|?=_?Djx!SFahH5tZTgcFs=$l4MqVu; zkHhvK@;o8Do7CN2)+xR?xsL^pPzFkrsQ%m{?v+p6eB}=@`)uvh9j8azvI>I1o+2~T zpJFLx(!OXYXep^hWpY>_CrvcvitUE=j}8t?$ki4Wz!S1xM{E=O$D)>lZ5lj@ie1-V z!@ynNFuOJ5BDklk?w~Mnz5MH#h&QAXj!@wXs3=)I#q(q=SZ4gCwwsM3~#_8we0doh%|KucLWH}^X-COyNtEX;QUTqupOBty#jK66;D@SmoEoc1In`0F3 z5+ID>dtw}fr>-KeK2+D*JP{K@BcdIXgBrw#)~Bq8ap&;w1l{$N56iW z$Aj*mwISNaU&K&`fmYsmqgWkr4IawNXQDQ-JLS9QglG+pdUq9Q%~$nHDUFcwC!r}K z2Z`Enc#`}wJj6G6R!KvZuK?w6(lXjS*q7QqEmYLgs>GZ}NjmbG{IYmj3h`@*G(-m0 zteTG%CrC}A###-+xY;a;62$hOEa=FRYOAaAkh&pbg}YVNLko~t633BAYBp&3xRRb$ z?5aq_4Y&17sGLXoVPM4fRCL~?qJ{1uvak?WPMC|oK;Lu>_KaP5)8dScz@l4?mW)2j zfOxxnDWifo=34ZoFYGzYDP;Zvj&CuF=Zs)ijoONx>Vyb0S4>u|0?*1LkQf;fMS!I#q=Qxnx=8Hy4VIumAb+`?o{Wr}>hbkx~ap$nfqB5EI8+8*JEE{@OA&ySb5pM+vk6DSPe+DwQ_Gqq z+@sn-^RrGY)4H%S^E&hdcwu2`e=oa!P-kdrUm?L6PkY1kK{})_SGn`*8~eJqSzc} zC7^Fc(SEvjmDt*4R4bXn7n99#jC z`UL66NmLW&xNisUIfn8sgSlq0W}1QUaPGUeRW9rt$BMUOQlFMYt2|$>CA9 zlZizaOS~?qVnXX95(mPxA|(7|ao0kxA2#LuBp5G9eS5&P3pkjN`7U~pz*+BCaz1)Z z6?Qum;$HMlK#x{OwE{fN2(-l*B&-!8l<3-sod%jNg)fbiv&|z8$(^#Am0qeIO=L=p zLwC_muue3zedwh)5Mi!Ih2FVGu_wNIfeaL9goEM^~vk6FBNoBO8b z=t`-{Q*@*C^y{R+g0eNH{N&eI-7lm(oedq>c7hmDi;x;}GDI9Ab=r`IfK^nP^SiGH zo{$nD{t9IA;(Jt0c~ayfjD>PT^2%nX>T}-L@Ga!-j|GRWs!}4Y-+UI9cxXGjNr|NK z?XP0Ew^dR~HfJ#pjv6=Xis;gZw(Kp5l^dSTwAU6$+%Mg}rGoLD4whRt75uurKCbfOXToudK;tCsbFFarZQs`wl3dewfO0*fT9U z%HhttQa|{jih0Bp+gv&LafWFhLpYW1UD{xiO__f9g;lJx-Hu?oyYzcA55mDV@{M2u z(cuJ{QO>Nu>4#XJBAA;OJI&`#=xSXCccq{yL}E@un6Sfw_ZLGaO9Wz_72JZzO8BG1{)tX}^iPUZag!Z9M+mQlhWX%;9|wzttLS;*q)Q8B`^;CRAGjbC_sBL(YXERG_PD9jvHsXHMIZr5Lh51){wPJM#49M>H(lY`nbC-`|Ve z{;;u}XdvUelk;Ke+QIb3j62~yl z^XCRhi}7)*NEhXY>svN-J{QlY+7+4D?#C>V`;i)tFU6T&PsOW6lV%$GvMm!uOgfV$ zMKC2$5Hg*q8kL^Y#@s=pg7PFfbRiqpigRWQ-A*ZePOlPMN;K@2`_~JND8E+1ZV@}} z5Xf2Kt{QaIO~-XA`4QcL^{p?r$5gFn!2KQ3nks9; z>gxV&;2x$ktKC4{eEk-<#4M)s&+0;{cUd1VqFk~F`kU(9)| zv7tKp1V4&5pv_WemDP>dy-0!81)Xny&uqK{@aKBm5+kI(f(TxCq#br;{gV?WTtXElAbYTT>$zP&~8y2*W z2=30-+)~)bC&S(kna(Z~D+u!=F)GDeN>;~GJAeVXxPE0ZV?91BA_z92O$|elkbxo9 zjP(wnvIeFdrXhjWXg`3r*XAmYLjoKBp2@DN*=<rd`-0fR0g9iioOp;h;8@6QJ2O3|^hO(wst@7qZWOb`jC zY>qFR7|Nu|2~b3TB1wlcI_cL@N~qcNQBSO8erH*(WIH|`*2r8_WA$opW4p|sD2-r+Phqbwk~__A+S-e(hNshP zbktuMC%$W-M<8UemcCCav}Z3r0_0)D5@zsz-ZP#&J1{~7;NfQ`@vwT0p93G6uU^VK3ZlJYhn^$Rp9g#C|-Bpj5Q-` z@Tk6C^S4bqeI4UZ4j0x!kPq)w#s)OBbwryY)I^vvx#i%rN#@}Am(6_HtoRi+IN}qc zltLjBPh4Bg^udTvOg;wEp$KA1@>V6>%BAg;`IE zJDy!iN~`_)sUql$A9qjS5tVNSd_&8SSjjAM`JfMgje352I=>hxdv<<|ur*a}Z(jln zsHgp?B&6vVa)bKgNtdJrVQ|e+S&G4NG_k>%t-rpcJ{^QdwO_zW99_+K)n3|pqe>;F zWo%4N?n09=Xo`=w z9^BJ`ff5ay_PHYPGBpdRO!rTwbs?Xaapk;plE|yvQYM!mhfqrVK2k>NGScU~)FPcG zD?V!hTweoz1cXE?Q=af8vKy%txx9zp(U*(VP0mE)DR91zZIO~R5z)x7gF9!xrBPFw zW=mV>iVsdEgl?Q)P~{E}UP_rF>%N@O z++?$|-WPM0H`T@A7f)}dZxo?pDBx{;HB?Z$s&o{MPTJS4bT?|Q5*RhU%SH5^W7 zEILR=p-~|5^DlDzdU_Pz^eV{n6tFOebH-b>W+_HDCP=3%N_W?HJhXbeyBmaxXNq_g zs*>&MEYR_)UpQM*BstS@7S!c-O4wvpnHpz*F_6K$(H01hX;G-3R?S7YQwC1!3CIue zDw)OJhfC7v#^+T`^DTXqg0&wZO&LnSPfO+X7_CU2`Z-5_mNT}+fq&p^*!$?qTj9SjRj{+?p|^Q0z|{&Tx}C1qgu zeX(Cc|NSK47|eCbiU^Yx4kdP7jih$bAmxMQw1GkwC<5eR{np2EnfbU~j;q`vLal0s z4(YY~K@2(w7teg1iKgWCEg(45o=7;pP&j!EPz6;g^)Bcd;lSc^{fW&jGnqP~!ZRE; z+~ukumuGIpxA)*0ZqS$JE&8Q)mGaE)%i})+UsTHk{2ztOkkM}Wqj?u8tAlEbKrTD# zIfT4z)cL15^7ZV$*BiSC4h(I59J>M}@iWzCo9a!+SzQHI;A)<`Drg+6f9}{$@tcIm z?JjmPkYDs!C0K1=^7CU+6gJ3t47y*eDV%>&4VQD~;5#|mvZxLW=n)f#f}}J_tfG+- z_4b6jt9g&IYX}4z*JU&|QW<-Rdv@u!h^3>zubJs~-_cZ4~>)&i)js zr%l8xpX~h=GXk682zS582BD4Nq~5<`h!tA-7-#j1zjOw7{PX^v5+ZKgz4 zhrmJ@qkb_s@hu)*Qmz%liF!wGt23)=31c`RJkYe&&J2uSX7wp{u4O~Nj#0u>^?GBo zFL$Tb(4e_s?rCi@QSteNkF9?&>6v}XDoBbauqx=Kak9!cgdWNVS4faza;Npq4Be3@ zp&!b{8Xl-m0KK#kSMXyQ%Etmr=oq^R8priqa2mN$rxDw4%9#~gN-phLnay58HR642ZKq z+?r)uqy*wc7>WsS?WOlZk8+BG~!5-5rbTi>=OWnT!O2}Vc zzm|3@2pv#pPMq@}3`J&Rs^qZI7k8t0$E31N`!x0Fx5(2BiE+pTX~>`me`7nMzIJtn zv(8a2Q@%G$mO(3PxNUG~{FtK+Y@3!xIkr1g*yCXxbDEU>$tU_`orrYmzDf8qVT0F| zv3m-D#}sRbxu>&2xQ(%G)c10pvl1Sdy8Ko=iLxd+uq-5e9!t5yOWT>NQBN1)7B*?ytutJIm6X6iD7x4wK(oh{1< zO_Hj*0j{fw`*u+_!q-Ch64yLdmxCDPheY{i{NP{{`DQZa?D*?~$c2@17?9a^5EYBh zy6Tb6Ueg4J`Gjf6u9Ft$s~>QX$GvTP>f1L|pHvUMCR?yvO-)$+)HK8_Z?U{` zBTy2CRb8W^N}MYiH&)adYlWZmP2K|6;su;Hu1Y{F^|CHbqS`Mxn)E9S)^D1Bu9ap}Zj}H~v>xH$fxeOO^2;u-X5o20Yr@?7&9zwBSj1I`#Oz8ZjorcI z1;4AiWVWNtV4>ULc35_zSRs?9R7*G6mWb8J+sC~XQ~m^sor|=lDt{R-DUh`Pxa~N9aXvWB%ym&r_Y(3rzssc3&`?%I zPzN9_wz7iY#4>a-Ed2^KyS;AkpSr3)bVI+jL|n{_{}xvL78Cs~F#5k%BMJ!#3M$Fa z2-=(JzlVGI4>2O!pQ=d)QR{al;=3N|U4yCrRl&we-}>Jv|Bxjz|3j9@#Lo2(T_Q6F zC+u%Y)W1J0?;^(E+utegIo^-i*x3KI{dwhg%HMhZ+W*TE2lIP;-S>^1o#Q>$?(YU* zB>L0N?<3B?tpC2s_+RqXay_o+h`FsD{{@>~E`*&Nv z{rO$;UnSm;|Gd}l)ZZUw_P_7+KkWabR{ulZKmGXQn}dbpJxK7sb+msxy#MSG|JIuR zQ)>4g9pMk9-Mh5z4}I!iQag5bws)x=GaJ!6hMw_VYsbj`|Esm*VtiNW{ab7I9y<6R zT00IV_P@1uT<_Wb7TdkI-oLbVY)tRn=eHbBf{6276ZbFG9qW5ASj`9lM-GqbTsUb!3 zFqxsBFWzUOm|f_s=u|zwkN_#J9kDNQRX>Py_ABmuf0Vk?38ONCYt>{NbphQ^4i5(_ zipnZcbaWGxiCNabRtg=}0#} z&I>`)Wz-rglQ~%5pyZ@F_twxP8jfm*+Ih{yht6q7b0MiiV?T^OyVdR*j}EBk`S0cu zImix0?~du#m|N{GzcEejVKSsoX%brga(jtUxA>MR=jyp#qAs;-v^Z9_w>Okfp%u^7 z@k}*YKcq5zWF;qW^Lnx;@PzV11?G~jm;?!pPhW(b+>o32(!rix-gfi|Zvyopy{AnkP%7n1jkYQlIFya6G$w@Ie%ou*5rS&AS;WYih6FGjl5VS zXW;UU5{r_+vV#GN1SCVd5Lt?khd8$yegZ-U_(M~oIEuu!mb=7fpRLYnicB`x+7)xA z$8%X(q83^uvKvlb#IhXhhcb@@8mA!8j-l%^GRI<~n<$u+)j@FBu&7J5h`Ev>K^_SH zN+d2g#xN+13mC{%zyy#m!f0cVr)*meMJ8GcPz?k~O6M5T!xXlVEt)&UMKD&g@(8Ap z4Y%vBB;(Ak7(2vS6WTF7@hp6|MHV#h{&m4|%TeQtzvB?mh@;N~6b$HwrbKo;rizfW zdyapJpI@NN1JB0MrvN(t_5u0&7_Eo;DM>gi*sUGMw>lD~^q2u_Ru_3`LA>9yhhNgA z+C>veHQ^L3Sp;B1fp;|G>feT4SXk%ZHudN!@L36u1|fKnVJCVcPkPWI90A@%qJ6dm zVgvD3U6TyO%dzLfEy24-*V}x&4}7SCNde- zmI?TY!cPQ-%o+fIhIG!uh&<2>(%d5LCgnXwI(FLZ4I?z@_;|2%%og3=tAlZXE8+0v zjpAE58>O^ZlDwR4_zxvf4L|qqFBNoa*s_tfRsKm`jGL$d<^dtlEGVa+$E*EmE1j(8 zLsE|^Z9s#%%i^Df^WHILGQ;(!3gn-Cb;M=d5=EnZCCV6P6RFy{Nw14mvBITO8T5=6lZ5P;{)^I z$(f}5(vabbU8o10T~Cjy`XXqSTc3n|RdnlvJUS@TA@s#dPOD^b&5A-gTr(*QtERLD zGE=GGhV608@P^|;VMYu(C5yKKv;5s$I-ALaJ+mRnMbtt%ro|XJO%lhd*)Drc2GCjJ zVidpyK?CoF@m@lsOfpbgm9P(}PKr zt@)wGb0gaBp>0KQY`AOY&V8fhZKLJU-+hKc=pzs&fYlGN{E9JxN5oQG%kN7#Bbq)K z5C%Mbe-y^?dvt!V+eep>hvhb$s`x=Y&I=CBuMI;y`|OWN3cHjA-3$THOMgp#?+k?G2FPGk~JHB8|Va=gt~uXq$YX2?M% zpE`>>;fqZM>-1S{;FOI;!MVNJ{eEEJA|khT9VyD=nkiJU zpr13OqYpb~qWuVsDt(gls|U%4z%qTdvCBbVs&Lm>GWkziCgWbkm!!>o=)-Lr6w2VD+HVi^klqqTs$b`tX$4^s*$))XG z2N`Qjxftd)6f4(&CAG-g`f38v)GLb1w~TZS=IAV|Zk%1#+2PPxR&k46Vpd8Ub(Xb6 z%VhkJwA}6-W@X7L9WC;y z9L-Ki>}3?(7LLM8^MD*^*)qdrN&Qec(zP5IP2jX(3)0$c?>YCb5-UF@+Z7R>M8!|8 zBe88f707vm)`3XdRAxtC`c~>OhctxXJel66O#K6YBATq){AO^bN3~YZakm3i0KHse zB#{J-(sjZ6IU69Okjj>FHCsQBQ(QlAIKXn~a&$vwMX=&ilkI6_>>wlFOrz)u0vmK0 z7~@z$%uu>%B=*&&+dBwif)f3!^R4g1{F+!jUh|JK%cvNx`Y9}lt~%_bz&`Ao^!5CE zis~8=Ff9O@qtV*RJ*P;9p(J!*TMa{*#!Bi@dZ=<+v1v22AS%sMLEf1&48J-FszW_B z$Yg;ds({dEw;%UAoW9Souc#ZGa*A-1zufrb7F3BG@!Nc((zo53eR$;%V-oiPTJ@pF zq$dHjpK}L<4f!507Q(EoASJCS6m6-f-Y{+omxz;j0}-y6?4hj~$me2zBaE*F`XOro z?jKdHNK|6P#i9U)pPIsTVsvD11`00`3>ljOv_umo5*$4rZ?ClvDEli1_z&(52n;Ad zn4x}UUvms*P7%hwHf~zm3p~WF8Z8U6^5#R(5mM0fgDJ#HPpKAoe@3z&nA!K2?u!+! z3njO`bK90W%stANZybtnWPX9w_%4B#EJr|;UgsBK)Fk=U-QyAYVjbJUZte*{vk;o= z-zveMgSlh)7;+8Rr0m@$2`fy0_H_mnXM`7<52yO6ABt1U*Ty|3w8}<|Q@!A8mZ-%k zh$>b9I>eU^iHe17GJ)~B0czhG`z$G;Bwp3Xo-JDn+$Xm#Uf_M9vC_DYs+y=@f^HdBl8JG)}mPA zX!giTT;=afhF(hGDelByA`Hq5(lu`tKTgvW3lzM@)61-hP=FDz=iIr(iZj-DZZke0 z|0>)*Q_40r??%aPs*BJG-Ew!w^-Md>lbdqp-GP4Ld4X!k^PXgZ=b@XbCQ2e41r=R@ zwN2_UK&tNMjHs$yxQkI8j73%Wu0+lzAvIAtGRcnjEw5t$= zY7mev`UZD=@g4zsLUHyishJu+OHH{^A<+ZOL!HL%NkR`MD=Xqnd*#(s5IZg5@D7b4riafSBYH){=(xq-!rBVLc3E)-9 z_*9ud7~K)qp@b9vGj7a@Vg+?YrWtX?+rJdMA~P?u%w}>ed#%)aY#e^yBl(z^;REQ{ z%f7r>t`g}9@y}Z31tgSRB%i%x!wS|65QPQV=I`3#OX}@4DjZ=5xoG_Wu81bDa4VC- z1x7R8@_fYsA$CRw;dD(e$R_Fsqe(i#&iPj#p2`{Z{hQ-}Q6Y^kQe{7JcV!$R%DkWb z;**6+3<6@WB&kJ-&b5Vk03W}PI~$UG1MvhT9nlF>NBsmU!ode&8!DX_;E(Y8KvoUO zj@U^(f65O(Af+;+ar;SbO(qd|SA|aV9d>92PV&MkD0O2feKB6*N*zU;X&)gqiENDM z7iE*4S;bTVSH*da1L1W5_s=eVo)Rjt-8CW4%I=750>fUU;fZq8iu)dVole^nHN0)n zWr~M8F8lE!Ia2^>e%QkX9qM?8XzLHkX5cbQaL1?M_*lf@;vq;;4lwIEi0GRjcz3?C z{XSU`oflGqy|14aou<^FniQ(#zu=piuS&`gvrNjxJWZ*_luo)tzh*(DP953(O5b*{ zhgtw|!@(sejb2$2}KiqfrZfT@sJ$S*Rd+!*(Zq<+wIPJq3Xp^M$z ztR{~Va|@S#GHyCgX*K_+5g?y=Oayoa5BEXaZqnH=#brF+h21L)|H)aLH9u&ZR;Qy~ ziOY$Bn>}sZ;T;!K(@s8Hy3la5Rc^@0b_^QjA-4;%rRgu3d8y|Tj0sFkCByg3S~g{F z`ebHd5n-ci%~jY^X!9v#inBWnkf4k0*Yc=EA(rVXgd;L4P`L=TVnNO=1GArw7N0eG z-lkGsds~#njSXCuj@z(9-@~%-O1l@xZi_f%l&0HT8-F_cdQD>MH_)F3?XE!Bn-q9% zx{6%mG>T07!3zOhm3+@Y6q77GH|1xEup3vPM69HAxLCt$><7@C72UTwLlv74`hwTk zb4f(Xz0Y(2lbfB`5RsaqVEI+_}?%f`^f9O!Joc-#RcF{pp|CGOSlBXtz9z-l?0!Nq%i*11FUkUQ zd3(G(dx@K2_o>J9%vmUMPKZLElpC=P>IP+Af*fUY}E_7FhQLC4F+XY7TLTPUZos3+T3=s`l^#o$X2-pJdJq|4w34ycn%(DkNg zLfokOES~9>f{h?^HoJS!xo$s9Y=CStrm2xvxKT*!URq{xL(AJfmt179exvr_q*Y=E zrihc!3yYd0KSX;(hK9NR4`cTjWjnLzjlO&JYTLGL+qP}n#%kMkueNR5wr$(px1W9P zKKtHr&O65YAv2j($xK!1L&lh?{Qfi{54i!xr1#@!wjQ!R@n~V-b8yYOaANXlInqQf z0~Gdfr1p==-(2EZBz3X*t-#L*-%=D4Zpt;-YBr57etbeY5baCI#ulSlIhD^qqRh#;F&xRtc1=u z%E=F)!bY^cDwm2$>P-%{C$@spOM74(!xU}h7GX4&M4Hy2#F#^T+AkdlN)?3S3y!g1 zx6dR?iZeW9T+55w#hsO%7kdC3!iLiFy>4=QjXAiZsgmYoufO_V&_P+MC|l^#bHf%6 zfF3w4-jDLL8AUOgw(7qJmDrG9NAHi#lLb^t81lsn2h0z+>8w-8=3G|lX`27|HWA+&7xArm1CZqBPO3J5Fu!I863Tznz97h`RG*iKc;eq>Xv7%n1=( z_xW40;H1)j*;z1fOMJWNZF6gyb~qLt=i4f57eSpElNMPp9+-y>FyWRHkf<;t`d@*7 zN!{g__$v8a@X%9Ar^a3V0UDzitK)#h=J;Je?0iaV95NS#K2_x0&aYmyXj#M;UD4uL zp;BE-=&cnQY|6xo8$HWU<>fVCSc}(m!=T`whb7x!ibVw}JsOB~?KDBu_?Adfq;KLjk1~SX|jbarJ+8d@9^4G1YVtt}1wRvT` zhFqj`b=X2H#Yp=C7di9DV#4A<7Mx~|mSNhSdP?KK7N*Kj^?WI{y4H4Sv$$K-yZ;&f zo$vY2EA3NH>z`^Si`LmYKoN<^vN-hSAZ$wTCeX>?Ij~e~k*32ApNQ0j-*}bg?*6jW zxE$6RFpYz7U-@9r&@_weB^3+%_6F?EvmO2|l8=c=Fb*ZRX4yKb%_p0p%MDc+tv8#) zTbi!NUB%1QzHg`J%eor_TZIgrH?3*br4OQ|N@s`1*PefPGTWX9#7};q_|)%^L}TX| z08Z;&N*Se~#4(lUd&PD9+3dYy?Rk-k5-O$}Es=xFw@nwimiCegbQlSC(C(pR&^3pt z&Bd?HWvR_gHJ+SK`O!}X-hNYsn>40SdNb1V<-woGrtY!2&QY|5y&xbi7^Xty>Q!$+ zk}B_emWeqxHpg>Nyne(M3E3><&7V?&01~dIJ;7hgv_D@HmmgS$RKtFoTx2l5u9V@W zoYFU8y?M}5p<|aV9y^?#RbZbgUc!X@3>&&Kaw}GId_VufIOf5@&01zGG&4JZ+yJ&! zw~0PUHENRus9{`9zq{UVnSaGydAD)b8wwG1gD>_Vgqs6*N@*5ZT= zYWcYRJ%i7+%0_yA`0B)2?5hT1{Yy^vkbNL;46iho6eg1tdeM3{*23j-w~ZTNLHEd< zUYd!fQJQ7@kbBg*$uZ#Z@t^4cQlR93BUCLauzAI%^!zg)NaGZo9$P+(0SjOUsWSdz zPevrW5CK3zGCX+`us|Zq=(XN#vy78mI;q#E8+bT+Y?m>rqNFWX`gN?OQ=cYy&nSry zGnCXs;swlH2>%ida&S#Wt zy&RB8J7{!)OtVGE(9pFN8!vhlFS-24hXCGLU3zfC5mc;rfWGR!E_C-pE?jm7$Dhkz zd-!eBkDIvL+}u~CDLA$h?q9F)%8qSSzK8deD4%zEl~w(?K963XjG);o43>Cs_FQCY zD0svfNV8#`lBiAdbcuop&?rY{Di)2uxcnt_qSr0P@`#Fl`7#MnhXOO- zKPl7;USlihBCA1@nV+fXn*Xvg`$T;`HS0`fUgPCp=x3*{6H718u~a+B#8fv{!(KCL zA%FO@%N-+#yKAS~GaG68cO%vKs8vfo?-&_4&WtEUmCcQoR2}^dz2JK4;AHg3a;S^P zFa`ZD`)Kxxgt8p*1s}x|M&#ZBWaf_o8ZMpX1ZG#LaIfTP>XG8X>Sjy})=)Kdz6<;4 zQuEOy6s$*#Aie2@%s#ny4pv625*Gu1GO8Mx4>UXvQZ}qJFq$YcjXFMxN|`Q-b*9(l zv~anbNL8OXm&T%dD;!Z2Q<5<$g4I&jd3;95`bF)0v6~UY`z=1b6V@!Nb~~`YtaBg9 zwb*3W^8(o4{pu#;c&2-8b=idjQTh0Ax_#+&dO<2Z&E&-8uwA8{>PI=JOJP}aZ<^rx zJfRO+k3PABN`^3epXL=48mn-Qbz-by+#HKp-y8>;Xj-VN`7D09v|K4VQhV_|>avo# z8hv3^DyDiu1|i&MSUJC$Vj}%Kt%_o-d)_hl>gUvb`DaGvhLschdR=P<4p^xfBUWAX zt-3NO$;~8Xsk=s@5s?B^n;nHbl@N2W<1-d1P%U|VlvK&a_q18 z>tTn6fLeCrj9^J%vEjn`v&zZB8+O7_$6 z9Iz{&_t%w0@SONXYWs9!yjW^9LtvdFo+R4>7AJk~xCqA5fDXNw5AhjC8)qZyJ}EJ8ejWGNm} z(ON4;bUDH=)&OdA#sT0& zdxQ|T*?_UKIFi{|&2xp7DRMX?IUv32!D7Z=O>yH8Ot({PlsIx^m3dR@c&1%vv(WlU zB74Pb<`E2_Tu)A0_4UADg?l9FQRu#D-zw7>v2ygLIp#`z&%45C*+af3u7|&H`=4SS zkBlCUy|>A|9iAMMFVY6yxgTD$DQC_DPToWwGze_3^W>)Jr|cS_Tjl6*tc?%T8ovFc zG*)O6L=4lII7t)Uei1B&S6H|UtrlG<5Rv~{-G{j&6Gv$Mt+hc^BS#vx7ny5^8TVTk zhyXfxX&4?T_RAS}#Fx;`9p*iKg+fe7s(=3u6#DoNyAn|)b>Li4@F8WHhu>KmuBq>f ze-b-PSAo+P9f)BEY^Z0}*3`nwud09_TYn2>nB`9p(*( z>Qx-X9uyitAC;PU=mJlU5+;WkI|gvE2Xy?5gl?bkX~j`snk2nkA;c9L3aZbQwvI3c z>QweSm(-}MY(lR_KS09B2c6PJZk;c6bH{`~pkv4(;qWm;zEI!-GsVnQ8H!J zvcl3jqY{Ks5h`hkzbULjr7zeP%@*67adY@86;qvwX+#1$=c-`LbiKukCME<+a41EIk6 zI17Q<0nmTN0Sxl))PM1g-cOHgbSs*Fzj9)hFpMJ=FsT>cSFr$Ki!JTobrKgw)&9sN?0opwa%bQau) zR+l58U%3do<1*)TccQaN4|oAiks0_&(6w>C1_;blivW zEA^{_PxiHM`%v3f<;8Yu%1KSucT-ZSK@{SbA_|uA^xy+KIf+3fszQE^{>BS>INQ zJV`d%37Pp$Ghg1FVZCFq$8%~p8Z%)!ZkO;)yr(Tnt!~y-NuW(PrftL} z$TQnE>lNdi`RRON@s-k$Ra|qDRkL}`R+05^8?JraBjz)^B2x!g;CrKm5e*h?|I-X> zY~D|i1{*D8ADnRM>)f9eRGsdq_Gy|tbT)Obx5!fh(K97}emgZ`KQ6E!(a&lwL5;DTl18|My;JVp*aE#A&H2#UcDUm&&$-_CT9gh`lW*j zs9Nry&E0FapVAV=pRdIUG0HG`xJ%7ij`L|Dy0V|spAWKU%FWsw4u>b{l=)lgVZEG3 z)SsIhz_zvxn-1?o<0Q#R)D&1vrjuv=l&YUVQUvG#UrmTg^04jA%KGf7c65z&2AG^H zV@;t_#xJ>Dxe;yXaMt|~NJcIf7G7+}f<>C=CA%*Ue ztb;qBbA~VZVf$WJ4W)JM3}W#=fk6F8t9P{69SiFYD@$_?yuV&?ze1j$=tmmdPbxFK zO49b8JX*JIg8859`i||5lTya}p$rcqpe-+CDTXgSAoXENlGmI?T(LUzf)Ry}G^<1b zJjqt%GI19L`~fY)-OL6UNBh&c$GNE8Dtj?vQpwy(XeI;OGXjx9<|C0v89sPMK4Mqz zq#S7Hlkh`IPLPkJq}4O`U*hURK=eBx^i{}SUy7Eq@)G z-Yf@2%4=7^FUG_`P(?!_T2f=t+^X@i{p8t2)3V90(}>r-qMmeqH+j-ri+O}hS}C)3 zU1?gMV=->GPPq(}t^?<0hx(=as1Aas;b>hbeL`f-QKj3sDuB?yIDPTB3Ei26v*RKG z3D#3iPL~u+{F0EN`1gI}#4y&7v-9g7 zKif2)lMiUSR6Vj8QPmLPE#R%TePO&KWM+fkhfj^F5y&0GWI9lgy|F5>XG6S-K?qM_ z)Q;=Jke560E2wjbGgU?T5`C7m)ue!D(f#e$Nl&4v!Nr7JFagS#_ht{iHwPeDZZ58P z2_8)*g7b4&R9IQ|z;TXaDhCL)IqXA3Qf8}Y!r|sPV*zWv?LlBz%KV|&$Q_!hG`PZV zLD*;rG=;!)d$!Od&ynp5SK_wjTy1B^mg0bDj-AE=yJ7(*`$UFlks5Z zRq<_QQJby138KZ5Nlv)CDIa5V!_#=+d)ye)Z8_GXRcS;P8N2n0KOd>5WnZW)RL|7i zg(?}&x4@RN*{e>waBt%JV$@i=3{|gnVW>LLp*8T6?WTGe%~0OY0e2DWE7AD4NIX)puIRClJ17uOshq z-`7>uox1P5<2)ELA)oMt13G&+xk~HIhY0UPhY*MP7rtY>B`D0}B3)V-Two zT_1!dW$Db0^{s>MqJM* zrI?@Xf(Zc1hHieqFXS+~+{l8#hlC?5?=Rx;!8XytzK-9@oshMtt*HV?e5`G0YlfY)oU!KQHJqFRou$Jn@X+ zB;wTYM{C0b5we1IP)D4>Ldi7t8t zJdCH5atvF^*$mcXGzzF(Lcb6QKekLXJKSWAxjn)8j6IkGSfDU@Dj?533}%jfrY~X5 zPv(0=oR>3JdFCtVW5{F9*}O*@?W|B|!8AM`=}7~QiVjSy!RkO$0cv~9R*oXiDVLO` z22a;JmW%y|{}P~v7}-tUp;~ zu|)@NT+2}^ZqBfjb}Sx|SUBWvB8ajLJXjMqTXdu45;_b#1)GGsQ4Dm)cP#haGXr+~ z(i1vkM0(Mer#?meM?J5xtx>&r)jS6x#I1etJnLXrEAaq)wYfB=Z{hD)Z>+d1$FA|O z(93)i?H-T`&JFOBO&Yxx)qs}IT0OkDT?#?{f)5EQ2#Gc(Lw$z{t|;X`jqU}8)a(s~6nusihM0i`HlBvPDSg;T|0(qXsU!@03yL^6<7 z+BK$H$nF(wnq+#O$*T5{+Qg(0eQ4vZayD^rWJU3H3VU|vjzQ)jM-cBZGHo68FB#t7 zVp+LYG20>2FRS}SOVW-um#?AHO;Bk*_ix3gPS?>cD;q>?W~fD9>2Eqw7iXEW*Inid zAEf-kJI{R@c=8^l%H#H!rJK5YxI4=q;~%gZICVVM%)cvwmI}JZY2RPHkU;tlS3$~+ zYD1)$B8WF=&~10qo7zh`FXxPGrGiQCwDBap5I|wbk4uxmg2tQS&z9NJUzjOp0$YG% zbjjR4xT4t#)a(>~+5hgpK8lSwXOAIa&;_FfPW-^LvNQ^r_Vsg#*K)x4YH>@Fyy-~O zCi$Zt1O>2#d#B`zhzd}a`&@cebr5W`!m?2&Xu5C?wG*pU!W}AT3fouAA>RORgWZ#W zY#ePStPaejSVR1$;*a*|7{z!vheorlE3l24qqw1+*lFTy2&^RRMw^%XYpP@xY{n4N zbl2kra!+Vn>FAtlPbk(3cZ$WNN&OxT%kV}@s^tcUVH0or3YIIXDCK0p25QD|OY!;n zQ|a^JD!FX9T!K(PQ1VMU<}GR=poFRsy5dU7fEx`cEg7GC(6n3!PM!p)`r4| zeDCds?O##|ES(sd@_2PZUb(UuZ8UTVNK^64?Jd84;rT#+J^Pr%jSguM;+wa(73a+i|=1I zWg1f3Fz`+(Qc_zw>+i_EM=Vb0(kjeQARuztAX<1SxAD%64I;FWUwY82`?tUqem(uQ zWLF$9E84Z=S{xRO&YE5?$0t zLpul54QC1OWSx&)$@P%koE=Z8;-@|=uIoV4(Mdrq7=+`VrZiR@Y9LYz%+kUS-lsOp{viHGM0W%^6w^z~ZCFefp zIM<)-j`K*wZ60%>f0f#!DlLpFW57l9YlI!5nl?8buY@b#9XQ!!MFPl|30|SJ1n~n; z|AXDF5lD}Z#3*WzAv=MrhL+!>WRGBw-JGBw#^D*OOIAO_UxhFrizQ1MxBirh-+~!{ z7QC6vhKSE1eWwVcf=mfbP8HG?SUcI!@%>TT(noQ<%VV}+y{rep$5VA45%zJyD8{jLMta5YmSB$W{)t{Y{SHBA(hDmotHVA=`4)iO3TEG-sYN`!$J!4 zN6^LCorHQhKPfmT=W=zb6EBV-7uurXr64_|c`d0pkCz>b9D#oYn=)eWr>y5N@;H`8 zxhLsA@l)Vml}pL98Zml68_5h>1au^mrTg%D(rZBQh&;2Y6~3|dxBXE-Nwsd!6X+;4 z*Vfk$zd5{Kq>{0C@8LtnG2x(548N?Z2|%DW)m7Jb@X;By+=WT7>F5%Uj3=Blpmr=( z``n066os=xPYMWvl8osb3XnShp-EZDf#^PD)V^q_Arl=q^R)FeS6^rPi9ve6bAa)+ z9w3MfP{e|}`c38Fo_UmIIH;S9wB|Wcjr>36TX!6QOWIRmcz{15zuAm7W@sYa&^YZB zP(h)i=ayTe4iZ;xTUYsw{Fr^0hB7J->0eX5CSF*tkKqr!51j(tq2o$sQ6tfa$ZGZru0AFZHlE zu~B&BtQdR57mTe?Gcl1q{{YD)nA{FF0h`Nh(2T@sZ_tq0gxuRQfp%-l>1oT+-H}Nf z$8v+2%d3XmN0Q)=T!+w_CJGxqfaPfw>FFy(v8oVFSe3al2dc-n$LU7}s`$BiJa5!{ zdtQ-GPTETSRzu*Q4&~)t;$Ln$yfbDOEv^L+*g9u-nHr0X$5jSpJ57^gw0%I0eS9&A z#CGRGTeLZ0WaoRv?;5-eZ>NsNbJnPTgX>N~?t3!f*H~ zCfFCy?RMszDTUxv%c^)S&xgeOl!H9W<|RY;C1Wu88qsN;)T~8a?kC!73vSx8v{^m&mm`j@6n+EbnKjA+=JB>E$^umh>y)ge zfCgg(bf45bov6%>eF3DM#1-eM;kZ}KTYMKy&P9Tnda~%wk$KCmG2OK&5?79&soag| zujtC`1RMSyMh>7au;XdgKyaO1dGebm(H;RhbuHOIyRT}^67AZtOkJ{4p zksN~_X-v{-CT$aSYU#xjjMPo39r)_k0Hb!tpun`qNP4-Th0~#-9{5;K6es}l0!aa9 z34eVF`j`2QUKH@=3tR6D4}l&MPn5lX6YTZK@)Nqf3TrX(Zzao8vB=@=Awf0FXu~=E zA{HPz9n|3WZLy?VQtzi`XV){*^NS!;TX2k6h6XG=b()U>=6#!Xc#YE>jZ{o39arji#Q-12X^;I)Vyvfp|-}w`!#QO4cjq_tqr)5l^xsQ+%}B zUk3qcBjpi6<&(Qb^t0om?=hKDlMv`WBGtiDF1bPY4Bqe1uriX#1uF~sQTW7xF>FZ z1UX`aw>}1D+K+76>+Bwjm#13xe+?&CF7doClqIz*LMEa%%Vc|5zI9=jGB#^5G#p1h z4CJY9d3;y0eh-+Rl8hu)>qBE7KwGPYMU*i_jUC>RbwGFrbx16sV}s=PnmMyast}-; zs7vwCqeC|L8q(74{iR7he)&EUpSDNdHjln{Q%H>j9^|yW9;+!t4?)UfCq&FNe0S3M zwa4F7R%6VZrP0fs_j2I}jm)V)EWf>Cg;<3qP1!=KY4SWe;_s%|TYGxD3t2|n?)5Sf z*H7bmJaRjn9E16$)Vp$agv@X z^g*gUjnYk|9<}8+&6(Tu0#uCS~w7<#aO>L3v)!7bIQ7r%>NIqm(& zpx0ghm&Lcxx70Vyw|@8373_P;8}H+|{x`PIkuSVu0&okQ1_pEoZh!+w6&uBtiD_V2 zVtbrRV=MO|*P>EJe&!q`Mm?%&{e@gI&O0q@Hdu0cyUZWaqmh%n0|gjuJ(zxnjO%5s zt~&)E_plizFq!aq4>N4O7dX+Eq%W}UG1>|opS;rB{+qe7Y1+O*Pk|)Sa54v7un7^8 z{Y^XuFPzv{3)`pR3Aej3 z2kXFP>%z|$@c1Z>F6i0^wuLvKPY@&SIS6CuY#@aR#_ z0+dc;$cCflwKSQ;?*6Jou zqLr={;MtjYs{-8xBnML-19v%98rMp7eTc&$;8JbVxb1cM)`(oI+vV~-052GRmFck= z!H6Y3J_dY_>cK_aiwV2M>D_C&jQtg>gQn47uyyh?!CUQA7?U(oTNOg-`wX-Q>9=d#Mw-xd# zq}?jo%n{D9u?1F!DjIrP(#uH9h6}Q97>KNvnBR1)A|s(xdv|uK`3V|n^V->Vvt9w? zhIuqG=otK~07mD3h`S1gkORONk`7@a`UiDh=xyk23A_(z zpC2(3NhhXgH4boxorvt;u+KiS7QK8qLp0JCUji?K_LDDH7gSELoaeK$i7EV8GQ4t@ z^35?PbX1F)jyhto3J_7a?SjDUS~Ef@-JTE~Z!bU;P!5V}zEFNHTIFir?>lTnWykha z`^AINW>`z#67P-pFl5SAZEovkXe>_mGi*&Y;@{p9H9~Xt713cni;*m!V7a}noKs-GbxY*PMje&X*}R)MD5BuZ#-O?ULHkv$#rR`| z!%=*fBH3FH3*g|VqAf*Eph7|6r+DV7XghK;{(pxd}NL?{*nR`TY*?7ctPcNyNsVStQzXZtEp%>4l<8+vLywK4bwh z+za;Cq)+RqLxYNx&)PByOsG&W$V=u5C^DbHr|$?w+sNlP|AlLgu@DJgG|P3bdTqB6 zq-iFrD681B>>b%o1*lFr{aVRR;d=(L2B4`~P23sd)mOB&5Gy&ZwP8E2EOKhA5aH`d z1#%r){ajcTzt7F8)9SQ_(L|gNJlD&63(OayICXVz>f(@*l*@zu3=D7Bwq@B2fL~kR z2O4sW1&@>eiw6wPu%u^c%n?R9GXo2O7^Q2m=qMZq zC{$?(Oerpw!Zu>nq#y6qG?BE8o+Q%MP+E%fOn?+f zdO|`veWqqr@D3{Y22W`x+!Sq!GfKWBD$`pQ1W#@uV?XTrj#8MU!?|E^E<0wOf|Rb2 z*vgoAkWYzzs&xaCO7ZD1rp09bg&{U7CvCl~Jx!mcpeo1mY1M2s!^OPw+B3&@DW@S7 z6DNdtUSp4n8LeG%OGs?*R5>&&`c=YJBbTsiU6x6z+o+Cq@#XU!Xd7(Q@ZtUwZC0@p zFPfmqF0eU#Ermfw=p}*6lfU(hQT@2g9^DBveJ8ic{;vROTJ=(5koN`9H9eWa3coIb z?B0~{4VtXci_i>&X!AQH#eBnB!_FVxK@~hEO7Z2sR5j)n z$T-bcEA9Bw^G~6+(*QLx6Lx(Hg?u-?UY%z0aswSm$SkGkj#nr59|pcYLyJ9o*VbwXxm&f`bt1G2sE!!QrlWuKy(W&64fBg5k>#DYJxu+ZRu#PyJz^@<&08pI4Da@H+cI!FE1;51Q)%^ywFNRP z*R-HD16O0$CPuB$3Sh^XD7@7v;iZuu_b<@y{5@^z77N&3(wDK~KFRf1o;uBG-CY5G z8n5Q@!0RS_Z>MIScyi|_56|m;)-^~KxMz??j=3`2p6eBf;F_t4gsDxfjVRq?WLL+6 zn5HxWhrsUbWq`Ukd$8tS$PIPzaCpC+cI^%AMnL|Pb2!H;DK~#de}a>+@$oPTBwUK3 z#?sF$S=Di4+3R&3``s<=(x*?^=XXS~iGX1&+q!a(E6;Pd_Pq&FtIzEEkXqQozN<%Q zGHJiBa#!3jme$LcNW~Fb*Ru(Zl%qgov3j8kf5wnfdiWQj9nRl}6taN;Bw_+JlhN0% zchKhIC3Q30n_c69DQ3)^^`a1d?)D{HG9wf4J6EEAl|_f?K7t1>p9(_GSFOWv`wuBxSU+rG^&<-1*r z1=+nzPVCbJ`B!r+rUKl6@&ckaEQ{R>NWgM) zlGCWE4bb({%;HGY=TJ=eBbpH0{C99Jdq+F~xJjx_U&;B}Wjm~P6lJmR?^(Uufp{7< z4{rJZQw3l?y-ewQ05M8sJK!Yz-T;ovD1h+RK*a6<nRrc2$l`+H;S9FVS(bv zNH2ZwwV&IAg6sBkMtiQ_eg@~gi18n&PU-ea-u7ma7G@n~EqMcNId&btO776IU#ED} zjWkX8!H#pSC%3bA2Nna-Qh3oi1?BS53OVCLattK5KW9qqlOHJ`NlWYZ`XteKytZMN^77OYK7J|6&JIMAnvF)|Fn_nT$8@^yk2|tZjdr>1 zAwF+^-+1}sUdVr>(Db;Tea*ved;C_qsSaS#Ae> zt!q(%Z#bgH5UmwR8@RYP9*Vdt%7U(IpBZjzFExxhaV8Gbb-5C1JGANFzGsL8_EFsL z3NbnoM<^nNnS)kq&|5LMU;K&d+Od!NzsNTK&>a789rUaWj4c1o&Z7UvKlwM}@qbO* zD9cF+Ylu?HDk)MM89SJ}7#mTW{N!`}PvhqQ4MzNrX!Cz>+%WuLB>$bg$H2t?FS&)~ zU&({4jP!W_we=spg@KjnM}T3&W1?rqV`llWV*bwy#J}qp{>8ZbXf!|j(%~_&{-fcr z{DcIuFtYzFjCd@}?0C#SM9NPo+mD7r&xpss_Ft+F^N(u6_T%pSQ}$0C%MYFMv)9k> zurM(F9K-Uj#59&4=P~{td=11;QXCT=HN#I3B>NBE!_LI`lUn(|=fu&|G5=)2 z{RgjM{~6`~1jVubgxUQkDDL0caLoV2$Njfb!~73$^WWKUKTys;zRZ8Z;r@p?*B%qN%92uN*vt$o^ZTOGs@w1z-?C-z{?PH zKscaC*@(i!m{A3LMrId+p@oYjbO5IS`{lm_yl}ia5Dtbx2?+ine*Q2*PHG8DKBoex5<%vcG@fU{UR^p1|p=87I;u2LCDZ-qUP8<9bdD?QkiPHW=nFfS?{34& zdjrO&``1STufXjR9z)>B1tvi*O{`Fd_=$B#5clw6LyhtCK;YIlp1R7`$5z+zWkt`` zTbmO<`~hlIMtRK=j;G2yjV_1jXIo6+}o1iEFjN z@Ni;YmD)Um$6*Q=B`bB4AdBb~G?@ktilA5;81+c}eL3eM7nObasP!3jX+CD26?Nyl zbhwj>vXVABjS5QEGK!%HW$uzTN6xZ+W?QhxIFC`0ha`C zAyV6$H>DS+Cj=>O2_`UC#^>y&sz(#~%BG#QQI;U&%-Nbe+F#o-&cK1lrQR-`& zz`R}gMi~LYq}lZi$TA}SaJ@W`sn6d{;*4DtS9aEB?#%3?a*~Du7Aaf4-$-u~1#QM^ zdMx1xOczSH{mnx%%6ELe|FfS^BfJ*l=V z`1?WvEWz0lLlblkMn%2ZSJ~N?d6e7UWHsrh`8OcYNoYO|L(q-x(;eHyhAwLn2OD}l zER0xx6bPu_{pYY!NLUV3CIg<-Clc!A z1ODw8dI7)6sDV43d2DoUM<`X2E(vrW-(4G1Fq0aUnIQHhq!I5`{*4Zjw>gn!wsg}d zIMM9ic!v#8G1O4i6A_kuhtO#_@pKszNz4(GQrx44KT^pyM2dmUVzPW5ybN1)07%vYGFSuRdtzMTcr4Mz7FY;{Hj2JFB;qL;fEw& zASXY=ZW1fiMLiGyFu$%L%N28qV;-+yx00V*^9Uigq?HG5X?YfL2s@eo8nk=j1bgI{|3Bwpz+Vr$`6 zMq=g?acK+Sey!+VO2!E8_*=2s)%}}Li=ICVCfq+Reqqal5XPK$Hj0xi?BesYyH%b? zIqfXQ9JQMb`CO%&No}1e$|%a57tQ7$+RSVeZazT`_p{nA({4@?FZ}n~u4}L66yyda z)y`OpTRo?*@)}(zWn0PGzp*D}&7>T1H?3#;UNRj=d)*Y@k9E3FG6=q)y2SU@Go~7% zyrMD+7$c39wH_xyRu&mU!@1ZIN5c6q<-Z>Myg@7Bh|U2 zWFXwgfUjMb9ez`S#Rqn6i=By~o#D=Ts%i8g2$hm*YG8P{fQ#GXcanye z1)wTRLN0osnir_YgkbGyx1nld+euMZ)KYOlPuZ?#h2Z zjnApyQ2Q zpv@ERhR33iQ)(tru_&|%pr$NthwPZmw5ilAJW}qr(C{!Oc1ubKPMM=@gm^Um%a+i- z?bqB>F?Sa(VBs)utc*Z*DLF3D5l%dcFcDSUdqG12L|Hh?j_fw&zUsRlJWIIE z(Qv#_f!_F#d=<2IvRe(|i=+Bjp5{xpMwqb#vpKW8vUT}{nUa#Stn5;a?(N8|?Rrh= zDz(ad6r}3YYH)W1uRIi)b6Kk6ZS!Gz_ptT(bvNc;$qixdJ(T+%L}=^^o4(@g<(|Wd zO6Q$lV_zzeIRgM=f*6H#s$`{)x>!JvH6s^rLc~H|FwSH|1E3qLgP`DgWwd?OKkeiI zvh6~u|9~=A+F4lN)26pZ#-Z_I>>{tAvbT=P?ilQSZJ|p{0$4A!0?vm8ek(uu9B&5h z44#l3aplQJ-g3wJKobibTT9u*;t^L95LF`3a#W_5AjA5IOK>^k3zZE&@3e`}uK^c% z=8%Hat~ldMr%4wiSCp>Nu_JLGqU!T<(_bFfmPEN+rQ7O|1^0KK9B=xir^SfNv;hrw zuA@8Kt^U^8<75ESHdD_DtN%a;0Gq)|0WFvw2_>~nzW31fH>$Ce6!{#{RCo;$gHd7$ zv>UpiMKp#|%xt%CK{ZQ~Ze@IzzV{Nqbi4nO5<^VjK6)XH97UZEqP*BaCsEN_PdyW2 zrZ!e4H=c@pLm)CNrbt+QP#;bZGWRVSJ+>@2c^M}a$Ay+NHXi_Wo`R%OKg4Y=_{FJ2 z%}`z~N;=XF@L1|v`ULpWh|=5mXmZqeRLJjLH%;Wv{qXga>9wxcKqF3>(VRzOUrk%7 zMAs_hjf|Y?DsL}T$ZWpxg{N@?O0!WyO|cftd|?o92UZqp^hmH`#<~u(`=WXl+dAq6 zxZ{gtn7{}_18DkMi_yV00D^>Y=H5b0esP$Cink19Zph!|>KBTO;1u__QpkQL4jwM7 z(IztJXd(_2l#Oi4<3oGS=j1&saGLUc@=!>@H5?J+oMMxgW3kcp_z7w;aTNZ?s|u7uVcNm zJY3v)U1Uy9yQdMj3_4&p%ArgkZ3ZE}O45BzFxKXHFd1;Db3|yywZe*ePTIohmFkWt z=PEPgC?S9(c4!XN$bDn=($b_+ z{0}XAdCK<#-OZ0{=4K5R#de`6j_8*|Yj=7&>Y+V-=vfS7Cy8+pqa%ck3&W-@6?wr# zqQQ`R;_-MmZlK_kk5#`BaOZ#lNE4(fP_oUh8mmf`Vr0pz2s{M5^bs8a-ncyvk0-9i zHT)$VnGI?!vWE;mk3Q&-E2BMZJBAS<_ETD)`GF}k#F=dIq1(B1&`i(_%QW$Isq+;i zm7Tdjz=j0}^bMq$onO+`^fRbhameacQdAP#y>+PGs@j8Y-qt|RST>r?Oz!?@2J#AR zx98dQ-W1W&nPterb$|e zqmhTjW`kcIvIQEi?OerARMclwe%N5e&!G_^Tbk1hA#H*lISq3I^aH-1%b1u97N4m= z<`+nkh}|n&*d$d-g=p6QXh3D^sd~z&nwjFs0xPh@K$3`jL*fKAcv)wb>e8M*^aktD z?%YRE-e*x?prl=ohp^Yon7Y|s_JeheF+5DxiqUq2bku2EaanEL#OD@@3e!GA&E2hf zUNYmQg{S+ju2$?UAlVd4S7gzG7+!D2M3GeDV)co36($xEjV%P+TTW+h*j;tu?+#0S zZj)PN=P27rZqhats#FXgjFg$|<=6&;*UYuSx3S`?s#ZZYa^ZvF?d1AKz!f_IBa7@i zg#KpWDg_GDWVf(GytPU!XOj2y+jIKs)6>0X{(LJ}5}BKf+!?W1>>#+}ejdIypDVBrzAhsk zfh45t?<~7@-WKuQ>zuu5cP~6{gsp-%V=Hf3v#Jqs;dWRbW*I*4tavV*rj{>FrZmPa zZa8!ET}<~5ZoN0z?&Nm7?mlEbZTml*Z?znE6>>R4x!wR0P}rR3+~l=~{P1qmx>hj( zXY}?KK{jOt?TR!(*y1qRs*b4=P-zR$$QNlCJf(ogk8H2-C(9St6l0HRW)!*hP?Sqa zm@a1+Th9;ho%@KDp&UtS8bHEnn+cImpn_vL_!!#Nb$_3!m~nW;szpozN;6t_k7FlC zZ`c(KH9>a27#r-?t%|;_u}*FfbbUZdvGTIfVuN3~vl7|6D~%4}!L^uTB!0a1*K!5( zJPwYqt*q|xhAP0()ckTkcMwV8B3k3mN=!#8O=%_mu6mxj+=6426*}A*5Alj@OM4>= zQs-}l&WFag^H!LK8#mwUu0L!{GdmHDi1kQbh4b5|X=wn+;l&sD_81sk^8LYAj`rYn zir6_dt27HvW~AX@HoDSv@oteD_BEPnk!iz5GQ3eT8JFo|J$EW#G*DQX5JBdqM7P~+ zmz0Mg6Daq01m+DK6Pk=CKmIn0KAipGAK)EP-p!U0ZVCbG6yE(L(0VJtIXtkmrWeeEHgrZU<2ah3SzqJ zwrIz;P=H6bl~r?RED3Ia!Jv7<`1tfP`34}&=o+h^_+oi zPfB1A;g&B)7erHxfSAzOqd4--trGL1EUkXg?t`>AM(ZoF;pO-#VJx8>^lYR*QADPL zt7KwvL4GTRX-SIknEGdYIQB8xV<}9nvm6DJ+-Socy%^NGAy`n+^*N#zCp;nNkd&P zJtjz2g@lp$$j2wB-HIstBQ;l^{U$t?9@Tt>!=t4vj8^>AjJV25M_R4$KAdZ1<4;)6EbOiSFH=2bUtRD<4SDtEU$Y`ZB zALQMgHEmg)IxV=bm%5O}N82UBe$k3J!)F^1X>yF9=EpOTzuQ)4T+q)AhK-L2+c99M zS`{KDIvpTa*=A)J*kgPk$nUKNyJ{@aTt>|#l~Y|abl5_D1CoWqcBa^S;2NHiNs6%{ zjmLvv2|N($83{aS=rxs+9x({|;7oMTg`&-3nz0z*v=u#Cl_VMHC%!N2)U zkBZaikfpVthxR%VjKkR`sgfpF#ok8r8}|Weo_{g5tm4-#rW{tGfM&v26Fk-MO%jVv zHIj-jUY%8RxVuB(j?y))vbx< zcVeVfc?376w2OIU1A4~^ox;qb>ntf&Wi;#HEgxwS#G1)MN{o&Qv(I z&0h;2t#_@EF72)HV_>}#L3cvhRT0kBzrVyfnH3U6Bu9ETVOxuA^1O})Cbzdb1^y)9@-AnT$6M52eO( zyT!_Tl&eQ%XO%+ETSH2M217JQNNGXARcS#@X$nO24>1WU5vT-rb=86Kfq4Vuz0T4U zc*z9$1%(jWz(Cg;bstD(uxcq5v=Tyw;vFw`V4MXOJTFey;l!T-0$IJ zn~%LAvju!_mUC{D-@J9Fnl|XVu9B@I?6tjKUzrlll~^r2ZO-Q#15qblv^!eNp7=33 zP~CB}m~5Y56TLZeSm?T59J?GPV^N7_Nskf(O}oCHUch)E#MwK`nHoJKC%GmG2gO2C zG42NR=G~ZY>V665RZlbD6IGeEyQ_=H0eRg3d-oc>D)tLNPO+5LQz=^E+6jIt%L{iL z+=V{?SrsY~@JpSfWSHQ`RAQV`un7OQ)%xMk?{YZGHVA$Td==l6;U5f#WAM=3BrF9p zVAbLPy1R<;eoYO6Ci_F4o)jMz(!F1s6{csm8YxXduE(Rf->8i_2jplLJ};YScfsxS zyuFaX$1qG2BkvXL4R9@-Q3q<2%w^pM_@X7SA~g)kj4R=gJd|IFgtK1^yNO^Nra+hL z3NimsI5(F z&noxlTMBBsOL4n3ioZH7lr_0rntplQgyH3==`(bjTtv#E-CHc`(?`P)k@ztKly5oz zirz2-bW4=~NW~end99qE;^na7GPq3lD(1rX+%(OkhB1K#CZs)tp%rn3J6OXAKhf)u zG^PadV!143b%2|S)1gcqQ{w|FI{tML3p-puvGWj+Df)f`#sj&QNvmN|&KQDVd@^aY zYuQs+@hgp-$f6G%7d<=#B*>Jz%dCbyR1m61P)8y^pA^lR8a!nO|5`r%Sa2vB+gd&9 zyzP3Mc6?2@LyGO%>*d-@l~9s;c4zI%i)KQ_4mND1@%L;i5~^ufx&9Z)0YE+-z3FAd z*C=GNBk|pyjnPrfk8fs^nr7Lw=fbie0_mGnH#pN@=Jgl|QSi8fMae_kSBn_P`0?vw zyI-ZTV1?+2&GosU*CVHdPY-fc>XmMF_c4{JkYZr0h|Sgg zHjUtl3~r@4&h4K7&Met0&;led5&#S3{PKqPvO*ZWxV;L zB}=mG+3%tySm&^vI+=}scck%$5l*$+Uz@Al25Z?%xJMjux-G^I1#Q|bNv+CV>M>nX z77+JokDk<6>z07pso6lF42C->ZM$lKwrXbK1c(mGQTzmF$ud;9HJA>C@FnY8U)G-0 zpvIU>WKa4KADY#N4I7;aZnfl|Dokr_*9a#AX!BI$v4bBY_omj_TzIMV#mub|OE2dh z1{=??&3rNW#G()IIHt^4m0c2pPql2;MTUyV(mcCdPOh&I^5|yxOV+I#ADSA2M^Dwi z8D0FAh76I0w{25-BB9GGP$nnuM8Z{1qFN+t$&)N{i|($mB`$IWT>3IJ;j6vOu6NtM zUue4?m6!XmTtKU_dY@aVDiv{Gue^5{f3#Gn#qP5(9m+X%h~vle5Ao?S^7F=>g{Kq+ zj0l~?g!UT}5=gdckrvS-B+$Fkdk+Iaa)h-Q#BIO?BIP*2DcbuHh_+us~ z)K(_+6CViLW&Zi`r^G0O@u|KjbWNP-C2rJ7n$MB8&Pe}mwn{TOa;+ihy*hfd;UmVk zcbQ6?m)nkcqRjzcjwL}0^YKQanjx*or>L4BUFb*^77}>&QOie68Z7+Xa&z)0G^9yM z3XUq01)L9<(AfHmVe2A_LGg*DTt@N}I+~(0&}=B~Atp^<#9sfv^#we0?64v@j2 zia}z(okD*+3jK||e97S>*k?DONp z8~Tl!BxCRwH3pW>YJ!nHxeW|>K}&#vP(YTS2y1P>ijQXwPR`e)zF$1`j=j`_ACk07 z%vyKuzdnULzAoO0O0%EJ{1}ZG`P1B_7(P|sh#ylr2*Cj5WxQQ6@LMH{^2=SVUJ>bB zJH@xA4(j^&Da@=;RYJ0X3g=|$x7tBn3Y23|n2{7z{a3K41&K}eZ%kkO7$ykZ(kQ-k zs(;(8B~CBzS15vNVX*Li(msIJ4`aF0n~NwGk9YQVx4j+n4(T|tbj+nRn z*3I1fr?c5=9YMafHlMADHS3K!;l{vdvW(>c4omWlW-6o2mHo}>azeXvbiW?+K2GP& zCFYFPvaYkgzYZkG5OO_zSHYCc#)=3$+WXP!I*o|i~}Mr>qY9ng8Y+k(#l3<- zTf}-5kmnF)xC#XIHw$#XnLzSIFj4Xnt5EJA?Vf?m%l6t>FG}PimJ-L|L2Ot>TbQ=f zZ{`gJ>AUagmbwnDY&d7`H!ZRC*hF3~8N>Xub&gX#?l#xmMay(nUdoctt!b#;@h#+T7B8u>cdy6w z*1k|Wt(Hr|U1Sd!{kVCY8$I5^kC+s9Usp}Qe+pr~DIL}54^Ns{S-|c3KqE0Py`Wj3 zW5^kI34goS)9LxbHs1V$%@aCWtrzp~{E|j4CLH8_2(D3u)q09@jj8;&-afW;ZOkrd zvq^U>AA5*qf`N1d&l-bg&}FV=)Fa#%INuxj1U$;cx%=X1DStVYsj9CxJcqz{Aku_^ zVM_=#7}am^oYn%3anWNeq14QeL)E}!Ss)zfha?G2>+)cLBu}7uIdc0zp%gX`nczv| zVG;R--@wvt;_p|{vEM$IA+;l{m+fuYaYjgG_anZ zxMf6gQx^~m93MI?_WH)!RS#~Q0**QER!eoWYCA0*j@K0=@+XAFQ!E)NeBiOqaU=Cb zG9~U0XGy!=s&zha@`wZvE80eNtYF+<%sc2 ztMzMi(DnOXHMQ|SFos|eDS+EsGZUq_!aJz;Dfq+WO_x?yqlg*!CI;>Lc7(cSvX)KF zGhtS8nFB0`89fibgdhx#_u6Uany03DvR|LKzlkrLSIzg^<`$;wnCCuKs^{N`+&5C1 zZ-_;oBjFm_dxF@R#k=)vuIrgOs~92&LF-7TfW8+MKG|+h!I2FB?Qhf4`}fIzxzx2O zAi#~wIW0ljdIL`yyzaxXQe6@yoI9LU!;3hwC6Sq$m6_S?^`Ai%Rf@u<;Z&F5@m$M9 zU+#cHMAPi=uwZw^9D}59V6p#-b^Q&^c?_W1Yf@S+VUWVZ4#neZFKtVe6>vG^Oe}c@ z4CQ^^@?>VJqxWN$wJOk}JHE^bgSIYyWFQ~C^f$kT=yZ^_x?Ea!=CxvrbrQF`63cK& zysG#hTgBpD=u2dZUjA-`$QnA&QURemApOapVpGQ}CiY=6g;$plfi8npeE;6`9kR0- zeXd%qQG7a2QN0;T?GJ_Sn(VW9L(*KY%dPwL_3mS4ZKz@RhtzAHnmx4MXk(REs483UmU=`4oU zrYrIJ8Vq>0%wr=+av_kZb!GOSnyN7~(M4Wv3?)Bb$V3bu$dpOpAKVgaqr4PNh4F8j z9QC4=ya7V9Bb@REd`BVW7aGcJdHsk&{cXIq6YckYS8b=&CGMR+{DV8IBU(2a)?iK& zb=6!W>EPXrA8qyRhJ2jOYuLt4FGOsNAuxjEFfKrg`Ti-q zm;rX}vkf4>0L8jK*JJ_IDq{h>3-e#Ctbnp#tc;)b4B*;6tF!?m8VlQJzijku04?^H zEd%ty!pH{24yd@s2C!tTYyejVDC7lr1Q1oM^Z-4^0Z?Xu*RnFO;j?o91lp%f1FZk| zr)LJhslWOLa4HrSHmJ}30ZYK=V8LessI$Kw1D-Q70`5$#_{^VM2YAd5*uuZR_xF+s z;H6mT0b31_ZGbi?W_AwzPj&XW{QLeN>ZN_|u>U;h|5~2?*JJ&!&p&&zS#T%1(hwCv&JW%H$LiAR6c6SZFq z4I%axhsV_bb;ny=CIc=yi=)rFDUgHx;hQ>(m0d+8BB4S=Ike?MLRx zC&k1$UbKk3^rQ??_it0@nLHr@U}$sl$~I zEzhr#brl^im-TlZnWr8*x`K_5G`qHaVcO~5kCBcF4%mO%E-wR%X}oylPf7VSo71&a z@@9fZ@FzH1B#RvkTX}y%CW10MKjG8pLeQcm7@LTjs9??reSDFExmzOs@CivF3~&(? z5gZL;LHY3E5h{uotFY{m+E=O274EYmU#!X1_2gWv$=6lKHfNu%;lnUzpRs|7VEH|5 z(W0jtMXP9t$CN|T{0S)Nt4Hi2I~la#HT^~aaHN9^0vJA5nqq=$E|5s=L*AYl#|a1nR% z(@+Fsu+CPvK9jzgcjZUhsxOg$2PfuWkV2l92TX=4{^h3{7XO=^{yG#tq6 z>qO&EU@r%rMONk})(UuS`+e}YiP{nW_d5ROLQrNkEhT8*D)>dPx5lLf-V^_~S)05buW64h0z<^#WZqaCbOLWEWD|U9 z5~V*5sRly2JYYMgp?RTvBzI7bWu_1X;Nv-WL>RN-h?oLE4FYTYT#gv+(rti``PF}P zs(fcGJRFfLjXNEX4b|t40%BL%#SkPih;-m{6&NM3?}yz}I{tbk!51&W@f~fXvBojp5zhAmWyiA|^QA?U?#Xy)91en7CE$ zIRd1AZA5BT${>T;zoJir$hIn>ZkT>YbylVgeu?P^&YO=#J{c`HH6W~irI7^ET!=5K z-KL^XmZBq~DOnP+DMq7VDP$|ogCt8M+#WFsKZ+|Cx8>*gAwZ`|wOq%yPZQyXHGGI& zwn15|Y?FTCv=Am+;;feGKZCiyFE6cK#J*nbP{tC+8pH|6iD9W769Z?b5<};M{~$vW zqDtS3(TEe}jHfN2(xvTx9Xl-kIva@+cU}NaL8v<$j}os{5TL;?E@o{gh(F*A8FwbNKOjXd=W^EGB1ZAqwETyVM!fgW!d62?wkwGD}@(X}e z{tfql@IGEy*m6fIyppQw;_Ps|CP#h!$>genZ1Rz1Yw%laAUJ_EBr61By?BAN#t^ef zfk@UraqWftPVov3;#M01f^9fFaGUu^llLGiFVu55+w%| z^FQjXoEwj{-ifSh(-RD2g@1Zl$;6SQNGMJBqbJM{MSXLnR$MX4K&rBv2^H9qy?wTw zu^*fS(Ko(b@AuUQ`H3UWEduX$K3h9r#B=_8xs%(hR(+TXk_-t(hMIpWUo^Vi}Cfh)&Sv8eEm5{=u2j*)tN61TuM4g395uGpuWi=#*K(YNyOmo>RQtYKX zk7|G3+*8EW7po3+8K18eKZU?1A+O3roqrTcqG~OiL880Lm8e5FA$358?&N6=m`$zg zkPS577Uzq4KOG;%ew5CzS#MS?EOnEk%mFb6qG1L{T1O~HR7XCT!kaz49%gstAa}p+ z1k(`=Z9;cLrtgED4kQwPMZyk)un<B%Itl=X&A`NEIaP}ERo!s{yE^%A#pOaaT&CjPDmLLeA)(+-6!96H>F5K>1*C-#fsUhI zH#_&ha3oL2us~8!4_`eu78=$>l!^rJCz}gp8Cn)f&>cy2Se`DIYk4YG>&k?V_SLA< zvZw3?5$jgHl$fjj^u8p^)y|+1hWB3Q-!d`zR6miA$M<3gg4a`N#DW+Jk0lF0LK0#o zpONA$bA#B84{uvyaEF>$`lDD4#KjE?;qr~rg=&7HG9$mQlLMqoZpUGJ zh=7O~j&C?o;>B{abc#=}1QyI?Dm`tNwvy?zyGL#R_s07ck#G|f(p7dol(X4ZcYhA2o%5}^x zZ%}Y{sSz?SB`r-0q>()n`_fJ0OJt%JF|;p6}o z?_q`MZaXv^zRR>>k-5^%f6Bt$d1s<8(q5F0L3;MJ34ZeJ zu=Q->MnsS|Hp17#HiiyzJTGRP81ZLJOcofVB@Dn5($j6%^Y?NG^`2|=8vH1sk1^aO zT&Xk`;Dx7cuj3^HIrojKS}8Lw{scN28U{sQHUz&ka4R~8vZK2=6!+#X)+>v#sVKMi zFldPVS|~&sdPN!9PH7{VOV09gynt_q2uul-n{{Wds)PkECzDQ=W@+ z5;cWnB(*PJ?r=^)EQFGY0Z)5?4)WzUf^CNP%bZ0wCVjmROe0UPi+ zf}PIMK4MfHPOYa@t2N8E$-inPG(QBNQyc{GC9K+&A&yKjrZ@-<27QcmOy5}peXaMY zc=7$%Bj(j))CUEM@<#Xfj6Gny+p}prjlt2PLWz2J5VBGEPDW0>TSiho zKKm9Cr;}^;3x=);eF8ECm-Xkm*UXQP_lu*~5eZgNK1Q@@(5dq(ZEMM*=6TDtO3Yxx z%Gy%1dcLR10dU?5m5*Uyb;=~VO3g`wJOc&`H>#Q#Up2o_5W2mqB8IIasetsofSyp5 z-2|2FxxrpzQuGN}#C0nk#GD$`D+GOt*?N4*JZ$oqSsygiJU&3a}i8Bg*MUCjK-VMdipP`n;ZLM!SPOwF`Z;6*7bquCfdo!m6#ul3r z;yjI^k4}>A4zn3|vqYVH3Ue%qU+A_Nz1+~=4}p2D@-%VX-)~$oCMWz_f}Jc|*>K}l z3ahDpk-Sj=_L`&to~CVu0&UUBiCvTj$1$>C^a4vf_6i`yB;#Y)OlH|N&EH4eEs#~_ zZ}+Uf00FHGrd@g6PucRtN(+Tme>p{}AxQt3up;pNlK;U%B}5_M%OFv#3AE`wVE{3- zK%Ce`?FFv^>8lA!w#kdQ3(GSZ1G%|={?L#J+m&86#{~qfv$nk!a&+>fX-~#IUltQ< zZoVdTEhP4U@mgA-{|v81op(g|rta^+k>y^)j=>Cx`jn)iksYx1;Th|0a)QgKjvw|d zn1?!6h@o=97SvZzL|tI>gJ!idj*e^m0KY#1BTdwokaIV@;W1$Bb%W6U8;-r-=bx*-;Unm z$59KXcLu?B)8~dASZ8#Z?;@^9COF;iQSPv{uQO(D#k~EkPA|pYX!&d4yytc7ZD3Yu zou#4(4>C2TQJ~?*xsJ8KS+8fmBfu0)hj)D(el#?_52*+pvYc8hn^MO$CKbJr+o_&~|+sg8`xt}%j zo;=MMw=|Eua#+vRg~cn zuk6`G&zXG-=7<{=u;cu#)Hd2-&S^(n=14UUKrvbQ`%KRxj0MzlEQR0cO|T*3v><>g zpQtZvOZcTl=VBjDLmu|`@XeyExn81r?vVfZt;=cHt4O%rOb~J;`_{NQ>YSvd;JUdR09P4^dNt7dm zk}6hRLp@ZG8zBa!a{@X`-X}}6MelU_qq`()zJ%mHqvUZBY4E|4K!DGOKyZEie<1 zYlq27acF&^tA%}Y#hnhUqbe3LK%EInnz>9LIGli2B({Szxb_-;R0VUnni-z=kp{Iz zZ?pHq6kdZ*_%4pTLg_aVtWi{6YcC?JK>*`Ws4g-`F9kWL;~(jeU*b|?AwN6i9y;#- zJS-t;4_6^JADD*?u`%bGPQACyk*QQyw6+bHo5fq*y^)%ut*5vfL0GXfFLd@*wsqt} z@TyAjl;lYN6nBD9YF>r)c1YH#ZCfvkX1OfBlPXNXwj1aR2#J;jT+4>9hh|7fswtS7 z!%SzA+cW!8N~2AY%Tqj?H6TvVIL>NTj74`y$uU@EitsUyek(zddpF;`49Rg~d{e1=Nz!1IvG`vE^2k{S*__xb$Rz}5CEct7w{V_lnB6~CUqFKvw+`V*+(Q>9{xXEi zW7Xbz;}kQb3_DwH6PKz~zGReq9r~!S>`c$!J*`txh(+Vet-EvVOB%(UQ`q)@nv|!~ zS}yNi@mO(l-W6ftCsnvFXv@e)8@NPI-FV%~9OP_AwrK^+o46H}ILsfFse2HHIUi9# z8;wzANJIvIYk!k(f%jF@g_1Ew?}#eQX%c4PrUT~EBl#M`u&|Nmy6(p4(?>Z`@#D8F zLDQNf&F_@q<8g;|awMDIIoFgSqht83u4vYzVT7IIHdQK%3%9F`yT6RHJC!;c*`?1q zybjvhj29<7DMiJi7S~x;UFJfby_maNiam(#;H$Y^ryp=p4?!3hj5XNe0h9pLXykZ|_%VHaWIcR&*WZWb+)g$p2XQBm z_gYgjTiz8$ni~;~xwPd*3zfy_9ts5{M56)1-I*HBL-bgWSKA|3eI{#{FA+!X8d172 zyPl__gf@<3q4SXYBTXPm=DVpakfkcC!Is-73Az*mlTennkYX8F~GG{9;J){x0!fjRNg498X_{LQ5LDtIE#9E)}b(J=Pv=bMePF zJM6@OBa<=;ge~!+CR~n_%iZCRxk3)+M(hsyUN(dH{($c;=c44q*>Ht!qlW(^jzUBm z2qO07ftQo*O8~;|zuOa_sZGTM9`#RaC;u&BkaOb-)Fy;Cgcp8}5Pk$@56KFMHPx*b zT9p09obT0*gZ+VZgy%pK%oT|x1KL-D7#&fzhEcmFLY2g!=$OCC87`W#p@ik%Z*;^4 zJOyLiPYDc!-xoHJDt>OUZ$ETQQ=~oc}tdLdz%6%_RDYk8*#m)SJCgBUM9~;agSlva9b63 zV<+v`i*3ddO$s>dS~oEG3oI8Ss*c!tF=ZG{m3R5@llx$lufCetNw5^fnUgpssWxnH zhAxpaP8id@o7<_`j}#XD5>{aY67I{linIsi8X2;-+J=kBDV{_lfua88He+8*mvHE< zq;!iW`i<5DNu0;INS+Z+*_9hg99cTC%(Prejh;?lvd8?f+no0bqPs$rApSHIqe(~9 z9rk=(2@*Ec=XqT*8V|OMsUiE0H1Az|6p~nzN2Fm`aTG!!jI!9IsH`l}sji(|-ejSx z|C=V0pE-a*Gv*qqw>c4%Wh5vpKs}#&vKFhY{im|3Dzcwi8W1mO<^d3jZ__!CkiAS8 z1LPKwQg1r$VOOs*Z_#sVV(NfG8EcU8@coNW%NLMGrvh513x zadc1&sz1YF@$4SsQ{njHEhsd%jAia+k=o+ipBFKaTHmS~gV>sz# z(ETQvGFJQXVp#5o{Eg>Mb@e=LANrsYUc6Hqhv_LEd1j<{*L{&X6$x-j_Wi|I?;nG1 zv}0FmZ?7Li_LApAkY6c$UuavYhrok5>=GzN{=~{e-mtu6Ra;xE)){7R!(&;>%F*N%7+G2B)TOBf22mr4kZX+&? zaYRzup!bXx+PaL|VisSO=~i*ba21Q?Cflb$DOaPjm!{qeRMVOpSFE*^S}n%64njQ5 z(Ktt$9Wpz^M0Jr_(BZkC_*~E@7Qr)lzCv8tSiEh2<=(x~eM9%;zBZnisr4Eh`31DS zhcOWuIG5sWJd?na#n|eTn7W5&2h?(t^DIjHy4*ARtTlPHK$2c;L;Lei!7G`p*xAOn z{X)f?SNX_k>?{J>L-`85N1k`V#8AUWU&#f`uI1_-s#(k)oO&5-;}}Fo(JG`ugGS)Z zEc{%zXLclKbZgEh$g{{$_4=|yva)?iQH70z0_B1>`&lJvzP*Q|Xp~Tq;)W`pcbpA$ zT_5Lw3LpAi;>>sAb;}%nFWMZwi};dK$s@!VI=`@uMdFTmSej)xtILd|ENR?f)C>`1 zAmm;R1RQu7qFUHzNn89IznTlpS0HA(<^r=E?#o~E z-Q9~06+3rjp$Sg1=jrrki`d16&Xt*Lhk;xdDiChkT`nh){v!JUSt>ORUM^1-Xz}DD zotK&Pid~?upe$l^H~y1OCxq$q19lXSrqvGiC%sVXu|n^}thN4aHQ*o--w;p>L8Nt3 z_=awlBwUoOYSUguE0~O}PSR!L&PFCK>7Ga4&{eg?6g2?bVNHVj%Z;|ww#MYj zyVQ1XDp%8AoOU8pUm+sKt=W^?y9>k26K7XbBE$n>_h}ft_ORxPwx3`f^6YyeaN{N) zwf**6oJy+x$2Jq+)$Q#y<4|<*rco!3AzZpe5=WKWp#?`fVlz>}V6NZ;m@EfBy7_ zDrN{*g?c(g>5WvG1g3a>G%+2KA zY7#|o_-&6{U-5)?8VA;{$S1edR52!HgrL)>>=ID>pOG8oy7av2a`kiufhB*Q^5{J0 z&o^HJM}L0qAL0tnWP^uK`1)mw21U8CNK#cleh9*DUmN$ZDdYiq!19~=2H;QgSzLmi zJX*9FYKCsYTDQzza(RXUA|brEbgn>D7p2oMX;iGS{+FS|W}1U3T#$(9ZfN9TjGGhZ z?4?{9r2B)sr=~n;UX=l^u!EP-)3bh*4(|m1@drdYPjm*5@Rr++zO8Oo7n;o@eQRtj z(RaxzGPW50J&~HL^br~Z`YdgDf@T71tS~l&4?ni=qp?7nfe+aigJzecm~i*SI!J^6R*!@|V&A99lZTKE4eH$H=(06mD9S~wWm0hSiJ z4u(R8fAz)73uW(MXQ*ol!8jt#FDB9Pu-Bi_VR$S(>IKQr)lJ zE+Y-FYs3VkFV~&;<_9>^(X`T!KO2>uhJB6LJ++34BGkpTTks8!0v5NYlmBE%xiLWd zSsc0A48Xhcp`XxQ)sh9wmLR;zkYM9fji?w1GC?gks%==Q;kSA(0!hCA&~mFhc`W^- z{=QUd^7gwkZX-n^y(w6!0SzMHL9sC9#NVe7=Q1+ z|1G@!HTL!;=VXY2DgZ=flzI>rxL22v@jlO`Y2FtiJ#pbLa5#DSFyhIJ5~Xlk(+~u zY_^-tjt!h@YHd>iKqd!7LNOY)1OjtvysMd+qp*tEtl~-31oZ?h7z9$>Vn*lVvt;@{ z)R+3hw8&%DKC~5Nd*X&v93pocD_g_L3A-qZ)8G50Hd?fZRN;}0^t{~o2gz9B6HVQA z=@p$0PmPH&fm^?3?U-(j;Re_~T=Ia3=pf7R!OMyDAl-(Mms`Ih-rRLZD;f#=s7+zpmWBQQ)U zMcEyQK=jiafheI20-mg*PPAC8VSPvXL&_fi93% zAk`(JsPB$2h19E-Li2()PI2&&`E*%!ljC;qKa9DHB86OoCtngw3l}EVkbS&w@!=Fxk6(Y|e)up;|!rX#w>d6V;GL&aGeCm==hlC2VdnVMpP!_c5$ZEe62N6YqZ|K1 zy#NNx@gLV`W%-Nv{xxy3{Kb9$@m{QeP{@CJ*H1_pk2V%zvdoB01N(n z<>!)}^)o*5Gbr+NngWnzmd`a<0TGb^W2<#J_^MfZk{r z0kW9`Ae%Xu|4%R%u-*S}k&uAU$^RPW{u>lE=g!a2R`LMj5IYbpdY7z`?c06u4|5F{~oz_32USIVEV zX_IPO?SIS{lG@(7oL=tMeWwaH-rCg48`VzK%q^%q0vFVX+9*9Mf32ThtaAbFsb4&7 zxKDnpyH7pwd3|g>ZMEya(DdyUc@oPHt;Y6L8sMBDGV^J2wFx#G9-B5~{%NYhqZqzG z66^cJcA;iGwyH}w6$IPoFW4k`INcNiq;4c-Ld6zwx@#qcX_!F``7M$oimybyQ8V3 zczK^n^b=F|?L}`P(mT@Yt*7wL6z?D;Ii8jn`OODIht?K|Kh<{U(ab+()EXosnhof`gA=QN7< zZ7BoPC~?6yY1VsaV}z5o5beRBO8j=kH*bshRD5H7{@YNqEds@%8>L-jVmT5VWucW4g&UM}PD;zzrI1ITVhS79a@^<9!^ zV49i3q_?LFEB!gsLMcR50(jLB{&>2VkibGi-l0H0nP(C)I!x~dl0uY$DKKK+(rni9 za)lOq?#{NU<&skWnOxS%8-h3 zPH3;C>T?SA-8FW2up60%VG2@2$v%Q6bK)yL$Dq+8TjJovtdYu$xy~-6G8ah~1MMUJ zFV4OJI+HGIH@0otwvCQ$n;oNLc5K^DI<{>m9i!up?VIkIp6{Ev^RNG|`vz6Dlb z-rBX+-sjm5mQq#$V`(&=k`_ZF#1$}#(s}`vbcsr7f%_;oHkmmZ)5@6(iGt!eXT~-4*FoVV)suNlNrEpaXKqSgWi~ z*fizfQ5IHC+%<2TQrKX?TsU%zxOavKAe5qnJ$)2Jq5`K(-4mtM%x!(9cOTdHuG;%T zD2u3iIS)O$c=VBKin>Jb1;qxP4plG7Ys=Ra;;0&rvK}{p_9$** z4*}MmbS@%OnqnO=q_HZDiHfBXjMp@#EHbDqgzrAt)7o(GAP0w&j>xA@O(iydhc$Ev zz6K~~iS~;%k6YH;!_Pb+VsrO!=+d^BI3+G2B2zxTw2U)l=TOf`lrlgY^RW+6z5k9a zgq|tkE#rllRhDBgDjGz`cu2IXLafab3q2+}B8-dj%F2VTs8fV)E_YB(2h8ow4C()DCN4olI)s?L00dD%AuM-Qm z>{ZPNlYV!=)KBcj9#eR{j$j)&z|mbmtI~*GnPL>+DI~DbJhAbCtgSqsn`soO5=kPN zZ2T{q?d!MwC`~NRzR6GqEC1Xp<2z$EJRI8MR%b_EECp=dgtQ{Ua|&B^ONA#|dev?) z%F_~4sgakLR34b4?JaY2%wLeKWxOLa`m|YZ`|f?+*ZxSZT5EJ4T)BU#w_a;@o|UZf zb~P28T^5P^{q{+VJb1}=Un762GXH$Q_Ek;cfw-k*C;4D}NwdXbxc82Qg@?8y>TwCv;^DH0s0V4Tm? zc!YWkH1*Nq{<$^6<+B#P&c5(c`0naVn-hG^@89f9x;nE(NnPIi_A#%SuS4q*7#+SP z(mXfZv&gi)&#-4RM*AF0gy8vJO#0Cn@pg07?XdWBpw9IIJ?TvA`7X#K5HCo=Fe;k# z58Ghw_!CNT9S|AhO1qG{D`4Spzf8!$Ibg(xt5#@1sc5xM%#9)3Iozeletq zH%PB3O_C3QtGLQ_+K%%0f+4(^bu>5@x0lVovrfdIX|`=0r1A-<=K^C_(5jZm z?>h_q6&4<#(kER1D6t$2K{^3u8K8tq2R6_#MvESeqIB}fH1d>A&neudRB~lL$?7iK zeg0U!4$s%Y>Q#>t??Xn|L+tw7tqn2mR3!OA9?MciI^}PO?674bb6?VyAb^~hXM?eU z%V4=&<@G0)z?Ku^GX)i>1CY??X^f|x&|t)cE;X#_kU5nnf09V`4;7S6sXrdxRojLy zrP<9Vt+#%E*;=z{bBRogr}8;iu-TMSJGiq;^K8PKegFLS6sz-yKf;Q=2phS=D9BqK z(TfM(pai!nHyg!2f~+m8)n6V7{l2Fx10VxB?T|**iUnxtA1J7|MIkVOu2jVyGHx_# zMJ|_RBd?46(2MSS-2Fl)r38nq#RN`H)-r2uFv@AGzebcem89x6hm?ND1(Eh zcn8o|gih4>m^Mb42q95-m#LykxnPdq8>A6ipj05y0o^6UWaoG?HC(?a{my{`1^G~} zSpg8R?AjHCVMv|~tr`jjx3bFI{M!7=YziE7Z76{uYatC|UF)b})S_vP$BCXS?$0A# zx5!7lHs`6C?2M>{^r3ddhO=DfP2OH4y;RqU$>|=yg^KM`eC@WImGXV2iS1?AzUlC( zG&MTTX^n+=?Z$2JP3t4=IBzEaL1PxXtArQ+q*uNEkX~@~%iL$^$LV`_KINi^8uL~Y zQ%7XhB4UFfcRrMojGew5VaM?T_fNd9*XvD8BQI7TOhxP)Zez39Dbk%@n?4zAQxc@8 z_;p36><8Um)J-^@O*?=_3rI35*~?#;jV!3Ass(9Cmlb81EkO5({MS45g;Y`qT*^zh z^N1o8Pz^rKUoST+pkxdWgn^J40mUVT>LaqL5hI}JUx@(}>0`S-y9tPtwTSi?F`i~m z?nFVwidG^b;v2>`cO2;Uowc4|#BcCrZqjnzN5PzF)mAuuc?aKrwoV#f)JfLx24=S6 z_uBbK-f7-NiFDGVH`FN17TUj;CZ4B zspH5TY_S3wny_QQLSYaTg;X{HJu%#$B>+gU^bdaGUqTY}CF_}H2g6_10Q>T`;Lfa= zEv2WEAUO=xF%EBmJ!2Qe@DFWFon^{RerS_LBtQ||O!(&n?Z3Yrl-I(>I`02s4b)g+ zdO!Dcu}$|n7|njoQmNYD@gYFNZg!Z{WW($QTiU22@w{7Le^Xbx=H}&dnoDcJtvKHG zglC(ty9`dk=eHcNrSEo=r>?W=qYMsdQEk}sFHKJR?0;ng_HC5CW>~lFi_*$eXThY` zvrT$F-8gog+3OB1@VS*qc86V8h!4ujX{c~wi6;XH$p99%&p`V3q}&& z)LTKX(-6yp7l!ytfslEj@$_t4M9%Dtc9LW9R-In`U%2O%Hu!}te7=aj(?{<; z7X}cQAIBf?q49y=#dhINmx8?e)Z}l)lUh9ZdO_cr2<4~j9Vu>g%&#E9lGh_{z$Ntt z3_xX2%G0C-3(yD7*DF}rd;w~xxY?CUS$g^SKQ5o$0hZqC>O;lRX5>< zZqtTE)6tsLClLI82DEDp6ji!rcU4|`Ty|3svIm&zZ4mN=d|e0vb3|*U+lvL52P*Qm zZ#rT9`~_&wrcjn8SJxmwm`Yx6!iBkID1%#Li_M~d`b}vX%Y_0y+0+)_B)CSu!lc<8Oqrc5O%p9Y{RKpXLXnIkk5>!k6uXVi|#1c9vq67;ICrC z`jN5~N~09`-Q<>sXR@%KQDrSSNLoZD9lA|~odyE0fg96Sp~`cW<59jIx*Y<{ z0*3VG(^}EZv1#^+r0V0(I+;=l9_=j6?-z35d$IZBCU2+aZ%ul(3JvT$Sw5UJ3qnhg z-8QMSQQDd!oL@AF8H5aEP_~7(iBM=6zuF_ax@Mdid0ufWz@Ti`?<5-_hF@IFfFD z+*Kto7X+MLpsxYC`5*yTR;2la)OmninyP65*E!R6NL^zDNtsIyQcLlYTe+zs0S3Ec+M`M9!bWc#A>^ zip`8DC;{=~lDJ+0M6s|1+mWmux4Nne5>Py*1?xPR>t=`EI?S>qe57iCks`A)^&!jE zr(xsGoZUzdY@Rw)CMLJfZ`8z+NU%>gw41jJ&GB~#vdCAw@I%UA ztRh^;w?rb3xg0R+*1z|MrwX>_J#Xx_$gT1z`OsHsf$WK;a_~@n->MNYA`B)kX|!m) zb2>&@e#J&DWbq;Uu0j$re{1P|c^h#6-;WS3$IJiTYBcnJukUX){8G)|+>yUm;g7oh zTHe3b=^yJRYw&9Y|Ldy$mk<>r0WISPB?}|NM~@$i^gqJUe=Xo&PA|rfkmKKZP}rFM z8xP7ylYjA`aD1?>{N3sGtMD%#l;7^lzdOBt3;s;@&v)aWG0i_tFU}9HoB!44Rj4HR z30VOl^ZV%;RpTt3v=ud}HRWm`7DRml3QeO7iX@8BC*}G2Qgs^$o8(3!O9{41dE1o3 zo^Z+CSmH|%@D{fZT1;i60Cr@0j}qgPpN*lH$0Xd>o9*xWa{KR<33#U49Om8rUToP4 zJuKr2hu#eD58lvSAl)6>Y>w4mw|xys#J{597adORQA^W)iO=)uMz9?-OVYeG zNVbtwL5obnc|K=A8EmgX`H`IHXckOR z__5(|l+UCeF)Hwf0XckFag{sDKws>^j=0qTLOQu4mIdfBnHc8Rd&=!$1};#(P(IQe z0et`?47|vQ{J9;6ur>T(z;{G5(H(gut1?X^WfSWN z5|>Jkr|H%zpP6LWwT`$$gV>=d?DSQ0_W826PU+)>^=G$@k%x7a>cg6k{QOU6>2q=O zCmTIxhdNCL*`5v3C^oC<*sa!U(!$8Tz!%-in@aYrG*z~y8JJn!GZK0Q!aJY{vYP%8 zcD}F_V(-Kgf{-y%i@PK9sT7kcA1Sg3`=@t|onYz30$hu461i=@>eK89If)wfh^*Rp zO|UC8yQHp46@32|P(7kdHR2JRkX`p0AS@j4D#s6c@sxN#_^PIjaq}cPbB>F$vnhMq z#lfB_joX<&h}>+lvE3E5LBcegH=tOBqSB~Lr&Sj;@binpL=p4#V3!}ktQm$Jbu*Wv zg@+}XTdsNu37*N=z?VDr+lcMU|6&yW1h3gRIe(S@dOUuo_W1Lh{ACpW7n?6Ds46I| zNUdyPW8z4wXlG+!`;Uk5FIxij;r{<^^Z$P`ll*&l;$u?<^T%d}zXb;Vbi<>t z>&V3Zk>BHk9qB*+{vr97{3F!yk8CEt+GF|Par$*+`)4@eqt-{v{dW-IUp;bs4Z6 zxr*R7W7)^c{f_}>{Kx_G|6sy9Ji*Rqi7e_&sOpCsUv(YG3atBN zzdvTZKW6*bo*mt1Ic~gl9+b{~!B^cU9y_25MI6K} zB*P^fAA1BY+a&8YUdAb|H)G~9E!7sjN=rFA!Cf7jOv{(;N(~2$fi3E3l$}HiybYU_ zqeb6iBg<#APW$XVyc~kednMn))6hoR{UC&|(rvKyD&4T>|8b≪5BK=SocCxm)M+ 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 U#-GV0VIn6^mWP|0hv&!P8&$O{z5oCK literal 0 HcmV?d00001 From 361fb16aea0c4d768c368c0f6e7cdbbe83afffd3 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 20:16:49 -0400 Subject: [PATCH 1166/1169] Add most recent BFF blog post PDF. --- ...isualizing CERT BFF String Minimization.pdf | Bin 0 -> 333180 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/blog posts/2016-06-06 Visualizing CERT BFF String Minimization.pdf diff --git a/doc/blog posts/2016-06-06 Visualizing CERT BFF String Minimization.pdf b/doc/blog posts/2016-06-06 Visualizing CERT BFF String Minimization.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b4a0cef56e39287081ddaaf473a05926e2826c60 GIT binary patch literal 333180 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


    5&y#nuwayA0%HO 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¨hV>%F{H0ql_VWfZgcmJ6JyX(W|D8W@u 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
    #9#(TmB!>pe}(ksW6f zlTL0M>d;E<%QLbWxhZF&bn*1`#Ur;28rG$*x#be7zGB zELdF_^eM;-X%5K~i)M`S9<|~?XL6qb`GOG@V}NMnxHKS(IPrN!qrJ1i!Nh zkZ}*5fpbB;7U68JG-=-9de&g}JUPBS**<|)1ZePBBb``POyF^4z1&TM`7)5CXzxXy zn^r-alUWUFj!nL%t_ju5yx?h@N@1K7=`x^sO=F6=SPPON_8bdJJe#E(|ArZ;MZ;Qb zi2l(LxNy&DAKFJ;j-OCJ+U&AXaz$ro^tdd{5@lrCBTu?>6Ffs_4+ zbdUES-LOQF6PhT{w`RS0_9}+MM=d`A^_s-T1^m7#N={53Y$Ob@^@Kh+EOb|H0J*>@ zlTH+w75IoJHti`1blJA!d9^J)Jaa~_T!5fzu)ln!Y?wEwa3Y^6uTO9pLop7 zo`%9@2g~?*0>qVB0;4OjOnEKZSsi`6bU9wMD1yxHD#>z1id@>xGX?J>W zXqPXEXz1aQ+836ryxu|ujK}sJC_pEx%aRR1Oox9cR7jL|AubGut%7p2d^_Z<>x@qW%Nw5;A;e$ zx8B_Y&v6Ygw*JaIYm)Ni5;GF^o0o=Z?C{FA&jmXXq~Dm`Tf$1=0~44j@Wmx{f)Jq| z!Ql~hn!u~fPllwI!)VrSGG5VW!4Ok$4I6Nxa!DCdL{2>vws54j*YPi0wx4LvK9F2O ztg(WhB)n8NU0`sJ2=ROo6ypiz3n1%M#jgkuynLCLQOTbupc)Y595laz*o}oWC}@>P zDn6PlYSu<;4FC%h8KJ*)3mtW^=1hfcj&E#t7$0sPn?Azc2)$2#uYp+m z_66ad0xM!hIDU`DryizF*+wUxzN9HaL*zX?ll!)X;CGG^^6mG*1DYOXa|I+dMmw4# zYoC*gI$`XC6|7}vVbtb?>zCh)G6#CK9NQ5T$qPp3?qR{Mf0rmMNO6;PE-7dcb5wGi z?F6a~?n}z?ILquZSxY^lSuz;W5&QO{^F#w z&aH0TYM;NCV=cd)|9;<)G~bMI+cda`3AcoRM41`U?*tTF@*=0uThZ%?hn`X@G3vw@ zWQby@ngbS_<83an;{mN<;M8aIv3$oCKGpnL^ZXAHrH%Hb$`w_Fo*H2R#!S4p5tDpW z9v(gVRe1Gh4Dx9}LUBNnm*)H{^%OmxP4GZg){bm{o2AW5>$f>(# zBW71J5HwS?4CA)cV`|&hFy;Cm59bmqs;uW$3pzwRyKmqhKHT`;(f;Uc@~vPpYnr?O z5*80DjzVwvj7lSw1)gXrT))%m6`DBn7O%|I)lY^Rm%~yGrmh$6IR_jXnr4>0 zuykh2R*&6rvemCq;yN}C#;)+(Bwg!c!~Uwsd~G>K)7fhOn!58|d%=8#_x-`)y!J}Z zS{_5&SyPf_(UnM%;`g1sQ+HpU)aIKWv3+0^ubOWp5!e}eK;t^ck_HJVQA{N{9#O5n zt6e9oowt(Vf(3Mgg|bjN*2#jWQXZ22cK?sFdkm7~>-R<9J#E{zZBN^_ZQGc3PuteC zZQC}cZB*OV?f-uEbMM))@4olMd6AV7E3C{YWg}vsPA9fO`g^lcPXESd4Oma7sVMU7D1`sF-lP zWA?jJ+sG*Ry*has_o9W;@SK}7>=-N%D=^j?_}5Gj1xQNJ5rz&em_*iC;64ceGTP1^P&i z6=hLMJ)DeLHZ8$X36Gn3zz1popJGwYUSU&7d3#66N!q}eI{KX1IvCWt>nROqA2$r@ z4klwT>s&D!99&)H#^i9T&PAF3*+?@zYSU57KSl$Kw;)Yb=k#Ep(8PMhD!iIL0E``34|nq$r(*r# z8p~OcQdJ;3|4IFb9kq7=lVw!?jUH#aKsppD-Klt+d!>4Dc$hLkHdarc?IJw5)xI~2 zgc^_`%WnFhaZT-=L6(!L#m6E6#?+$k8%SZGE$CBh`Feb(@XfnicPYt?V7B6;_ zXdANT)7kZJL?er1OEag$a9iuUj!#KAKIvS~cQa!Jz9eUMB3k4%ZU*=N>fJ^0E;T#! zJ_8T*ym-htpXy)PTy*0@S3TUHY+v}CoKwiou)6R%ZCC521<=gvQ(M>GnJ2nGO&Y>9 zU`_2{P$G}qrTfH&$0?oR0!-CRTjFpUTH+y+%!~B3pCm7q|5k~Q)}4QixvgZa#+=)f zNvH!Tp+)&19cu)X`1#&pL-*0$h47e$C2VbMW9@t?SGpf~mA%$7zbc)>MTg zdzhsz_tYvip-@BUa-mbDk>D(~ei$LgDoBAC+VdLafx}!y_vNyb#G~{oM-<;|qSR7F zU1jQrrK0f5@NVNUHqn%-Q;6T)YKpe8^i--mp6=t4Xr!!g%wVVbI4;EWu&`ebrf<(xY`x&}JkGVe3V8k9FGF#^l>ab31MV&m z_;G0!Ixl&i)-ltRAdwcs99-{AD9yQu%fpm6A%?v?sE5Bx8#y<~)C)i%!)2-G+`$Cd zwP7QEgr0+5@YAaOLA=l*`94ek7!`Bh6Ne;ymu{M4e=%11E(9I-7luZjwagd1Y<_P zgGj=QNJbQwnGq1Ydmmb_W_2#PE-sU3d%n#7tUH2%E9`6XYUGn(;4${$$mIUm zcbm%B>CrjmJbmzu@BSr+X7(%y@G5q%MQneSuQ1It?a&C7ZohEc*cC6)tz(&7~rIuKA&sYZ0N@ zGSCy_odGWw5&rnBr5a>1)R8Xpz}SD<0n)s$HWgC?n3iq*}s({yy4*xhP>(P$m0+$<)C>K zCf((e2DOGkQYL;_GKhrjM%mjlvhn*4+pVFizL{lQql?1rV=$eKE%BrA(#ZRs!F=a%z%;n{NbMge_qO!$KMG6oGL8 zVf}~)8WPxP_!Jnsn;G5cQMUYg;lU|orI??{Z0%*i_ts{}M_I=?&L2EV3aZLJJmRiZ zbaazEm#*B_-N)Eox|ThSUPSl0_$8h}kL6)}YF4yxl-1%s_NgL2hxpf>xfIl0Uz8s*cNf4bjc@l^K=)Sas~qe-IfmE22&Jz$gnEgWLkN?o0;A|VIw?;E zh@=`~WZzY^2nMs4iu$K??Ot1KT~$q|?^Nbh3`1oHsqbk~djq@aV`9=o` z+s~7%=Qmdb-#EO9+*-M_45WRdP9jt-kR`))LEI3R|^hzHX>Uo1%BbZXXN zY@-RGeHrlR?-5O#%sw}=hjLIi4&P~Zt03Z-g!QOY7MqBfcs0^DuXN6~kGh3;=h$bz zV4tx)o-HoD&=_+_YEN-!x2)MKa~y6Xc1(E1e&keU>46D-ZM3prB4F-+Sm2H?1Sr$v zVTSEPkt~0n1#&=WG9J}E&QOKVrS0_Ql1n zi4$IG!0`@pY@#tyd@};%yh)9r02Fg9f{L~=&V--6r%eHh9#xw_ydIz%XzlXqH*4Pp ze#{t7g(a1uj~J_-1f#gMKr>>})`cIQE7Ta4gkta0$9?zom(JJhv~bw4Y$y>!$Md75 zXYJ-gR;uLVr6e&{6+WL}xkbl$Aw5iA{$u9jUj9_IMVH&@5RgGru%#K%$8$vYvAF?e zZ{N7-^fo*}o{~&QjoWNKbvi(!{sAIGj0N=BjH055(9xo5$d%^6*u-dr!?QBp94=$} zoY$Qfsh~!z2=&6JBEhN?%nE}(L#U#U%lu;UEC1O$RB$f~d7Dci0XYjs4XSS^LlN4PoGhozRAAlrPW4f3p=EDDqCJoLe=;n!!3U6%jcPj#7_`r#qj* z77-@&u;1QY3C>lyXD&ZUUEM}57GDu8_g0sKyv=$(PQ5lke0 znnORmK4En43Fth_EmMSq&f!C|?NUwkr7lBlEG$v|7^8XSUB0=i|?iB7t#!LGek>tp}Hzl*73S5U8&pnpj><@#p!sJ#~Z0F%5@ZtK3%ygtuv z+F_e|5iDB|#m5EvL;pb&oKVZzwn!F0YRg@%-?S=(+{iL>{;&zxm5smSCIti8TS3K` z97^ikqj^djhzH+`H38Q5-X1&Ym;EcqCmn>IItJ>3gt-LtE^2ZFci7eSWlxZE=BJAv zM2AcRngwa~Fv$(@jjm%+f-_83W5By#t-1;LE%Q_cNQk4UI+=H4f|^kne^K;~`~9$w zC)o>xYnUr-WyLa6wye#RkazLj%_pF@$lU0BQX!NWecX4mm&lhJm@+SqK(dsOJ`36P zDIz+eJZJDY*Ex+FoX!&AJ~BD0O+4{%bAqLiqrmR4rwFKYExfdkfqxibiWzqCBI+ezzLLSUsjf<9;r=vC?cJ)OA&ITUFd{ z>vocK2{6TjcsuQ9YH56u0CqP(olj9SD(51r9J{f4=%hL3K0){*%?MU@{v%L_R*(Es7x{G)PZ@jf45UX z)563;(?eT>V^Q_63|eQejda$;qJDWjlQ$It9UEQyc)jLU2Mqx2Z}Bqv7XNisUDKui zCNRN|Ef@BIR5Yl!ho7gi&UT3WMtTT+Sa9w?E>McjHbH)d2^H$%Wi<}HI@$eBVpg8P z=G@RWL~G9a(+s%4EdT8nz@t4@n3u``k_@W=j#(Ux>wruPN?-nt`jnF7$8-23%(a|C zR2}_qzH`NlLW(ln;-%}P;CNWlBou9vX{%M-&Qc&WY6(nNCxv{WO5w<>S(Q}F(_Kg* zVEORPccjH!7Po78NTjgeXsY{5_&*Vv8Q|gHAry9jf_sAOO>mWJ?W+hU3_U2Ff4W13 z>X0!GORSVnmoZv?m8_|s=X6=OrMx~4fkbL}Zf0BOygi(<%}{0YeRUe|;n`QqWJ~yd z+at#Yn{;7Y+_!5)v-grG`$`-D)qrWh5?|^%`%2lSOq-V>mJ4P*i^=kv5OXJ8!#y~^ zR~Kd%elUcm1a&xAFoD+N^R!(!)kptI}V}#V- zBAi)j=vSG0;n){)+=zJz>OFu29oF1mrfVd_-f~m{T*-Q^(Ke3%cn_6{JV7(L5q1>R zi`a69_U!Q1+QBKLLN__L)fQ&&Ge3_@s&+KUzaJGW_Ncw@9~}$8U)|cL)g7i&*y(ui zY##XTGENm2A5*Tkp4ILA8Di*p>yz%;@t(x2TrKI9fN1fwl21#siPoH`DyWhQ=^%EY z$~4Q^%fW6>m{L8$I&RGr*4=$ddeY5(7I62y~eYAqd!&t@GVogjlnsnUS?_u+AJ9kcvNYEjd#8smLj zaVxT3ARfXVa!(h$(i!GNx(a8I2q^%K_-cCaaYn0y&4m~pv0J&y{3pCJ)>{1CZ@6xb z?}5v}S`w6xwGVgKBsq@h0xf|HMu_{9fxyQGG@N1P^KqUvHXi;D{ZDol-6e%p6t^3v zY&~0^7i%MSUI)u8Jk83MX9q}=j{}%TK5tXoU5|6?HP>tvU}#goIi`kF#a3I4P^NX< zWs;W6%jw6Gkx50vo@T=6+h9Yr@pHx3I&R@3VABZ61REtFw?B^+zItaNkDq(PXH6*1 z`|~xY$$n|(H?=oumaJOlLWg;DES+T^?CK;PK&`fv#r7}$8Sjgel;_?x{SkgqV5Zv( zKFPBI1=yrFXjKnt{irh_jNhdeHY|LXqJ@@fXEhd7^h5?P`-y38Tj#+g--P6o3Aa#N zA~W^Z`bG56mj;XsM~$LE2@~=b3pA(<9g?=p5f zZ031&zj#^J+3w;cT(%iD-S6(ToH#jP%(v=1@_Jk@bo3S`Jc3)1Od&WvX^n%Ur1uvmRck9Xb} z&O!|br5}!<16N0Jv1eSd#ZV5V0*q^N)$AK{>zQHG#7>-jj++~qxh00osEz=%sSKkaE{g+ z9$z*#uihJuyTX@hHD%VYN6$X8k(y?`!EM|u(6NH+jv-Dn6|{koInr8kb~bf4C6M-+ zZrVYprNhX}-MNzxXgd^(eg<&}f2yv_JIqVoQoP-`Vfld$I6jy@`Kz;`^sd-- z0vcB@mcAnB!j1v9?cRt&kp!{pnSWlWeH1BNAMc#IN(Cz9yf~vXZs=f)Ix;!kh<_W2 zLA5KRWp#ZoIg-e%w%y3k^A@_EoKs40xihfR(YS~)&p(;Sx)Y5qtnb|2HnHJTj_{ES>`-0s6W2Xu|6 ziEor$58WvZFg-k-kD6tGSY3#1zn7-ew)<8rc;yZJ!{Ua=zK5a;x(RK1$2!s4zf5gE z?9p7bOchxd&lsMtanZxHl_=}NP`hx%n$i?$a8z>G=jllv3_>-tAwXe^dxVfWxB`3m zj;*KqD5fbrc#H#DgsE&$a8;vzQtfu!=?-U2MThOZvsS{7${_Alw)iM`&O6T!q# zkn&l^o*Q1K^{UH?637|xlKvRwg05l6%OEV}E${?OFH->5|quJElbdFLMv#+2E+i>LTN|( z(<)=97`u{E1yeyC#vV*J#n}1nLD$+(dA-|fu5i7)7unBSeE|jGVbUblw%hIW;_t&b zgq&TUP(HLyZVv7|JqP?ANt(rmna^Sxs~eU8TMp}K1e1-9nGKWO6&<&g49>TGhrK%m z`+No{Jfq@ucd3^wiKPa> z39c%n=G%;#++&Vrg)9L(Q_3^G`#c&nB6~%h+SQ3(+XmVr)1*-7Tw^6N(rB-(uOEJK z`#j5};0oL!g-zff!l4^~+SCw(!)|J-ukR3HvFLb;lH)NlCLWnix@f`fSgZGYkO7oM zbHV|I#39M1j827UoxpGu>{K9(?{XTS^mH&uPCWU#2HLAHvjb${y->MeL^}7-WJc%` zq1^-K3W!hqs&d?P%_cevJQyZ{?+a}^PGF@SY4H4D?@?czCL6Q#Q688)4oVmha53|L z+oBJWR&Lr>1x*6j{FjF_s}7l7(tIYLIj)Y84t)<@f<58lOXo16Fv+MgQ07e?k&8;& zDbG2JNtoK{9xbAVI=2Gon&Iqtml7OpDn)askxtr_yR!vrCUnFbMhB|}cz8W+H28X7QcX?S z%YbSi3(kZK2rLW!Z9cp;-kbwo1L zD2fxt@TThhu_4V4pS<#xA=LWs-rm)h7+e_)TB z*)!Hu;u9U4lC^w4a^Dkfs%*EDfJ}-eYM@E(mF4Oz9! zp_D5Wmvst@R!s%_hP*Ar8Sk&7<)lw4z{4Oy{dyq;rA9VMg% ztvI;6G17S=X(2-N>DAiQX-Lu88wN88^gkr;j1yIqz32Wo(&Q%WKV6no?YPY8nR&5O8~3QNxi$wZPAVzwR2&9+c);ue_VWW;7rm{+G7-Jb~5z#cts-SnIznOf{m2HUP9`Z_O zl}$HmpRCu&D4ArTYfkGV(!2s1b2x?oV?aYS$b%@F2@m%o!hNJh2T~MD4mwTz<4-*B zSJ32HiEyE)?bi4ZI+J|*DEhb;_)J?!z#FyB2UY%K)8nr+c>L>=<pB?u})ltcyhgH2sNoCSCEWB!ic|W{#h;3ELtULNEK}s zg%$1+4#MzefM>^~Z(#Mp34(!(q=i`|+Y;@o9__f4_sIp_n&LX*PjO~Vi1GO2BqVF1 zIx4IRxLv|JJwEyxmn$<1gXd)r{M|mTkc8CkK(z(RcB$G4wY0!Bv~DRF z9TG!d;DPenF-{_Y`5`Rdqu@9bh&dSZcS~-?X0NHE53l~&(J4teBa|Znz~h%NcdY2v z``~QHkv&(v!$Zm6>DK)pBZ=0_{BMip$sNitNf<41IX>2}-FRgzEjrAN$5Ho#`RZF< zUsW7mgO(@cqe(S}aCirBwi*$U<;>CJhqsiS(7qv^Qp;F);01jau3S-S#8{=8GW<+f zFfDz?3=DgJ=u?iLzm6nl9MQHdW9~eZ(xSkIcbC?+h94u8Q-)(pbM`PfKAIr~VrnYQ(bleQTQ|Qj1 z_mFBvZw1WoG8sy;QYPV9iLmxfpXW-XwsCwAnY=Y!4^4YE`UW0ZEJX&pl>`R9pkDX2Q zj%HGgT}_0u<7$ZFbxO=pc`g$AY#kwOsP(60Ijx&>4)+`SL^+zxzN zeTjU@e9?b>M~_}2yrsShJWLpV;rSi;BUvYcwIXU^!*vn_Ie}MmQg4}=2bU*x#Je@M z@g4Fmsbm&p&BI_fV3;?YE2QARF>vHSret);{SrSK1?(Lt!Rs2p4>)CB{nhEdRr2$U zm}P~O`@P_0f%o$nQT#di6S8NVp%ULOzwBn~{-ZB~ifKWdTrxl|G`Fj3^XLhuQ-8B^}}pd1}$#JfkTNh_^LKXkThEV)v3GIWBx zyOM6y;JQH+;45MgFQ&`l+i0&2@wtWEYV4Y}eJ);`(CYNN-M$8qgnwUVd2L3r;7U%6 zgPmb`@sjo7AZ+pY_F4bM`;61W)M_-^0(=*EYn+JUP(h}3cYb!prU3=bd2k(o06ya^Z-iePG-OqX@t4wSGixO! z%sMh4%n2%3%VIhd>InHVT8ZlJ3doM6GpBF)ZwWZ-m^*t{KYqB)u`R`;b)k9eDP@%XPC*^kxA z^|RV9mUHxD7~rqD+JPI-DZk*Mvs+_989Bs8!)f0`05 z1tE$fl01$08O(J+0%;zh{Z`HnL~yW4AUS=&I|B*Op(*;Hyf=N&9kwb)uA%3^k4xID zL#pX7igfi(vk|}ZeumO_ddGmY(LGq>U<*NGH#G!f#5E{2=-R$XLm{ zaKvly!8#qp`!7dv zwH+2BB2C9wiveK6A&{o|=c^ey^Rh$`SLr?^@|o<)NzsZOO)4$9`N!uCN;&idYYbpu zVWOZr)!rzn!eXrC+!mKmhG8r*!X2GS+*>_VtDPm;yd%UZM}jztzN@B?O!u| z%(#W+(tk7ICdPWmgpk@{+hj1S_{cXe)1eD^F(7Qp1D}^f+mvCy8N`NWfGiNsbrL&!W&d*!zc(%TRd~%) zQFX#TABSF>%NlkwSpnF5pTG^+&oJfb%R6&7r_AI$eyk@jB;)oi>lPrQx`uv;uwz^# z{DL3+kc7siz02dy@UmH%xX5JaouneJZj+%)KLWx0#UM%fjHx z2ym6QQL3i`1TJSt#YP~8FJhSJl#UHBSeoJ?mseQm$(RE3Y+bM zSt8X@8)b%H5W77F+RmCZOvvpq0^pds_{{cy1j#aJmXU$KEkdjr$Q4xvbQ9pk&?Z!e!eSHkG%mgP4`y_ zlF|b1o2L;=91Bo~mZ5$Z;+b!o`>t?C>KZta`RQIo>4me*W!BWT zD{C8oO=QU2>d4i-wHij{QKNB{1m^~!CxO1{92j%w&dzztxhrl(LjGjAB$ZjUp+c|8 z15nPOVxAL8&0tU4ahX14CD}^QK3&<_iBWfT$WgRtn3d?i$lz}Z%W=ydE{5cw{CZZF zmF2%?dv+$ZzV&s4Z+O+IW^!RdNrSz94d;jz*nVpN3)aB_tYX((Rs*oLLPOx45w>OK zZR*~{t`k`S?mQJovN<8SFbUxM0nt;ir%Tsrh0sUwJYLc-y&lJ3uRWu`D?pzq#oKEZ% z+_U``s6O5ivZW7ZLsRlMQov4!?uKp?upreTLYvHz{uOk<$=Xhj7?``2a^L8}M1- zPB6~icJUmgJZkTLI?0`S6pSX(AaWka5?029^h~oy=wfx1v3ej^5cwfg3vnT!Y{0CRuh1IZ0W$>id~P+ ziP4H~QngT3SM98`-%W0v=vuL_c3f4Rx%rgtmVr?EhL?R=BctE`WqG04<6a`nlnQtSIxeRMAzTBOxCKsl-1iOIAI##P2EN0^ZCmgkS_duW?Nki- zFqB8%$Y-gZfzx>iONF}DGYYEcC9$!;zI}D{-eZUtyJK$+LsO5wdQFVJQ;v6 z!MmQ)!POI70z#jMac(~KT>6a2uB}XQG(cu<`5F3-^5XPa1N;1ZWius%>`polMRh~7 zH{e;(7nLev)`AShk+$j8*U`Ft-n>EQG!$vWPG#VHqxlfk3@p5Y^Yq`$^Gn-S)o z=+MokM?S{aR*N7LXO^QmUK-!-7B%OTrD>3{DC-8pWz^~(Z3@cMpe&OvFCq<1Ib>oV zlgRb;%uq$YhwzNG$aIZ(b!RQB48%}aaKuFU(Yy_mV#;c&bRaab909Yp6M1R`(co?* zToJQ>_SJ@V!WM6O4?b0D@(~ZsxMsKXGtIpxes1C+r$OyPPZ7+v51;E7O-0Kk~@9BWO}cccDynH{yLI}Pi6VJ!59jQu^4=|ulA1EF38LVOM4UHD7)h9So)4xzwI4mMldE%B|*9`R>JLvHv>ENj8P%qO9tGb$DXCd zm!i|-Fzbx^Du?zpRFD=E zRHBhrQKmC7b+U9bHK8;64*2-*#?8M%HW>a-jGJ!-=6^A6Sh@a3xrLeIn}Yl%A6c2$ zzUjwrCFcKsgJ%9FBmWusKj$(MvVOym-)ShU-_i{W*Z0Qq%}8>6$Cdp9oUn6#J2*@% zgv^}(@^sj~JrmAvwdS9;?_<#H-)PGBS>M-TXJ-9=5BqoO$Ui0y=YO96k680hPyc%T zk3QM{>G#{cVfhzT$;Qk@$ntG){zuz)`@imu?Vmos?~?O-bpJl`&;DD&`G>*$W-VF1 z<52$N2qVjPP|7!J`Mv+o?Z2>=|1r4#KE?kJMf9&}`&WX?|2(t*4XybW5x)`6?^G6M zLay)0;$mg_A34NtEAhX0h+N;$%>P2xFf&2_Ct349dx-zAHve~)%Quhnk1F&1mHST* z@t;rpzt}YYH2iza|F&uV6CnM!P4kT^a{h1FG|6629;l*Q?yCCEX3Tuzm&WA6$dd=X zS>R|I^rEOF;pBh{@E=a1@bSW^@yf)AplF~-iq}8%JHlEcF&V32F?BSSms-yv#>;_q zYwT4eiaq^R`JezTS5N00GYbp4OPi_>JzqWk4_T}3vZQkLY2hZb!NafV!C$CQCB`U6 z(@<%)hTPMsG8vxU#+_Y3h=zL?3m7uY>nUeYW4gSaM!rF-W=Ho+edSErikR zuH_w7vW8Y>yACaZQJK$ zU?164SvS+@_>&-H(e8#a^;r2{_@=n0Fu;uqKX0W?U{)oU6Dh`==B54=;*CI<(5%Y2 z++y3?lOiN?jKUm<-a=cF3!AEvgkayqELK;SEmdb|m&DKZqSjVFI@KckJ}rZ7&6T!r z7D*I@{CZ+DZTvks-Dcvv&Gf<+0B)Zs7*Bb1A}jGCDl}`f1@uxwGNaRqfOc$VL>~xKX%vv#?h72ShJ}Skso$wBhEl)Y z1A1V1adMQcI$LlCtyW&~WBon*#tqsfQ4k=nFX&{n%Z~Ziwg2O_MkVhoYdbqDegy)Y z+(<+yc+g#t*k3V;0N8X!P9YFf3OYtX>Fl&V_1{PzA=qAh81HC;JRvn>3fKcLM##vo zaTj|QdnqY#n)4qf1TkK?aGd21HbZr=BW#@HyO<=SCOp%bgT@C=ZE4R3GV{b0Ce^%A zjS>udUhwG*Ju+IsLF+A1LP?YI z#tc?zkh;(%lTH_^biz(S`aV~XGYrytSt*jU6-}mVTyHlv|7K0jfHUNIN?cMdfVYB@ zMBN^-=y?cJlv79?(agmMW#bx7j{ zPKJAa4+wV-ioZ`DCzvD%DZOm6Cde^MkdFz`;IM#6!dDBuxmr-V)19mXc-k0T!j6y=)Mn}(RSsMf;QRyTATcT6r@y{&Ei-Lm=` zk3aI5FXJrX-j)R9)~=blN4stAubp}EIR4!GJ=n5&c*(P&UlFv_bjmN^tGusyWjO#1 z%8cp{cdKzdCiL4?N{T*Gqp;Cl%2o99x;~t8CjmfwmZIrC_@~faA3eYF`Ci?6wUPWVva<7Rh>t*9XO2KTTiv-1<0ZX}&xk!J7 z7qqquc<$UGU{S|zQ+%KuEzx}M+_?;YlSoyag<7Shq;fIw_l+=@P%=YVWn;Y%-3-hj z<)@}O_ZH9Ho%dCCG&`MUz0*@z{|Slp=bwdLWxY9e6nd2l*Ob&N$KiJUaV2^I)fSfq;D8ZCqA85N<6zW3{; z_YZ614Q%u7Hj`e1&AhW2VrFsSk&354MZ`E9P#BUvNrkL99w~Z%id+F@mvVGVRo;S? zdw?iPJ@)7X0~qm6=#Kk#Mq}_xFJ)DE5to&UKG;p*Zi|Q|tpseva131od3-7w+1oc% z7+Lt&AT$@CD#mn_^3<~Xp}uLhC#)wJA!~QI4~rYwC)w4NPW}2OrCS8wt(J-??ZYV= zF!?*@rs!B3LrHiGOHOyK^B;6zCU_C|rj4V7f|nC1WI;&ruwp3)zTL3P$I^8n+Q?bH zEXe&{KTxdGB2=ZRmcS6xb;_aaHld}Qh|Jk$^G{a^HKxpJ%H^@ooXXj9#pS1t2Gx?&Zy9D_7NCnE9hL6xsP}b7|M=yij-MZ*|^Rd#6@rm7Txe zb`9)5CIvrD^ZVTL4Z{F;he#CU)3|vwYa!P?SV+S-t^^0yk|&>toG&IRmA6T=YKI-s zno3nh6d_eaD_W@bGG-?0u0e*Y=8Lok{fuhVLQ$C-644NxSY-Bx^a2~0X;Fcaj%jHj zU=5vD8oA!iJ%iTCmOe46*J#zW$DkY!bwr zg?_P0U((7b-)H}RG7(NPm|=E$?OE4fPWPfmSezMtv{L6c)KG2pb$ee@^Dcrxd6!}pTV;pkS43^rW3rK>ObR(HpX+Dv4q7v0(hA#I zzSo>Ck1a_W;z$FtHt~Tt**LU7A^O9ack=elDK%+YN2x63(oAOV!(dvJ6JjC15PmH2 z2ieklxm^>Z)T_xaR{y3W#dK3{-i#)#M3t$2(2DkSM{@1oRDkhd%l>%Bgzy&eSggJY z0GWQw#%#^bo~1hf=UQHNOZUyv%m{Z>4SaX4N8fnZWo|3GdMheFUC35eVlxxln~nD^ z8Avq2=X?4MS5;xk<5R5NcNr2ZF}%$a_GK8W$UYe94_1_ATGI`67PcZSp@_>pr7aGJ zr}QG3;*6%1qSbYi%{_tr`GaP@-=dG9Y!%)JN$5sHU!HYh4vCWL>NZ0|sIgj(>|{~r zo6!8>I@v*f#NjCls@M;DB%hnlhslu}itc>~_7k6U+EKz{?KkJkRgG$+LWqK_>7Xx# zg8XBnz*3_|$TuudMZ!Yt#e<@83g5-@LBY3Ja38I~N`yoqT#5Xx5{_q@aj_7o_$@~! zn_#W^@ofHWxYGM( z4{rbSRoZ1La5(G-&a%ulY4-=PEE7M691hQoKQD{5vD@j}6rW;_!({m03Mu?0KOuZEj{A9@{N;+_Edf^{XX>3m#O0foYFD5)nD!%34H-dZTG$S=KJ0NFy@T@(8*xy@un72@-GcaHm2!lBm+%odMC8KMr*xWN3WG0X}eHZA@*{O_S zFlx9!oGfJNkT_-$fA9^oGAB#+v~UjCvqoiFPz6Iu1mPQ*(@w?}ws9G0Ns!KJg^Uwd zLxQYneXM&6#%vs|YE_IZW3jHCgUpx_D*n=+1cXL;~90E zoUWKmB7!>XAU zFxHPsIvR28OhX6EPas$~1)@G|W*E(bOWb;&ph2P&aY za_TOjhO>r@v3F^?6RY+RUP_OpAR@fSQG?K_QYR^^7AgcxmVmTR&(kVeG<7xJtqNo> zUV0Lbc<9EBM?3p7w@2yr8)!+`x-823t6Z)-HcxrH#p(vZ07mKRl-Xzil!-0O0C0PV zVLER7W45-+A$lk2pe%3B2^(q3C0t%4h*-~Xz%CU?mgy*8kd?HG2MKnxF1>XfPw<1e zb#PIgw`>kRka4OZKaxfEx*G$~J4YH)0UqZdZtx5JLeP&+nkNqe-KaBQ2rcrXsBoRr zKd|JPKVhhtM54(~Fi%xtgI=>XFB?m9@8P~k zf#aKs4aS^sUF~IwgQv5fxrt2HY0OP{%2fidnkXos;P4lz^TuU=<~ zqxY3S7o@ttGJmWpqd$Mp$;&^WP1rzlTF)E>u@OkQB>`Tz>tZ1>4nCKwjyMorn6ZJB-Oy6z_9)26gCKWE>|Lu98UM$}oop4Xgfl=BZeNnlia z>8iCnc#L#~4?aE5s%q^rfnUeu(-g=26bm%$!w*jMsJsqX`Mv_8Mudc-=#{>m=6I49D@3ySvGayv#sY1E!<4IwBEda}&k~XvCef z)QSE*e7Jp-kZ4#KIn7>B|LD!j1wClyP<$@BmmHMFRBjB2Wb5+F)uYgbZ<#pQfNaoM zGu_--$3^IVS$@X1!E1=}>t*2*!F=^eN~S&maOyNwN>*V_Ha=%|i+1M&u3hbyswV;0 zi%IP^m)cvtHp3#g%An0xri1&$CK)eedy<#Yt!F){ZPYTgRHQzTgyzozH6Z6xS?Nrm zV-lit;K8oW*H?G!8-%6xOgzoBbm9R}Iz=D2#0Mc%7ONzk^?8 zWF7FG00Tw(G^>vkrK{Vfv1O?A%#o%jyT?1szQqNHP{R3SR_!Sc`OXD(N*-($47yA_ zG0RA>@Xi}mzK(9r@r8M|{qnk3w!d7#ybdjtYSWIBtGh1RQb85^KoPD363%4@r6!<% z-B-&W*bx@si833W&n+fNeh9-QAPdrvX6oo_Sa6%Gbsb9+(WI~Hb4#q+-i6)Tt<8^B z%O{H6-qqmD)uD;tsNY#^0g_}MSZ4xGHaJmXTw|;C%QgJ;Y3*05S5~jxPoYYS0%t^V zZo(`4ekD9$R2a@Re#eBD+9#D~sdAM5LlwG`JhLGzSb%O8Z6-h@$*zZqehorFUnLWW z>dS`f*8EWakOFW8I|lR&TtqzhCdT!bTLNGjb&;0<4=4CM(lioQ6+Z(Yln_WNy7Nl1 zgA~;yReL{>Z?Ykqqo&jJQ(%M2C?!VH=N`WdC+9T}8=#pEJ8K_<=H8Rn1LTiPeoErgyw0ilc}^!#E3A!b6!Sc2|Z;1ET`wuP=*K2Dw9?4j)fOv*)Iyo}S@E4gY-KRqHv5U0^E~_}iPs&**8R#~ zx!ksuO;sYC$9aE2G-GC;&)AFepw&dl`|r255_okU>#-Z-*jQg-v?CrV z7jJKsYE&-?DihgF%CXTP2tN0+t+ZIwdCJ;$gBz#O#!GNUT&FiGTBP|IHJ(i;e`!*M zSZQKfB&g&2U`B>uf1~t~zyW&*;yv>11c~cvFHQuDLwOc5@teBpi;6`OenY05Dm_gg zi4p3@ZO$dd82O1ek2kXOCL8it8@Z}w?&In3qX?FU+uIw|DDAd{4?Mmf$GO`=K@6sy z8b0^r>E0G%^#_t3Cc_QII>yX0Kr);BtP<089*EZrC_>*>iOCZ4R5E^{5$4)Kqtt<} z7nFR%^t*9e9JB~3Eh#&zk$~bwMWZRZ2(cvc2!KN_SxA=&F6v3ARP!H5lnCUn{E9%# z1WLHEm#eOt#@|timl^gAbwH*ZlT}4~?z4MzD#{lJ?I`wfjYq#hkLE2Q8KcXqS>C6; zqt(g&exW{W^2gi(_Xjm{v;kTarBmPCrQQW~5r`-l2TxfS^1C2svc6BTlnythfwWjXP+nC(`S1R%A zB;z%^_u?GUfy2dL9`;>*A0iy!4HHr-P%abaU!&EkuFaba^tWHeY-Vg*%n&ZR_4;YkU^nPcg}T$k?y*=cmcLQJ9J3m?Q*0$MkSY2Z zF2c52SjB4`jp@B%b-)j=74#9vWU^w--MiD;{hm9Fm3A7Z1IWd z-6G=-W=BM$aAAVuFY5Q1GvIf|LW1xLOCo49ow@~Hw`Zz@4+D~tk6teU+mRwiv7#VO zpWHMU2koC@E~FuAsLBeHUcs*TaK>LA$L7Xeom?^d5~NYo!)7v#Jy`2!V*6*t--W_R zmKkOQZEy;Cvjl**fUJ&>O0cdN<-*P#!xlU+kzy(}6lG$QCzR^Z`bp~86OBG{QdXX+ z8q^(?xp}-_lHY0%O)jh=H)hzcb%yfP6^AB9l3#vxm{~2#2wndvr)_A_# zOZlB&cMzqWPItfQ(^o0RWndU=nsFH0ng7Z>$JEbP3gq>Hf_r{l^V!2&z2`@DZl4WC z@NL1|fD{LtT_3MGTQbK>(zxKYA4+Kx&qAlpr|&ll9F)bZYyBwujrT0vV2;_qW1C#& za@Jk1X--G?QxJAvvrH@GsR75JZ@im_zjKx;io;i{+Q)tdB@|{jR+f(TYCCsXqeV>#Gd{foOI($Z>~$t8P_(i0qd~$OX_hP|J(=R(cjrZu zdKM4aObu=BmuUaE%eZ>~iKVxC|J%ezHT3TD=TPd%g)EW*Dr4BO{Id?dl#Gxbd1xws zYA_Fc;w{{FCcaK(NBzY)P=@Vwz(0kfSivgp?@tuBq|734Ic6(-j+pQV?bnK=(UzPq zCZ?x3hZFfkNfh^nq3;(jXICMw$>-&tzY4okRL5JNT?4B|sn^^#TZfZEjJp$}9x@$^ zgG+Dt`tXUM6wyx5CtU_F4pWg464`JnoLV9qtKTHD;Hu-WtLhrr)aWYcN#dswt^>M&PUw za1AWI;T=wrUx+reY%N^DHur$4AE;A-RhVYqFsnh9j-jp`#L?8cx2I7!zBcY?$AKwV zOLkg*)5ZK{`S8=&9Zr-?Ca0DXc>!rOJ2M|Izp^)fc?T^e3WtQV4VK4!mnNsid7BFN znt0K;uZia%hLf`|Jz#&?KaJBVvN$oAJ6Bj!OUNI3ce(lr651f*QM!%c6`|Buy7R>% zuly7~q3O|El zt@~~)lW9@v)h1r<0~Vddm#b4g2o{IU^|?JD>e#MR^u?7R%mg!M9h+c~gXtYU(RESA`bYUQpZ6sa1pnrvFQfCY zk{DIy&0!p5s3z?I3)*<{bF){?y`~6l52j=aoiy<9yPWLz)`E+W!E~0AQ{qqVPU@Kl zfr%_cw)0z%M;5gN7a>`f3;fUoyuAQwpe~JO!V=y&r0V3`bJ`EWq>_YPobcuLgzXb_ z8Z{g%`|}OW;^5JoXR`=gUg9M1a(bL@di-)~(${_Qc0t|OOPd>V)8kto+n{P3)d-}Q zBWr?Ibh9T-DZx_wjL}yNDI0t?LZV+pyH_&n6Jg9}Vhl|Cs?c6gEmbCaSDVqWx}NW; z`61zT#p7Aa^33LgIF47{pFqcA#TEYoQTf}rk)58A?LP^XKYYdCQ~Cd8wexQX6;Wwv zB{il01489LJV^g95GqUnLWPn24|wp8g#o~QFaanICWb%70(klWs0T&>AHoP=Oqc-H zq$~hJgau%7`e*$I7xIs7z&^m@^v^LyfVB^R75U43iRC|1VFg$Ou`>RlRTu!23V;9s z;2kV~5hrZ)Yyipxz?1;^5hm6|n+Le*jwm4uh2*a195**$Hqh zD+AyvfQ2gH3?McjM*!XO_xE3^08A-a{;(ws?0|dv6S4p-Q~_+vpKZX(2*?G%&-^Lb zpZox&`}@q_Arl})7J5L5nE)oOfEdi|9MFGg6+rl(>;K}c^v@=&|JB_8npXLrTlrtn zDuCMhi&kL=+%5+r%m0Q}0oZ%~lflye3t9z$;{4CF3V=HKUul)U4*dOy{|BwY{^zk* z_}hn;0pK~x1~ByexAvVsGyt@Ua${#xEz~yPXz@GN_30N$WffWieNLg?O?M7uhUxrj5ZZ>}ZSg zGO3v6mO+pUNJZmS46Df`K}Ot6HLVuyE2P@&XmY{;U?rHnST2*ZuE%N&DoB1Q>318^ zCRJB{m%W9ukde;~#_vvZWySJ|&BM-EM_KLj4kn^=r%bk`iISMN>}13C?yisXSot># zbPQP|pA^4F{+%l+LECMfGf(VB#XQXm2iWJdyPBH|&?#0&j+N(Lp%9B}b6s}gk73r9jmK_)orRf;cm!Ms`{w2zim5eOP*Z~X5!w2-v(SKXbI;^iDw(Pr1r&s>WlQ%^37J~ z>1!jI3+L&3a?Vy4AT&_VhKcs^a?ged5Vd+zLSQoR-eQ$;5aqYboc3e4vU{NtE6+H^ z9-FsLD}~-M^GItP2EfFz-n^LSWioa$^aC@}VU&-a^(>{3AN(d5qG5g(7tQ1n%iD2~WnSi4dcU z3XUwNLLByqg`-Uii?=+$_xpGM>%Gj>(V3R?go4LHD*85C5o6<8QTiwnF}Xu+V;=2A z?4}c{iKzuEj!t*01aI`s;&6;5aIf?v_YP6fWHuExmFMLF;}IWU5eFjMXhuz|UUEfF z)!MOGi8GGQK&`AJY~4;1U^}l)OVGY@(&tg{-Jvw0>dk@bG=_#>Gp^RVS7rPKzZklU zVa$w_T2w-G1^HMjRa7Q0n}>Q#Tl0x}F6ADanR~xBA9D`7cnY%)hvslNf^^0V?*Z#R zhiN9hV^)&e1_>^KV}f=Lo??P3OCuJ+z8c*$FEcjRe5F+M({BEh0fxjx$1O=`Zfqhw zQ;~yBGSiwIcX@x}FA%wnrp8NHi?KCu)DXv@!QxDcSw1FoMfw==@!}$WWQn1KiweB+ z&l_Q%>6TaPv{G^pdm6sHz{SkxjYvFb)0C=^-A*tNwv7qVSq8^z;V-fa_*!6U*Xd84 z`@^WHI<5jy1%XXlzy_s?EYOV19Mw@G{f=9mb|Ha>-NGI~vS1EHvk%U}yGuq4Ie$r` z0{oO7o6eHHjv%%~)k|2~Rfu>ZnInCP>zf5*l%yoE9yPzS9$G7M)bWu=BW3>AS{qQ97dHAmJJpTR?r|Ejh7>vI8L}eL7Y*d zhvUc3c;3{W*wap8{#l7HBgE=vHN*3GLqOPPbQybbS_xt|Bm9hE$Z_~D&=Lp~q$GQc z3X)$E$41~Cz*`cuL`H`fjL4sHy5O$CJ_i#~j378fE145viw%!%@8al%*~wv14#Tt+ zTA77gWi$w~;>Y$EZp0*$7?m)}t>iaqiPbNvt!gu?H%xf+SgZoAcda%>NkqQIrW@m# zeMY`79gQCGFw8JSGe8)&VykkF;@GllPclrZYI$oHXy=r2oJc!Iw@qvB`bA&K=7&7}QAyFGB0$JWp;?D2Ps6EOfYZ8nV)a9t z7%D44s%hGa2b#xaqhM&-iqTGgJ zg%My5<gJekdX0100KatDSD7)A*n=-o025vlPCPcX@*lE4N#0rfSuvq z8wDCSRD3Ql-RbMT)3jGXXC>O9IM7F*PpcX24uxmsvGZzZC46g?np{fYyc2FvlyYKt zJAp1z!W?d`BIIMI+`Dz=n|^X;?aH#)Vd zDs*XVhkA=a-(+zru4avkN`*6CZAy?r8ZP5qoMM_dV-$h|!w>FZtvD|n)QyF+ zaY-&_(oQVE(wk*JXsgn^-ZS3))-=Gum2-j2p$>vTF42*!U*^Via5uQUxv6LTYKEd0 zH?Ke=v2e{oy%(oXYaqjDt%%61-}lX6Oz#0*v5$FsWFikG^s z6I6X$H*foqgyXgtocH#7PfpF}+jQ@?#misl=ZK1yeWoxGBG(`sPV-OqGL2;5wU~`3 zeM{UhNkl=&q?&T4gDu);R?fv_;`A|^A@(o!!(>duXl5g{CdlhByLUq?IRNn>;z%w6 z9$d_y#rkk=S%rnI>)-Y(3e$gDpf=805cDVvY2J`O@Kd5JYL6X&pqP2x^HF-K8HLE;jKS-&7Gb55Cku}%uv0)3_j1cZfHBrq3@1w%SG)n z7oP?)eZHJF#yq2sHttZZ20~r-#HqxCg9b&*^X zGQ=Mu&yc_MWWxrjW5kCHfbTb7kQNqdB$siCWIC>U)b}f1};y2w8kNfvCVQB1RS zGzAlNh6P%9&CPnIUhc_mZcj(w<2P5X2r{TaKETSq<*=Nl5E4V4MiAbD4D*@U zEe6h+_HB)0c)Yf|-~9#2hMq%J>taLfy_QNW-_P?*)Rl{x-pYJFaD=Z%M=(x5*`Ha7Jh1!{%Ewz>>al+__(PgZG8yPH!~aofuO-hY#2N1Hn%MCX4?=eg z)P%WLtiJyx-p1ac}P>UUisFQY&6I1vBVarbDi;aT@{8!baL& zUH0o9PlTeN-y^QN@kP1Tx*satFH2xH!#OYRtM9kG!fqiU>R1mE#tR+go4K5TuTYv( zIfG1c>33T}w;BktEp1R`TMao(dU?+>eyA(4OO~wGn;aj6BzkUC<_M@#%A7X+O6ES4 z`*?4YU^x#{J9tsU4_!OYa$*R8k$phF6#0t80WHiS_^@1LI<~op4`g@PsH>u1|d#2rqIX?uR~%G&+ud-$5LzV~`1^AU!3g#dReZSw>o`qANd%&&w+ ztYOt|?WB!A==m+{(%0+ePV5rT)P^EHh9R4&lLKn8%U@;or{Z8r{nYUsS;^E>!l7J| z?+c6-61Xtzd9VpiXX%2;q34iGX+|v?z1)6Kp<1q9Hgr2$(FqPR$cL~81MJ(|uiL{< zW9p(?&88+BHq#H2nAtv1leJ>|)MZNm?W!51@QvQpfLz zY2?Ri=6(W}Bj6f{a!K{P;8F8t4*uD|JqC&RaDvM9WuBP-!O;%I)4mv|w(cj6%(cw>m3i5=0%rr}9K4f=Vf5y;tzOFnS z&dB9h=9m9CWUI(l{p{Z&I6uvRo6P7?PW~bGSdqqYK9%y#z1aN}nA3Vz6Z)2d9756; zESZ|RO=L<$ry`s@*Ds#}=PMIInyCY0fRuQA+5@zID8n(P;gFBjy_R{VzO7!=(;Dov zhaT87r_~GMBNGYno)N1uA+J;O%MB+vCSZ-9=k_Po9MbTu>(Zf7;PIqmTX)(C(uk*3Rjeb9ii^lCZRxTD!2yMQloL+rbWt8SPZxB%y3$EPG40JaTp)b%pw8QLEIHk&6BrLTm>aa4 zJf=p|!F9*ESR#z0uR3UG7sC4yayk%)P-h$U1X% zC2J1l-U73>h(53O5)YR$9`5@(+w|b548_IW2_I1OA+ac*a551@dY~h~XCb9AZ$Z@H ze&d5gE>W3tt+z3H)dEzrcoAM*qv3MkyKz=tD%m!2$$kWP8zcNe@IS9^_h;1Yy$w;3 zq7D}jkt5f;Iq&SEh&w4|r)X(oDVRz;i$kc1jUxplU~ z=B+9Ot+rJj&qGO(^_Py|**eJvYD!IgBuql~SM+`&z|ptjE6FT*zV8C6f!cz$6=&-q zI*3OiMOsV6Bt(szdw*@L?aI$U5{V zpm63F5CAsgkpjK*N6`BAX%9A}?2!GluyM>o0r2*_l-cHj&w(kHn0!Dv2?N1MVr5~g zhJr#OFbOlAAVm$9ZJp@ z4I5=47t>7jY;x(Y_BORdIChles|qAdHeA`(Vc>{t^MfoBk>MvY`Rls5SelH-CG+0; zv{g;sX#2^?7|ff&_9E;1Ix;#Bp2N_4&(h25MPsIv@vcP52u8!ZWt^!{xg=Q%N{p4y zhgKQ5R6IWGgi>%1D z=`D|SQ#zHKTF3_tB+1ol~#4D2FQc z9f_Gwb@Xoe3u!<+rzvUpVG!tIW5@XCJXsHX34dZl&T(1k(Izm zMl4VhpeAL_Ry0<2Ew!StT2^~YSV(rGp#JfKaG?GLYw8X^+pSCewc_W4>W2?Cb;qL8 zl9T#X#CqfgtB)2jq$4YDd;O0PshGKdsz8=fx(|o_t?$M}X0Tb5#XNVTt5?oKH5?jA zT1pr;L+L402#$rqp29@+jP8|Yo@vA&CB~ z!2m+@_FhYkD|$xsY|X7fM8Pm2ZKBdf2|GL+JJNPup#!hZOAg#Wu(*G`m$6Qgkn6e0 z&&lbtBSR8@E9X=vXT<9Y1yJwn8pS z({ECg5_#$z990>bHR_2E=2jYfjaR>_!`F`1FN;{C;d(}_6QkhI zkMx%@(i2kfb0tz+NHq_wVB8R;0mhVHVi_-vtwM~-I6mA791IgB$+P^rPKE`womIQ_ z^%ICocb;ls{)DgiP4nbOP~DLIl!aXfc(?JFG0lzzTIqRWB7}!2-FcHv3>XXNNJm6pa@yzq-~e6iGY? zdN(%K>F@VGpMuP8r3}fv@2RO|dmK*#5qYfIsJ($ga(L@U?9hgYfP;1X1yg{UkZ4 zC*-)$FsvXJVjZNR6d(SHLLGSg1#}nHEo^0iLT#X5I~V3koILaH0GZOWmB>LBA@Jml zB(t~ROUwS+dM07K&L~A$)&QNI0>f_b&WGF-V6H=G?u?8vy5^=hLp1jE0NFL_YZsYS zc?n0&lBb{Kqe?ojONARgA)f)mRCjyaXMU^*Z~2Pno@pi|1G;R;%(ysv@ zmKW4gD!RlZGLP)H8AXGpTv{k|c*jKFY@%^>P2*3Jz#H`G4l+WPyHIcU6M{eUP`gb& zrW$~5RvUn9x!t}|ePK`4t5<4z_`Y80P>*>uB&ZxxMna!a`x{8WKDUjvy9bE1+Y)qZ zp(9+=)!HuwK?=I3n8u@TXXvof_q-5}wTrz)w8YzB8x&6ZbG7_jOhflqhP1iwey0SJ zO^d$hi{Ie0qb|Dl5aIM`1j(E&Is+}yYfbQ2#*osuK7)V4ILs#0+~YE z!i8)bk>?=DG{gx@Cl*?#g+g$JQa}4gy{H??4Nox3i!i3u3qvE5klX8*w=cj+w^%BA z^aY0lAc3fTk+SWyW0qejf3ep%rBTLB_Ur{H6$v-fs~aCEej7Wia+_SDrzn%RblYZm zaoygy8eL+WN~4B8)Lptv@7@uGeLK+^o-029ihI*#3C(PA;^!avndTUMQZu_%6^lj< zUpLhib6rI$yb*fcz85)sD-?=k54ma7lzZa$`_wiW=juY}=h@ZoVMV^{yQ|sSzF4vi zlMXip>tr0OT6LG^ovb;OGni1+Xy$tDMFXvL(1oxi*^NF&iOBK+iyDHIGYYBP6{-~B z_<;R{Rq*l&n?_!p)86>oUob;1m$1d%l;G9ARA zRG*BV=)IR94Pt=UPc^+{2rq&3tsaDlNlVJ33J3R@c7<4y1s;%7kVw2y0%_dY!2A-P#XD92hjzf)BGNublEKwc33JMV1ghsQJ5}p4#Z4 zE+gL;H|d=s+z#|Q&j&Xg6t6=vLc3N6H!KZp8wv?IUpzMqPeWZD$Jv%>JDV0?vlDd_mlWh6JUHm=m|6i!Xzad*RWK`sorT#tH0?;k_3m{OkRkkrVv^6ru|4WesfE0+D zJ2*NCndv(KpbP+Dp#M)hs^&&cW{&up0C5sF{C~zW{)+qez5b0T{9#o7O3%v4*a48P zmA;d)u<>7c@$y1DIyo5YTSL16YsYoi^y9+^d1f1oN!j;yViJg;W$h92MI=IWSF4F% z2P`&UtwMx3#w%F$jC;{nw8P4Q27kSYF2G?3gxf=3YOWDUwSy&ofoayewYNyumPV}Y z-t9Ee`f`Jiga~xgfp2+$BO6OA`y}6>>N4za!tSjzR1~EtsoRWid=#{}HJx&rE#tue z6JT}ZZZ`nu&WCnFcU?mkG+P4yE=PieQ#GPyB*X;0;H9c=l(C+`cJj|sVYWBroWr|ug3Vh z`u?k_|25U}KezB-R15PzlurZ&ZQTG8EdRfOTK>oj{j&rM``>y)+}wXtEC3+sA5YAG zO0cj2=%9bgv}pg3vBDBT9@?zvXmdea(&3WDY0RrzM^*mqoH%G5Or@$;x=ZQ&eJ&|? zof0Io|2f~hAWoFPLL?YWiRuTDV16he0We6vW{OZS*bdOzC~#g$fWsbqxWg{U^VWn1 zQeMoS`|ayghP&;?MNA6nFP2crM+SN9?1gI#@1q2nJ@@z)>!yf}(E+qSsKO?*~qZHyMHBBMCo-=3e#u z5-_=iua!(3wI%3>oW~OkL+ZZ>nihrs#1C=_XlxQX!Z-UB5{G}Z4Al^d-=oR&HBsNf zTpVythm}=;hTtZ7oR>7i>!w?+qwll)isr{0uNeW6Tb2hCH znA-7pE!?yT-vL;v*aU4X`!6y}!-oCY6ZX__iqB&4+ji3w^)S3 zkM(>*!+8XWhIeUokbN;rS0`FuvlqVgEp+&aY7m1oHq=TTQv?Rouf82_vE#5_iu};Q zo}5Zuc)@mk+HcKf-^1uW`z)+R%?37|&Li+s(UPNY z`Z-<9I9Eg{Y1J$hYhTPpVs+0_-!74>A+ptf+?ktmI_@}a4eMsSbyyf%?2a%e$?dDT z$JcarKlGnD>_8+IDibejRG)m|YUwq{@EcqJ>qIgcIT7Boq`OPAqPb28V-;LivwL)APks12k^{>L0b;^eC)=cS=_BxoX;_T(@9|HI0uLXH0C0QC^v6-4o^AwQOZlEY-O(Ki z^YKkBy{Z{0akW>WcgG0&xB3gg9td|FruVOp_XbOtlJ7pOEit7ZCXMA2t*VnIJ1TWl zR3X~%P4pQzj^j7?d##&7tSJk#K7tge-o zmvrtSnc&EID&kmJ6b&^5PG3(}<>Wj~Ipt_zD>s(cwBjkL;zM^g4mY{inbtclCzNfy zcJ21I2e*$#e0|qigqjZZ4!a1+FKonIsT1cdRZYr4y~RL(2yl9VKraB(@_g<*&!jtl z1V=uP4DOsc7&7uX-$y(dKEa)Cl`s8^j^XeAo`I2`{qLUPFU0e|_YD7L5FnH3N>;Ejk_6KKV1`v;b!hg500@T6)B5MFtmgSGI*zHb#G1<1dc)zcsCi z9H$;T4qvmZHeDX!oAvOn;?%hqtHH;`ykJ8UEybFsP!9l(?c1$1r(zLLu0U(kFB5Hx z>T3BwHm_IH-MPWr&@nQ7&3gK?eue{A!w*@gs43gaUC&hw1DA!K2*sjbTu}G>zzp^$ z{o@HNeW!yB9mceEf7@(#GhnwhT47OMkf{UJ?yaBV;x|))SmUSL+J|vKZp-2)Z=89) zedShKbPhG$$kA(DUvs-zoqAabidK|Ezi(V<6tm00*=)r18uGE-etkkK%1-4p(wvZe zo=4&kE$S2;S<1`$%~i`u0QY!gtw30=U@OP<&1uI-cS@R)#G{Vi-I!q?DY7J&dh)ie zB?w9wtXCPI0KQA}Z7_siLmA8ktTCC;4!^oMG8fFBNC!+7j08nwOK@vGA{W_TiUx!& zV73|@@*esAW~b~>ls50yx}c;eYI>FZn>XbCx4G%q>Bo@+AVIzpRWG(MdOu85eMtRk zbIcHgkOQq?9_F}iDHIK1XwxtlouFMi5S>uAZRU0uJbvJ80+gS@&0(>^!_=8<@Lbrz zDcjU<=o_KFMElj^Nx))aC?oGkQ+BZ)Hz!EaH00G$ zj3QQA>lGv@e_2u!tM)FGW{gmA7){Vx^-ETCNT-l3m`*FiSPOqkRZGV(0NS+ANH~ZU zQ(`HLI=sTXD$MxabdhgUf@gC}%wpV5uBBLe-Xx+d=~2r;>{ORLm$Wv)VeMz$dsN*e zjYr262seSx1Y)(*76U|C)?f~b_4>;gpq)k*FMJ{3Vl4&HkQq&hv*JF3c9p?@18F2~ zvMo64GY(_f^ESrnT5up)#Z+D?aNZF79XqTM-j;VRZ6Xk)(h}_;T+l9eR6~OmnTcfp zDNrJ!+>P$3_zTYuF1W}r;O^pL+OU0k(#as+ZF_X2>>!3?#+|~a>pseDEO?|oJ)~rT zx+|c12h6o0-dXqM0Jp>z7m!0}qh{4^f44yR02gHc(y?(S+PE$7Op2?NmZ3HJ_d|=x zbe*ShC^(R+NVk)QSZmDX9$=EN%sHwhSv8l=m!zG3YY;d$fiF26k3v8J)Q_k+cJ|VO zh^Re$hHozM*zTd4qwf)Iy=+%v!7YNnsukUIrNcc3@Fdv8;iPr}gSyaHe=Ov-8iN(R z7};DGoek3Q5Bx%@o&I+2scmGZY???b4%@^es@t2O%B8U~Y*k}tgkp4*J$pm7v(CE? zkzOe76lv|$-&3l3RomY!*I=OpgEascWvZqr5v9E&0V36#Ppp#TH8Li~AU#`0)ayFD zC|49et~r?#9ADbum9h3VS*&QUO%-#V*iQ+;1r6xk)*&VdB8rpi6Q6|XtguWSO3K|@ z+&=fO`|hL+>cmbnbgv~m-#nHBkxbJ?je?Y}zccVO!G@Cy(&Mj~#Ov`-?^u+T*5oND zVx3M%1VqK(2EiN8wZ6Yb--7SL{d9W0*rpjg*4^xQ?Ccm4@pwxSl2aavIe&>|+rZUm zGd~@B?+|)F()3n53}Wj~;SZz})G_uF947mjOdMk*$b=o<-d(~FgX>@E4)7{2f=*1w zgL>-lz>ttvmCV6N{rQlDD@JJ?6#-G|CRi)iBsM4BP3I|q(wnD;Z>ISD)&Vf#EGX^C zf(;pBrJ&`{n;_up%J+e8`bO(&_u15$`1;!iaevr{pz2A^|A{_8JaEkJ96BZm$A>ttCIW*K;Drni;I>^TuyIU9qpVr6-cAlY?--?)H+v#%Uty2qs0 z35_-^Ij{v}{MR1yiF$}ftyy^bOumemR2e!AlLS68Y?zAqCd@ z!+xbjgMAritzq5uE7jxKZx##b@VhfA&920<@yku-Pbx|I=hek~qBdy0o$`>SZ!sE= zUC_G+Waw-x)mS$R<3s%kBkgJv+~UOOY>bXg=v4zoji`~;&1s5K>$@*j#0f?ietmIi z*6AV11PofRMRyv^^H#7DEvf1^`E}Sb{JXqm1goj!uQAPm*TfpohGpsvf+cyiloFU& zlqVUyD0;(dl4w7ae?)&PXGior*~am7eT%jqc*0Y7>=E1K9>%!jZGOG*)_om{{akXp z?e#sKiue6-vDr{mXQ?goIzl8inc)41J+OuOo5|~RmJ3spEavTrZ{z~!S527!e#o;O zvb9AZ>hP{Y?~Usg8AT4^ypB|%ee%XlzL5^h7Zn}E6(x|bG@-}GAEHbZd#Tl<=**-+ zssOn&Ewc8}$sr43j|sPQIP^?6UQUGo$)gRGukq8wBs$s#RXFMM3{G~F+{$Kq*NjH-oZ)(TerfyDO5=9CcAHeq*Q}!k>nZo$w|4D z?HV=F$BWK)X^)5qG2I5iTsekHvp=vkYehIWGZ}JI7BUoaRP?6oM>Iy~X&#lSzZq>d zzc~;c&z0YuQ`};{&b2m^L3u1;R+0WeUVsKs+t5Q$LZK{ZG-3kU?Kq4r2c4)NSlk9 zzXjRVjV_|8TmYrr%Vutw61=Eu_dpk_CoD|Czr{4-g-JWriIT%ij)nA}HDjn*j&nmc zOc^!rXhj}^EReGv7mwD#bNI%#G^VyWN>!e2S|gt&hi+aau8|$c58e3ju&H`@(yJQf z=zGE4*j{@7i_6s}%hPv>&R2@^g{|pfz`z@ZwUR^;FyTv+HRh0_%W<#EqTKa;z9-u9 zz*PLLFBEQRA>q2UC;)zO z*9)(do>V+|+Tj^(=dEd@)}f#5aIivA-M7aBgnRH!t?~;O=w=Eb+Gf*6-)8e{hgm55 zDT5tFo~I#o6BCoTG@U9>9SkXqAk_pYgY!`)x5TcFH>pn7in^OKMzPLY5%duI7`c~5 zY>m^gxT5-^wg4(g>Vh|CEU=-;^OcO-F~O-V{5_-g3@SfbJ7-E0d~EslJ?AJ!F&C%JSsFz>cWrxHlkI-`+$D0O1H{F#ZPl&VdN8xA-r?``DQ%9S z9S@{~XpG@0xUt9|xqFx{6)X76dN;kWLWf=^DLnxhEvoQhT_Pcz#*_@u6=5~}LmBNl z&hALp$*epr2W>-9O|?8U9d*IUWHzTf9N&-jVPh+*WVD{+);S<5(O)sNb z-76sm-|E?EkDwrF<=r<8kz;^1Rn))KzYxTya}9TgIawPVEtDC~3?25vL5&gdG;P0?hOwh8a~I;!Fpd@>(~#L{sbNM{+w%e z*EeCvJjgy4OD;KhGsRBQZ&gaOqWUn=|2hJ>!|?8te|dnOA#$8Nd;>4uYBlc){A?>? z(W{{a<@rh(76Y!VVFXLB+yW9+gepzrex;Y%tU+!%ln^X#y2s)=x(pmdR~vn1rj;kB zmeLzxmXZxCa;mYxRvI4}#jLc6^|}$Y_p-JGtu4N^MB@3eum7s+PH)EB=U0DCu80r5 zIBlfw#Tx1H{mIl6%LRK}m?Q?4GZraoT`b>-h}5bkdpAbOPj;IK-80tvJlh<)xnsho z?y=p?iWr9J5nUcc-Q)ERGhC`PY^qnsB81E$jOE==2Ws2^)TO6#8gku%yCQ_9CS#c8 zemk5O=_ht-h0e-CJwa{$X;VgTo-MXmIGFX~x-0n$`Q8Vh543IZzkzN3Qlv7ov;PCo zXZ#D~|NX!6KLP#!1-6lv(o_%^rT%SPF6eIRY;SA&Czk(DSOJRjHyZJY*!+{W`IMjj z(ej^Y8`i)1>Qu!+TRB0|6&6F#~^><8UMxKd}@IIYvBKlzv;E=XFw3Sc#h;P z4ksuvH>RKj#7|7+dIJ!}!WQg6vVPj?t}aYQ@tPK__hPP}9e(dL&ynz#ssTod`Ic<} z`CVfgHp$%8gY?k$xl?swl8h>Q+fWSC!R*uOS(TDFTU@OKElk^l6? z4=;zYj&}dCB@%PO<%ChczA_M%F4&g;va#PPzsjfVPhX`CvM-j-!9%sORU=|d7)D;& zWZ8D_a)Pq_hK*Xp;!n1sN)kSQXXSTw7kvmnfDk3m%m2}4Jp4cR_m5@xy);Y={}7%3 zu?v4K*FU%SpZoOZzWwQ$|8IBo-%vG-1hkBw-u;Z96yWbo`j7hK-y8T(+=r3v6L9&P zSw0)nzcb7K9Fktp&HNAkj)3D6Wcxer^ZUhrn∨#@PPO)cn!#*Teqx-uN$W?=Rek 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Á(u8=2eTem9hP3@u=(qq}rKW6jP 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 Q;1~)}y5-=I*H8fc4^g1KNdN!< literal 0 HcmV?d00001 From a6af4a342e34cb685f4d9e1cb9130c1e76b4fa4e Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Wed, 28 Sep 2016 23:09:06 -0400 Subject: [PATCH 1167/1169] Remove unused argument in help. --- src/certfuzz/tools/common/updatebff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certfuzz/tools/common/updatebff.py b/src/certfuzz/tools/common/updatebff.py index df4591e..edad243 100755 --- a/src/certfuzz/tools/common/updatebff.py +++ b/src/certfuzz/tools/common/updatebff.py @@ -55,7 +55,7 @@ def main(): hdlr = logging.StreamHandler() logger.addHandler(hdlr) - usage = "usage: %prog [options] fuzzedfile" + usage = "usage: %prog [options]" parser = OptionParser(usage) parser.add_option('-d', '--debug', dest='debug', action='store_true', help='Enable debug messages') From b588971e15f720dd165e20365b94302bccf9e4f1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 29 Sep 2016 09:53:45 -0400 Subject: [PATCH 1168/1169] rename other files to avoid special characters --- ... CERT Fuzzing Tools - BFF 2_6 and FOE 2_0_1.pdf} | Bin ...ERT Failure Observation Engine 2_0 Released.pdf} | Bin ...d the Scenes of BFF and FOEs Crash Recycler.pdf} | Bin ...pdf => 2013-10-23 BFF 2_7 on OS X Mavericks.pdf} | Bin 4 files changed, 0 insertions(+), 0 deletions(-) rename doc/blog posts/{2010-10-25 Updates to CERT Fuzzing Tools (BFF 2.6 & FOE 2.0.1).pdf => 2010-10-25 Updates to CERT Fuzzing Tools - BFF 2_6 and FOE 2_0_1.pdf} (100%) rename doc/blog posts/{2012-07-23 CERT Failure Observation Engine 2.0 Released.pdf => 2012-07-23 CERT Failure Observation Engine 2_0 Released.pdf} (100%) rename doc/blog posts/{2013-09-30 Attaching the Rocket to the Chainsaw - Behind the Scenes of BFF and FOE's Crash Recycler.pdf => 2013-09-30 Attaching the Rocket to the Chainsaw - Behind the Scenes of BFF and FOEs Crash Recycler.pdf} (100%) rename doc/blog posts/{2013-10-23 BFF 2.7 on OS X Mavericks.pdf => 2013-10-23 BFF 2_7 on OS X Mavericks.pdf} (100%) diff --git a/doc/blog posts/2010-10-25 Updates to CERT Fuzzing Tools (BFF 2.6 & FOE 2.0.1).pdf b/doc/blog posts/2010-10-25 Updates to CERT Fuzzing Tools - BFF 2_6 and FOE 2_0_1.pdf similarity index 100% rename from doc/blog posts/2010-10-25 Updates to CERT Fuzzing Tools (BFF 2.6 & FOE 2.0.1).pdf rename to doc/blog posts/2010-10-25 Updates to CERT Fuzzing Tools - BFF 2_6 and FOE 2_0_1.pdf diff --git a/doc/blog posts/2012-07-23 CERT Failure Observation Engine 2.0 Released.pdf b/doc/blog posts/2012-07-23 CERT Failure Observation Engine 2_0 Released.pdf similarity index 100% rename from doc/blog posts/2012-07-23 CERT Failure Observation Engine 2.0 Released.pdf rename to doc/blog posts/2012-07-23 CERT Failure Observation Engine 2_0 Released.pdf diff --git a/doc/blog posts/2013-09-30 Attaching the Rocket to the Chainsaw - Behind the Scenes of BFF and FOE's Crash Recycler.pdf b/doc/blog posts/2013-09-30 Attaching the Rocket to the Chainsaw - Behind the Scenes of BFF and FOEs Crash Recycler.pdf similarity index 100% rename from doc/blog posts/2013-09-30 Attaching the Rocket to the Chainsaw - Behind the Scenes of BFF and FOE's Crash Recycler.pdf rename to doc/blog posts/2013-09-30 Attaching the Rocket to the Chainsaw - Behind the Scenes of BFF and FOEs Crash Recycler.pdf diff --git a/doc/blog posts/2013-10-23 BFF 2.7 on OS X Mavericks.pdf b/doc/blog posts/2013-10-23 BFF 2_7 on OS X Mavericks.pdf similarity index 100% rename from doc/blog posts/2013-10-23 BFF 2.7 on OS X Mavericks.pdf rename to doc/blog posts/2013-10-23 BFF 2_7 on OS X Mavericks.pdf From 8d1070711ce324033378814053e3949dcda08e01 Mon Sep 17 00:00:00 2001 From: Will Dormann Date: Fri, 30 Sep 2016 16:35:37 -0400 Subject: [PATCH 1169/1169] Update README to mention drillresults and analyze output files. --- src/linux/README | 5 +++-- src/windows/README.txt | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/linux/README b/src/linux/README index 8a8d899..24161a9 100644 --- a/src/linux/README +++ b/src/linux/README @@ -94,6 +94,7 @@ results/ | | | | |-- sf_--minimized..callgrind | | | | |-- sf_--minimized..callgrind.annotated | | | | |-- sf_--minimized..callgrind.calltree +| | | | |-- sf_--minimized..drillresults | | | | |-- sf_--minimized..gdb | | | | |-- sf_--minimized..stderr | | | | |-- sf_--minimized..valgrind @@ -127,8 +128,8 @@ The "results//crashers" directory will contain the uniquely-crashing test cases. The variants that have crashed the target application will be stored here with the BFF iteration number appended to the seed file name. For each uniquely-crashing case, there will also -be a .stderr, .gdb, .callgrind and .valgrind file that contains -the stderr, gdb, callgrind and valgrind output for that case, +be a .stderr, .gdb, .drillresults, .callgrind and .valgrind file that contains +the stderr, gdb, drillresults, callgrind and valgrind output for that case, respectively. The CERT BFF minimizes crashers. In other words, each crashing diff --git a/src/windows/README.txt b/src/windows/README.txt index 4b225a2..850ba71 100644 --- a/src/windows/README.txt +++ b/src/windows/README.txt @@ -85,6 +85,8 @@ results\\ | |-- sf_--. | |-- sf_---..e.msec | |-- sf_---minimized. + | |-- sf_---minimized..analyze.msec + | |-- sf_---minimized..drillresults |-- \ |-- ... |-- \ @@ -136,6 +138,13 @@ This is the minimized version of the crashing test case. It is the "least different" version of the original fuzzed file that caused a specific crash (hash). +sf_---minimized..analyze.msec +This is the cdb text output from the crash, which includes output from the +!analyze -v command. + +sf_---minimized..drillresults +This contains the output from the BFF drillresults analyzer. + ===== Fuzzing on your own =====

    G8#Qk{<%67xs>|3 zEzQ_I>h~kDgp39LE0j_V#|eZJJ3cY9k5bp)9Hvt=1hdP zL^{Eza5fJuef~H=7b2dK^2(8DPH_fh#Nmo*k5DVkBU3kUbRY8PQt8^YF8e3}c3Rd| z-tc_}MIL?1e>6M=T@y*)oLG!a!QKe`*1bcnGa3q=C1uz@A=dJ(L-C`L0hhaV^uIt6rMOeF`Swm1qR z5}exi$J~ouaD<0QwA|$lXc&S;cDvQ_BGzNWOURx*eC$zl$b2oU$exV_TM4S~K`B>KV^~cHQ`XLtH6Y72HQD;nvz6Qooi{T; zzk>@xR*zyk6XS2oSS(6&aYOXwSyw5!LDN#GRO1Rd_0}B#%0l3G6g^1H`fIxc6IjG2 zoUm!9a%ghOJepF2B1QQFxQrZ3tH_}hu5E!N$J(ahbRn~y z5YcgLDK*XiV4~qG?fVmX%)8Xn;xW0(?S!^BQ~^rHclLHvn^g~2nGk}ABf0*Mfb^P5 zM0PH{sCL^bo~p~DtLW2!3A^M~)S_!pUZ`AfEO%W8vc*4yVRwM+_&^bc4B=8LDmcEl>H zznbAn@02OBa{3ija5VqCHDAq0(d6fLBnVFHRRO}&nHHI;AmXxdzODBJ6jpXVT!_wy zY6D>%M7LP6bRTm(VgfY~2k2Lm22Q^JU>ej-Sa6m_0aJ_OD=3V=VONOh_6I9~4I1#g z_ylsFmy-kA^#6=ul0i{hR$P|g!nY)!6o#-jp=O@xH%gpoXQb|+>yp>IO-*c3oo?wk zl4VUcO!-q*$=p0ols@m^EB%MRxxp{zx)TVFbtG?r(jSk(nwc~Dkq(bQLi8fv^OO!F z53QC+`kbk5A3DdHcoM+=lRKI{i$siqgK{5x#JFsZXDNC=lzMX37t8~xDsp)MQ{rb> zr2HicimArnfcJBPD9(L-#eh`>6Q=z>T($1M=8fCE4C!gdu`%SCvZ$0!W# zV|nCE(vF2j0QGfvsPFP8w*xLHU4^dOYOJLT#oof#RO=sT@sDmC)Ffc5J`<=FO9}@I zCD{B$x;KnBVAA<~O3lM}3XnM%N0M68E^CL51>QGt#~y)lT@)k@GS*=${D&?IWP0CI zpb;2mlD}~LYV#1>k0$pbp)(kPhxG!mS$y7RpgWr(y6G56JhuRjX!%k4AUHa!WtEe- zakO`2isrXc{It|8!UhUZyYQKd>g$l=gH`nfF4YB=YZ|Qn$aYfg9~=C4ccSC~UA;bR z^l!~>2y*q?v#XapyUa_eilYo5pGkmN$TuXBp!BcAlz>=4jwM6LG6hc+G66GOp0wcz zLp5+nq;u$wf0v0~i=ktYIfIGhQ2B$uxvYu4V3ed160kHq?j#2Le7OHIUr+^EjRdPnw%`|dKLs7>(V%G8lX()EfT1F9+HBKBI3>1HXKqVFZiobyMlx& zp4Pi>$5z+`3`}m31)J>2MHn;UpUE0k0tVjJ2<)!G%^0b+kS3UA*xn z*z;T8x^BO}+R0CrODe$qyV-b__K?@@vs4eNmDf#uDpIvG*dxtz! z>rn^e*=EXEO6AW{fLNOHoH95)JFApOM!#;kL;oF+m5O>@>c)3oYPbX-d2szS-vK8xWbPAtEK3oe^PmJH55z)GSoH>Q zcuuopGql6`Z=-sk-E33mU{!d9x}{RvrR)*UvzrNY^PjcS9-IaFKw9}E+wL{-8XlGp z|Fp$(r=w3yB0z6>#*GK9>qBOnh7rnxzJ6H=zO~k?2Ou9JJ)=zrKx2(+XFe9$#tN^Vv~ZgXw)R`?Zxfomk{mM&He>=(KMRKe_4a3e z4(P2H46YiDg$U{|*P_7;$#j;*aB0m0>w}7mzGg@X3uy~Cmt9{90WS@UU=Cn{2#V74 zD1^cf%#<3Mg%a7Em$YMQEIM6gb{?5 zwsB^Ub7l(g!ze?7!{$o(BhLpis*75SoX=a$4yFC+E((E-Lq1>?Xz;fx<&q@0T3~8V z|5#K1jgzHROsOrIkk7BHpBuhNRC=twpY-}zeBaaY%5Pr0hovsWo}q&CYW=svt_?wt zBa^|E_lk**AzmuGD1#=`aO-edkf3X&2Q>)c6%lm5i-Cs@gTFEC`tQb2tNqEsTMeac z`G&hhgo#73G=6eiJ)#0RH88-42mGr5y;xV9oK9*qjZJcE7bQu4Xt&#rCL;mmkZ5Nv z2<5Oj$+b=M@1Gj0m=<5GSYb{7X~5@=o~1TTJx*swVSGXSpNhhmFdy|pff5F1LCLnq zD21Tkx$)pt5>Wkt^^h!;Otz#E)S5H#xu$>ka$g$-Q#Ye=$08UD`ulK+CnY7rmG zM>G2=ap#-yHLV%9tdO0uiBe+cMf@)aMmUH{j-yhM&~S=zdqGvn@CKq8n3rgBskIFI z+C-JseLN^p=5Na}aGuD+V zD_>)2j==f5B0_Pg%Ed$dFFC4CP7x&2@Wv!h^4ZaEU(VKU5a>5l{M_DhIznYQtjMM5 zmuADRdjy~ks9?R221;PPbjCp|fL(ls61-*64oO>g5w~`XvAaSB=T+nls}?hT{O(|i zeVF}slf>Fcu-u*_&rt9|>^Mr1+sRkKA|a~415`NolxrfK4Z7lnoHmN&qtp|bk!9k~ z$F_Q!$0?qUr)`$`^GtS{rAw0M>AJX;lz~DAZOO+}E4e{DPBUi{svE(OlA75yIRiMk z(*7DT*>io@O#5urBQHqIW7-6XYANYFs^(s!X&x}X@Yep*3^_PeRR?j2CC7#jGOvd| zQzP-nJZ?UMzpO2Qm+aCXO*;0ZyeLyOG()-|xg|g`DDhnSYD3MVov93;qfi#vRP<5J zfG}T8mU7r*n`!rYHjK0zMS#XS@bv3nH-;BMBIZSev^AYur@Lr02%!xZD{GMw5WRj` zRQZ@=@!PzDo*)9UovO*<2l_#|ue!WuMnqomT7sZLJy7OF;3MHTA}B6pst`FLJs!&{ z{Wl_T{bP1$n3vuho63bCJJ@GcevPac;4HMn%HS*RzNxHuWtO?D+OR6v8EX9Idd$%C zfNyMAS}(_@^YL%G#3kGQSkPwR99sD}3l$HThhgZL?DH95QeJft@~b1nP9M0rP{7~i zC*&CTBWf?twIl~+$5ms%zENwwvra@lymFL`o8mH^orSYMo;zdF1tshTJrX%XzSX3? z=+gGQ7I>Ht#%DB!8Hr$Ov!IwxVCYyPCm$RD;^;{0+-yz3;S zz7ihM1ROEdj(pH*ndN{McmKVeXRnz!l*W=a4IDi|LwkCgp)aO;9(b^ih|s^B_3eX& zo76g)Lty7HtMIW2h8p4=KJb2MgJzX5^ZFskKI(Iz^vOc!{b$Tp-A|)b20s2PRAijf zS4;jp$T%p$RKA2Z3j>*W_J2zjwdDVf z?a<(8zu_2Y2KgfygbeOiRWMNj2LvD~%GVKodHJlS-42_Q^(j%(^w;~tcG4*rN$gi) zUa`kzP;9Z8fCE(WKd7{70f@h4xoS-A1*7jXqEV8r@oa5Eyk<^EA!9=p)2hgaDKkv) z;e~P-DA1l3ITqj=vPVe6)5Ad?j*Z9fGbMjGX?OR<15}E$n`Pl&$bS8e?<}-E98|c% zMbR}Ht4Nhk6sD&|^Ew|?FXMact(DUAM1_lA^r_!iV3!g>J^6b>p9E`fh$@dsDy{I9 zw69TKd664A_zT{RCZwiq-5u2SoGDp&<|a;A$c3#7d2TxsA~7fW6jwzARm)b?--8fa zPNGxZZeKeGMwwD}KG-G2e2EhnGs$)tLE_YYslfArCoGgV6$?Vn%@txB|8JGu4UgUnw{B4$ltb3CPVyUXpW1x#wX!zax+7N z$Ui)0PZo)fNbZp5{n59vUXolf8z9x&(1K)Mcvz|y4=1?TF{~g}munmd3$ha!Fq}`R zGQ_A!j{N=VTl*mU%7W4cF!7f?D#F^4Tx0&N->>@*Ngz~P>jwWjv%!P02cOgf0j21Pnm5FeP>3o5s3u#(DmU5oBmlcr90tR z{QXtLK%5_W+}B4m(B}|CTQf7vK1WmDn65Ays+vL7ntxHPrKIpNNqz9&;%83M9iE+8 zB>GH$l^AlyNx=5ls`Zq2Ki3d5C2eq6d3IQYU5TZs>gU2Fpk{PmDA4$!FbS)WF=CQ= zpy)$+X14!4F0|h3_(dN0up+p6T?~IIx;r3kgLXh4loB8G3+@+9EnlND)5EgaKeMn% zJCvEaRi}($hu%wRN%C}y*;I0&JZ7l@MoooL3XDA&bUHd+uP@y@gV)THlDES-H5bYq z#xmv;Eh%aFS6K1qiMQTeabs2oNVq7TV`t_{$-7l0+|;UaZbLk)XEc%We^dXJvw0#N zl0pP=E0~7qgfF&s*ipf@IZ8AaMCgYexu9t6cj2kIN{P$IRmrx$Sx6azQr4$F<=X1| z$4woZid`q@cp)o;@MoJv5nmK~hnD*<_xOJ{eYuCT=bHR;JYTno)`^y6 zsOvfV7Nm0ZSF{&K(WHP1HqcVq%iv_i7v3(GQ4R$*GgC<8*P*ho4;t@?IgGbdsnLGl zAp>$)DQ+)p3WO2(R9;~zmE)RGzt(a3rXTEDXK6O-e=mqVonqAdF{BK2RfvcaPT+vj z|B|a4NfF8<{l3>1y_ie6lUWh#jlnZ*gYlrGszNr+gtV-wM@zV-xlF38O;o+r-*dG> zsC~7bRF()*jI>IwF9oZxw6|cSHG(QO#5Ny^p(|%{RuHQ;Vd!Pm!tNs5y&?W~T9r-R zgB=G(qj^D&1$#vQ*rlFHw`=Z?-}uk0t&SdXd9k_1ZA*CX#c6V)sK*K|lmiG-(S+02 zgN`>|_3#>I&^_GaunLCTCMJQ)CxFo~F@5^43F=qVN0GOG@7jw)mGA4dFk0snUJHc4 zqPC8Nh|>Z{mn;=+N*U0r;4dRS#q;tI7aLRx=#wNj^NN7|20-i`*qP4!W4jCmh!`_z zAUC@o1smoWq(sT3@%3HUP>O9VBUyeupiT+aVa*M`r+w`sV@ap~P*^2WUWScQAVV<8sz zrO;8PCf0Ie{OgwG0spNZ#zDVlI`R^Ue$&XYLNy)(yf9&Ba`ZioB_!kX5a!jPfp040 zi;TgspSyQx&4l!`l@S5owXVRH>Q7QwO)#HgvDSAmG+krT9;&*3SnntEe3Nh#Q@NXo zrO*vy=wTF$5d%}Hn-MGa;YDQt#Vr2b1b7}v;TN`|uQVF5z(|H^=}#-wSMOmr%~MLt zfN~kFa4$w9pLQzZ@M%VvwTy@cd)m?9#uC*A0vi5Ms@ z8Bps)o;;||{Fkqf7K&Sdk2t%(GA;b0D;|<4k6PGF0%mNd=;b==b8n^B0^2$ZZD->_ zANKG#dS0Kf_o5_g_{>jERYogB zBOlIlLDepB6KakW%HdEo!HYBsC6`AO(43AbYw#5*B9ua$5&Z4k8FKb>n^|IpCRPnp zue4heC|merJ?l%{dp+`F6`{g9a;OQ+?s*3X-MXa;zd97j?+upoa|;70&ah$FC`7|o zN<_xvG$vlLSW!?AsABv@@%-)=GFYV3z0y>?q(ytb$zJjb)}+o1Ihh6zjkf7;tSjI^ zS@$6|oZu)&$QiOiVY##vm);J%4TPoh*Jbk=A>da9gH!)Vf;!H;p-Z1!wbrmFCa|4W zsVLNXjuegzcTT0x)7AVMfKBgKmN0Z%$S4v{uEpHY=fMl28)jaTOaa$n12gF4{thxDOUG>6RmbFJr`qs`LsMuj)m+RZVDg`s zDn`x{9~;$uE}d_wpT3+pXUGD?ZgZ#?dENkqz8C$^ox5~l;C*>wr#!h|rADN;Lz zHw~;2{qsL!BvH3yg9_PC)xq7vcWq}i{9SVjJNeS}y*T(iVL-eEfSAhS()U7`i6;wk zl1)H3@RH+~^R*CkQsNy;Mm>p_Eu*z7WZ;57aRVDU#$m&MpqW&%AhUsH&pye!NUfbH zZNN0d*?k~pIrygeHH0WZ3E%np54`eLL%p2sTy4;(swK;?QCmM_d5T~^BY)se`xgA4 z>_Jj2MU{)Wu9Bj$Kd@}clP!+IaNf#c3;di;5_yt4)m%^(>yJNJEogoshYHHpFWESR z*|ZIaSr-B@N>Bl=~l`kF|OwZU^s^#?05dy%>zzwQGz7uf9ehK69`Vo*!bZYC&*e+ z6i_^)aTBE<;ZVD`QhpuAET9tR#7d2MdG|xCw<6hO<5hqK-nZkgVyUy46q%mG0&J3l z^VBdC!9NK!0>4YS zzf(*XtEbE8R5Ic}(OaPvjh3=p=-?T&Vh-%ny9gn3Sh#wQZ_r?7r=6&wm+yyu85OaR zws*CUde6r+K);s7dREq)k?E^Xjmg(T2&W9ZDW6Z|_!nkxpsM3|E1gTpYyX!HuwKu;xQ4 zY&b6a)21yUMujPdeOjdp63 zzR4$sc24f)c%v%T2F7}9#RmWk;-G~!KIT-DGu3|+?OVc-9~K}73Fr6VIyPFii@^SW z8sQCyK-0M2fiPUKVu+w#4V`whmAi??!0lplFvMQ>%ZF%f8~3Z|kg>Y{!m6@%ReGg- zdNH|hp%2~2U?tZ@hR+zqqS?h<-5JsjwNDKF0P9TjvndHBiSBLfp(tt_NYgk#>>H(| zV(+gouaiL(@K;e)-YF{>5$#WLcf4MEgO-%nZ|dJ{9{coUizPFOm{k3bOY6*%Nd45c zoh4avG~2=KXsVS}WlI)C%|0n&Z2yXB+#?Nf_>m3K1-8nC6K#Hseqn{vY72LL0Dgkl zhx;cV+13K^c#~R`t)P;~_%4X1scUhLdJt{l&{-W0_y;=OffCBt0UBY zK}NvkhGZedX)*bCdSr#MMS-rTzKvb@v24#pnZfr$8b74?oc4q{6DzBR=Gn0Y5z}{; z@}3RHSDIhU*u{_8Sk53UxWUX=A;2^3|lKtWlE)VBYRx;aTrJilAWPTaUy=4Q3yUY1?h z68#>5c8og2kgUQohBy>ox^-P*~lkvlz$o-*K8m@a%a2YCv?R13T zL|9wc1h=SNq)QS@*IZ$2`7sqZ7ZC>MLj;^gTIghU_>^&+l*DcbMkWuVV_@lLrBZu` zCk*uKp;FQJa!I`@1Pjd>W9s?yg2D!JkfOpISWO41nZd>`N0h*I?NOp&A$lV(zB|om z8x2TWA$kFA(R&s$rqsnkbY5ogx<~LDJ2vd8gWZ!@WtI+?0GzD#TwM?Y>wAJlK3&4uLMo~%e-YvVIsCs=<(9|0cQ*+nKb8B`$fB4Gs@^%o3 zrwI66oFq%jEGB!ESu{NkuFdgr(F2An+c$-Z@8SxnNe2mY745VBZjTq|s2ESyO!w;7 z|4`KmZii)r_8k#HYdaGw&MZj52Sz>*kO&_b?&RtUV_?7cr4#6p4V>p5A~D(d>@W{B zgAWR6Xk52)DT+%D<@V6-iF0z2tiQ39a2)d{oeOE9ad9nu>thiL(b#~T+(BYyQ{;l zr#`7;!lXsEd=lCW1fi&qnBsTY7AWBs9u5$Xfrj+x-^*A%h1%E>-F5P1ZXyh&s_tDV zQ9LTSG(ijnD2-caXKj2vvJIg!3$e+^)fAn5s?hIvXi_A8ci*Fm1d%gel{FVcGP|+*@33xi^ zzbWAIOD2^60LzqIzIP~sfNA`#faJGkh42J(yc0U1DZW0g!zEnKm69jP((p;L>oXD~ zw5!=?3(g7BmMo|Fq4YZhD9ywmo7Uh8X(EQ|brGCV-fg!4#)EVR-up!%+b?*^#P$z1 z$7IySWXo~a|FzF4*2NgFCyN;jB?TU1 zeuRmfwMiwk7-2SOdnvv;vQ1{^+MAG50h`u|Y{CS0h&!XlPdExRYf&bv1056=J)O>& zrT}8`41&pxZ?QW3;DB2c`&CBv1jb>(A~_*_%{4t(4wD z0OlT{JRTZ$yb?+yw&wB?_+6?@c2e-je%-}q_34KA&J0-e>|%4(o`*8DA+|D16;-Xu z01X`E!@sywxLycO$cMMGV5(HHvfQ2$t1t9O9N^MC9SFy-D06bcHOcye?bh1Sy1KDK zA6Z60>Ky;_{+^n;0S!XZEb+k)AZrX&0|*lt$*4juyj(D5ozpJ1HoH8Wm#OQF^W>M7 zQC6yL_q{X}69exuoQBzaGk8g56XDYCgCQeC^kM0>xsv8{)K?fJbE3T1IAk_R!TB75 zV;rwow2*Z0Nuq3a?$0kKApBJq=PCkJg(tk8A@vbM7!7qI?Vq_5;e?Y_tz4poIh*9Q{*zlXXx{4-b*R1Xbq zbB^}RmFWIrGktV{8vVNzE;A-s+dBL7vLPaX*9_BtMAObKRiW-qv=p~;q4aznRGA{$ zZeyJ~5ZejKNSA)p2XSwZL3_>h;nDAxnr>PIphlRmZ*Kt59%3V5KHH8ke0+t%^7#a5 zpaDQh!om)65c-(Bh6=uE;WFf7(GiZ(B4N2S$F0@loo$gVo`WRjfVS9uz|{MNYUK@P zkfZ_!NSv5tPc}BJ4jZPCBTj*OtO_5-AmF&oN9M#LC2pq9+{Kd?0w(1cJ0AgA{M!Ak z<|hKo65V49<;?AOb>t-uTg*#3BM5z*rk*Y{E;d0g{l4l;u}=J|=xKkkOZ{AB+UdRR z?6QP2h5wWgkhb1XzVBi!k?W`M+m2wIqt%>E_~C0Xhj1Dn{z-iN>5pqt z{!VoCU1?z;O{te7fQ%Ao<^<_#eyYwxuZc_!h3+CHUCg+s0{p5dTvznj!0mb8nIN9W zkR3@VGlIfZAXyT23JyAzxk(5W&VLG*y}u6^If0G#5WMOe(dd{_c+nyxetFx!ndA;{)asJd_ML-Fawr zq3^$Jam4m|pEatJ@yY#7 zlQ{a|$L>?zE<)PSjuTBCet2M#gie1yUAkbA>m`Q-LGt!|y0OT!j?o1)Q zENIG!0b!HVf=8ds?!yaymW!w^$94}$MCxW=*-H5}(4>l1Yvws2mlfH&-lQ6Xg$Hm+ z5v@+V1e@`<2F{zFKH*xh{<_0`9eY{K!ldD0qf@CTrNr7(L5q8Lh4@MNi?dyrGMxs= zyDL;hETfmB2yzS5r$Y=81=_uR@0IO*GW>-f>2VPBpyXQCwf;DQ@hOoM~({s;sRxb}wTV+h^s@g9L!QsGFS_M_U(7u-s%tEH9WmZP;0f@<3ea*BCgw@hPB{D2LA|hlCN!S^C}%9Yu&(P3NvL zd}qB?4O6DLO#xixUH|xOOCfOR;D#<1u1WhWOuYvxj-bh)RVKSDt2)RlrXK$KS{*LZ{O?&oZ4gf*7jHT@ zKIxy!!`=O}wtA0dJVrw3w(18AHBzAKKnatkoVDp57(<1mFJjtOA zwko)&W90}-i(!e0S&yN5MKbSo!f)VfKTKk&`OYh3I}aS1;asd znAe`r^AQ&`m2mMqzL-pOz5c;Y&ftcwY)h9ouQKYMYIK(v=FLl$*FD?!B~8a|d{Rb} z*42)!n{OAs>)Q1-ldMNJSH_&V%?41BX}~bQ+QwPrJ(}f>PJS%u>y8ZK_wjH(WcH(q znY)%X+T>`H%_HDSVC*yi?QtiWBJ<$-lkD2}n1ZmG0VijB3FY%^g)Q;-I1Dc#R;O-_ zfeUblQjiIDu#u{b*VtMZKUhZ>dZAODj~FI&h_Ua+LVH+N>NS|Z`1`RCMXRi=mZ9E0 z*;{@t%q{CMv-A!c%p&eU)N_F15Pn@IkfsJuQN_dNLjJ6fvPtiV@dHg(=?L>P zsfhAf3cx&IEJP>UV;CscCNvP2%bj@TMY5Yw>Xu2w#aSzN$1l3!JU?KGy>ea`C#`yulNG{jon8jXUgG`1^27TaPyK;i+O@V_<1;j{t3N zY`qneP5nEVuY1K6~@B8)_&QQCJhX z&fvCf3LA&Ek4Z_)*XYR;H18hSePJI~*tU1dvoNL{w=zex$9SAES;yJv?U}yN?=Owt zmKdN{gvmCAAD_X6f9pbd8;Yx2!5kyc2BXEO6tNOuq^X6&MHKt8#J#1FUSI3bhS+vu zm(wG=%CYEa*alRX95|Bf)h?@)zeqkeodON*xkx^;3*tseBgX*l<)(7U_sJAjc~b=6 z|5j2~Q)N<{U*&qq6CAzbLaoXFTPFM%G{9@Nw8H4b8@sB+-PR+Ff~FU~$x4fim#w;m z+H*qLN=xUl>5?rMM@xlMi5(>+pB)=)$h}W=-Ufu8pjk({_`yYg`B_oljxOphewfc* zBq!NZ3<3=*}e4kh~l{9A>B=V@ezKkcKR))66T$;Ut>TB3oMHiARPMKD0f`?+MnA{&KSfpzlV7rErmU-QU*UPo8OGMge zEYy4$qyr0lDEVR{5Sdmljem7b>kj&}EXU`?c!cspKMV&v*d-5mhLO-q53)>9G?LOh#)kna>;ViE!5 zX>9~FS}nj?-)?XN4+*z1B>0%YMIP>shLD|&wC7I=ty`5ej&xM7x0F~t9Wtg}T-$Le?eL>PWv|F zO(({_QRX7fZOV=EV0zS~8qKr9C@aiX$$gg_Dd^!!K*@%1nQ0h}8IAmOQktoUj;vr4 zkzvpiTDc{e)ay}s152vVFuM-Hr~%t4eeE?}KEZYfi|M=g*lAxjX4QjOkPk(Ly|8N^k zN0gO4K4wpfNKvu`Ok9Vbl*J#Lz6z6UNx>T~seV74h7R|J-T2ag-{{`Xdu4|RQw#0X z&K*K78L(;zHk%@_hI_KrUJt&TS~96v$>{xtpfv`>B%hv$z7$Ci`6{*-bMYlXt>tO+ z=pNMcq5V5mz+~&s&cmR{!I*-e=byDKAY1j?yK-1OWqWea`3ppS^8yQ&_qH~liwD&@ z9pweLqk_Lq(8Ov6NZ{Dv3u40b%#xi_l=)ZT4j+xwgj`V$Z>TN2hY8dXnGMd?bwF*O zaL~&!-=Z;+_>k%xKIvn?R3SkM)U<1o^tBm=V>6n%p};(5`bzg`85kdNkCC7~#vvWP z^XN9K#B0KPDb!>{dHwXaI`^;(dofc2Srfd={DYqF%)kQD^eEUMjlQjf-X^5oU)aY;nSq(=m#ulS_UDrWMEeJlX(`K&NS~^Q<2~G z_PDPY8s6BPhtb`S7SmCZdaKZwqeFO$`c8O@eRU}KL}9bU3sK?>-W86Dwy9fYSuDC!d#7mYlQGRMoFthMd@182wV^RlO*CU?tJZ zPHLUGZZcaXd9A~B-|e(m9${s6ZE<^FLO`s3z0JB$m_)~!H%q+egQp&tOM%4PS&SNTl$TOJKw=l)}c-SZqp05eudC>89H@F zUeOa=@u9ZzVrH*2>>(YMER)adE%!Jp^0|vw?yJK@<+~EXRciQ?B{)4sQ#h>|oPRUN7tnF6ttw ztB9O-^_pkP1gOI(q2Ae`wuz{72k@x?Z-QOkx9w@b8X|rJ@x(vo-cUtiWNn|u&3ks~ zBR$Euk-~uS=^)fCI-C!4ugD}%4U}qP+_b7H5if_RGEz-n6JLpX&~>IV141g zM0Y~P(bWN;O@k7&&Q0o=s%W;7t=0Yb#5F>Qwy|y6ldIulNitQkaY#8q2ehbNT`Pd{ zzk?6huw6EjXe*g6M^M|5KSaI!?(Z3{B2UL-lv;`&cfl)X*R^w8%$WtMeSrTuyD$hF z`k7jZ4$0>0A{&tQU)o{~_E!Gyg@4cIUDtbR${=M$iZ6VKWvlop3v-H#Z93j+xLP4j z@ORRDRo1emr?FqKT|8vQ3rmg2Kd>(0Q~C_S?*Ru(J{VPf8HiJ^`fg(-Uyxb)gOWny zC956U6~M4v+k22W~1<9h20 zB?0h~h{>P?dz8^rIEnzZg>_!U9*mSfbzJhaZ%*9c?KfoPI>06uXi&0v<=kY#@BkhW z;zNf&#|6|o87ss;pXH*}S2rb8ScmBwtO+0rlXK?Ie1@UzY^? z@C`AHc9i=R5B_ijaeV(XB`y7qgF2v3Cm-pph$kqV)V-!_AGD@hvj>qDjj>`6B+=gL zRcVwvySgJ~EHI1a#j|~%sD=U_6aarG1~RHz*%iFMYne#49&-@G? zQG!M2*51KBO0iYsp>Od-u*oY&pax}y@fffJT)T?vu~jW;7l;4p0Py4XS@JFglZAmX zf_?Ye%*HeXaQ^p$%1)Myv>OflQRyh> zpA7uUC4jwB+#xyOy+XH*`x+PA`aI2x9HOPDaRFP?f}}+?Gjv>@1bW-#H*?4IzBmH+ z(9ND&p#$a6X#-!uZ%mOsB$XYkRBKs0CjykqV7X;Ky3Q>%gOXWcWpDY>Zim?1!|wDP zQTDKa8l^S^2(in+8=WriqeJc$fX=vNC8M@jJiK<+5ANxjnmdUo+*eCqy?a^R5%|$~ zhO!8|v|%d+fwu9&*hA_1&gM&ast@HESS|?Z^@@lip)6WaDOy9FvOk_^s;IoR8|hEK zPTStIyw3l-r@B#GG`Pf?2)r84^U@?hI&3ku9Owps^o2Ky<~8c)yFgh1c75qZphcKV%Tr24qqe!lmU{+mmZz3oOF)A|6e6v~zp4O=1dmf&TZ z_0EB_==MWo?elYU4FHfA>Qo=zRl}-8Jb)6b6%i`U^snRUXt(+#I#f9BZ@Z)w7j`q{XVCxm@J&%(%xmMTUP~7<`|-RhoGI+ zI0;pY97bD|^1S(l*|{MhR$k4E2sOT4VLE+1d&m@E4aM#Cw%{|`PNIgSBZZ0 zOSW3Xwpt+{#yM(y1>8l>1P9Ue&NP;AR*IhW)y3b>v%TL&f`nKtJ>>DG3dI4gh?KYR z^gJSr0qg+)!&`wqnD)4)Yf1N%jOGHR@UKTDWkMz&f-dc{s! zR!*=K%1DPb_-Hm0f2HI@bKUtDy!bJ7OE|+X(ZFjLEb;3o`JVqIAG$}k6WtjV&664`E7o8R$&#d;l~z}O&OG( z5tC_4J5DL+Y3pLe9RquohkZ7MA6SQk8l+kSBN5eX)=s%bTUg^)0F*}-;o52-ZYB>B zf38f6R^A2v+SRBU$@Q%|z(@4tVfIC#Ci$cMvWNGL?Mn{Eul77SFu zFVT~)*OS&H#RlYK52)nJy9#!2N%s8U4VqSff&BZV`TJ;tf~A(gvIP~ZbIg0K$=X4d zmrJG?MzE%WorN&N8NeqLX>M)rR#`=Qd57Q*M%8CL#9$8qZ^~Kl#~(KkPp{wY6>+g- ztCGo=>y1S8Y}UL(()^lJhBm3h{Y^#9g&s#Rxs?Wiq5uB@8bRg0*JVlBMB2jvYe+xH zX(a7nT}Ts>a)%r^H7I|Bl7x*)Av<6H^S^uL&wj@dEnr@0d(_dSMq`j5p@n$P7+Wq& zF1H}5=)U1|yhli2kP>mwJ&-X4-sLep7O6}>cyHnK(8I{}e2Dg<6>Q*&W*8U+g39SQ zUJ!$$Rp$YmCV6O$=3UK>h#tGsr>1i@1;-K`O{n@Y9%pW85Dc?EhW~+2S~%zEhR!%7 zog1Zp%N5t%1#;4f(|v$cN+W-71L@E;Ak|xIRzX_W`phVrEm6tu367~Wf^_hjR9Xwt z0hX*5uD#=wEHrA@K+7qVwINxE@mH|fS_`)Zq!G&@#eCrwH;9MD82v{jVt`@k4?moS z8BTTR^bnh}HOApi{R^8#cASRuwb4><_($XV)*)@s zX2RO(p&e2+Yjx42o-#2|I$wsAi6G_xsbQEv@~!vip9LyOU)K!kdz2I#us0|$rT~Qy z#WKG5`)~{u)z^`b--XkI>xJ9>t^vCn8%pH8WC8VShs<8E}6>#mILTDpQcx7>Y4 zkf7~gaAA)R?{Ao*b(30M)dRFy%c(Lh3Z zodlAoR_rO^)*wA9x|~3gwo+A+#(r(bw&o1d74`*ecXmitM?x}?1lNfRdT;3VaHEo1 zP#dox$_i?135st|KWAtt?N|!-~50$lNTuwJCcM& z&@Ig(2P^L+St{G?wbIr?wyn}!t*p!HdxfuzN++G?EF=u&qKTM-5P2a2;?%B$&V4%8 zn5vPL9VY?O116D&$vd*7uknv$yHqwMV5?E*l!LpvvMcJYPr``1?5L9c4kJUz;izL# z8V3fQhQaVQZl?sKCcH_;MgXZaDI6&E>aLuR)YG^hqBMf^@*L8M1<5QZhY_S}ZOX_Z zl0h4D*`4gYnROu9*_V5ijU^aNBgU6hqU}!kACPS1p+iC1GlO(mgS6*mJbO^_Q2FPUXLHjO^!JXhLYi3t*xt>8|>cZYJNr5Lj2nt$AGz{+ISd-%P zs%J>Dok}FL8D;XR0Mae0sURH%(!tC$3~8pE1H+e&YVX8T-%^k^Pv7aJvG0PUA0nR} z1k&V8TgwpS7;$*{zTz|bo4P~kaBQK5}pVrbw=k9c5Fby$b`FVdydSGd+5iEh+9?9P>_z|1lpnK z`~K7&2c{H4D^S<0L0VAh>B?$?Mknt1|VT$?Rz#zRA08As9DOP!&p9`1GU7! z*(jkZaRO_IwH#1H*(Ko^TMnnGE)#{hYr`Vu4<4@oavTE(zlv5?fU+_o&FtL>b|R44 zsj5FM`MuG`c!STZ^K!_mI`=#~m`=G{cv!O$q=Q#MI@v(F73&RVH1vcNC7_!lJQk?q4u0%k< z;(-gIv~mh`j;$yRiD(!VKls=86V;6{XWU4PS2d9Bu4dA+h8X`LI2hR2ev00z2J-0_ zxwV*6GZc>OU^X6a6&ybO;@)Z?&C%C!xCo0lXHZc__7iP|&mbwwd~ATUss<7ybYpNy zG4@^w3A}=o8c2|yRF=7-29iSdb*~#*fHbRtv|r}gD8>}R4vB~njWLNF50GxVo35)G z$QhMHYDJ|F9*IhUKP5v8Pa2iNzd9<6El6j!vdp${=d}7aQ3G289xC}9L7MN9Pk=c^CDx5?;z}u5;g(=m4kVWY zH!8~YsxA}MhE)>pP`NfMtrTCyRvLGg6w{n62yS;uMR56O8-j`9?VFn^HuEjojGhq} z6Ii4}3QArZ{;fyA%*(bW{pqwI-SD}B)PA_ZXM=R3l>$lpiw!RQ++Do)25B4hSdgy1 zVFA*`m*UI;(gTm=ZNtkH*zYT)8PTnd50RkNrM(EnlE60Nb?5(lAm+=R}bmws?T_ z@ht`EktgyJLbfK5&To*2)j~h5hM^!evu=Iq7K7G93im%Ol44~*V!?e2qhb$tg+d~o zNk1B+`F+p6%#G)bQ)preIy2Q8NT&$l+WX6_vu*|H;!7==(1|{rbj8^KDQ2A%ln8)y z`#m#Ar?MC2X>8hrQN69SAk{+(H%@bKWe(}_ z6_98%JVvZEt_NKo+HO!o6HKEp93X-17)U-U^%8U+^dd=iD{53(TD8x9(55&iu&}!txXutMJY(lj%ZBum}~?O zBv;n!54li=9La{|HU3U5B4y>2a`x$#VBeyvMf5Yx+rRPV>;LCJam6vxTr)iXH8H9A zqCDoOC3t#lI{N!01pu2Vl^Fkdf*%1}_TVJf`M2#2v73>XQwkgXeCHES$5OW(*ILh7 zvd%@uw$*}#qO2?;SC&zuuaMC?5-$8q(v1in1u}(n+zCl&6r_4X?Dw%HwN`%e>1VMH zy24lPa^(2)<`p#?wKMLG3SuS#iGoUigRyDCg7n1GV8G)a#hW5hv?UtjP8=&MNF2}8pV%N(eH5RzLCUk1--=T$p*lcnZ-~_=Viu&O0EYsR{0Ssgf7qrVU33YW z6r?AgVS)>iH^kP9SU=;gHo!Yr%Y_uGxR#tA@Qv+sa3TR88`zaO{%@+v*f>Vqnj$!qwx)rSu=jA{9 z9m~5MO$ff|Tj3r&!fnIh6F~vrm0*%`w<{x-~5zILsui~9M z@7JN?F)L!5AXW(_sWNEY<4-lVL8^EBMrWq|7!nN=mK%3P_SJ3X zibcekfh5ff61x*aGl9f5jXQCyyc{Hib&Q>g)B}}3ItBwX%J~XS+o{Aw$t=db5wuG9 zv_GsqKzjHwvdPF6q*l2Kk}J|gp%SG!?!@tNr+iWoW+3774oQcU62FeGm9L1(J0cr%LRXM(xw9aBz7}q|EHgdivqckkNrRxjp;eMo0@y>E z_wqaMQ&fq~(2YwuL=z8gLk&i)MH8o3O{xjtiYDsquRDbgbkQe2^_hBXvmC8YYePBU zG&-LFa%wg8$wKLh??=7R5&|d->bp*fxk)jg0-OQT)gax>uV|1S_H|BCq6s}1U50#! zCK@Eqm0+X_a6mfmQ}hf^emwE@I%by@f1-Ih6e)}dzKPVtWy)Rpfsbn4oq89Lq?zCm z@u?AfZpyvN5vy=DqpSHseBJN;{eQ6veV~q?o;2}TRTrslI)S?qDW=$jkK*#zKTwcnd$sf-Su02_1w>Dp zd;bcPZELk4^*6!xdj}kVm?FenC`i`~kdRA~ut6Gb z6V}CM%&`M_$e^(>swq1S0fj}>CYwkQ^hS6Y0>??S5N+-Osn42nr%5H_m8WI-v|3Ve zTwmX{^bDxZ1*F)%MCr_P@D*KW9W|RnI%I%k2fQG)XR8~e(#8a2fYhEvrTL{txSbJ9 zjs!@{Vl^hk@AI)`LbUiuzt1=~ux(Iag8~~A*r31$1vV(KHwtjeMf&}&U`JNApV~9! zOZQW*j_o`SK)>SH6@DGBkMGoPNjdU%miMRn7jwXU^%E!DeqNR=giNFC0soEr#O>rv zdX&s9|2SjZFc-OJ8hGc@q$|7pyu>ty1SH-X@14H~n~Au^FZK1>^6=u0^Dp9U)~>-U zk{Us}s6&!_G^rV+V|cTW%ph$Qr_{Iw>B>I$4vaHDf^>F4I_*q@as@#`_Bo`FpM(_L zDtD9D>YX^yxX?B#$(D033)0rBbuK;{LlWjVt#LuRbZ?Mq=gvR=DoCOsgBltDF8v77 zmN}%^dkH$Kf&}d%B>s6IA&U?-qwvx@B#!qADyc@hat7)2vl^tcRzl*;8yEG8N=uMB zwvB^#PXT--4K5%}hDXtwJd&J%=w9c7DYxu3^w`i6+eS%jo6#*$<+KkulnlQ7>%XzV z?K-&=y~+prw3KPNG87Wa%PHAclE;4b$G@_DDKqa_dQ*;??8vh@_ME1>^5n_uY9K8J zOnT0z9YAR1C?u?;a3yDCAVt0!lP2$6)sHJrzVTyeRa-5tyDM5(yE5(vBx$0Cp&(Vl z>h^ov>6hi_L6W&@ke+i_k}8`}8psqaK`H`*Bzb5Pp+VX`K-zk>>2yfQPHN_m>StEw zf1=|tByu66J3#uwZITQUWr5^pg{AIguAFKhuSL7gJCZg$`I9q9-?i03Eu+8)(q{)q zZ~jEymAdPY1kvMuK$6BU439tk>`#78MHp)a>Cri)-OoMqC;zq}J=7uHy8@DxLZz>N zS3RvnUx!2?+m#3;FHVu-b7cf+Mb*a`Pug+9k@k$gic@x?8MoTlz`1e zCQ>t|yqMv(yL$~JZD6-Tn#CTOrN<#J{pzoy$ytBdT6#uhNbN_oJs?8eQ(Mb`4Qh06 zam;2qeWKf5@WTR1B(gp(GGyK+q*+fVj(d}F!aw-|;%pJN-?bcG$I6&W6{<#)IF(28 zMKhxM#y#|76i|B!ST@rovWOBYr2&Ho65`mOq6zI+7NlA3u6N= z$(o|4MWJJT-3rNQAC%C8>IyxK$NJZo5BMIO%-AD7 z4DNMW{$V{PR__*w8dr?rdOIx@ns4!VlM_Qm&A7h4?k=`;n!Kn}lXc6Lm_Kct>MfWF zh)xPan}Tl*r_D#*2S|tLVILuBwvK<zZPjmK&num3H8&rP@vM%QT=5yqmyPf) zj;DV33vZyZdB>PC$6*O7rB^ypG6*QK|HHk!xy6jomK59Kq-w}4!NKh|c9Q{C*u!ez z2b%&)DJmiC)nre-kQ&qpL10saDI09x2ix21bKnId7Rw*;&T@|DYE(XcyNue8><${xObkVHAD@cn^LZw?lO+otLhwP5ox-zH(P6CGUM{0Bh zk{w37IqoX@wOzOj2`|w_oCl+TM28ES3<`tBAeowr5F%Yc@{8e5t@wFvjRiHN9<|lu z+3_z(?dQ1%NbQA?AlVuoAg%g&Zv9{b`_u?%^n&DpxHAh&WQX{KD8Rc)ZHoSqILi)kJE$ z^KM}m}Z{#N{76Y;+-DQ3`E{1ji(X=DLCPL1Dc9y28Nz@i%#I?kxE=`?y81( zq=H<=7tMv1Yy{ay3a`S8W}pqmngK9e;fuV8l^|IPZ82A%XqY3BzET=x?#Sk&81ZP0U2UEyOWIE);zampKZ{n1YeNUlt} z60j?DX^`rtPrTa5xYS4>VL3{Y7vYp&#L@MfAQc4Uq#w+zSjF-cq>pbQJ_Dro>px}I zzwrjzx~oEI2I;H0GU*C`BDE1soKOsq?)^d_kx&ZRdcjO;21wW4yaG~tdu03*DVGij zD;m+77NnMIa^>Myic0nnBkWU1mtM(fDM$w&Ow&I8-J%lWq|^kta6%wqoh*f#e}OU-2Uy7PLWnahwnv_m*SA;_LhwR)8F#12$8KKm(gQ z8WM1*xP_S3GQp3A99MN3Bu>eMiSW?u#+o3;qd@3$^r7*nEV7Kw_T_nfM$z8aP24J* zwPRH@=0(-$(k)epXCOe6PsGhzBh zM^&7^LNoZ;YtKzw@41O59nkxxwoH%))!Y(@&(Dm zM+Iq%;S}KUNoW`{$cp)j1SXtVg)D=V!pLz~RFJV!C~F;!1rkP8n1t*$wXsbgDLyn! zki2{hg~y#Z4s)_=oMAKpi8X}H!l@siVL+0pC`j$$Bampkg47Bl$GuTOAhA;}Vm4zL z)OXdTgCvDWSWtkmcEvOSR{OF1w#)(2ZS@Q$niQntLHclP2MJ<3m{fN;5~2VlaQvBq zwDf99^3oxZIR8L0G@4K>@{_~?pKu>qIqsDp)eAgZ_2zhL)8kE>IDQ;Xowy}5z_bXC z8=YcWwnI5+z9qthbMOlqbJjQENLR1tk1G)fbpLA?vHo^X>5IUg+4hY zJN1U`yhsxyW@YDYj=H-bSvZPifOPP|6m|C)L81ngi>Duuw!ii!KP0DA7Dzpo%*A#2 z6ilfSC*ziY_EW84MR-lvm0ZXJyNFlebO2zGED^()>?k7z(njCn0~AbEyO>egsBnMpJpJ}A`yLRX|qF6&#AUda(Wt1 z=z!o7p}C0RQyne$$mHn|>n06?!PHgiSxc@_o`4v4W!))5tmq=1FG$B6i&yDDzZ)L8 z9w41r?~XQo%LEc4jQeqnE%}_eh$T?iF#~}Cbm&K0%+VE$!Oc#w4UNP?*%(XD zh3jB)pr{NaLRyf#I=dhpqsmvgc=IYtuCwoh2dY5*kVAe^156!kl`GA zxwjZZ|NKb~xhwb$-9U8=Ht_k%-aaW$wt#|)D{Ee?48Bcok~Y#QSk&6-!A!MO>xi%@ z2@iIP<4UoRZg(*iniCvr%9nAWNpl=RU#Ad$g9$`WY5j(>a-yjsL*76fAx6G#xp zDBzhCYIKR_;gC~;qk(4Rlut`ExMw)()!+UDry5{^p`Vf&D)8HT-f-g&{^S2+tZ?sm z3YM?Lq}BaWa>um<+jrqoN5x1YhZ)4_Rufmt38X##VHW}k zdoKq#T1HVj3?uvF1jFH ze=8Ma#Ro_?-@&O^7OU|}?ccGi3ysvjOP_NH`zgnD{4yT^&9i&T}BVqF1TJoZ zbCG64bDYNfjgh%ctKx;WA1#xWX zJ)D4IzOHlD*|)exe)!m_rFTn%*fptgA*aq!#$9rxTfIdS>OZHZ&>($upCKJwkd6YW zo^78%Vya@t-Fh$nVS7_RkoO|^4a^%9_}`EMR4BdLC+=k7w7s&m-*pfr+^-P!v{wX4 zRr=_+n#1t+s=!yc$25vIT;fhU!o|1J@ zJSEiS^SkZ|eF?X?Vf;5?k>G1>Y`9J&?#XWNpzw9MUs1n^UN%nv!Z*uP-u6)B5BXFr zDybKZ%E(Q6_Fb9G_Ac2!60dPlBqj7|$5T>BT29OsZ8+y;`Uvq7qtKqB=V z(sehB+QiQ8iXSJj5)WJ?K8JKVNT*1LdI(xX%xe{i2a;?sn%iEN50v!l8G{Z?1gU6uUD@FZzfQ0;EQg7N1D=s>88Y zE7Y>tDodlc1VKzExsG?yzw3Zuk|Gs;T2 zrGbNle!Vy1FSp~(S2C88pLCi+9gtL>P!wJds~@=?1q?QY0h1_mS6)c^sCQD3-Zz5u z_3z@w2I)%UN@gaMoU%AT5*?%^_{yB`u7w z)~g`-ea{EC%^+Q-9+G&HQ{_4Z>7nfsUsFh@o@P)?Qjo5`(bh0XGR7cDDAA|@V~iQ3 zJq5{6+U?qt3{vlnDG8M*Mnzv{VK{>tn(mrI+Ve8K>WU3DcNvt_%=CUX4%UkTm_&}( z?xdAcTS7TAd<5BbFFI@Q7JEq{hlDL#VEMem2QRgUBTk3G&XnQ}ik+Fj9(iSfC1w|_ zn$1wk#AY#uckr2;Zh!t~zoB{d2HxnTM(W9qt$Zf;m91gm(jL=FeKRgD5)+1-waQcQ zlMes?_U`@ZuByxvcxAc+Ay6Eue?fn;bbk?MdWJfFO9&o)(K z${J@bz0+yA_vpIbKdzwkK0NwWwZvMK6`K3AVbL8W?=t?FM7mx4Y*#K3J*}uo;R8D{ zPgaJoEY3;;x(j_`@3hG)a|AGFHNqp(c+%hsA10kE8>iG=9BjLbAbA>b2@+ojh6Rw? zS}%SS)7&7Ppi{z9uMIh*8N9~K(>0Lyv^9{HXS6kt9&BM!RiKCi*5Wz)<-gI`l!<9F zgLHxmtaX7$7ZHWgP9X8SIwZB)(a;8v>Z^_#oIu)fo0)P}h*6N<{{h5Yg7mo^NHJ=P zv^LYQQ}5!AB}i@kGn`7pTnLcRZ3$9aAuwvH?-EZAp{gS7jU@;;gme^3!A&Be+Tro? ztV|z;Zx>XPMOBK?=rlzTJ}~pg)x`M;oYjl@x7^Mw_`xB(DeiHVDYxyYs->4iZN6Mn z8f4m-C)eZN+CAgnYe#Dqn`yrw-N=Xh2uRqzFl_$~uaiU|>(Z(>-Gx@It(cp9^i7H9^wHj~IuYI!Jzfy7*b6z69MX`yDYVle(# zLAvplgrWtBW>~X^?v1ArI|410&Lx=F4A?^~$!>zgaT;j`5-PEf8t9B>XfNUK^qJq_ zoDe{(gM=i`s=aC%yiB5IMV%n!X%})T4zgzHh@+}pZW*>z$tNzmTHc9S!6clztjRY8 zmSyr`tTafn7om7rh}l$YJyIk;mf@f_vWPTOvS(p2PeE$$kIiI}m#BBkHb@yriIk~U z?Vwwn5NgfKw=uKqT!XX;q=jqp=SlIZj&Ol`G~q!%O4}?e*EXMRA0p`2xCoR0fvU7F z+Isb`{q~=EeaxKIWxA`?(f8JO&qUm=>nLLCyhD@fC5g13M&BJ?o) z>>FYiL=)vWUFlKig2e6&q6x2KU4mr#IP)t+6L`BIwbh9d&LH(@!fc`phhYL~<_)n6 zqKSAZfyB@+MHBUg*mY!CW(f=V6NSs$G{g#Jc&&C#J zCF!%I6WcrGpBWeU$c<#zU_DF7CT3UY;ZtaW*6gzTmi&b!9P#wJrt+#oAZRmu8>?ZW z8PF@+b3m?CXJJ`*r>XvkBP_8aJ3~l4okG!p#Ggs0)LlVZ^Z|lgaL~V|o))6jQI8I! z$(PAM-Xu|pZm5ClZu|OoJWfTd=sg7SAPFMy^o-r@{32OO(!}of3}orIb2eEX89Oxs zEX^*v7`joF^ma0kHaKjOT518v>n=eY6^bzyW*}W4%O}uI`_vSBQMt92fut_D6VX5L zP|KvlS;HS?Al=Rtlnf6f%U2qtNe0rIwCdeUYui8Io%Q{lX}WJF1L+bbfXdCqv@2U* zklN>O8l)$pfADan`<610GwD9JTZ#-)lpX=pX=NZ=x{n|0>tL)>dI8?qX$JBx<~Yau zuSX9!Y1zefT(W{x>AoG>=T1b}@vtL<3}ihBGyKOxc5xuFCZ=hUfxP`5Ov{DtFRc~h zOd!cX-ZsuaYSTf<)ih2vlN8l8nlwmHCJ6u%y)nr^UM~ZA*$~fYoA|>+4CxS*XXE&R z*p((Ue4IvL!=khwlG5R#PK<$vxlDwDC&0-!LG&9W8sU}69!5d^tqkVFbV>YAK_6ra2x zopKs2r1u7C=X>UgVk1%<#4Z_uyf5pelRW|hWRxmRi>n3fW+aVMaiB5~u0&oog~MQ6 zA_d_9LoAEMQP(Wxz&6Eo7hk9}$uqD8PK|IHvS^Njl;eE7`Zmg*y{O>`_Un>Gd>rJ( zdD6#aqjv1##;lOH%_5*PZ^twtvJu&h33Habpg$jpkg`gnrnoNkpj?MkUvThIo4qul zE6s~IJ+~C351-v2?OC{3w;-MNp@rd?fJ8$AV15hI^i{VF(vAtF zxfPxuxj6N1tx=MJB(Aqh3F|1m6eM2?+w;Ix1?i+59R4y)QQ8iIgaSH6jX*N+tXFgv z-9U9#*9(_Z_GZ~Kyh>#WQKbTSR>FV5oj*cNeu)(?#VE21Z7OKAr8U(AfgPu2%^+js zPf{01D9YIdSb{`&GQwqoKB|N4IIe81Yg3GuOpv=|E$9dSvrYpEPTmD< zQ%LI!)?IcB(%Qqgi*z71)35NBB5IJ%A(#c}w9{Fod9mtrIOq*+4x3$hS1r#W z|9!i5r)s2cxQio%6tTkdP z{jaHNO-HropJqq94Ma{Zb(;MVhSBc9LK=>Gb$gWps}xwJ!0Sf=e)Vu+fQoIB!0C}+ z&7VvdO)wv>ZQC0H00nk=Ms1 z6HPefj}yhSa?mLk%=_C0l8mKnhR+0m3JG^kzE>}vf=N5Bv1pJcuYNzY;EAd^NS?ZF zPgG4H9kwi_Ie)uDLY9s1E=a=Cf^_2h0YJ9_q~rAK_%e_tj}f`v*tuRKNW9rJB^`li z0$9WJ+}UuZLVs(qLZt@jy#>kROA8@wSP4>E;|UAj`dcZmid}QGaITFPu!W}rqnECD z7cvz+n!`&#`pvyK#I9==Hnn>2kWj!RT4SJ$g47x$;G%Eol!SF8%PB<%9G>n>id0hn z+KYoCTrs#Rn}UTuuaZ#FFo`NS@_~N4H?szVZ2$|`s;ydYf}NVhjgkKOU~3)+!R0lpy3J%vq70;!%= zZ|}mA8+We+<_x57eNV2>=mJTynl>9ly6*EGk_>PalIp?m{gRfbqzW*A^zY+sV^)+noI0ZP?CX~k`PdgBlU_wcFhru zF~o11nNU}iZCj`}5Xull=s_!{mLw5PV=A` z*-nyZNuHcJQ)r64ZbGV7{*!Hz+n1k|9denh!=|Q?{;eyqcP+~tyt!oxsXjI*b47zG?zo*~DY*{Yu^mYA zjdHnx!~~N~f~=4_fpo#e#xDC!_W4u>TaacK98Mq|1JX`YFP)yMrGlyQJpy_FiWCQu zm&v5IOQL}vL6V*GFbjgiv+>>UF@w~jDK`7@BII%Wxr?7kEy(%P=O(5525FG5XypUN zPNB&PVmaJMvC}gUqKUNjJ0RIb(a@D00qeU`0NKX6`M1n&%R=-#!_39g zokeH+If9_&>F%i0m3Ld_s@>aRzv&xDtvu?NX0Bwf_dxbFkoRW|cgFE#{q5 zty64ey7DD;G~)R*OB}Soz9^|Y{pw+B1g$~ZK;+fJ3evgW z_I78aCe;=Rd21w?Y-fpT6v(g<+i1>d(SlwYq?43&K#HQ~>S#fA7(z=UT%%Y}kmla@ zrYXL<7OF1hWq00FhO zgd0HDDP>BYdvgwzZoGWaT2I?P7VMh*Q)S?q+V-9n^J^QS>`MD9QHhcy;k{u}VC$gr zJpucs%*KlYjlVUh{ zMvbx+0zOV(Ar4ZTBd6Fb(?Qn}3XJ5{k}{G^ z(!n~LCN*@90Y1c&@mo+{P{6do13$y+Y_V<=_TES~)UB?&`$5i04ejzR4bl@J#Z)PW z89p|%%S;DS)zPMf&fXfGS+6p?;9`u#btfznNNuq(wPWDohz98#v<;*Pdv7EgsExc> zS4@Z6fn-wh7?%P%^$QY_;i6dl-~!ryAtXuwqZXu7PMbn{sFv`xh_WDks37qkc|-+i zx}vE=(yW6|{_>wZF2S$hQhKS^h5KqJr z4RO!@?4zH+8%}-v(t2$*7f4@VlU9_(9yrs~3wAiJoSAw-Q>~_`my=5$F<6dk+`R^l zbXWq1(D3FLr#Wudp!8CZuDQNJs;6r$Y}T)}-ym5?+{ydqZqN)K0+Np7uoEAQ{rP?M zK1OLPwuav3JeSX~;W+Di)nV}fFa7p!K5G<4O8ntx8muD5CnQ&vkDer(R-K`nvwk@e zHmF}ri`nLfOm^@&uEwY+DRwp&7KC$1(A*QC7Oj~O)-@&h6TWS9+Jg~U^b|X#aY(b)Qs9GNLPHuH2`Dd38WL=r+3oL1{wRgmv3CFgm;- z{WeI)frJ{=HAottLi!80AP!T@QAtXH2n^e?jj=)EQP{SchzRJEqM++QVlE3(`$(%` zicWJDGxMNE?T{cIBmYhBPRS9`A^HW?+MDe@liiLxD_m_pLnU2ZGtvx{{KRMQy2!g9 z6n79D4blu8Jma0$47ZhLSW1kKfc0G*Wpo(QggQu9nA5%nlEWZ?GAX)n7Jt&E;^o{9 zqzBUZ0NFLgOdGS3T^iL>;rK1Ph$e4Cfn=BXYbx0}jZ@kLil5+YL26$z3M977yw@7Y zX?`^=yt%)i3g}pK$$Tuwp%U(yZo@}C3OonJ<1{DNkqEG}U;f$e2pYM|>S%XSAhh;}I)YlN&G;dm#<&NmZQqoK=~ z9={_3VyZr~SRy{T5nX%dK8XtqpP4l~SR#%Dox=|qwWDkyIT_7E-R0vcCQMXk^__w=cne(GMA9fpyVOF0di;jxLE^(?!7V3;eT>zn0a&Qq-hbsJ^&ZXB zQbGG3RJfu2cyXwc+xnA5TeVnb~+ZS+Y&y8FSnb|9H? z;^{0+#qi~p{!0V%6Wo#`O{Gj8AfhF8=7M(e;Cp= z^uJCSAeVXS(5FlcC%5oPymNkD3HJv32ByN{G#-8BAP*1>T%FZrB1A+puK0nJJU@pH zq#|4lwiy8cRY^B)p@dDP5uvTn!!D-?FVTd&3%f2rmeVl1@7Vp0VXxs-`r(~(Cx zs5`rq3C&QD+UhbLp@wy~PWi{n!YT@F*(DUbxIS<}u_Vla|e3k|>)BQhyAW%AF6Pv;}JyO?`uopJ#dnlH(rFx!{*PdMd+ZY)Uc zoy;zl((GP3g=3+$vv{MgQS88y$H;>}EDN(Tfpi^6M`PWB)c%e2Zrg;5 zh{51b>WPXl%Xk3V^4aG8(JGWJv}l4uFg#HrZaBx#xEWu0QD4MdAf8E5PYbAppz8QdnY*W3f8Bh7_E%+Wq{k2F|GQA>pA?_eqy%m; z%RB_iN*Y0`!RRO!EZQK|Cl{R+Tu=@;lY_0Pw6FGuCo!J(nKmh3CQ|q;0CCW-zD_;s zW0axb0-lF|{AlVcCwM zkCq6D$q04P$<2=7gZx`uL#Wt?<9_)ov|GdkxOyUcHivCyXIm2mkrMrg5|T_TKE2W^Zu*#Z|cOOnZO z@)I(rIL!u=ov0I|VqLpoGj6Td9cVd?E5(v=At8T52SPJ=XpBg%;jo_CiR5a;l^XYI zeHW7>M>I~~+9hS#El7>dLr5;8_VwaHSJotMm0LWfL`S6s>PyXC@D&l`=J~QJH>S(Z zm;`TyeREB$Fu+>@>?+1p3cP6)NHJy7eftnnkJo5jR;GIhmC-%^oh0i`!@An>Dg{<4 z@c$nL!~!YZ8ak!K(G}hBcI;A0k_N+4BR7U4H(X6!j;m)g+v|>b_(GwmIDVHnabZw7 zPh$>SU(?9rlC_hy;0kh3J%cY5AoP~M<(E3B@Zr@rE*rJO%@cFU09d_HiKD|k-L_fw zQqqrm%=M~v+S#hIwJ-L%QkXN+x?)?=rMjMr{RtfSczO?{36=iC$17V{ACh~Aw5L$r z^@8ZG-a9sapa4OsB}b4*BE?;HA(XX-L0HNVaGpiD4br6?&fY@b;1ctw!OIb;OYyil zvTNcDP&k~mJ>oc@CI>7&aBm?S9A<^!9whSN8>C5mA@yY9dR|M4OyImQb*wd>Ga}23 z2?>SXZKm#!?MX}r9#rX+$1AenGmpS@NG2k0RC5F*Q_!O@TqMj#rKDJ~oR}R|^}DdO zH~64Ti+K?P3UYfe^#)@&ofw@MKU80t2LL*)8cH@q0(2 zG$uWsDHBNgE;iO8AoB1MGP1G>TE!6#E~J}GtemG3u({Sg0kj%K1dYw~E2V>@Jcp~I zZKKu(od4%o*V?Di`q-+OZn{v;(>g0PO{xZXlchp|X~l+amr_<qb_qwSp)_b7*TLGNVn>1*p6-SY*EQt%8M>Zq+AoX{R@)SW=fJRkZe$8 z)C($k??e_|44a}@sLHSW?SG5HC-S5Wq$ENmJ!I8P9O4jmBI&cgd%yp)g(X5+9T*DI zg_}tSo{`NnD9eycoZP-|mcgud2qh99``Paha|kBeG_%|z27rV%0Li!@OdbGWb3{?# zC3J>L7@qHCqx^hpZ~PIHZuJO+a>7y6Z#;%W+oN1)-buy&N(IyK(w?N7sM^J%ew`xw zjx!?u$kc;B`b89lNn`?Wg8Gz28VW;da9OsBN<@!+^s8bHwnUcPa?iu}flwDT0H%^@ z#K!W#RkfUZL#$OIESE-9G027N)^cx%)hW54!0VN9(>KIcF}>x?Gc>>Iag-q#;K}fG zJi`r)XS@i~-VnQiza($ZDMA+6D!C?AkC-|6D1A|4>7_SWJ}vV9&82?3?V zgMnJ0e2ADDXhEts#O~~k*x*i3N``OXG^Z89FmRvH+(sr&c8>P2OD9l7gpn?)m)7=V zV{1_tWPkU!o=aa83++^pT1}19qIWc;rc5H;NVIo9_5A#z2#~x+gKji-o)ukMIFCGv zj8kjKqH4^D(4UUm!3L=g?3M8I@HU$x`!C<|rmcEPLvh$#)I;< zSWbv`klhr|YX&$8eiS2B%#Ij{6%ke%P`pg1UEkE!h+g0XgMDG^MU`=0+PwJ&0tZ)$}jCY0`FGFUnxs#Vr@d6M)wwxlO%Nc9L+ zPUcKZZBoFX8)05KbB^nbjtCg?3IN_hrXxh}6B@Igd;|UJ#l+!Z z*RvoTNE_5RW9pO(EC(_dqt>v!)+}sxTmV7|=-iQsFfhve*pkr%AdVNv4G=oJNGy|W%YX-{7Evn=MGb?m zkjYum33j=568?l;zjo}sWpG_RlP+pzwquUH&CJZqOffSvGc!A8cFZv|GdqTunH@7b zX134H`@P?MGw00QJ9VmV)%`L1$6l&lOIlK^C9QtC)jEcz?}M}Lrc-cS&A>M4%Uz_c zy9|*}1)+l2eVp5+dz^rbi+;O&o)V}fJs4;Bz>*I55PVGtMRQ}a51bmcgF~LkiyYRP zd~Qk|fKp~rxKSA;*0vq7A4J9nXO1|0`dLX4Dq?0G`jbxIsErkE{B&%8QIPoxp_-`G z!xGU%J5|bTmwJxC0AW$kE<+z2Heg2Tr&ajW%93D|^j;rGVWA{by&!?DV%mAl;pF=4 z$7uF%Z5Q+N5H&$Z*!DoPgap*cf}Qw{a_olbapkVh&q>$$p^d!_^*=}H#ePV7whmPE z3KGld7{xCODJ#^_t6xZfUMVf=od_f4vx-EMM??lp3MEngt`p+T0cv*B?2;67@|aXJ zT?TkmB^!>bvt3#@<|H2>)$oO|e!8Io=R?5I2!m%9Z<_G>5 zr(D`mp<0Y2SD^SyIT&B;dQS%XPVpOL46iGnji`S|Ie+gHK5rJ&WXX|pcL_n7>(atx z?&yFRWjk6#T^*9IzI@P12?Y46muv)D#>XASKeN^Ng8QLUpA>~3+g47Z8#Fv_+HkNN z$3##!9j^$o^y@Gqy7RvAV#!EK7AoT^ho+7=z3juu*8QF~eos}*v?k(0#DCv=8HF3> zz$WIP=eUMgA1^C})zIpfD?kvTrNIT`6w7syoW@oXvi@RU8{0fU#mw6 z;;bxGqNao%F!s`n(|lg2nlckU+WaCzvBahlaueN8bW^-m-EspMSQg1^^m}PlKeH?| zIu>>YvnZpPWSj)`J*YXcR0O4FG?+n&m~|W~!~*c2`p*XqiwuT-(TfP@cJb`GBhz@2 zxt#9-sZ;ANeH&Aq7SH0@6B0d()U=i4MGPb66n8 zoFO8UR){+kjx(j1+%^J9g+T+t=a^MBQ#Gw)DXppyj~mw6n*<-OxnP^*Js~SWxNoo{ zsfQVPxWz!R@<>}2>DgKC4zSAPbQ)2@VDkwEkP}D%okBP#f;YO*`6#d~GP5lY#K-D4tbM8`VV<4+8Jv(Z9(gRAm zF{wx|k??kQF?uPyxu&=>SJ0jpFTP4O)>I4(Cfuwi7IxWgtE7HJp5~JvuY&i;eTfC9 zO{{mJJ{dT)RK@GbH+z5m(crRv?~ZS;&EBS3M{vsD+YOg;AsYw8ZCLPqu&u3)yYFa- z4B!g70r|sCk$o2GNk&$CpwCwAo4A|zPzWcw`m<~yWbt;E@zc5AnH}eAZ14LW19U|) zy+~FB;Zj3`3Jeutf0Qfdqh&XO`o0(2^X^W~XVISYKl5$T?Zkw3gjHR&!r181Rgl~i zs8r-3M!^)yx}!N@eN9&3==5!~b>Fx-lll6($HAJSy=M-CWz$%SBji7l03WvbPNZ62_&6bfFRBb%;kd!^e)>^>DBH4)pmGw8oQjzm0_^z^7qX=k~ki`D?%YBO(pO0 z!5DX#GS|jE&}xLyAp=>;=4MK$PW?CBwAfdrZ(e5|14QmNBGg=^TNxSGpjXZFLi+P! znL9)eRFXWU$MX;a?XDSvNTX~6k>K_Eo!GOz3P8`N7awu!Imxf}N;$Z$c#phAAlh-# zlSmXuovS(Wau>Q6OQy`JvRUf%OTf50Db0P|)bAl27xZ!M zJB1@O-v~@C1}^&y7Y!b`G~N8WKCg*LzO@FzBpLo*VstpKaLS=>9v6=vB)?&@w?D5X ziHm?wBk>ZA@^d3d+FbY_x7(rg~t8aQtDgLipxG8k>P|h7@ zrZktqx2dI;^AJh848^Z{aE$1(pCNF_=Fq+;aQ-5a%$^lY4EaSk*+Qj?F-%=I8dRSD z+kgs=D*E)nj|(|T9`NMgkc7marQyMuy7}F^7^fJzVJvNpDkO~_CU=*zt*p7Tp%L^- z)qR`rek~l&YNs4lbB2^{ri}DhRG4gCkxGoUqlD(=!r!Sh$V&zCxB#jwML;&BOzNqZ z4d6%%`HlU|s?Xh7)#~|t08ukJ=yqll*MsHgiT{^dwUQ0BWRQcY|? z(;UFmJ7-T#9qg^+A$$>cz0JJ)SpoU`(-Nkal-HvzyC=$N`Y6`VU#m^9!Gn29ksxO1#@kmv z=}>{i;UTrDiIJq!7*4|9t!1Nvq#A=QvW7>abDUyfkkaXh;TLF86G(Za`$QwD4TlyF z6hB+8aGl&JWJbLvQnAi`8Fztaj-J@{2|?~BRaIr;&davkvH93cu*d_YQEF;kZ<2Z% zxAm7ZnMEtiqC2&Cw%~W!FQt26aJgfjS1cYfqoIz;l@-*pFjTG?sgTX$AFow@**(wvmI}*+VeST~p(-^Uj1hsq8|HfdHy+ape_O_*E%v zJB4YdHRybrO7lRuJtygVDq^Alx0Z`jDH0s|F7FDE+F_$!h)2 za?!%taaCvxq%$vfV#3kkzBhC;r||Xh_4pvqOhTOo`>!3Gf#phmu*q6_mJ*-Q9|{unUlk-rmx3AI z3v@14Dw#olr5Zy`GKThsprU-Kq_cTXj-nIq|DD)~xinO*Br$FYb$^vMm%eHT70G90T^>Cgea?_;GFnZ8aB3AnA3@^1rx`4vl zGz&bNNTi|mNnxJU!?mEOHYsjO1UQ)49i(>d`0`-rC6jeLIq(P?(*cNO2r(8E5n<{x ztLPwt2A#eYt*FkCSSlu)%)n?P&SZ8iU{D;crph=sO$?Y!h7PFf{9?BFSK^#&0oZy( zw_NJopN}1Wmxa9a6$g7yo;v4{E-X}Ux=aEZnVLrr{x0%SCrCv{4#YVOm&e1QA22HPmViUy&k%jJ-@k z&4I8i>p*u+yy*jd366Ak0nQ0xstWbzx^GiIzc;+2Pt-omvF5v_E+NQF$!Q3a)H_@y znmx`XDN$wijh&~kflpv2v<&wS#a=`JZ<`B&6)1sQ1ai0j-@9d}VChIZUJ8RotcrVnZRozAGUd7wmTpH)T z2B)%8HV94%I~_qCupJ<=Cl4(}^GuvPEJE+R|ETeM6L4T_#Sb24WB2{aAIW27AEb;^Bc%qRQ+hkVS>|JocJ!WHNUk6g5w#{bZK2zBfR zUred+Ei6Tyz8OHl5AKhRrUe;l`+GU3DFJx!?1$3~{A}3nBNSjJ;|eEcgiTm-Qy2IJ zAK^ZD6-9o~?+^upwr3mfR`@k~#S7ZD7bF1dB{;;wW0XNRmT%RnxA-|Uy4aj*7#Iwg zEF|aT9v=L30BeC-wWOs-7pd1-1VUM91&5g`b@1! zDlRFNpAaT#aAJnCDY{C{`FA0(Sw}Xhl&t}ds5_R7Skd2}i6JB;rLoz};b*r)qeROj zv=L`Va~@Ss;o@gk^Pih!g=6gXPAuQptPxU&%?=>o-RL@9#vrGKLj^3o<0^`=Fkon2 zN38|E4&czzWb+|PjD;*HJFjac27$jRk6?g}zx-D4CwXCwBS+%VX^)BgfzYWF$CO#v za7fnX878HHTf}gr#5kh7ewRZys^BH-)F=syEMiwhMQWlCrTmHHUW|txcvQzaB$|Mz zO4?;Tw}yY$nfUEUafWVMk81d6U1sLu+?csG5ogfs*0%dLM%o-+Z)C}&~t zI`<4#J@tGRh=(9SoMn$!TwpW>$V7khjnkqFpU_n13U$eCsMbWV)K-cd0~_shp6$#4 zx{H&}1*YMWCQWv?&N4rCXF5V9wpg9DA|^>Xqqe{a+ds<`8gTX=#s3Z?VeM?V^~CzQ zId1{p;iN<>^b?MZxzuZ@*L7czF1~3!bnVjrm+6&^p0kx=D!DXjIG@KjCjG2xI*yED zKJzap<`67|^-~)kuGL_#TPhBA_3=gsiKFgAzeFrll>lK6P^~YNfudIh+R5OI2dWsk z&lqLvFK=mej#i!l0G5u$$+91YN6Wy^?Y=d{*}XFKbgJH%aZRfdk^1Pj>>R=+pK(c| z)O@(>0J`(B?~3W2eLAuW4{Rai^U78i`mYeX1(eCU1^&tMq+<>|CsOUo!7qNlThui4 z8@}UoPdy>1IIVI<`Ko@8)Tk4x|4xwFH_Qzw~15!g?t+V0P^)$FmU3 zLOO$fv`*JNTP>N3#fy>KWb8ID`B&#B{}leJFr&QVJ7bfemNpHbxu3~aI+ZqcMJ<>5_t&KNVd->ew18sG=OXu4QKOgdf| ztIf;23CtiOx8wyR99<>(dZqL$dj4{Xq=wW}htv4XjX-qF?PqzJz_Hxr1^wyUW60Gf zXT+sAnPKzP;Ch|>Z*%rSC~Ct`T!NwG)mY(9v$Nj`!u2TS2oz||yAq4N1Gr}W`_Ruk z9?2w1UHGjec!EfcQed;)rMcK=tC&-BThFZI3o%kLd9laEO4gn}cs-+YieMh6FgRIA zbBNE*H|>AyCU-RMdTmnW*r)4J>{|}j3lTmVG&yU&;TW8{CpDx##MCo!eUfBG036G} ze2Mqr@ZpopGAGKJ2I#hf%zDdXi*Pppq|kyw(7(r5qp6=PIhrfEBF$=@f~Ep&c?l?h zVM&++T$n;=jfL8eW}o(xy(0NH`9H6se@|&5s;_kSKmcJ9X0p!G( z-hs`|O$}6Lqe@n%HKToSSEw?DLYy!I!>p~ubjUQ)%#odP1~je+PXxJ;NGiy5&jbQ} zEl06u50H)_2bg@ackf|~Dr$h|!)9U0M6 z36W8lI;xlr=R0LYnt`234?L57joaOCB#}SfjI~l1Q4M(f`?24jl4Ypyby^8NSvyMyPHH>=9 z4bt|C5v-k8da zUNYXyYZ&YjwGXj3k6I?LVsHGO%S@>T-0!LuFCVEC%%@#Z35dPKC020F%6Ym&uyh7H zjzOmBo9;p?paZ$6r+cQC^!I{`&*OHITx2OaebR3ZzH`X-%|Bm|*wNkzY(DM6Pcf?* zU#xN-G;Q+sSkvv{XNhhx4rYe9-T@;oyGGUntM%*B>C*3bmwIjIOcje7Fs-_hwoXsx z_I_;t1ZX=$dB5pho%p&Kow@XyC6HMeXk+d1z6{@SQDJWw-WSL3wk!8lBgg3#=TKLW z;M4kx%*^HUDIxFU&XhaL&w8=?fh=dIf% zzb%96%p>HIj=g3A!OVSwrbBDZrf*b{7Nz9FS$K1~5?~@K-yNYGs{eO4@;AeL@+2&# zBJt`jYr<|LtDOC-pcdB$-)<7ihjp6f7`^#*iNK1S^6q>8yCdV%jhWnzhlI%+bw9%v z_i@D{bV&?cq<6P-`g zRYJf3*%kQPpNo*bSt0J(#Z#Lo_K3U&nIP_I5;{Y$XspG1BR|zdZdY5}>r#9}K+tyf z(#HCc#;zxGT<;g0W{n87QMGyqtHL!k{Hzq>tXSG$T&>QbZrwfz)o*{$ z$rR#SI8!?lXBQ_^L)$+odm}42W-b6D;7@{y5%^>NCz*x$Z!#}0fI-&O&fLWUz`_Eg zDFYZ(JsnH|3>peXR;I=-00tFTBbUD@QnrTXz#Hn8CVw7gV+1frnp&D$xB!?r*#Hb8 z_BQrTDh`Imrf>|Rrf!zTrV>tuo&W|ZI~P+Y2YVYs7gK|{>U%Hd|0$+_$=)9^Wo2jmHyLI9Ph^zs|5`@b z|38<}Stir~K@f1EE6O}UuygBeA*f(X#kx;epz3EPeC8rxOUzrt5W{`5p<6Edu(NXN za3gM8|MUMp9QYp&{NHonKE3|^;eI){gDBAa1%w^i7XLq)T>eZNejI)q$tZRBm^)sv~m7xA^LkYW#RaDx&FCqa{gmV`hQ!(HnKFG zNrzB>Y`v%?U#7-tZ7gv7A~EP2Xj za}C<+0Bi_wzWYGz$c5u{dsgjK;$A)L?_LtUNlCIsx9hVdMPhF|Ydq6ZZMRx*O-HzS z-yKj3<=Msl=tSGz{RwiOdk|OE$f90I{n^%*B=If61jl{fBQQdqu3>%$FNKZ}4{8WX zZ8;2IZJolrrNn&e=cR(##XZ`wA->s| zBuHY9VArB%_Dz3*qKAaHpIPomEuZ}|GM$eOZ_FT*SUF*ld)%`!!E{}~=&tZux`}u{ ztHbkEa(AV3R&ldd^q`eNF=2hwTPSN4qDo+GwL{5pp$;5F%SA$46o(%DEv}DzsWA!= zr|ARisv!8e9Bx!}*bKx;5N<5Ei0bOMRbQ9iWhj#o4{h&6qQ7TF`tB8@1#CuvHIVu8 zDFtRw#kbW^I*33ER?}y0Ck9$|J&JL-i9x^DKYS&j1ZVwvHM6@b4fgA*?WKQTfX5-6 ztnz6JSx1wV{v-6&EA2y>hdS*;8rLfA;~Ag=W&9f3$bjgR(Zf5<^!xqGGgR7#ydUaw zoZGa62jGIk#1pj`_fW})UdFo2D7x#?G_ye6G6GRo(@v$XY>_#+Wo23%nsF)3(yUh1 zwC}LE`}ZcTy(!~o@}h>X%$C1IdR%_^I}#GuyHB3gEyG=vQoUK()k2LUW)y=N;Dyn$ zY1q^Y5pl#Lo!0%>;_W7%r&YB5nj~XRBbP}Fp8KpVn@XkUciK6~;Q58Pv-2X%2*h%Y z@c?~r6Fs!xwWdJGe&^EnHK!sdFq>zjm-x379ah5r&kR(Q#Yk6nD7*@(9=;I*)*0gs zf#}@VgJYgj&3>!q%QHJn(%CWy!`{bK*<`v5m_r4nUNmPpCM9LK z_^|H-g3L3A-=AcZI8Sn_-U>eIv31`AU+?K9#?#APpQ>25qtsV>N0O{l#Fp*?z2PxO zh?CjK=v-RdLHEt50vU=Tm}$0DXd%&Kq$6STyvq6;)ytwG&6`ai?%=CvB8E~_^^8&k z)taD;1z5$nY{OzK)}dG=_xRoqC*$kkYXXSo=b2k5c2gb=h)m_QQL_h=Ck?2ROR$lr z5iwTbvS<8MGOTsA<K085>c%odIvM7 zNCG}R@zPwU`Jt0cn~U~#$8{p6UpB078rAs2U8~&)#L)TDV46^c(iJ}^8%a@aS%1&t zP{g8lx~L^n8ptD*Om!4{$)htSO7y>iINMzkNbVjfD6jcWRoETE?8_$etUUR4%ZBOq z`4yy^b`<1CiDv26m-1GOZia&!`-=dIUk@9Fg-bR1Nu>GF%WmRTyVv78w2sAC|&v^P*pPfH**L?Y?0zmk?m$|Fl;8WEFqvR`EW>Kc^oX; z^qeiBXNcSEY%^ND`2F#;*6yZLuj6ldFS{19bIX8JHi7tyJO|hO-bnH`T1BQ&szdhJ zs_^slJ?@idMJrhYSJVc9Xrk2}I_!n0fp043Sm5^TN{CjZ-wR~xo@3mJt>EqtlsMLuh-XOCf@n)E9^)<1iZbVxJYKE+JMN z%SqY=4<$gnD*uQpC&uwxBUt$c|3l1T-RMz*4j)T-FyIsD;zJ6(OsBH)+SAVl7Ls2< zdc~wV*oVo#Ol53HdcQzuz}DEx1pYSg*l1vWOz5|!Yf3z-C=2Fa)cu3HqpB z!Lj;VD5NGFbSJ2wu)ac+D7h=y! zA?c_;HBxp-=MVL^8GAPC?K(M$w9#Vg9XXcitSo0Jh7a4SNYxeUXwoSdWgAwel$Mp& zwhxDBb0z8-Av%UEme%pZ=F>#3D&<mCHrXV^4G(+4+jkEPxy)o-88yRUbHq@u`-9t`cBG$7DG(?c0&llR$EK5KFA^bzE=jp;V); ze`K1T$<$(XkBN45Pwf;stF9v)5rzyWxP-V0A!l zmA3KP*6I``5KL6H5Z8{74KL9(lR}!JUwz!2p1Zu61Tu1|z#3l9GBJ9LN)cF870aoQW4N_{^GRENibfL0jrh|4!5u;CA(rL~BKqN7Q|<*X z-+f9P6#_?{Kc|}bNN!J3#q(tWoU{xeOT`oio1E#IQ^|#C{c*S_*f-6Kc}W|M94U51 z!Ct@oh2Ts3_4vtce2UYaW0*KK|8en2axNz;vFjwkA(G%lg|BWjJJ7inoR}4)sg=`q z*(x>1lZR4+cTL)bh~$_3aad}R?1t`MurLgfL`a3YUbWdMRj1zIEXB(8Y)Ny$SJJLq z07n*Pw)#b%j8?}5c82!FJQ%u%2K-e)e>vuIxD~flk$PLoqb(oY3I6!XJoTu)C)!kRd(csY$$8~X{<67 zAy8&0P97w~JQjelMV=fa>z!hB{+W~HfY6yPuj;M{cq;uV4>n%(@1CLbyP7A~Zqmv& z0r{Pn7?>E>}`3pcRoXJhjvk8tsP@I zKg2J1nLCg0ko2`?MVC^XGQ1il#?`jHNGTu>hei zDK>iTxEs$@epoBSSVC=KBHwtsAQ)XPe^0%H+h58?z&e^&xnFRnXax9KP8Wd_U6-)d z9o<|l3i_>W2Xc7Hal!p8J+g~ox8F*LXp(r^EM6=1d;Dt1xVIgRtD3gUx2$GPu;v4I_i$NYugXSxn5m%&*P7%)Mvn zoG=JvM`~q~2HJVlP(l;e)>d|AT3Jh@Fz*m-{h!G3g{0wgLxqOopiJK$LH3Wp%JZb} zoqkd&N5^RIjujPGcb!Ua(o7N|VtmqFaneQ_3KeQ>B*EW-Mk~P~h5Gc8c0#Je7=eO3 zSXoE(>m=FqhgRY9F_&1xEBwWJIZu*I7Ltb{i0t?dy1Sj`d{ofy3$ShKbmClj5gt?9 z-6~hhTQ2BCa3#{$9~GiHwMaXhz6%jVH7K8sX!iOs_1lSJP=<90)IR0G&dn{!OJ=uP zc6D-k4i4wio!9PO3@zro2>s-p4Lm7^rwyL3s6QkN)=NkLpU9E1Xsrxm z>cPurAg=No(@eg(?CZO>yrn_y;i>vXMmCl6&P_fhtAEMh%#U4%&%b%9h7o&NR(Av5pS%}p0 z@|4uxzJKIR5)ntY>u~~?YRWH(@)=di&Uss{tcjiZen&l8Ptev@R6nv!ldHEZ#{c5d zHiMzjHb>fiI5ZZFYg1b`Z)Syl9U@pbF6wJOH_HVePZyvqU|;AmdLMWkWglk*=L?xn zUqtn{y^10{8_{v z2pQYC011DzZ6HC;)Wp(I*xmz9n-NH0XJQ6$a53xt!H55;mj4sv{)1*KI@uemn7RP8 zfk{zuAe?RL0YtI?0$b6`gg!J;7@iDXC}a3?}g*%2Qd5#6Zmsa!kG=g z`Zoe8;mizR{nJEX1tpwW0IYvkR>GMT!1@nJ;7Ju@PWV^WzX*T&_D}hL zLmoiv|F3HOXOPE#X+99Qce4G%4=39{6@X&`8UlcQ3eX0=fSrpGz{11{;9vrFD}$V& zi<2er#vh(A0{;33QvW3EJ-IVPe(~5_fv^-45A!N z3J-)x5mr^isXD0Jyt*t%rdt@G4glH4K|B3F2gYBF`ola9mVe6XU$y@yKmT`G{ja6; zKlU;kfbAc{=RfxHAKE{Q{%2SHQ{w(RMjd8$u74SIn1SN-FWU{=KWsPuvho1f{vHS_ ze@PPC-%|VEH0}Tg*S|OJ1cCs60|5V%&wHVYhW~{zXA=slza576KjHE}4F#;6|K##t zgW!K-1?xWt*?)%>T&(}bi9gBzM^5}bkpG)a{|}Pq+6n2Q^P}tiqo=FI_JmY!meZh_ zHrg>mT}I1t=;zO$F*KG#HY@ce(PYs$wi!}p@;ic`EoNCg@F7`pBAUBNSZyz~C z#wRCywQjDR6tC!3UtiaAzX(!NQjTmXOHw>?>9TheccXD~0?>t$`aliEh9moocVNi+ z!y8G(e+!LR1VT!$K%mPcfkS^#!1ae98Or!5AfvKTg?@YuQun`w zJGQ;?omUqQyOq=<6A+RS7o+@@ZV?Fpqha>Mtaw0{e4qVKr9-IS=`#1&Oa=(r!6X{N9NE>U_TT(_Zu@R;XU9R zOy=yDnOPA?%oxJDZ4ix!5DR7qqyEWcVW^;V3C;sj3DgU}5ZEg(9FQ`I?zQEn-4vd?DxkPBy+eKkrdfybsDgkZfsd zV%)a1y4btygq37WineBAN|u(2k&=>*_;|{MOw7k z?;S~~PY*OuPm69lIhmhsD6i2|r(I1I8=Mb>nZPT=q&HoZ*$+Y(TFi))NQAvnJ`dA~ zN|aiZ&cdG>%|$FJzee&Pf&lpdRYWX0;SkfTy{(MQC@kK(&oqmf7V#0mnCDw@yrl(0 zQv&?Y&qX$|F_(r6s_=9{Iw9}XK2#q8!Sg!wwW-mXUlzksW59*;KZV$K4dT&T1YN8i8@IjOLEX6{M4D_`9?*4%_0~SA`o8 z3G(Z#`kw9Kv=iFjc1mKLcAiG7e3EripY)o~LBpDe#urCsZEDgwy28`6w3Uh$zF3UA z?tgSBQk5{UGWT22kkuBT&g=;3M4&?E^+H25XVRskU=8J$eaqdmlP_q1yg7433LPssxAy+0e)$y22&r{mk`)0CStTkmz`flh&u_kf-6nvapc8*=;G?Jss1Z!eY0|rf0HJ^NRA@2J*cY z^3#gtQ!erj_LVOVS_Tr0H#3|Mt0+Hs1x#y6L?)O-kO)}U+uScJ!ooU4g{!N}3ko_5 zoMb@pmDw#VEk!w(qbz913J4oX1#ywOKmuX=z>FZ^i$dotzG;u5JT^+g;4mXFTWjg2 zd~Fet?_7JmO3Xr!eIPF=m zIbQubT)~e!*S(VEd+6{yTK`+6IN@gHJ(!9hJdrOc2Lg$gL}fR}8&Pkq&4 z%|&g0VPQc{O^x-N9Q(X_|6vJrzeQw7^R-Y>qaRE*p;V{=5d$hYtFjh#ukhoX;J4Xn zI$@ezQyngLi(oehWT3v{M1u^t@$^@rt5LGjU^hj58$@d72*AsQ#t)A)>p@- z_ln|^8g=WBTGhKzpQ|76p_d8=Z*E0Lei8>=!;jjouocBh;k&FeaR?3I3|+5d%}e?p z6V^4|9^1W>At57~nYi&YwwU;Hq1(i}PYFO*@0mm@ahU>=K4I{PoJTt52*#PLX_c~R zVxq&{b5T-OOfU$#HhQ+Oa%NjGx#DJKIywhS#g|2$mkxM2!s>bd_VwQ{#m@qjY`)I* z=UGj0_6h`V;~AR*xu2ic+VTk}k^NgXAATh1ZdDO}>}s82$*|>4s`_wiKlRx>;Tc0d zK+O`~mTYonx2j@%hgU4ehVpjndc3|rbi43#T&mu}XI;m_I8)c(Qq|Rk7M-3VC;I$2 zEnI+_;3&Mt34+Nz1dND!mJkrW>WacV0$|Lq3-|;flCF4YYc~@qZ;=o`IU|sP>U4Yp z7gJGR-dH~qQVe#=vYxm(0$>06^X$=IWgh9 zO^2C;J_`ni(NGsMnp9s2&EeU0T+Rb^FGRTUK?@cfi}0pCNwiAkB|nQa=ulG;Ei2^S-qj zH#0`(hvTvO)$IOttGDNE2bT;k@5aV^;T47cYw3?ax{7-?b>ChJ@BI0--hZ4IUrs%I zJa`SL%e2=VH0H`;_%bR#UKnh?7<+zwMq|%34r}m5*A0Yp`|7 zqZb$bwX&iDD(m^#>o*ak0|-Pipr}f#fLuYp*-X=u>oypKigVBh0Gr6a4PJ)QG~FiR z0!GfN9Ta*iH3f#`U>TEN@V)!HR@ygImW4z&`}+Gn)u-_2rXKyur&p8lv~fCph8DlB z#ryLokuCkBr(Y+J0yPuwrpB)f9ETrQ2Twix-H#6s^sl=!Wj!`AzP{ym4LBR-Nh!H8 zX}+v_&wHZB-n(M%(24A;K@@PjQ9;Rm(s2=8`qb?H=XY3oYgf;kzC0dar(wgZ&(Ba8 zy+n{c4uTbuUk@!szT$_lKPB4+id0=HLZ-(f2*a0wjDIP6W%UIJ&$3jhF_X-n9i5z> zxCXx0$D*e^h*uCy9Qm~HR|W ziHBuh!K^8_hRwfw_VrHdrNtc7{PA+Bcs67wc^| zLtWl2&YX*}v95A*5DFwnFvPR`a*S`l@Xs;$1LPT@8WA2xk_jX*gc=bo0SNp2&yW$4 zp&&vsEJ75b)JLg0K|0B@iq6K)j+Rzw^zWOJLmPnwKb+MR4rp>|)%`k^-;S*|%q#A; zviWW|p1SR)kbS)^_ZT?H-+io47?8ibzaRb7C&+$&sag=w&3wFA-*zu)@75@LF=#&; zHCYK~S^at-Lx^-}gFn$epWs=~{v_%qs@dvXThS|IX>0r(l`@1B3|@1#_K=ye^4{D$ zoSK??Fmdg#OuS2KEDl3y;xy{=5rHg42m(r`8~`~TQ4B)zM$ecp7R2AeJOkf`Ixr>+ zo5Pj;U{G}3XK|&i@`-R_!KYUB;bozHY6-7L$Np`%Z9u>##>d}s?}et&zn%ZRg=g-= z<8*Ipi}!6Hi{j&ObrxCupqqc=VaRB$1il^FOgi_c+J{^F>!|Ei;ji`RD$7&Xx{6PA zBpJWB-jVz!iFqRPBg_p3*3FID@wAOdc6||J*=%iXtFlaK$Pke8U}#E@N_-?OELu*& zQ{ie+%20YB(Ssl^`9(s+?^>et1`xrOI&;OZQo5G zO|*J61YYk}jP+~W-_KVcIk3V$8kz1V_WJ~c*dG>qdOF?@;gddmt+qRy-#)r#?_Wa8 zzaESM$IQ#7%Zj#f8^>4jZxzu3x_C{B`0F`)IR`br73)~x;{G`fpHeuoRs1?9DycXq3^4ltjr_BVp@V#fTA~QvqTh4k)v516G7USWW+}T z=-OE8c?IY^eDfYRBknIk?sHO}-OU;mFf068blz6HPSwr7`non>em(r^?{2x5C1dR0 z&Ga_ZRxO}M@R$pY_tEP9Hhz~c@Qv$!;@FsQLe-};@6AN-C0uM{4PETvd2W(Dq)IKU zS~cvUxZQ=c6?mX1{v)($5V%u*8R9_v5K|NLnz>0M4G+)TbiT;vEXW@asHihvQ4E)h zKrm1RP*6EVxgv5HL9=tEArJEcU?ElYf4k+8FW+4k;TjMv4aMviKUdxF>0Q!wa_D*j z!wLfgH%bnTAc{OH-je)4%ca#tS~;Ng2+C{CD~bn|!h+g_7t_-OY&=~1}z z<8?o}xtl~*w-V4A#w>mAik#(;-_z&M_Gsoe%jdKp7igrIXkZ%S8qPSSV{uQ^plrh= zPGAc+H8HJU8}rdOG@K;=bV4&CDBKw3HwdjUGU6ymvGj!#=Cwo?fQ>Szl3%;+qX>!3 z?R#X`XTnl-r>~t^5nZ_0?p)()u7gMARjcT3Uho`y6&9ZK>FaP~kFL$@KwF`{5Ox}c z0RPIzen#CE1EIhF_orWqD}{kO(0r>Da}oktJkNa@aqj9_{CcMz-u16$3I|OhmWS3! zY2)bf>c!tiVoMR6W8KgpM{H7TNN|>dEG-SpS~u2@TxnQ>pW9DJB;xQlX<5kt!cUe_ zhU}wziIJOQ&_pCYq%(_RII;$9qQLL1bd;_H0t;GEiV%c0FK=xLW6Rg;gwCeabZ0uL z7ASaoBupLt%Z7&-IcxF>rXB`Mymp|qv;~etw1dY-j8Mq1A>EsCnx2>A^~39{rC4(DpIl_sQJ@AD72KINQn{b+Win3 zc__r6vV&Q!=&#+tGK!Gtg~-98hFpQ?^pWIzX2a1Xo8j9Rl3?LIL1!~X9eaj&D3lUn z8FI8oA{MgxCcD(#GX3iD-D%h2{)%)f%0MnKw|!(w3h!hQdzUYrkXsM8c{M%2t(^JM z#d{~?dAaF2BDBA@cs;!>`)Cs4gOrPjsWk)ZuEs?Z+lH}qck~4B^ycRD!U80D zQ2+u}7BROh$}vlfm4zs26$-{Lk-=b0jd*?oVdjWdxL8OE6j~1wuK;-%ltu^`!=MV| z@7z3VxH3CV!Sj}VZc?C$it26==rod+NoTa&nP(l<(Z2IpA7AYIa*RWfpQG(u_P)G~ z?69rFKzHY+{~~FGQ(7M6tYhwaM2wW*Sn1PU*$fT^OpU70k+ad&rhv8UPwSELL<4r@^p&J!ge79t`>Bbhk-Sg%GA zLnRlOg*6neJu?3_{c8ZYFR^IXLBd=r$UHlou{ZFXw#9XiP?5#GT}F17^(;zoR5kEn zA#7wQaod`0bP2bBocV2BhYa?fy&DYO{J>9QO?{r7XD=qFo4QcL-R^s?FxI*99>uBT zJy*i#W<`_^jYQTQ^u~BX=J?O}6=shWHXCZRDa)$;Yg3E@fIYmsTYNsh*GDGbeAr(+ zy}%Fk2oD&1uQ%d@0L-gI2qJlzYZ&GH1z}@QDGZSO3=sHsGVVTAKz_njdUj!s+-P9G zhq_Whi%etmdX_nEzLCVLvq~5K_#3N)$hvs{hB&$pIWZr_ZZ`LuYVqW&2^ov{Fnsdz z@G{FTMhrHzcp?qWkX-UXndDtE*-N?8>jK@?`g07GhR7FURND?OsO~cktcIplU<^xR8}iO6My8Z3fHI8x=7Y^aG|44q<{Z<~!`4o@eL8HFY0wF!i`;Zp zJ7yvKB?D=gL2Z2|#eRK^%Zu9bfRntq@#ZP{uAb7C)P}N)F^e zP8x+>Rj1KCbnT2Z!X+~&E;JYGWv}@b>*t(*yh+~(*1Jc=;EgoH>}x{6P86KAW_&x* zQ0GC0uAoJSb8uVNEk~!~(OHqICwX)FKUu zZAt6Fa4azuB2CDbM6VNP;;wK1%-KJ z#=Q3~9BCQ8|1a9!F-Vgr*!yhTwr$(Cd)n4a z+s3ptZQJJ4w(Xvtwx70bZQmR3-S@_OH#YXm*5^8L@#cW7hwDo+lUo$iiC59s@2)3}ggHJ5tMt(C^U zz97KI!tq_{0HW{;rqbO#s`~aCZ)u+1v(RS1dgv&uO zK&47vyAMU7%2k#SJK29`HRl+=-2EIhfHX;Atvw(;J^eZRI>i$DKvZ&ISgaY6oI_*9 z5mF_b^X+9-^v50IDRGV<>_Z)T$-wFF|8norReo?Z^mTDh?sM&(bMy!zXO^HD`{ho`GrRC2`I9Ch@wy+>R2r?|xI_(^L3=TB8%TLgsxX)MR2Yi% z!0UtI7)yKwg*EZ)?R3&pV5T?wiPZ3I_2qT1c)?6z8 zYTEPBnFLO(w77LX)wQbLrk^#JhUO_i>dC-*Tg>IsL+8@MaU$`6gse=(C8=Cfw^njN3pUsRHmonpTlhayp zm>c*N-g8DC?W9&E9F{e7_5$oY)6$34LwA+kI`)a~%4rCxvSno*5u>xaSux88F^I@9 zfBqnxP3{;A(-FX6Rv&|!T~oj{6~_-p^FX11!r$lt$NR*(AQgTiQST^RQ^-Q_cr&QL zg@z{n`~vhF>SJgLwy^#4tgYJz0{%8v7B_blGX;!RH03W~I%#FzT|S=HdK~q#*M2zf z&IvQ!_pTdts1koY0)n2X=(jT}d>nvtIpUOevZ&ynhT$6*>wf>b0b6P7@RxBXM2Z?h zcr~6bO|R4AYgq=oHYvMzl)K`%q$T}wR=8S>T=a-4rEs>Lf*8y44Qb+ouQ`H0q8CQP zpntLTiJw95Z2*Op(>#GfFo?b=RB%Aa9S*GT_(wbi+5OnKC-w;_AA~#b*Vy)Ff3Atb z*v`(i@$5-KVU>6W{-76j=U;nw^No|4PHvUQCG*WU`DtY8N+u60p^S=`OIv3Dh5M0& zjjV@A2ZC|`m8^%$UGq`=4^0Pz9|0`ePMy8CmyW}ojc&bq_TCN+ zhcy`u{DLP(mE=d!Vp5HRZu2urWTmayf!lvtq1rZ=Ed=c8R>$Of<5S&h$*h1w*T=`l zk<;JbEfi3s-Me1)vu!;I5;8{-C~HwrtVD@EF?Ld12nH3{s9e}Z+o;9>$8zX9c3)FR z({if{{=3(ohtn13{7JaTrEo+_SUu6B?eq`Isg>NXf3FZR{rHw2FDkZ#rh;A{57*(=&NHe}snO9tyd4tXETgYp5LZntk$jlvu0gXnwA${zkK z>MxH!Z-Q&PY#3q7DhI<|1RN*hF>75lugP*K+r`cV=qFX1Z z5^?Yk>M*C_;TyA~SgY-`Sziyir%(NbG@EcCL;Dx=nv102QjgN6R%ke3efzgVg>7MO ztX?BR&wr|(b>v4|yBlZII!~Qcg-0ALOf5x4uqnmtj~M}*dany9^A1e+ z0S$j|w-gpX3JUc$GmfnHj-m&{*}!9q->gE{)*BHUHZP+ z%BV=g>>}#0@R5o?h6KX?9sF9JB@l4)KT^KEK`9fF;3M>cSE?W29jx+kmaJ0b85nrD%2myG}oTz1LPg zGU!%nTM~(vQ3y zCg0EBH#Vlu&lyUx$|f?hb92RVvWu_tO|MxjJ&||8<+nI28O|co(PrX)kR6A__uGWP z%AhrZ85)zC!(hdGWzL&XA1b@faFRiA-Lv`8dtA=xy&uNYV{D_{jgc(|9H4U(lT`%o?pCyH^ai*sX)c8h*Wd7(}3VJPai?WH!ubU@+Vo;7Gm}}ZCm6FWt z*0=*cq{7Cc4jM#?D)1ndAVS>y?2C)~LO%$E42V4dtvf%I$3MhhJaikjP8r&_^?3+! zbuW+83$ZLZ#?=T*M8)u#!<<;w$rfX^h>u(J)4l7uPd6_+!Z$Trk zWL@-~oV|g>_yfBEl98S_YN`0ju6JK4U91V1zCgzdEU ziTx{_EkccexRQ+Ck_k`lESM54Z*;j+x4GN9zW7$*McLT!Jy7%lGCMC)ESr6 zmRCBChOa<$q;pTEfWsX(b&qVUrvH0n?zQxQb+K3q#8-sGj!v32AI@P^GFU#$tr^do z{1Z~ObLdpSR!YCvO{(Tcvsxy&N@#ux+@LcPa3R;f$~K(2a}vY=&hPJaxi`7^AzE0t zx#40u$}HaLKxGdPOZJhDH^YYG30a5?r9FU}K1N=NAh&%j30oGpf1)7NV3FaFVFFtz z{N^zEAQ08cZ5Wvt7-txnr`%5xsjAS@Y?6+g$&MDt!_C0Glf+6J(XkqUmM%c=!Vl}s zea+fRDu*NVaa3--Nn)ZZ?+qiDZpJHD$pJb4A&tT>Tis&`%X59zzvG3)B?Bv~?-8qs z$F&-DrJkd;2v>u$Of};~n1EWzd8Ur{=f%@vf%W5!AdDd$m8+fYNOR`j)Abx?Z!z`nR1@4k#HoO1V_pD@hzqw zD8cuc;!NF74C)3{FIlsROc0f>*#VI7pRtImj|!55BgPse-T5QZsKk#@n=w@3Ik4C!9GsBojM0Qg*I~amK~ShCA(e%l6VOvh z!^nQ+r~kd09pB1Y2#2r6KK9mv*LG4~I_?!*x-Q`8Y zeU`=ea7F^t5k z$3d{gU<@cpMDWdK5|pwK3`6S$SedYp5)K|H7ZnR2eg@0r6SB+6h9B-kC+XFd=73UuSp+(-;0>d z3y;0`oQcN(&#v)hk>RVkZxLtKQ+}F?NcaijEkbmnUO~)H(D1cQmR_bU5|#=#P96{H zOQza{k*aQEHLHJqR$V6t$Cr%yFNu&Z5tU0lo+ky*jbW;eey(hDyJ%~)W^=u0c=4k? zO*uMRJ*!Nk~!p3Sb4*SS14)JWA*Pt&sMeo zr-n3=Dam-E^R3m=&JHI+Z6^J8--8Lt_;%kH;BfSjEF}Iuwz;^jpR?ct@mj`%?+*|w zmsarvQOWf3-@zQ9Sm)YY8Sw^84=d1+Hn49LMcRyW$Y-<4mSG+PdpHaG)(lyA@347+JljFIJ=lb@ci1Cp6 zlCGigKk2(oJK{IFYsTRdjxkJ2?Yx_rw|!2RcXw2`j8nh zG4xt>hEN1*6%3s)H!6`(2gj86{{mW=^j1DZL}*?iiY}>CP)QOH!M0ruPLc3hg8kUi zMW-A=>v+T`7_ND4**3(4l~;1vZq1I}Q+U1)Sh>i{ZSnEItoXIwZMwA$`?IPelv zg?$D$h^PMUQ$IfY=TsJkSQb7*7TJLLPsd`qR~7GvKXLBaqp!fKTUF8X?Jlq+>O4tO zXR>{qeHGRvl-5i#Qd;f3lKi}iJja#wvKgRsvQ(dRKM@#~~=X(c)f2$u6MHM4CLP>xq| zt+8$^vejWf|F45%YzDfTo$?4m?R)&qvS`#F429peUoB$b z8W$`E#nvMgio3?j>BkO;UC`I6xWh2vr*YRB)Y8h?PQ@J4MiMe9WHHEPefyR*lNk)M zxzbA*6tnvjOvZ|{(PxS=r*eq=iD+|C!e|kXvUhF>9=qWkfbM;0%`QUgd7); z@5r%8e-wCCBvSD%j0sZyj%m)!jyNgN(gcJ7*nI_WH!3fQuFw&&)Jb}+@)Y3O!z4cb zvcqPF`|Ex(6S+tlh9?DYEi+>?HRYp>sfvf3wwBoSeERRpkewy%-q^Oj!*mdXVh5*s zY1v}rB|>7#*~OWuz}^K}<9t*+9$176yC=Q|gzobxGLC0@P79OzLvL&Gjtyoq+7r`p_E!~v_SuenbJEavM1JLo`>cNjQKRSNs*;sPCT;Te`C2+M#ugDq zp}l&NGFn z=%ZsPR(QqrBSsYm&vbA>nzn=8v%*jB^2}ZL6IJ}v3^9b+`^)zF?LJpX&nWAp5ikRmuo5sg}#`Wi>$)k5Q2A>yo@1xc>@KEb?Gz#dCKP9pF79%*o4vHIJ%y4=D z@#uNO+^0U&MuapVCq-s9styU}u94{4p;OL3X2{3`e6l>LO=`oNIAjcPN}YCe1fJe! zjeKu1mfgFoE$!?ln~~xW+ufe1G{AZK#MT(c-C_>KDc>wDKV*2ldH(Ss@u|;`I7Sd?`Cckd@{7`4;b%DuFFVC*0UXb0{#D~m=OtjJI zafKub!B&0`EFmUkwVZHaDQj|$*d-yX9|gtrq&08!qVLvZ`FDrKgU~NlomC#%qCZDv z4T?S$WXPyhx83Ia_)hCWU$kuGXSv>DW$5{6CA&puS1s+;q!(3ZP#<=|0C3*VY>TSS z8RoL?Dap30-p?$o^rT{267r!ywaN=>hhI!+oXxP=nP9z7(vhmjcqSi_6ZVP!6>&@R zphejU19p+_W8`f)TGiXqz}0-P!t(NG2PJ^v;OS~pG}Kat(h-IjC_quhV)-Y}lg+eX zoJ3&^gp|C5%Y(TsuP{m|vV2sfys3M~{JI!0XI+r710w5pw?xSg>2b7`X6W^T-TS=A zzV81(T=4iFMf3EEQ+5mErWDV&qv0ZdO*Q@iBb?tS<s zw2KwMIBoU@U}uu1H0-Z)%R&;YxsT;Sn~gO6od6|H#Nf!TF_$3{W$9~hVX3*TX~$_M9I4=k>+NczVM3>sxz)&j-=vJ<#wvVVdu2G1tlklYZS^jQ>O6Deti)Icc*pH%r z=#uMa4shracfOKHCiD(y4L3CL{hV%TIAB@TZwZ#lNlE&42Iv&Sl<0xu_xABCNPwU` zy6`b5?td(Xwzl;?qL5f(P*G&K5;9^e5_A;e07;UpP<-zChvF7=?Gyq|IsDS$naw(( zrz*SNmG2eE_g?R0{($1+mgoSoiaF&oFs}%$8)Y2YrR_6Dy6=Yauz_nX`8&fihh|g_ ztt?#Tmzn2PImWdT9xxWjWocj=7PL0;4nPJ3aVDev%Q{}iyxPPot$>SPf?U^<fGn`{7n}*h;JGQNgSv2ZRuc5K&%&` z3qe@zj$O{QbiJ5j zroX%QM;T`Rg`O_>pJd=EgM&^-E5o!_>Zw`UF=>=LP=c=^=)A^PGu3}0c&ot=l&**c zI!gw#B=wf$EfuGA>)-EBiK~Ktb4qRY&8wA_WUv4w&vziAa2e3K{xfh^GADxf30VnN7w%eampM<^crkNOC zyr*E<*5Lm-K*HIC!~c?u7LtV98cBww8FZ9kaE?p<_8Eaeq?a%uGI78<`#=eftDjhfda=Y^@g^*dO(PP;~sxzRdfo;N7 zGx?SO2PEHVP&R##0Ua2pr2nJj=P6JmoPb#;EQ@DTGdX{El(zwmAXF9!$bjwC(^)^D z{1U|4+I}N?l<4*Y^oi7`_7&)*&i=GUB&C!^oyv>E8 zRi^32RCKfy1@cLnCoo%i3%=!99n?tJAvWG_xW3*(FWOuzQfz9Ul@cxza$)1&$iF_v z@ZC#nA}1kxWgXX~p-4cj}1PHt{Z#DLVXv!XFyM*8R)NTDp{=Y0EV>J4b`s z2=e&b7c`xv$N%ne+BJb*_&krRFtnmrS80g8SR z;%*TgWdC|7L`o4=l09YNHl9O?3c);_60+L<8R(vn+rR-v>+DC_2TEPYu;2<);fszM zg0TJuD^Ym6Nm}!nCx zop`v_ZVgRM9)25hwhC>A{afC0gjB|)!Fo#_RxgBt9Fg7c9W~^PT8X%vNjb!34Z((% z_2FBj6>`{lbE3JL8-(TnVCw7P8^ScV4mzLoy>JbDKBtOYlGl#a+HB(!{#8;|Pph&@ zRCpQXOe}L93}68EvQ3&TH3Oo(MA=XJ(SL!0M;iJ?=5|*Q<+PjFmPTgO065o&f=tZq z&8%%r%+3Ah29~n>o12@O0TzuUwt%b3OBP#} zJzpX0p3N0)WN^_ufc^lm(bJR(6s$so zT?AsaUXlU66!Oz;ZNj`PdQ8p|Aa36{YxFDUaAe$VFZFe-DG<-N0!d{FqH^xXvy3Xe z=(v#~I>~}pv|iAK4{#Fz6>}s}t5KR=BWpk($FGCct&hT&Ld2H2A3#QF%roxLM8Za| zVrOS#YwiVN&VobEZYGW%h42i5M2W*6I2X$te`_uWScygE;c;|z6M2BWcfWBxj0{;) zfJ*g&3ZFJ#bLL_2o5YA{b1u;+Nv+Enawx8_Ls)8yh+wJEU;re+Nr(Y7$PyzCtV46; zPSKAKuz?4_5`af4J*PVyH138%o`h+ITo8D6aMOV9xH0Rrw&fwT(VZr1m*!B5_zW4u%yERgWmC~c%Ny-d26?RIzbjL7|28&;{%wp6 z4Go=>1lxt~I!W60Km;eR@K_vI9KtQEgjMs}b*EdTr zt<=}(_;P8{9G_gk!6Q5WE60S($Wb6ex#blwM@$>H1d9BWE{W7;3Ssavb>=OX1i5hx zVaVuC)<+F8xyeXXU`VxfVM>ksAdS^`;Fp&U6s_N2Ta7e0CZ)ZJ*MQ>FfaG6(rpE_6 z#~07W6VEvnBfuxiJ|oXY8twEL_CY9poyC%3N|DT|P~_WA_io|($Yyfu7O?N3@ok=S zU|@FS;CAg`IhXj=@Y~LE8gK~5BIg2c1H#J1B4BK8I(66(Vw%f1no4f&EP;TK+uvvk zZ)YM61By1Fax)Jr2QA$m^@;!)0%GdiOgPYA3y%2->kSsUvN|kU(SR-+3I_O1zt6^l zP>>!+*FS4+8YvdV-Y-G32Y|GVMR9ENX3*zPfr!Z0)`!mx-f!4#Oef&dn`*Meq41<9 z(|_KAgE#6?ZKwi4M!DTAF6p8BS<0lcN&uY00csOR1V`knIE7~;i$sb&hPc?2^j(< zF(NrqHe_{Z5h5A>3i0|1@%;Ae?djFo{^sck{p`6%E$W{uREwlU2U`uUmXL_VOU5fP z5c(cJkh~%xVBCKYzc)s%j{#!Gg8r&`x#z=SRLI*`@$%^g~r zF{Dth;Hb5K#EZ#F^N{g$l~DL&R9`nSKC?OqOI6|SoH#)kqLwkHL>kyjV;+wyAVHS4 ztJ$s_#zIOBwJc>U9UAF5Po|!|!C9c%FN*5_Szu^w<=Ft)lR?K$vo^q zGj7E;7$CE(kp7dW+5KRp;+)rvhd7~B?l#L9J0?(}ckc?1Bf1Vf*E8kh;_Wv-WH z>QN#>8O&=-UbepdF1$-t4(_G{_7^o$Wiqr0b5oq)?hyJGQ6<h1%6OIU}UjcC>UP8nuydmLL^C}){dqE_g_Tbu{05oWGED>K(pxvRX)0F zaSB=-AR3QRiE5&MK5lKgoE6n}BDD$3xwbxR8sl!J^Pjda5?#rXXWak|svh*aHpt)~Ckpk1mG-_(fx1N0@Uh(J~iVqx?Aw`|2C!;B*OUY4V z)SP%Aji|2@l^CN9)POMSf2M>(9q1ne_$*69=wbCmtAx7iPx1Id$wD7VAlD=L)teHi zcm*;XvX&D@V;DrvvG0cnIk6cI1Zw;Ww<)k%LDK633&4-1jdO?z*hN>NGu720glTPE z2=)5aectFt!I@C2m_UR`I&;B#nRByMgli%{JpN4IbMQArUGwJXG^w`5%9dcJLs-ST zwRiu<^We=Tg_v2Zt9RwSb1^nfnX{*L<#B6VJX}7BLQJnTNLL$>#&2xhHEFz^>*r$! zu6R+U32;47%Q*=4|KAA7AyC(5HeX!{$cdsk1SPm_dbhsxyYPx5?r4fRZPUJ=w~v@L|*XGB3DE`c5P1<$Hc9DVSw`{BqixBUbWI zBBpa6{34pPaP}b$AKh>;uQ%Ja2S+ET!q1{F$mv!)%m+q6g7MQDMfrg8*P`sICsjGS zib(W`3Nhu{|D%$KtZZ23;cfSRfq;-68nV_m03ZjlT)X6sDLL>YZy8hl2Ei4fLqUVd z2OjZA4P$BT9a5;%7Kr=8GqB(`dBQ6#oJ9=+O(f0khRJMNVi>pK_)X-gE)^Ig!NStS z<}#BwvQfL!vH3Hw*0Km#GKpBVaD=r`dJQw&n|Z&i^=3A6fzs4;L^D4E+SXqBClWlC z@PvdmAN%Fbu+v_@oUI<$C&A;E*9N;&Oh^r75EA5uf7I9iVr^vEg~e6NfJp2EDTZs$ zQHz{Y2~{&j)mt&u8XSUQ<-*58KQfLEYEGvg0+OaoEV2-^ck41F(f_kvL0-ZD%;75~h$37Q@UWPRy!=Uqbd%K-!-4r}d^U-CeZd)vr-j{|Bo!iCAQ7>MLeB#& zkNG;(5Q?v-R~~9&KFr!`x~k2tqZUfhH@c}@#1M6lyHFZ)B&~ELJ&iq7qd`%#NM#_X z>-ZwWBs6&Rnf{#ybujpAbG?y~6)*^;Nr5_+EEXz1!<#-HEZSV|aDfVkGNO=Ve|2lyl}K^ zvN5-&k)|2~(xcm6e`(Siwhy!*Pc`g$N{{ybdff9mR(HGZ-W&Z#^1eK}Y* z@-}(9>)qtQQ^D*cXf1nZJ1 zQQbvrE0OFtS!SyoUeB<_KTmuXLn7jGZfjZMvRtXU;@pTiQ7=ykEFk3>)$KEesUG&N zN>;f_2ANiHd8{e8g*IFBIJDl*tn>QLCY-UI9_(-MpsLDaXcaU(MsnXbZoMQ1PA1)@ zD0%jgxw!t|ZLcgc9jw0=d>UZyeP0atcux}+e~Y&Iywh6u|J=BneZS54E(t9;GW0(f zJ~DhBZk+u(Pn-R+eVf6qNyk!&Hk4OOuMe-|w+DHUr<1QN*2gH=$PLr39ucOU$kJ>$+)oGUUAZ(PJLo`0iz{p~tdpy%4?KHrr z6!S~`>0taY?oe-wkg_gTCv#rl%TCcy!Iy|!wywI#r^!EPya^vjh(OvK7tdad&oRQGeU7FDsVhdjcUSqI^ z*l=q4c&pS>{w)Oqek`N_M8^Ot(tY5bR|sc@_cNLR*52|P!hM(z>xQ1HvN4IU%5W@X zt>i(ee-eTmR*PlZ_IA6&sC-?|^ZDIuzM9L^IdkqXNUPx8MqQrY%fp3dv_?T<1%a`X zFGWZWp%I1B*f*f7*tuk7S;x0c;AX^jU9ZZ{K}kzUr`pG-=i$;cymKRS)=K*}LqB7P zn?a_yA*s^5EoHsg>Zs+&zxiV^&B)7+pXd=-i{}C1^LViliCpIUabVDCwBCNUxa@Af z-LuDCceVOC5o^@jVe@9wX~fCK%>`hA#_iSvV#Q6MLWK4e_`-mX?if!u41u#kvnEP47QlV+u;bqu0+L$n!D;))lL4sO=ZRRuE0iMGD&MW%t%N>)>O#&YZj1 zazR=7h5dNB9q3mVSP%#}5q%f>w*vf~Qqx`%lR-JSk!odO4etXGtxwnSWN@!sK`?VD znnh}(hn}Qq7F&&U{OzE6!{}D}Nv)4+6wv$oj`B-5_by@Re^P+{6YujM$RAc7HunFU z^ZlP(qW=Q~#Q9(3t^Y3|p#PJJ^uGy6|G)IP(6(|hT}7o7a#j)}lYv2zF^5fXk=nl= z;|)g1=ZWHH=Qpvsxb?0g5Neyy{4DE1{&k__^B8>LT&YVFh2L?->vvqW%!qM%v^)#< zz5ticigPb~@g8sx(T@(R&4DLRqCO_Ekrp(9a^eaX88J%Y1Ui^DA6F=0CaZuH9W;uH zt4|asPOYA<&lD|J$il0CU5d^U0jd4kAEHREt|LH{COS~aijn~qxWEd*mLWqyI@l}y z3dVvLBefC`$g%R0O$1d4;B_T~x$}DAJ)C7U96|BW8yGSyopMtP^gO$`nS!D;LY>_`2M27_MSX>2%Vs}M_@F2WhAm8qu?F(*6q@chGt7e5L4 zo@D?0i?6o(>3d%>GPFSG!eSHQww3X(5xMojQJ`djdw(T24bIsfX{{V}36KOV1lmP_ z&94;XIZ#s|wW}ijb~w`yGz3yTgk=D(UX+|4fuJ6YqCdIqq6P_SI^AE3bPui14eP}yt`^K&#>Z45 z61&SwS_`%kwE;|ALmFEa9Q;maR^`(P=?7@bqV(}?RT*B4Oc1A{y}4(_`XbZDmiSMd-8$R}s(xwVALRBh0sA)tC-ZmLtK&U5c7asku%m{FcCT zgtHKj7rkrJJ8M#}a0c6x)sMz3KA2iJv1pf~ddxcSj#=V^UADRj)ftSYcqkw^*5?Ux z()}oTxIMcjbRXz!&Kl5Vm};vYuislNwu*NnPDd`P=Q=VqUp=bO_qK8!d^J6OniIeb zM*6-l6opg&>G|4IA35N_*Z(q9lOV&+Qx;BDvQ(H)|Y@7S*p-_OiA{N^GZ%v07?BD5y zgg%wpf3lu=_6Llgdkm*7!=PV8c#fwPm}fI@RQOEd3B$8fJ)dkBW5b-=WhjW?`l{;k zba!Pq(uoYJQ@gc6h-QCiDUxH7BRuVbm#SF7-fwLLPm&2+8@31lUg$`dHAh)dWu${T z9$Z&5g>p4tpN?RI^$&A5IL@tEpDB3*M}@m)%bOOzOU!a0N2N0-4+@DJp+Wv$`?`JY zUTjV?Tp)F;ly`O?oS5ncrsZ8WeEyun{S`Y>?BwE?DwGR*SoY7~LtQ{=?mAwS?K zuyFQc=tw@-|E&j=fgvr%lnR3;hv~5B&!$%1G_#ftFDNRwBBzE=hDIlOw>!*}{9rxEK8Z>1+s)v#);7aVf9KO+hyk&-ZyQ*uuR!LR z!6#600h`GG`LHQI++}!Nr;M9+RP>cI^={^cyRK|OlUvKTK*cFcH(Kjy;q-cu=X@3m zu0~dH8<$hpWr(o~guCJ0IyH1OSnQXEuw`MzTl*ujk#3flFQql&Nj?@Xvq` zj!w_cO69G9baVZYo!dP{PTP&&bNaK#pNqu{dIB4W(It5Lr^x~Z*1QH_G9O+1!Vb`; z!K&SlV*M5HT+~M%3%-CP{)U6o9NE#L=qBhQRA8!VQvHAmbZja$q9)vduF9F6LRw?f%;Fh1D;GJqXWon!{$|a(qy{~D z#+uP46LH>z^Q+O2%5t6e?)j~$=c{8b2d&5EURJ4#(Li6|)G_lRw!^kQLn=5wu;lpPagFv#nVGVEkB-f5_?%r*l9PdhJwE= z1u9m0{z9=mHtubW(b1N*$wC}dCRqapVeG@j^YPXXBOmcxY?Z(AhD8z{`M1C9E!Q|K zZ|A?W)ersu;wVH_eEw<#wDEQN;S4$oL299$G}evSm(Z%yCRcufE8DSTw3;5AwMR$p z^7ss8BwTfD5#tB6a7SeIJw@T4I>$pqA zrP;7NoesZ6l1G;hTi9&O%CawY_HOCouS-=jG7#Q+a>cU)xt z_gY~`5LHN@8h1P;@=X()DO{4#++~H4D_pABok#E*c=EPww0=TEFq(c5_ePnvwcW{cp*b)kA~TE5 zTX@JADOhd=#(J$6xNFT-4jHSjngtR~cG)H%$3_Mf)pF%>k~4e`*xGAd)8~p-WKJX` zB-Xva>QtxSYQ-Od^O(DM4-8^o%A8T2j4kqf8M+Wy%3SDn;`g_0A-6AOgppJmnsCx; z<2xSwK}w{kDU}&)!tGWLo4Q~hF426Pe;oV8j;3}^fy0f4^5aK58LWOA?(`)|*6 z(_9rM^y?CSGJr%ECEA=$76>|t?6YgGXK;VQL~$ZE9AC!3`2)Y3X~C&VS-ib7gmu|A z9ky-tW(WrZ+~^knKif$K!&W62Yaa%`qcQCIE2q`YaHvq%Xvu>p$t*pY8>&PitW<|y zAFWhDbx^M;asvEG*=|O>va- zCxfS74k*Y#72)xuEpK@K^>SHL%sH5lprMA7qb%nvcc|_GDo|oY$bMtN^nuPb8Hvcr`Szc0!?1YZ;;xf zLBLCpIjXO)fk6nIIRK3D5WqT-?0~#HjPpc{tqGsh)xI)qrFF3VlQ(5xlnmVV>7uNfX^qixJWYn4BX_fsV^Fq+cpChB(51`r!K!!_g zDfxQ|+q*LODIT%o9hmz}$^1z=`$cMokb95*HTptK_QIa>RSOmXC8~`D{y#O*e=42- zP(SRf|63CMC&Bo?B+>t9ePm|xjokfj_}~9YasJ;-Waj_He*F)+u-bng^dAE2|7`{q zCQcT%|32Wq0Mh?SO8!q-_TOgtHi?Ca=f7LU`QPvVFVgM*!bdB8+s`ceAMFI~|Ff6V zf9C-IKLLyXJBhYL3tBsA8Bgd-WPPTfO%zZ2sJ1`;*Y@^cTVBldqNUnc#K+h={>h zicEtOh)67t5gyv4Qf@}}K=CFxoCtO~m~{Tc!GY|`{5+~79LiA?;{ui(X>^Ivz9fVCn|Har> zK-H0S>jp^B;O_434#C|exVyW12oAyB-QAtw?ry;$xCMuING3D$-^5%jXxNF`M%HpUYa6Qs9gZpva>Rqid;ww-m8h2$aWc$D=MZrxa5eZ-BIM{A#+ z2Sy(y?nx>%H`13rQdqT1n58u4=6eOE9gKOj0Slt&VkP=i_By-JsM8Vo0}c4oq3>NU zk##bQ7}F`oWOg zd$8aiDgAgN=ym*6xtiYY5Mc6<(Rw#rDvSyLxEqE+!=Ddj8;(EqLyHH&94h^gJ`Y!z z0d*M#mP7rCOK8Q$Ck#EK1)h3K`h&Yz3673a8J8mv-QjHV_;B4o&Z-zo-kPi_tr%(r zeWd8_O#FK&q6}d7LIRM|i3Xg~(LJ9VQclimWI<~<9De&W~uqz%|8_U-eM-9IykFu^E-(C6siq%a=vkBNK39NYC>zWuu%vrI&&;5-vZZK-G$&1C) zZ47>hF@x!tgPEXY z{r0XSiSG;s?%gIILn6o%+*e6)T?2_u23Z)2E{sZei+7w|II>_Eow_#fYyGCbf!RQ4 zyi5B=^O%0+N2&wY`nDS_V3Lj_KlnhOB$3!CkX`>Ikru^YNT*7LAwIxRM^7>!PA5Y* zZh^ysvL33WyS+ELYI@<_5rz}kVb^^XZ)F3UZ^<{cMtG~wGF_{-EI1MUTQ%qJJ$SUj z5;jWCMqQw_zOZfZkZ><1bTDpP*$#J6G@aNwl6xEPE}g#sP5y{19SO#e zg!ognkGynI_KbXl{8$&{s)Vapp1h-u*NVD;CK_ryWvTU_m1Q`^TBHy2+LUFL>Evvb!Ieej;3#7$6v>Z1vSrz5ZbW^p%udeE z%P!E1CD5)CwXNI4=|$HQPX z6SF|f#=tka+c4rVGhxClj;*5N?BYBMb;k&^Dzmun@UxV~AZ8(E%4R@j8g;#9vu2}a z4Ko2V%(H1TIWzV%AB#^vc4V#<@J&}vflqxik2O7-W6L?0jhSGYe?PHp>XqLla+zvW zfuc057Mc@Rq*QbrK4)0QX9<05x)i+hd1-d3zplj*9Gen5Lb`s;EUlYElf&9E++nd9 zcon%Bx_RDF`#@^yavpJCX&-y$aK^o4d&T2zb<*O<=t#{7pMt=&tU}TO&Gc}7x6zytgi)Rm*yu?LWl~E@L5ek< zH6v~6@Tbj|zYUQ~x z;e2}QWBTJsQXtneu=4CghjUy=g_qzt;>^vVGX`D}@n8elOm!4~#SIDfOolS6z*XmtAwZ2&+ zSwn2+^#Jobr(@xVy+fAzPDcmt3qe6@Y_b#YDB;7E;=w!qr4mqxl^-#b` z{zn(q=hWI%qpAQl2@jTewg$SJk&6R7 z3D-!sD2k}9T9J+4M6=wo8Y^=K>P>_ub2h(iR-i^hxk4?AOG?DYT~X=LF4du_b6MLi zBU}<2O1LEiP^~sdnySuK<4UOJ9g1sv#9ygOB*`sKRR?ASdU$1~%F$4F)UBA`jnNL! z3TkU;(L46WBcD>}DqkpTJx(1Ak7|3V#I=9FFq@y{oRggons+F>R&TLR zyKmlfgqc_kFbmdz zZlzvzJnQuQwndyjWNww+n_+xg3)hXoStct#pLS0I8i_T4jJHkz^_wlo%CSbHM zJkD_Mb#J-pJ>gW}Q~ubnZu9aTY*QpCr`0E)|bte)I2pF zZoeanlMYFHu&H|3?Zae#5@* z)^*8$DRdLsPt&Ajc4TwFcAqo+MTE9R%c^zLDes`Te>UEHZC)C%_leiN@V9VWD?2N# z?_o!+b8!=n56a)mPL>~+J)NshxgLtX6>PLfY!J6^wl6&ug9X5vJu}~FgSBY3e7Sl1 z!{HI@=5jT#HBiQD@L@;Oda>e4y96hRtN76V#QYfcq~qRYZflJ+nzS2ar=#el{M^1pJzmwjBeS{8=$T(3y%N0yx?BG#Je&!w3F z22B<=#ZBg6va@(sxy@FpK;%8sakMzhuIL~}l>?WFeKO`b1`~MmQSX9e7U?p8>17xz zNVi29#)d~eP!GYfCWVjz)@>RUN6Ms#Gc z`qS1G<##)T$2`TrG+NrdFgCnR9nUh>tMwHV8He@EvirqRW``@@|Pdi^2YIOy;`lQ=7J;-Xm1MRZ!0Fg^mDF;`< zSP-#kXSF)g2Ud;nE;%Sl8kS4S{w(oBD6J4XL{_%?gXt>Dj#m6a)W~p`fPgPu1uOavcK-@sqFKkJ@AYqQSPpRl+l}+@qY)(F*5LW?ni+%k<9<|0BwO`7R3rs3+ z8xjx$i8mpHqs}Y{%O-+V>5~)C5%xv3&%9u~^5zX3QaJ{_1I51_@2~YKN*%h&DHj-+ z3+-sEGit6ISB%x=TTF&Rqc_#DufX5CL@4yD#tOgN%~AS5<8`E zF>5%Rz9%1_DbK(=RkjaO4cG@7s!@USppWqfd1Vv(@KQDy8~bjT>f9BmXC*L>9;e`W zQ^wcvngx7WBq7_V<(8{)v|T9C%&_BYHYE_CH6cR}!k$e z#*F0;VzWPK%zln>01~hNN@Mmj>2Ki~|Jekuv}yqM zGDkg!pJ1V)oulEe61;w9e%(|S6PMzbr3B>J&eYKESKSC2+Uwhy+BjI-L9+ZoUnOLv zZ*5>|1;~n&uD-mrrLNUq5@Az2dj|m%T{}ER02`L1?jIg{K#6HTni@EmyjB7W9UbIr z2LJ5`xc7Sh=kag%{~`NdWBSLF|C-*fr(RQh&C}mg{_Xzz_4VoB{@?Nc_Wwuwy;jxl ze)+4KU#s}9%KTNi(z=$0c(nhhb_-o&Km!A6>^00U$~S5jfO0_1M8^&wxC3;0dOCJG zO-NcXK$kGp=d&`lFa)GW%V)3uN)yP+4CrsKNxgcg832@fv;w*|qK2l%CJuPa?96~( z;9zK}jK>PNq4G-%$pm zzqOG+77P9I0=NMg06c#GRX1r%a<@^O_v{}Btjr{?GMUuRGgLLLD_Nawd+?z=7U#$7 zM2PPhcCsndHJoQzz%bw&ft4IISHn>ufZqrz;Mbw_dhM zZri?6oDFPGdu&__!(c?;UZ3cM6d?dEmHLUFeNHYLe$HU2gbt9ot8 zW?AF*>n+hO&STN*W?kaUD}Wto=CxC2VZE>CW(S7^;wo&r6|j8s09&0*4BB6nG_>El z?qnTses{f_gTSBCi+0j%oLb7IOZ|$8G#AwS6{RdY$}8@GHF2>`uTF)6VVW9e(pv!c z92BH1PvK6#eBE6VmD1Y7LSFH#J%lMH_`R3?@(R#eB7#Vd*Lf>j-_5%07ZfL`td|cX zjM5>6a1NuTNpR_PcNr#2&H<#gMWiG>A1+muE7sk|A0e^~38K0zLAI8JDazkN7m*Qj zBcfx>BMNAb_XiN_M^!1M^b-g~O-iNohx<3%=?^bdVxyxTUM$}}=Xf}@TC*`}t}t-5 zCfdGdhxsz$X^{wDk1F!5IQhuj!OnP2uv<1g*ZW7&Qa$9#4R9C@8wv*P3}9Uh$}&T&&h6Kb!h**Ywm zvGx6{m2h)gPCJWQO3<9J$a`DI3Y_Nq!>^N&>j@dh=R`ZTOy{Zf(*sQwMrxzr$nR-H zzBSF&=cySx(9UgwYe3-n0wlr8YepOqK1NAsd zR6z_p{bW@nrH5m4d;;KfMpE|Gt%T+RP6s9J9R5b-0;>U>;q&rl)_M31&PWXhrvnP+ zM=H#Ks04H?{7VfW7N0U5UbgoUzNlU1->}ap&){#d3)@q~;BUbZ8R}cw2)iz-!=b6l z>!diF?7Pn*((WE3P_33d)|cvTRs$`0lJCoP8^46QDYM`g(|1@kI5&D?z&bcO(ayx1 zj=e!%LJT)5B;T=ZaeBARzY)8PJ=7O78V6mo`jiGyQZ%Ja{KR=3Wn17S%oLn>dxk}M z6~qxn?a7Xxd~Q>tsb>30!}inJ5Hcm=NEEbvF2q={5i-O@7kdcI(%zHrOPpBi6jDdm zdu>sLm19Y~p0^liv=nIL;i%az_(PVUr&*N<+g)UAZ_Kq^Ng-z)yqS%yj<3MQ+gEFd0oh;<#2QCEuqXa z+G}}T1v4%Uo^rp8*5~j3_#jfCY+01;t8U1saXi|tBiUW=~zh9O4K#P zqPl;n)N_z!!)!|hlS|I9JJaN^qj*wZyP^UTs7}wCV5^LKe!_H8!`C zmuK~{nIjX;Jd(L1ws?p(PkMK(|CCNDyYR^np!59l{V6U1#dm9LYK`t@JJw#<5|hm9->xn%d#@9 zX8uv;F#O^4kvXd;TG5=d3c$MZ=qSKG%mHs{LwtIG-CRO{-5DbAU>={hStU}+GXF?; zgS(=%upsH6Hk?wY&``EGG3$C99oDb%)yxSyM5RgvFG`GB(`h<7L5ub(VAQ3x4Ur?_ z>nH1jiV&@`8!V`s%6FmxOtK+l`9}G!s|xm-9~omy=>2DAow(SFqO+WKl$x4);!Zye zH9%i!avU#<9|R0BjE_b`=PFIB>y%j(Qk$DZLrWR9Q(tT~Telc|p;;8L0+z=%qKA-Z z#5%8o)20}MG3H~3IJfG^a_gg3GaOjO8_Z7SN?a_^{?5>E%x5sS$Yi^`evg5Vu?ga4b_7l;8@|Zt z2$Wp%K{}tolwJOD3gi~ShdAF57^DLF?&{{wQ!VNBg!6O7^)?_|B5%zGHpqJPn4AFYXw1NoP0oZ zwLJ=_an-+`>J9d&oW_cYjnCB&6!Gn2FbEV*WTe}-dUL7+XO1Gv*rI(2(fg@F$x4B{ zbueAtazRV`By_2s;WWM_5>Z1ha$iaTLXY*WX0wWdrZ$8|uFm3DlnV6XXCX=ztWo`+8%=_`|yOUk4s=cY)C@<$g-{MQo|Js|{R^pGU8VvHhp(>rYL&j$Bw6x5Mu1(;f<&X~zW#CadGA&j$+&w_geb zxvYdY=_&PYbIBesS)iej%UiwMD~$%Ow~W*82XCkKxArd%ie$pxG_UM^PlkIEtc1N{ z6eH(@j+Nkvi>*H|+6;!4v2fArM#M~CIO_=@W#OvY_$5ksm8zp>M%z+igWl@+RP?;T zeSg;Vucr(>pB0rBMMc_--7bxTnI=Ok*x>*hRe>)S`{anXhSj=B7LoW_HJAgrrs%`0 zsrp&I1<+iU-%LSdFkO~9r$N(M&T4;(Y^a#nP%!fJv%0DxFE39ZCc72e>;fZ4&Kc4Z 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

    U2+05ikgyPP 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 literal 0 HcmV?d00001 diff --git a/doc/blog posts/2013-09-30 Attaching the Rocket to the Chainsaw - Behind the Scenes of BFF and FOE's Crash Recycler.pdf b/doc/blog posts/2013-09-30 Attaching the Rocket to the Chainsaw - Behind the Scenes of BFF and FOE's Crash Recycler.pdf new file mode 100644 index 0000000000000000000000000000000000000000..13fe05275179bb7f9233bd5cb9a7e0b3be90617c GIT binary patch literal 325529 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%0Dv2B-UI2eu*XtHl`wv8vHFLj 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

h*;9M2s2kwq4}bg3=GA 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%0D>r_Fv@Y#*C(dA`_@$D@>FK5 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ⅇ`PCix~Ye~J`e)> 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�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 literal 0 HcmV?d00001 diff --git a/doc/blog posts/2012-11-05 A Look Inside CERT Fuzzing Tools.pdf b/doc/blog posts/2012-11-05 A Look Inside CERT Fuzzing Tools.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1dc1c1084274b76d6ecccbc864f46513c1c15493 GIT binary patch literal 110794 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%0As)FvP|dp&rG`4~7(C^K9K( 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Ξ3}p#Lx`um$>d4)#S?0 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 literal 0 HcmV?d00001 diff --git a/doc/blog posts/2010-09-22 CERT Basic Fuzzing Framework Update.pdf b/doc/blog posts/2010-09-22 CERT Basic Fuzzing Framework Update.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3da1ed5d0797e566fa954e0a8bbbee031b1cb6c9 GIT binary patch literal 100781 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